Skip to main content

deltachat/
mimeparser.rs

1//! # MIME message parsing module.
2
3use std::cmp::min;
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::path::Path;
6use std::str;
7use std::str::FromStr;
8
9use anyhow::{Context as _, Result, bail, ensure};
10use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
11use deltachat_derive::{FromSql, ToSql};
12use format_flowed::unformat_flowed;
13use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrparse_header};
14use mime::Mime;
15
16use crate::aheader::Aheader;
17use crate::blob::BlobObject;
18use crate::chat::{Chat, ChatId};
19use crate::config::Config;
20use crate::constants;
21use crate::contact::{ContactId, import_public_key};
22use crate::context::Context;
23use crate::decrypt::{self, validate_detached_signature};
24use crate::dehtml::dehtml;
25use crate::download::PostMsgMetadata;
26use crate::events::EventType;
27use crate::headerdef::{HeaderDef, HeaderDefMap};
28use crate::key::{self, DcKey, Fingerprint, SignedPublicKey};
29use crate::log::warn;
30use crate::message::{self, Message, MsgId, Viewtype, get_vcard_summary, set_msg_failed};
31use crate::param::{Param, Params};
32use crate::simplify::{SimplifiedText, simplify};
33use crate::sync::SyncItems;
34use crate::tools::{get_filemeta, parse_receive_headers, time, truncate_msg_text, validate_id};
35use crate::{chatlist_events, location, tools};
36
37/// Public key extracted from `Autocrypt-Gossip`
38/// header with associated information.
39#[derive(Debug)]
40pub struct GossipedKey {
41    /// Public key extracted from `keydata` attribute.
42    pub public_key: SignedPublicKey,
43
44    /// True if `Autocrypt-Gossip` has a `_verified` attribute.
45    pub verified: bool,
46}
47
48/// A parsed MIME message.
49///
50/// This represents the relevant information of a parsed MIME message
51/// for deltachat.  The original MIME message might have had more
52/// information but this representation should contain everything
53/// needed for deltachat's purposes.
54///
55/// It is created by parsing the raw data of an actual MIME message
56/// using the [MimeMessage::from_bytes] constructor.
57#[derive(Debug)]
58pub(crate) struct MimeMessage {
59    /// Parsed MIME parts.
60    pub parts: Vec<Part>,
61
62    /// Message headers.
63    headers: HashMap<String, String>,
64
65    #[cfg(test)]
66    /// Names of removed (ignored) headers. Used by `header_exists()` needed for tests.
67    headers_removed: HashSet<String>,
68
69    /// List of addresses from the `To` and `Cc` headers.
70    ///
71    /// Addresses are normalized and lowercase.
72    pub recipients: Vec<SingleInfo>,
73
74    /// List of addresses from the `Chat-Group-Past-Members` header.
75    pub past_members: Vec<SingleInfo>,
76
77    /// `From:` address.
78    pub from: SingleInfo,
79
80    /// Whether the message is incoming or outgoing (self-sent).
81    pub incoming: bool,
82    /// The List-Post address is only set for mailing lists. Users can send
83    /// messages to this address to post them to the list.
84    pub list_post: Option<String>,
85    pub chat_disposition_notification_to: Option<SingleInfo>,
86
87    /// Decryption error if decryption of the message has failed.
88    pub decryption_error: Option<String>,
89
90    /// Valid signature fingerprint if a message is an
91    /// Autocrypt encrypted and signed message and corresponding intended recipient fingerprints
92    /// (<https://www.rfc-editor.org/rfc/rfc9580.html#name-intended-recipient-fingerpr>) if any.
93    ///
94    /// If a message is not encrypted or the signature is not valid,
95    /// this is `None`.
96    pub signature: Option<(Fingerprint, HashSet<Fingerprint>)>,
97
98    /// The addresses for which there was a gossip header
99    /// and their respective gossiped keys.
100    pub gossiped_keys: BTreeMap<String, GossipedKey>,
101
102    /// Fingerprint of the key in the Autocrypt header.
103    ///
104    /// It is not verified that the sender can use this key.
105    pub autocrypt_fingerprint: Option<String>,
106
107    /// True if the message is a forwarded message.
108    pub is_forwarded: bool,
109    pub is_system_message: SystemMessage,
110    pub location_kml: Option<location::Kml>,
111    pub message_kml: Option<location::Kml>,
112    pub(crate) sync_items: Option<SyncItems>,
113    pub(crate) webxdc_status_update: Option<String>,
114    pub(crate) user_avatar: Option<AvatarAction>,
115    pub(crate) group_avatar: Option<AvatarAction>,
116    pub(crate) mdn_reports: Vec<Report>,
117    pub(crate) delivery_report: Option<DeliveryReport>,
118
119    /// Standard USENET signature, if any.
120    ///
121    /// `None` means no text part was received, empty string means a text part without a footer is
122    /// received.
123    pub(crate) footer: Option<String>,
124
125    /// If set, this is a modified MIME message; clients should offer a way to view the original
126    /// MIME message in this case.
127    pub is_mime_modified: bool,
128
129    /// Decrypted raw MIME structure.
130    pub decoded_data: Vec<u8>,
131
132    /// Hop info for debugging.
133    pub(crate) hop_info: String,
134
135    /// Whether the message is auto-generated.
136    ///
137    /// If chat message (with `Chat-Version` header) is auto-generated,
138    /// the contact sending this should be marked as bot.
139    ///
140    /// If non-chat message is auto-generated,
141    /// it could be a holiday notice auto-reply,
142    /// in which case the message should be marked as bot-generated,
143    /// but the contact should not be.
144    pub(crate) is_bot: Option<bool>,
145
146    /// When the message was received, in secs since epoch.
147    pub(crate) timestamp_rcvd: i64,
148    /// Sender timestamp in secs since epoch. Allowed to be in the future due to unsynchronized
149    /// clocks, but not too much.
150    pub(crate) timestamp_sent: i64,
151
152    pub(crate) pre_message: PreMessageMode,
153}
154
155#[derive(Debug, Clone, PartialEq)]
156pub(crate) enum PreMessageMode {
157    /// This is a post-message.
158    /// It replaces its pre-message attachment if it exists already,
159    /// and if the pre-message does not exist, it is treated as a normal message.
160    Post,
161    /// This is a Pre-Message,
162    /// it adds a message preview for a Post-Message
163    /// and it is ignored if the Post-Message was downloaded already
164    Pre {
165        post_msg_rfc724_mid: String,
166        metadata: Option<PostMsgMetadata>,
167    },
168    /// Atomic ("normal") message.
169    None,
170}
171
172#[derive(Debug, PartialEq)]
173pub(crate) enum AvatarAction {
174    Delete,
175    Change(String),
176}
177
178/// System message type.
179#[derive(
180    Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
181)]
182#[repr(u32)]
183pub enum SystemMessage {
184    /// Unknown type of system message.
185    #[default]
186    Unknown = 0,
187
188    /// Group or broadcast channel name changed.
189    GroupNameChanged = 2,
190
191    /// Group or broadcast channel avatar changed.
192    GroupImageChanged = 3,
193
194    /// Member was added to the group.
195    MemberAddedToGroup = 4,
196
197    /// Member was removed from the group.
198    MemberRemovedFromGroup = 5,
199
200    /// Autocrypt Setup Message.
201    ///
202    /// Deprecated as of 2026-03-15, such messages should not be created
203    /// but may exist in the database.
204    AutocryptSetupMessage = 6,
205
206    /// Secure-join message.
207    SecurejoinMessage = 7,
208
209    /// Location streaming is enabled.
210    LocationStreamingEnabled = 8,
211
212    /// Location-only message.
213    LocationOnly = 9,
214
215    /// Chat ephemeral message timer is changed.
216    EphemeralTimerChanged = 10,
217
218    /// "Messages are end-to-end encrypted."
219    ChatProtectionEnabled = 11,
220
221    /// "%1$s sent a message from another device.", deprecated 2025-07
222    ChatProtectionDisabled = 12,
223
224    /// Message can't be sent because of `Invalid unencrypted mail to <>`
225    /// which is sent by chatmail servers.
226    InvalidUnencryptedMail = 13,
227
228    /// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
229    /// to complete.
230    SecurejoinWait = 14,
231
232    /// 1:1 chats info message telling that SecureJoin is still running, but the user may already
233    /// send messages.
234    SecurejoinWaitTimeout = 15,
235
236    /// Self-sent-message that contains only json used for multi-device-sync;
237    /// if possible, we attach that to other messages as for locations.
238    MultiDeviceSync = 20,
239
240    /// Sync message that contains a json payload
241    /// sent to the other webxdc instances
242    /// These messages are not shown in the chat.
243    WebxdcStatusUpdate = 30,
244
245    /// Webxdc info added with `info` set in `send_webxdc_status_update()`.
246    WebxdcInfoMessage = 32,
247
248    /// This message contains a users iroh node address.
249    IrohNodeAddr = 40,
250
251    /// "Messages are end-to-end encrypted."
252    ChatE2ee = 50,
253
254    /// Message indicating that a call was accepted.
255    CallAccepted = 66,
256
257    /// Message indicating that a call was ended.
258    CallEnded = 67,
259
260    /// Group or broadcast channel description changed.
261    GroupDescriptionChanged = 70,
262}
263
264impl MimeMessage {
265    /// Parse a mime message.
266    ///
267    /// This method has some side-effects,
268    /// such as saving blobs and saving found public keys to the database.
269    pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
270        let mail = mailparse::parse_mail(body)?;
271
272        let timestamp_rcvd = time();
273        let mut timestamp_sent =
274            Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
275        let hop_info = parse_receive_headers(&mail.get_headers());
276
277        let mut headers = Default::default();
278        let mut headers_removed = HashSet::<String>::new();
279        let mut recipients = Default::default();
280        let mut past_members = Default::default();
281        let mut from = Default::default();
282        let mut list_post = Default::default();
283        let mut chat_disposition_notification_to = None;
284
285        // Parse IMF headers.
286        MimeMessage::merge_headers(
287            context,
288            &mut headers,
289            &mut headers_removed,
290            &mut recipients,
291            &mut past_members,
292            &mut from,
293            &mut list_post,
294            &mut chat_disposition_notification_to,
295            &mail,
296        );
297        headers_removed.extend(
298            headers
299                .extract_if(|k, _v| is_hidden(k))
300                .map(|(k, _v)| k.to_string()),
301        );
302
303        // Parse hidden headers.
304        let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
305        if mimetype.type_() == mime::MULTIPART
306            && mimetype.subtype().as_str() == "mixed"
307            && let Some(part) = mail.subparts.first()
308        {
309            for field in &part.headers {
310                let key = field.get_key().to_lowercase();
311                if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
312                    headers.insert(key.to_string(), field.get_value());
313                }
314            }
315        }
316
317        // Overwrite Message-ID with X-Microsoft-Original-Message-ID.
318        // However if we later find Message-ID in the protected part,
319        // it will overwrite both.
320        if let Some(microsoft_message_id) = remove_header(
321            &mut headers,
322            HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
323            &mut headers_removed,
324        ) {
325            headers.insert(
326                HeaderDef::MessageId.get_headername().to_string(),
327                microsoft_message_id,
328            );
329        }
330
331        // Remove headers that are allowed _only_ in the encrypted+signed part
332        let encrypted = false;
333        Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
334
335        let mut from = from.context("No from in message")?;
336
337        let mut gossiped_keys = Default::default();
338
339        let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
340
341        let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
342
343        let mut pre_message = if mail
344            .headers
345            .get_header_value(HeaderDef::ChatIsPostMessage)
346            .is_some()
347        {
348            PreMessageMode::Post
349        } else {
350            PreMessageMode::None
351        };
352
353        let mail_raw; // Memory location for a possible decrypted message.
354        let decrypted_msg; // Decrypted signed OpenPGP message.
355        let expected_sender_fingerprint: Option<String>;
356
357        let (mail, is_encrypted) = match Box::pin(decrypt::decrypt(context, &mail)).await {
358            Ok(Some((mut msg, expected_sender_fp))) => {
359                mail_raw = msg.as_data_vec().unwrap_or_default();
360
361                let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
362                if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
363                    info!(
364                        context,
365                        "decrypted message mime-body:\n{}",
366                        String::from_utf8_lossy(&mail_raw),
367                    );
368                }
369
370                decrypted_msg = Some(msg);
371
372                timestamp_sent = Self::get_timestamp_sent(
373                    &decrypted_mail.headers,
374                    timestamp_sent,
375                    timestamp_rcvd,
376                );
377
378                let protected_aheader_values = decrypted_mail
379                    .headers
380                    .get_all_values(HeaderDef::Autocrypt.into());
381                if !protected_aheader_values.is_empty() {
382                    aheader_values = protected_aheader_values;
383                }
384
385                expected_sender_fingerprint = expected_sender_fp;
386                (Ok(decrypted_mail), true)
387            }
388            Ok(None) => {
389                mail_raw = Vec::new();
390                decrypted_msg = None;
391                expected_sender_fingerprint = None;
392                (Ok(mail), false)
393            }
394            Err(err) => {
395                mail_raw = Vec::new();
396                decrypted_msg = None;
397                expected_sender_fingerprint = None;
398                warn!(context, "decryption failed: {:#}", err);
399                (Err(err), false)
400            }
401        };
402
403        let mut autocrypt_header = None;
404        if from_is_not_self_addr {
405            // See `get_all_addresses_from_header()` for why we take the last valid header.
406            for val in aheader_values.iter().rev() {
407                autocrypt_header = match Aheader::from_str(val) {
408                    Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
409                    Ok(header) => {
410                        warn!(
411                            context,
412                            "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
413                        );
414                        continue;
415                    }
416                    Err(err) => {
417                        warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
418                        continue;
419                    }
420                };
421                break;
422            }
423        }
424
425        let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
426            let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
427            import_public_key(context, &autocrypt_header.public_key)
428                .await
429                .context("Failed to import public key from the Autocrypt header")?;
430            Some(fingerprint)
431        } else {
432            None
433        };
434
435        let mut public_keyring = if from_is_not_self_addr {
436            if let Some(autocrypt_header) = autocrypt_header {
437                vec![autocrypt_header.public_key]
438            } else {
439                vec![]
440            }
441        } else {
442            key::load_self_public_keyring(context).await?
443        };
444
445        if let Some(signature) = match &decrypted_msg {
446            Some(pgp::composed::Message::Literal { .. }) => None,
447            Some(pgp::composed::Message::Compressed { .. }) => {
448                // One layer of compression should already be handled by now.
449                // We don't decompress messages compressed multiple times.
450                None
451            }
452            Some(pgp::composed::Message::Signed { reader, .. }) => reader.signature(0),
453            Some(pgp::composed::Message::Encrypted { .. }) => {
454                // The message is already decrypted once.
455                None
456            }
457            None => None,
458        } {
459            for issuer_fingerprint in signature.issuer_fingerprint() {
460                let issuer_fingerprint =
461                    crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
462                if let Some(public_key_bytes) = context
463                    .sql
464                    .query_row_optional(
465                        "SELECT public_key
466                         FROM public_keys
467                         WHERE fingerprint=?",
468                        (&issuer_fingerprint,),
469                        |row| {
470                            let bytes: Vec<u8> = row.get(0)?;
471                            Ok(bytes)
472                        },
473                    )
474                    .await?
475                {
476                    let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
477                    public_keyring.push(public_key)
478                }
479            }
480        }
481
482        let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
483            crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
484        } else {
485            HashMap::new()
486        };
487
488        let mail = mail.as_ref().map(|mail| {
489            let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
490                .unwrap_or((mail, Default::default()));
491            if is_encrypted {
492                let signatures_detached = signatures_detached
493                    .into_iter()
494                    .map(|fp| (fp, Vec::new()))
495                    .collect::<HashMap<_, _>>();
496                signatures.extend(signatures_detached);
497            }
498            content
499        });
500
501        if let Some(expected_sender_fingerprint) = expected_sender_fingerprint {
502            ensure!(
503                !signatures.is_empty(),
504                "Unsigned message is not allowed to be encrypted with this shared secret"
505            );
506            ensure!(
507                signatures.len() == 1,
508                "Too many signatures on symm-encrypted message"
509            );
510            ensure!(
511                signatures.contains_key(&expected_sender_fingerprint.parse()?),
512                "This sender is not allowed to encrypt with this secret key"
513            );
514        }
515
516        if let (Ok(mail), true) = (mail, is_encrypted) {
517            if !signatures.is_empty() {
518                // Unsigned "Subject" mustn't be prepended to messages shown as encrypted
519                // (<https://github.com/deltachat/deltachat-core-rust/issues/1790>).
520                // Other headers are removed by `MimeMessage::merge_headers()` except for "List-ID".
521                remove_header(&mut headers, "subject", &mut headers_removed);
522                remove_header(&mut headers, "list-id", &mut headers_removed);
523            }
524
525            // let known protected headers from the decrypted
526            // part override the unencrypted top-level
527
528            // Signature was checked for original From, so we
529            // do not allow overriding it.
530            let mut inner_from = None;
531
532            MimeMessage::merge_headers(
533                context,
534                &mut headers,
535                &mut headers_removed,
536                &mut recipients,
537                &mut past_members,
538                &mut inner_from,
539                &mut list_post,
540                &mut chat_disposition_notification_to,
541                mail,
542            );
543
544            if !signatures.is_empty() {
545                // Handle any gossip headers if the mail was encrypted. See section
546                // "3.6 Key Gossip" of <https://autocrypt.org/autocrypt-spec-1.1.0.pdf>
547                // but only if the mail was correctly signed. Probably it's ok to not require
548                // encryption here, but let's follow the standard.
549                let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
550                gossiped_keys =
551                    parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
552            }
553
554            if let Some(inner_from) = inner_from {
555                if !addr_cmp(&inner_from.addr, &from.addr) {
556                    // There is a From: header in the encrypted
557                    // part, but it doesn't match the outer one.
558                    // This _might_ be because the sender's mail server
559                    // replaced the sending address, e.g. in a mailing list.
560                    // Or it's because someone is doing some replay attack.
561                    // Resending encrypted messages via mailing lists
562                    // without reencrypting is not useful anyway,
563                    // so we return an error below.
564                    warn!(
565                        context,
566                        "From header in encrypted part doesn't match the outer one",
567                    );
568
569                    // Return an error from the parser.
570                    // This will result in creating a tombstone
571                    // and no further message processing
572                    // as if the MIME structure is broken.
573                    bail!("From header is forged");
574                }
575                from = inner_from;
576            }
577        }
578        if signatures.is_empty() {
579            Self::remove_secured_headers(&mut headers, &mut headers_removed, is_encrypted);
580        }
581        if !is_encrypted {
582            signatures.clear();
583        }
584
585        if let (Ok(mail), true) = (mail, is_encrypted)
586            && let Some(post_msg_rfc724_mid) =
587                mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
588        {
589            let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
590            let metadata = if let Some(value) = mail
591                .headers
592                .get_header_value(HeaderDef::ChatPostMessageMetadata)
593            {
594                match PostMsgMetadata::try_from_header_value(&value) {
595                    Ok(metadata) => Some(metadata),
596                    Err(error) => {
597                        error!(
598                            context,
599                            "Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
600                        );
601                        None
602                    }
603                }
604            } else {
605                warn!(
606                    context,
607                    "Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
608                );
609                None
610            };
611
612            pre_message = PreMessageMode::Pre {
613                post_msg_rfc724_mid,
614                metadata,
615            };
616        }
617
618        let signature = signatures
619            .into_iter()
620            .last()
621            .map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
622
623        let incoming = if let Some((ref sig_fp, _)) = signature {
624            sig_fp.hex() != key::self_fingerprint(context).await?
625        } else {
626            // rare case of getting a cleartext message
627            // so we determine 'incoming' flag by From-address
628            from_is_not_self_addr
629        };
630
631        let mut parser = MimeMessage {
632            parts: Vec::new(),
633            headers,
634            #[cfg(test)]
635            headers_removed,
636
637            recipients,
638            past_members,
639            list_post,
640            from,
641            incoming,
642            chat_disposition_notification_to,
643            decryption_error: mail.err().map(|err| format!("{err:#}")),
644
645            // only non-empty if it was a valid autocrypt message
646            signature,
647            autocrypt_fingerprint,
648            gossiped_keys,
649            is_forwarded: false,
650            mdn_reports: Vec::new(),
651            is_system_message: SystemMessage::Unknown,
652            location_kml: None,
653            message_kml: None,
654            sync_items: None,
655            webxdc_status_update: None,
656            user_avatar: None,
657            group_avatar: None,
658            delivery_report: None,
659            footer: None,
660            is_mime_modified: false,
661            decoded_data: Vec::new(),
662            hop_info,
663            is_bot: None,
664            timestamp_rcvd,
665            timestamp_sent,
666            pre_message,
667        };
668
669        match mail {
670            Ok(mail) => {
671                parser.parse_mime_recursive(context, mail, false).await?;
672            }
673            Err(err) => {
674                let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
675
676                let part = Part {
677                    typ: Viewtype::Text,
678                    msg_raw: Some(txt.to_string()),
679                    msg: txt.to_string(),
680                    // Don't change the error prefix for now,
681                    // receive_imf.rs:lookup_chat_by_reply() checks it.
682                    error: Some(format!("Decrypting failed: {err:#}")),
683                    ..Default::default()
684                };
685                parser.do_add_single_part(part);
686            }
687        };
688
689        let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
690        if parser.mdn_reports.is_empty()
691            && !is_location_only
692            && parser.sync_items.is_none()
693            && parser.webxdc_status_update.is_none()
694        {
695            let is_bot =
696                parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
697            parser.is_bot = Some(is_bot);
698        }
699        parser.maybe_remove_bad_parts();
700        parser.maybe_remove_inline_mailinglist_footer();
701        parser.heuristically_parse_ndn(context).await;
702        parser.parse_headers(context).await?;
703        parser.decoded_data = mail_raw;
704
705        Ok(parser)
706    }
707
708    #[expect(clippy::arithmetic_side_effects)]
709    fn get_timestamp_sent(
710        hdrs: &[mailparse::MailHeader<'_>],
711        default: i64,
712        timestamp_rcvd: i64,
713    ) -> i64 {
714        hdrs.get_header_value(HeaderDef::Date)
715            .and_then(|v| mailparse::dateparse(&v).ok())
716            .map_or(default, |value| {
717                min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
718            })
719    }
720
721    /// Parses system messages.
722    fn parse_system_message_headers(&mut self) {
723        if let Some(value) = self.get_header(HeaderDef::ChatContent) {
724            if value == "location-streaming-enabled" {
725                self.is_system_message = SystemMessage::LocationStreamingEnabled;
726            } else if value == "ephemeral-timer-changed" {
727                self.is_system_message = SystemMessage::EphemeralTimerChanged;
728            } else if value == "protection-enabled" {
729                self.is_system_message = SystemMessage::ChatProtectionEnabled;
730            } else if value == "protection-disabled" {
731                self.is_system_message = SystemMessage::ChatProtectionDisabled;
732            } else if value == "group-avatar-changed" {
733                self.is_system_message = SystemMessage::GroupImageChanged;
734            } else if value == "call-accepted" {
735                self.is_system_message = SystemMessage::CallAccepted;
736            } else if value == "call-ended" {
737                self.is_system_message = SystemMessage::CallEnded;
738            }
739        } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
740            self.is_system_message = SystemMessage::MemberRemovedFromGroup;
741        } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
742            self.is_system_message = SystemMessage::MemberAddedToGroup;
743        } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
744            self.is_system_message = SystemMessage::GroupNameChanged;
745        } else if self
746            .get_header(HeaderDef::ChatGroupDescriptionChanged)
747            .is_some()
748        {
749            self.is_system_message = SystemMessage::GroupDescriptionChanged;
750        }
751    }
752
753    /// Parses avatar action headers.
754    fn parse_avatar_headers(&mut self, context: &Context) -> Result<()> {
755        if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
756            self.group_avatar =
757                self.avatar_action_from_header(context, header_value.to_string())?;
758        }
759
760        if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
761            self.user_avatar = self.avatar_action_from_header(context, header_value.to_string())?;
762        }
763        Ok(())
764    }
765
766    fn parse_videochat_headers(&mut self) {
767        let content = self
768            .get_header(HeaderDef::ChatContent)
769            .unwrap_or_default()
770            .to_string();
771        let room = self
772            .get_header(HeaderDef::ChatWebrtcRoom)
773            .map(|s| s.to_string());
774        let accepted = self
775            .get_header(HeaderDef::ChatWebrtcAccepted)
776            .map(|s| s.to_string());
777        let has_video = self
778            .get_header(HeaderDef::ChatWebrtcHasVideoInitially)
779            .map(|s| s.to_string());
780        if let Some(part) = self.parts.first_mut() {
781            if let Some(room) = room {
782                if content == "call" {
783                    part.typ = Viewtype::Call;
784                    part.param.set(Param::WebrtcRoom, room);
785                }
786            } else if let Some(accepted) = accepted {
787                part.param.set(Param::WebrtcAccepted, accepted);
788            }
789            if let Some(has_video) = has_video {
790                part.param.set(Param::WebrtcHasVideoInitially, has_video);
791            }
792        }
793    }
794
795    /// Squashes mutitpart chat messages with attachment into single-part messages.
796    ///
797    /// Delta Chat sends attachments, such as images, in two-part messages, with the first message
798    /// containing a description. If such a message is detected, text from the first part can be
799    /// moved to the second part, and the first part dropped.
800    fn squash_attachment_parts(&mut self) {
801        if self.parts.len() == 2
802            && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
803            && self
804                .parts
805                .get(1)
806                .is_some_and(|filepart| match filepart.typ {
807                    Viewtype::Image
808                    | Viewtype::Gif
809                    | Viewtype::Sticker
810                    | Viewtype::Audio
811                    | Viewtype::Voice
812                    | Viewtype::Video
813                    | Viewtype::Vcard
814                    | Viewtype::File
815                    | Viewtype::Webxdc => true,
816                    Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
817                })
818        {
819            let mut parts = std::mem::take(&mut self.parts);
820            let Some(mut filepart) = parts.pop() else {
821                // Should never happen.
822                return;
823            };
824            let Some(textpart) = parts.pop() else {
825                // Should never happen.
826                return;
827            };
828
829            filepart.msg.clone_from(&textpart.msg);
830            if let Some(quote) = textpart.param.get(Param::Quote) {
831                filepart.param.set(Param::Quote, quote);
832            }
833
834            self.parts = vec![filepart];
835        }
836    }
837
838    /// Processes chat messages with attachments.
839    fn parse_attachments(&mut self) {
840        // Attachment messages should be squashed into a single part
841        // before calling this function.
842        if self.parts.len() != 1 {
843            return;
844        }
845
846        if let Some(mut part) = self.parts.pop() {
847            if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
848            {
849                part.typ = Viewtype::Voice;
850            }
851            if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
852                && let Some(value) = self.get_header(HeaderDef::ChatContent)
853                && value == "sticker"
854            {
855                part.typ = Viewtype::Sticker;
856            }
857            if (part.typ == Viewtype::Audio
858                || part.typ == Viewtype::Voice
859                || part.typ == Viewtype::Video)
860                && let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
861            {
862                let duration_ms = field_0.parse().unwrap_or_default();
863                if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
864                    part.param.set_int(Param::Duration, duration_ms);
865                }
866            }
867
868            self.parts.push(part);
869        }
870    }
871
872    async fn parse_headers(&mut self, context: &Context) -> Result<()> {
873        self.parse_system_message_headers();
874        self.parse_avatar_headers(context)?;
875        self.parse_videochat_headers();
876        if self.delivery_report.is_none() {
877            self.squash_attachment_parts();
878        }
879
880        if !context.get_config_bool(Config::Bot).await?
881            && let Some(ref subject) = self.get_subject()
882        {
883            let mut prepend_subject = true;
884            if self.decryption_error.is_none() {
885                let colon = subject.find(':');
886                if colon == Some(2)
887                    || colon == Some(3)
888                    || self.has_chat_version()
889                    || subject.contains("Chat:")
890                {
891                    prepend_subject = false
892                }
893            }
894
895            // For mailing lists, always add the subject because sometimes there are different topics
896            // and otherwise it might be hard to keep track:
897            if self.is_mailinglist_message() && !self.has_chat_version() {
898                prepend_subject = true;
899            }
900
901            if prepend_subject && !subject.is_empty() {
902                let part_with_text = self
903                    .parts
904                    .iter_mut()
905                    .find(|part| !part.msg.is_empty() && !part.is_reaction);
906                if let Some(part) = part_with_text {
907                    // Message bubbles are small, so we use en dash to save space. In some
908                    // languages there may be em dashes in the message text added by the author,
909                    // they may look stronger than Subject separation, this is a known thing.
910                    // Anyway, classic email support isn't a priority as of 2025.
911                    part.msg = format!("{} – {}", subject, part.msg);
912                }
913            }
914        }
915
916        if self.is_forwarded {
917            for part in &mut self.parts {
918                part.param.set_int(Param::Forwarded, 1);
919            }
920        }
921
922        self.parse_attachments();
923
924        // See if an MDN is requested from the other side
925        let mut wants_mdn = false;
926        if self.decryption_error.is_none()
927            && (!self.parts.is_empty() || matches!(&self.pre_message, PreMessageMode::Pre { .. }))
928            && let Some(ref dn_to) = self.chat_disposition_notification_to
929        {
930            // Check that the message is not outgoing.
931            let from = &self.from.addr;
932            if !context.is_self_addr(from).await? {
933                if from.to_lowercase() == dn_to.addr.to_lowercase() {
934                    wants_mdn = true;
935                    if let Some(part) = self.parts.last_mut() {
936                        part.param.set_int(Param::WantsMdn, 1);
937                    }
938                } else {
939                    warn!(
940                        context,
941                        "{} requested a read receipt to {}, ignoring", from, dn_to.addr
942                    );
943                }
944            }
945        }
946
947        // If there were no parts, especially a non-DC mail user may
948        // just have send a message in the subject with an empty body.
949        // Besides, we want to show something in case our incoming-processing
950        // failed to properly handle an incoming message.
951        if self.parts.is_empty() && self.mdn_reports.is_empty() {
952            let mut part = Part {
953                typ: Viewtype::Text,
954                ..Default::default()
955            };
956            if wants_mdn {
957                part.param.set_int(Param::WantsMdn, 1);
958            }
959            if let Some(ref subject) = self.get_subject()
960                && !self.has_chat_version()
961                && self.webxdc_status_update.is_none()
962            {
963                part.msg = subject.to_string();
964            }
965
966            self.do_add_single_part(part);
967        }
968
969        if self.is_bot == Some(true) {
970            for part in &mut self.parts {
971                part.param.set(Param::Bot, "1");
972            }
973        }
974
975        Ok(())
976    }
977
978    #[expect(clippy::arithmetic_side_effects)]
979    fn avatar_action_from_header(
980        &mut self,
981        context: &Context,
982        header_value: String,
983    ) -> Result<Option<AvatarAction>> {
984        let res = if header_value == "0" {
985            Some(AvatarAction::Delete)
986        } else if let Some(base64) = header_value
987            .split_ascii_whitespace()
988            .collect::<String>()
989            .strip_prefix("base64:")
990        {
991            match BlobObject::store_from_base64(context, base64)? {
992                Some(path) => Some(AvatarAction::Change(path)),
993                None => {
994                    warn!(context, "Could not decode avatar base64");
995                    None
996                }
997            }
998        } else {
999            // Avatar sent in attachment, as previous versions of Delta Chat did.
1000
1001            let mut i = 0;
1002            while let Some(part) = self.parts.get_mut(i) {
1003                if let Some(part_filename) = &part.org_filename
1004                    && part_filename == &header_value
1005                {
1006                    if let Some(blob) = part.param.get(Param::File) {
1007                        let res = Some(AvatarAction::Change(blob.to_string()));
1008                        self.parts.remove(i);
1009                        return Ok(res);
1010                    }
1011                    break;
1012                }
1013                i += 1;
1014            }
1015            None
1016        };
1017        Ok(res)
1018    }
1019
1020    /// Returns true if the message was encrypted as defined in
1021    /// Autocrypt standard.
1022    ///
1023    /// This means the message was both encrypted and signed with a
1024    /// valid signature.
1025    pub fn was_encrypted(&self) -> bool {
1026        self.signature.is_some()
1027    }
1028
1029    /// Returns whether the email contains a `chat-version` header.
1030    /// This indicates that the email is a DC-email.
1031    pub(crate) fn has_chat_version(&self) -> bool {
1032        self.headers.contains_key("chat-version")
1033    }
1034
1035    pub(crate) fn get_subject(&self) -> Option<String> {
1036        self.get_header(HeaderDef::Subject)
1037            .map(|s| s.trim_start())
1038            .filter(|s| !s.is_empty())
1039            .map(|s| s.to_string())
1040    }
1041
1042    pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
1043        self.headers
1044            .get(headerdef.get_headername())
1045            .map(|s| s.as_str())
1046    }
1047
1048    #[cfg(test)]
1049    /// Returns whether the header exists in any part of the parsed message.
1050    ///
1051    /// Use this to check for header absense. Header presense should be checked using
1052    /// `get_header(...).is_some()` as it also checks that the header isn't ignored.
1053    pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1054        let hname = headerdef.get_headername();
1055        self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1056    }
1057
1058    #[cfg(test)]
1059    /// Returns whether the decrypted data contains the given `&str`.
1060    pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
1061        assert!(self.decryption_error.is_none());
1062        let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
1063        decoded_str.contains(s)
1064    }
1065
1066    /// Returns `Chat-Group-ID` header value if it is a valid group ID.
1067    pub fn get_chat_group_id(&self) -> Option<&str> {
1068        self.get_header(HeaderDef::ChatGroupId)
1069            .filter(|s| validate_id(s))
1070    }
1071
1072    async fn parse_mime_recursive<'a>(
1073        &'a mut self,
1074        context: &'a Context,
1075        mail: &'a mailparse::ParsedMail<'a>,
1076        is_related: bool,
1077    ) -> Result<bool> {
1078        enum MimeS {
1079            Multiple,
1080            Single,
1081            Message,
1082        }
1083
1084        let mimetype = mail.ctype.mimetype.to_lowercase();
1085
1086        let m = if mimetype.starts_with("multipart") {
1087            if mail.ctype.params.contains_key("boundary") {
1088                MimeS::Multiple
1089            } else {
1090                MimeS::Single
1091            }
1092        } else if mimetype.starts_with("message") {
1093            if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1094                MimeS::Message
1095            } else {
1096                MimeS::Single
1097            }
1098        } else {
1099            MimeS::Single
1100        };
1101
1102        let is_related = is_related || mimetype == "multipart/related";
1103        match m {
1104            MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1105            MimeS::Message => {
1106                let raw = mail.get_body_raw()?;
1107                if raw.is_empty() {
1108                    return Ok(false);
1109                }
1110                let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1111
1112                Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1113            }
1114            MimeS::Single => {
1115                self.add_single_part_if_known(context, mail, is_related)
1116                    .await
1117            }
1118        }
1119    }
1120
1121    async fn handle_multiple(
1122        &mut self,
1123        context: &Context,
1124        mail: &mailparse::ParsedMail<'_>,
1125        is_related: bool,
1126    ) -> Result<bool> {
1127        let mut any_part_added = false;
1128        let mimetype = get_mime_type(
1129            mail,
1130            &get_attachment_filename(context, mail)?,
1131            self.has_chat_version(),
1132        )?
1133        .0;
1134        match (mimetype.type_(), mimetype.subtype().as_str()) {
1135            (mime::MULTIPART, "alternative") => {
1136                // multipart/alternative is described in
1137                // <https://datatracker.ietf.org/doc/html/rfc2046#section-5.1.4>.
1138                // Specification says that last part should be preferred,
1139                // so we iterate over parts in reverse order.
1140
1141                // Search for plain text or multipart part.
1142                //
1143                // If we find a multipart inside multipart/alternative
1144                // and it has usable subparts, we only parse multipart.
1145                // This happens e.g. in Apple Mail:
1146                // "plaintext" as an alternative to "html+PDF attachment".
1147                for cur_data in mail.subparts.iter().rev() {
1148                    let (mime_type, _viewtype) = get_mime_type(
1149                        cur_data,
1150                        &get_attachment_filename(context, cur_data)?,
1151                        self.has_chat_version(),
1152                    )?;
1153
1154                    if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
1155                        any_part_added = self
1156                            .parse_mime_recursive(context, cur_data, is_related)
1157                            .await?;
1158                        break;
1159                    }
1160                }
1161
1162                // Explicitly look for a `text/calendar` part.
1163                // Messages conforming to <https://datatracker.ietf.org/doc/html/rfc6047>
1164                // contain `text/calendar` part as an alternative
1165                // to the text or HTML representation.
1166                //
1167                // While we cannot display `text/calendar` and therefore do not prefer it,
1168                // we still make it available by presenting as an attachment
1169                // with a generic filename.
1170                for cur_data in mail.subparts.iter().rev() {
1171                    let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
1172                    if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
1173                        let filename = get_attachment_filename(context, cur_data)?
1174                            .unwrap_or_else(|| "calendar.ics".to_string());
1175                        self.do_add_single_file_part(
1176                            context,
1177                            Viewtype::File,
1178                            mimetype,
1179                            &mail.ctype.mimetype.to_lowercase(),
1180                            &mail.get_body_raw()?,
1181                            &filename,
1182                            is_related,
1183                        )
1184                        .await?;
1185                    }
1186                }
1187
1188                if !any_part_added {
1189                    for cur_part in mail.subparts.iter().rev() {
1190                        if self
1191                            .parse_mime_recursive(context, cur_part, is_related)
1192                            .await?
1193                        {
1194                            any_part_added = true;
1195                            break;
1196                        }
1197                    }
1198                }
1199                if any_part_added && mail.subparts.len() > 1 {
1200                    // there are other alternative parts, likely HTML,
1201                    // so we might have missed some content on simplifying.
1202                    // set mime-modified to force the ui to display a show-message button.
1203                    self.is_mime_modified = true;
1204                }
1205            }
1206            (mime::MULTIPART, "signed") => {
1207                /* RFC 1847: "The multipart/signed content type
1208                contains exactly two body parts.  The first body
1209                part is the body part over which the digital signature was created [...]
1210                The second body part contains the control information necessary to
1211                verify the digital signature." We simply take the first body part and
1212                skip the rest.  (see
1213                <https://k9mail.app/2016/11/24/OpenPGP-Considerations-Part-I.html>
1214                for background information why we use encrypted+signed) */
1215                if let Some(first) = mail.subparts.first() {
1216                    any_part_added = self
1217                        .parse_mime_recursive(context, first, is_related)
1218                        .await?;
1219                }
1220            }
1221            (mime::MULTIPART, "report") => {
1222                /* RFC 6522: the first part is for humans, the second for machines */
1223                if mail.subparts.len() >= 2 {
1224                    match mail.ctype.params.get("report-type").map(|s| s as &str) {
1225                        Some("disposition-notification") => {
1226                            if let Some(report) = self.process_report(context, mail)? {
1227                                self.mdn_reports.push(report);
1228                            }
1229
1230                            // Add MDN part so we can track it, avoid
1231                            // downloading the message again and
1232                            // delete if automatic message deletion is
1233                            // enabled.
1234                            let part = Part {
1235                                typ: Viewtype::Unknown,
1236                                ..Default::default()
1237                            };
1238                            self.parts.push(part);
1239
1240                            any_part_added = true;
1241                        }
1242                        // Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status
1243                        Some("delivery-status") | None => {
1244                            if let Some(report) = self.process_delivery_status(context, mail)? {
1245                                self.delivery_report = Some(report);
1246                            }
1247
1248                            // Add all parts (we need another part, preferably text/plain, to show as an error message)
1249                            for cur_data in &mail.subparts {
1250                                if self
1251                                    .parse_mime_recursive(context, cur_data, is_related)
1252                                    .await?
1253                                {
1254                                    any_part_added = true;
1255                                }
1256                            }
1257                        }
1258                        Some("multi-device-sync") => {
1259                            if let Some(second) = mail.subparts.get(1) {
1260                                self.add_single_part_if_known(context, second, is_related)
1261                                    .await?;
1262                            }
1263                        }
1264                        Some("status-update") => {
1265                            if let Some(second) = mail.subparts.get(1) {
1266                                self.add_single_part_if_known(context, second, is_related)
1267                                    .await?;
1268                            }
1269                        }
1270                        Some(_) => {
1271                            for cur_data in &mail.subparts {
1272                                if self
1273                                    .parse_mime_recursive(context, cur_data, is_related)
1274                                    .await?
1275                                {
1276                                    any_part_added = true;
1277                                }
1278                            }
1279                        }
1280                    }
1281                }
1282            }
1283            _ => {
1284                // Add all parts (in fact, AddSinglePartIfKnown() later check if
1285                // the parts are really supported)
1286                for cur_data in &mail.subparts {
1287                    if self
1288                        .parse_mime_recursive(context, cur_data, is_related)
1289                        .await?
1290                    {
1291                        any_part_added = true;
1292                    }
1293                }
1294            }
1295        }
1296
1297        Ok(any_part_added)
1298    }
1299
1300    /// Returns true if any part was added, false otherwise.
1301    async fn add_single_part_if_known(
1302        &mut self,
1303        context: &Context,
1304        mail: &mailparse::ParsedMail<'_>,
1305        is_related: bool,
1306    ) -> Result<bool> {
1307        // return true if a part was added
1308        let filename = get_attachment_filename(context, mail)?;
1309        let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1310        let raw_mime = mail.ctype.mimetype.to_lowercase();
1311
1312        let old_part_count = self.parts.len();
1313
1314        match filename {
1315            Some(filename) => {
1316                self.do_add_single_file_part(
1317                    context,
1318                    msg_type,
1319                    mime_type,
1320                    &raw_mime,
1321                    &mail.get_body_raw()?,
1322                    &filename,
1323                    is_related,
1324                )
1325                .await?;
1326            }
1327            None => {
1328                match mime_type.type_() {
1329                    mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1330                        warn!(context, "Missing attachment");
1331                        return Ok(false);
1332                    }
1333                    mime::TEXT
1334                        if mail.get_content_disposition().disposition
1335                            == DispositionType::Extension("reaction".to_string()) =>
1336                    {
1337                        // Reaction.
1338                        let decoded_data = match mail.get_body() {
1339                            Ok(decoded_data) => decoded_data,
1340                            Err(err) => {
1341                                warn!(context, "Invalid body parsed {:#}", err);
1342                                // Note that it's not always an error - might be no data
1343                                return Ok(false);
1344                            }
1345                        };
1346
1347                        let part = Part {
1348                            typ: Viewtype::Text,
1349                            mimetype: Some(mime_type),
1350                            msg: decoded_data,
1351                            is_reaction: true,
1352                            ..Default::default()
1353                        };
1354                        self.do_add_single_part(part);
1355                        return Ok(true);
1356                    }
1357                    mime::TEXT | mime::HTML => {
1358                        let decoded_data = match mail.get_body() {
1359                            Ok(decoded_data) => decoded_data,
1360                            Err(err) => {
1361                                warn!(context, "Invalid body parsed {:#}", err);
1362                                // Note that it's not always an error - might be no data
1363                                return Ok(false);
1364                            }
1365                        };
1366
1367                        let is_plaintext = mime_type == mime::TEXT_PLAIN;
1368                        let mut dehtml_failed = false;
1369
1370                        let SimplifiedText {
1371                            text: simplified_txt,
1372                            is_forwarded,
1373                            is_cut,
1374                            top_quote,
1375                            footer,
1376                        } = if decoded_data.is_empty() {
1377                            Default::default()
1378                        } else {
1379                            let is_html = mime_type == mime::TEXT_HTML;
1380                            if is_html {
1381                                self.is_mime_modified = true;
1382                                // NB: This unconditionally removes Legacy Display Elements (see
1383                                // <https://www.rfc-editor.org/rfc/rfc9788.html#section-4.5.3.3>). We
1384                                // don't check for the "hp-legacy-display" Content-Type parameter
1385                                // for simplicity.
1386                                if let Some(text) = dehtml(&decoded_data) {
1387                                    text
1388                                } else {
1389                                    dehtml_failed = true;
1390                                    SimplifiedText {
1391                                        text: decoded_data.clone(),
1392                                        ..Default::default()
1393                                    }
1394                                }
1395                            } else {
1396                                simplify(decoded_data.clone(), self.has_chat_version())
1397                            }
1398                        };
1399
1400                        self.is_mime_modified = self.is_mime_modified
1401                            || ((is_forwarded || is_cut || top_quote.is_some())
1402                                && !self.has_chat_version());
1403
1404                        let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1405                        {
1406                            format.as_str().eq_ignore_ascii_case("flowed")
1407                        } else {
1408                            false
1409                        };
1410
1411                        let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1412                            && mime_type.subtype() == mime::PLAIN
1413                        {
1414                            // Don't check that we're inside an encrypted or signed part for
1415                            // simplicity.
1416                            let simplified_txt = match mail
1417                                .ctype
1418                                .params
1419                                .get("hp-legacy-display")
1420                                .is_some_and(|v| v == "1")
1421                            {
1422                                false => simplified_txt,
1423                                true => rm_legacy_display_elements(&simplified_txt),
1424                            };
1425                            if is_format_flowed {
1426                                let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1427                                    delsp.as_str().eq_ignore_ascii_case("yes")
1428                                } else {
1429                                    false
1430                                };
1431                                let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1432                                let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1433                                (unflowed_text, unflowed_quote)
1434                            } else {
1435                                (simplified_txt, top_quote)
1436                            }
1437                        } else {
1438                            (simplified_txt, top_quote)
1439                        };
1440
1441                        let (simplified_txt, was_truncated) =
1442                            truncate_msg_text(context, simplified_txt).await?;
1443                        if was_truncated {
1444                            self.is_mime_modified = was_truncated;
1445                        }
1446
1447                        if !simplified_txt.is_empty() || simplified_quote.is_some() {
1448                            let mut part = Part {
1449                                dehtml_failed,
1450                                typ: Viewtype::Text,
1451                                mimetype: Some(mime_type),
1452                                msg: simplified_txt,
1453                                ..Default::default()
1454                            };
1455                            if let Some(quote) = simplified_quote {
1456                                part.param.set(Param::Quote, quote);
1457                            }
1458                            part.msg_raw = Some(decoded_data);
1459                            self.do_add_single_part(part);
1460                        }
1461
1462                        if is_forwarded {
1463                            self.is_forwarded = true;
1464                        }
1465
1466                        if self.footer.is_none() && is_plaintext {
1467                            self.footer = Some(footer.unwrap_or_default());
1468                        }
1469                    }
1470                    _ => {}
1471                }
1472            }
1473        }
1474
1475        // add object? (we do not add all objects, eg. signatures etc. are ignored)
1476        Ok(self.parts.len() > old_part_count)
1477    }
1478
1479    #[expect(clippy::too_many_arguments)]
1480    #[expect(clippy::arithmetic_side_effects)]
1481    async fn do_add_single_file_part(
1482        &mut self,
1483        context: &Context,
1484        msg_type: Viewtype,
1485        mime_type: Mime,
1486        raw_mime: &str,
1487        decoded_data: &[u8],
1488        filename: &str,
1489        is_related: bool,
1490    ) -> Result<()> {
1491        // Process attached PGP keys.
1492        if mime_type.type_() == mime::APPLICATION
1493            && mime_type.subtype().as_str() == "pgp-keys"
1494            && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1495        {
1496            return Ok(());
1497        }
1498        let mut part = Part::default();
1499        let msg_type = if context
1500            .is_webxdc_file(filename, decoded_data)
1501            .await
1502            .unwrap_or(false)
1503        {
1504            Viewtype::Webxdc
1505        } else if filename.ends_with(".kml") {
1506            // XXX what if somebody sends eg an "location-highlights.kml"
1507            // attachment unrelated to location streaming?
1508            if filename.starts_with("location") || filename.starts_with("message") {
1509                let parsed = location::Kml::parse(decoded_data)
1510                    .map_err(|err| {
1511                        warn!(context, "failed to parse kml part: {:#}", err);
1512                    })
1513                    .ok();
1514                if filename.starts_with("location") {
1515                    self.location_kml = parsed;
1516                } else {
1517                    self.message_kml = parsed;
1518                }
1519                return Ok(());
1520            }
1521            msg_type
1522        } else if filename == "multi-device-sync.json" {
1523            if !context.get_config_bool(Config::SyncMsgs).await? {
1524                return Ok(());
1525            }
1526            let serialized = String::from_utf8_lossy(decoded_data)
1527                .parse()
1528                .unwrap_or_default();
1529            self.sync_items = context
1530                .parse_sync_items(serialized)
1531                .map_err(|err| {
1532                    warn!(context, "failed to parse sync data: {:#}", err);
1533                })
1534                .ok();
1535            return Ok(());
1536        } else if filename == "status-update.json" {
1537            let serialized = String::from_utf8_lossy(decoded_data)
1538                .parse()
1539                .unwrap_or_default();
1540            self.webxdc_status_update = Some(serialized);
1541            return Ok(());
1542        } else if msg_type == Viewtype::Vcard {
1543            if let Some(summary) = get_vcard_summary(decoded_data) {
1544                part.param.set(Param::Summary1, summary);
1545                msg_type
1546            } else {
1547                Viewtype::File
1548            }
1549        } else if msg_type == Viewtype::Image
1550            || msg_type == Viewtype::Gif
1551            || msg_type == Viewtype::Sticker
1552        {
1553            match get_filemeta(decoded_data) {
1554                // image size is known, not too big, keep msg_type:
1555                Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1556                    part.param.set_i64(Param::Width, width.into());
1557                    part.param.set_i64(Param::Height, height.into());
1558                    msg_type
1559                }
1560                // image is too big or size is unknown, display as file:
1561                _ => Viewtype::File,
1562            }
1563        } else {
1564            msg_type
1565        };
1566
1567        /* we have a regular file attachment,
1568        write decoded data to new blob object */
1569
1570        let blob =
1571            match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1572                Ok(blob) => blob,
1573                Err(err) => {
1574                    error!(
1575                        context,
1576                        "Could not add blob for mime part {}, error {:#}", filename, err
1577                    );
1578                    return Ok(());
1579                }
1580            };
1581        info!(context, "added blobfile: {:?}", blob.as_name());
1582
1583        part.typ = msg_type;
1584        part.org_filename = Some(filename.to_string());
1585        part.mimetype = Some(mime_type);
1586        part.bytes = decoded_data.len();
1587        part.param.set(Param::File, blob.as_name());
1588        part.param.set(Param::Filename, filename);
1589        part.param.set(Param::MimeType, raw_mime);
1590        part.is_related = is_related;
1591
1592        self.do_add_single_part(part);
1593        Ok(())
1594    }
1595
1596    /// Returns whether a key from the attachment was saved.
1597    async fn try_set_peer_key_from_file_part(
1598        context: &Context,
1599        decoded_data: &[u8],
1600    ) -> Result<bool> {
1601        let key = match str::from_utf8(decoded_data) {
1602            Err(err) => {
1603                warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1604                return Ok(false);
1605            }
1606            Ok(key) => key,
1607        };
1608        let key = match SignedPublicKey::from_asc(key) {
1609            Err(err) => {
1610                warn!(
1611                    context,
1612                    "PGP key attachment is not an ASCII-armored file: {err:#}."
1613                );
1614                return Ok(false);
1615            }
1616            Ok(key) => key,
1617        };
1618        if let Err(err) = import_public_key(context, &key).await {
1619            warn!(context, "Attached PGP key import failed: {err:#}.");
1620            return Ok(false);
1621        }
1622
1623        info!(context, "Imported PGP key from attachment.");
1624        Ok(true)
1625    }
1626
1627    pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
1628        if self.was_encrypted() {
1629            part.param.set_int(Param::GuaranteeE2ee, 1);
1630        }
1631        self.parts.push(part);
1632    }
1633
1634    pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1635        if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1636            // The message belongs to a mailing list and has a `ListId:`-header
1637            // that should be used to get a unique id.
1638            return Some(list_id);
1639        } else if let Some(chat_list_id) = self.get_header(HeaderDef::ChatListId) {
1640            return Some(chat_list_id);
1641        } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1642            // the `Sender:`-header alone is no indicator for mailing list
1643            // as also used for bot-impersonation via `set_override_sender_name()`
1644            if let Some(precedence) = self.get_header(HeaderDef::Precedence)
1645                && (precedence == "list" || precedence == "bulk")
1646            {
1647                // The message belongs to a mailing list, but there is no `ListId:`-header;
1648                // `Sender:`-header is be used to get a unique id.
1649                // This method is used by implementations as Majordomo.
1650                return Some(sender);
1651            }
1652        }
1653        None
1654    }
1655
1656    pub(crate) fn is_mailinglist_message(&self) -> bool {
1657        self.get_mailinglist_header().is_some()
1658    }
1659
1660    /// Detects Schleuder mailing list by List-Help header.
1661    pub(crate) fn is_schleuder_message(&self) -> bool {
1662        if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1663            list_help == "<https://schleuder.org/>"
1664        } else {
1665            false
1666        }
1667    }
1668
1669    /// Check if a message is a call.
1670    pub(crate) fn is_call(&self) -> bool {
1671        self.parts
1672            .first()
1673            .is_some_and(|part| part.typ == Viewtype::Call)
1674    }
1675
1676    pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1677        self.get_header(HeaderDef::MessageId)
1678            .and_then(|msgid| parse_message_id(msgid).ok())
1679    }
1680
1681    /// Remove headers that are not allowed in unsigned / unencrypted messages.
1682    ///
1683    /// Pass `encrypted=true` parameter for an encrypted, but unsigned message.
1684    /// Pass `encrypted=false` parameter for an unencrypted message.
1685    /// Don't call this function if the message was encrypted and signed.
1686    fn remove_secured_headers(
1687        headers: &mut HashMap<String, String>,
1688        removed: &mut HashSet<String>,
1689        encrypted: bool,
1690    ) {
1691        remove_header(headers, "secure-join-fingerprint", removed);
1692        remove_header(headers, "chat-verified", removed);
1693        remove_header(headers, "autocrypt-gossip", removed);
1694
1695        if headers.get("secure-join") == Some(&"vc-request-pubkey".to_string()) && encrypted {
1696            // vc-request-pubkey message is encrypted, but unsigned,
1697            // and contains a Secure-Join-Auth header.
1698            //
1699            // It is unsigned in order not to leak Bob's identity to a server operator
1700            // that scraped the AUTH token somewhere from the web,
1701            // and because Alice anyways couldn't verify his signature at this step,
1702            // because she doesn't know his public key yet.
1703        } else {
1704            remove_header(headers, "secure-join-auth", removed);
1705
1706            // Secure-Join is secured unless it is an initial "vc-request"/"vg-request".
1707            if let Some(secure_join) = remove_header(headers, "secure-join", removed)
1708                && (secure_join == "vc-request" || secure_join == "vg-request")
1709            {
1710                headers.insert("secure-join".to_string(), secure_join);
1711            }
1712        }
1713    }
1714
1715    /// Merges headers from the email `part` into `headers` respecting header protection.
1716    /// Should only be called with nonempty `headers` if `part` is a root of the Cryptographic
1717    /// Payload as defined in <https://www.rfc-editor.org/rfc/rfc9788.html> "Header Protection for
1718    /// Cryptographically Protected Email", otherwise this may unnecessarily discard headers from
1719    /// outer parts.
1720    #[allow(clippy::too_many_arguments)]
1721    fn merge_headers(
1722        context: &Context,
1723        headers: &mut HashMap<String, String>,
1724        headers_removed: &mut HashSet<String>,
1725        recipients: &mut Vec<SingleInfo>,
1726        past_members: &mut Vec<SingleInfo>,
1727        from: &mut Option<SingleInfo>,
1728        list_post: &mut Option<String>,
1729        chat_disposition_notification_to: &mut Option<SingleInfo>,
1730        part: &mailparse::ParsedMail,
1731    ) {
1732        let fields = &part.headers;
1733        // See <https://www.rfc-editor.org/rfc/rfc9788.html>.
1734        let has_header_protection = part.ctype.params.contains_key("hp");
1735
1736        headers_removed.extend(
1737            headers
1738                .extract_if(|k, _v| has_header_protection || is_protected(k))
1739                .map(|(k, _v)| k.to_string()),
1740        );
1741        for field in fields {
1742            // lowercasing all headers is technically not correct, but makes things work better
1743            let key = field.get_key().to_lowercase();
1744            if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1745                match addrparse_header(field) {
1746                    Ok(addrlist) => {
1747                        *chat_disposition_notification_to = addrlist.extract_single_info();
1748                    }
1749                    Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1750                }
1751            } else {
1752                let value = field.get_value();
1753                headers.insert(key.to_string(), value);
1754            }
1755        }
1756        let recipients_new = get_recipients(fields);
1757        if !recipients_new.is_empty() {
1758            *recipients = recipients_new;
1759        }
1760        let past_members_addresses =
1761            get_all_addresses_from_header(fields, "chat-group-past-members");
1762        if !past_members_addresses.is_empty() {
1763            *past_members = past_members_addresses;
1764        }
1765        let from_new = get_from(fields);
1766        if from_new.is_some() {
1767            *from = from_new;
1768        }
1769        let list_post_new = get_list_post(fields);
1770        if list_post_new.is_some() {
1771            *list_post = list_post_new;
1772        }
1773    }
1774
1775    fn process_report(
1776        &self,
1777        context: &Context,
1778        report: &mailparse::ParsedMail<'_>,
1779    ) -> Result<Option<Report>> {
1780        // parse as mailheaders
1781        let report_body = if let Some(subpart) = report.subparts.get(1) {
1782            subpart.get_body_raw()?
1783        } else {
1784            bail!("Report does not have second MIME part");
1785        };
1786        let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1787
1788        // must be present
1789        if report_fields
1790            .get_header_value(HeaderDef::Disposition)
1791            .is_none()
1792        {
1793            warn!(
1794                context,
1795                "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1796                report_fields.get_header_value(HeaderDef::MessageId)
1797            );
1798            return Ok(None);
1799        };
1800
1801        let original_message_id = report_fields
1802            .get_header_value(HeaderDef::OriginalMessageId)
1803            // MS Exchange doesn't add an Original-Message-Id header. Instead, they put
1804            // the original message id into the In-Reply-To header:
1805            .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1806            .and_then(|v| parse_message_id(&v).ok());
1807        let additional_message_ids = report_fields
1808            .get_header_value(HeaderDef::AdditionalMessageIds)
1809            .map_or_else(Vec::new, |v| {
1810                v.split(' ')
1811                    .filter_map(|s| parse_message_id(s).ok())
1812                    .collect()
1813            });
1814
1815        Ok(Some(Report {
1816            original_message_id,
1817            additional_message_ids,
1818        }))
1819    }
1820
1821    fn process_delivery_status(
1822        &self,
1823        context: &Context,
1824        report: &mailparse::ParsedMail<'_>,
1825    ) -> Result<Option<DeliveryReport>> {
1826        // Assume failure.
1827        let mut failure = true;
1828
1829        if let Some(status_part) = report.subparts.get(1) {
1830            // RFC 3464 defines `message/delivery-status`
1831            // RFC 6533 defines `message/global-delivery-status`
1832            if status_part.ctype.mimetype != "message/delivery-status"
1833                && status_part.ctype.mimetype != "message/global-delivery-status"
1834            {
1835                warn!(
1836                    context,
1837                    "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1838                );
1839                return Ok(None);
1840            }
1841
1842            let status_body = status_part.get_body_raw()?;
1843
1844            // Skip per-message fields.
1845            let (_, sz) = mailparse::parse_headers(&status_body)?;
1846
1847            // Parse first set of per-recipient fields
1848            if let Some(status_body) = status_body.get(sz..) {
1849                let (status_fields, _) = mailparse::parse_headers(status_body)?;
1850                if let Some(action) = status_fields.get_first_value("action") {
1851                    if action != "failed" {
1852                        info!(context, "DSN with {:?} action", action);
1853                        failure = false;
1854                    }
1855                } else {
1856                    warn!(context, "DSN without action");
1857                }
1858            } else {
1859                warn!(context, "DSN without per-recipient fields");
1860            }
1861        } else {
1862            // No message/delivery-status part.
1863            return Ok(None);
1864        }
1865
1866        // parse as mailheaders
1867        if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1868            p.ctype.mimetype.contains("rfc822")
1869                || p.ctype.mimetype == "message/global"
1870                || p.ctype.mimetype == "message/global-headers"
1871        }) {
1872            let report_body = original_msg.get_body_raw()?;
1873            let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1874
1875            if let Some(original_message_id) = report_fields
1876                .get_header_value(HeaderDef::MessageId)
1877                .and_then(|v| parse_message_id(&v).ok())
1878            {
1879                return Ok(Some(DeliveryReport {
1880                    rfc724_mid: original_message_id,
1881                    failure,
1882                }));
1883            }
1884
1885            warn!(
1886                context,
1887                "ignoring unknown ndn-notification, Message-Id: {:?}",
1888                report_fields.get_header_value(HeaderDef::MessageId)
1889            );
1890        }
1891
1892        Ok(None)
1893    }
1894
1895    fn maybe_remove_bad_parts(&mut self) {
1896        let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1897        if good_parts == 0 {
1898            // We have no good part but show at least one bad part in order to show anything at all
1899            self.parts.truncate(1);
1900        } else if good_parts < self.parts.len() {
1901            self.parts.retain(|p| !p.dehtml_failed);
1902        }
1903
1904        // remove images that are descendants of multipart/related but the first one:
1905        // - for newsletters or so, that is often the logo
1906        // - for user-generated html-mails, that may be some drag'n'drop photo,
1907        //   so, the recipient sees at least the first image directly
1908        // - all other images can be accessed by "show full message"
1909        // - to ensure, there is such a button, we do removal only if
1910        //   `is_mime_modified` is set
1911        if !self.has_chat_version() && self.is_mime_modified {
1912            fn is_related_image(p: &&Part) -> bool {
1913                (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1914            }
1915            let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1916            if related_image_cnt > 1 {
1917                let mut is_first_image = true;
1918                self.parts.retain(|p| {
1919                    let retain = is_first_image || !is_related_image(&p);
1920                    if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1921                        is_first_image = false;
1922                    }
1923                    retain
1924                });
1925            }
1926        }
1927    }
1928
1929    /// Remove unwanted, additional text parts used for mailing list footer.
1930    /// Some mailinglist software add footers as separate mimeparts
1931    /// eg. when the user-edited-content is html.
1932    /// As these footers would appear as repeated, separate text-bubbles,
1933    /// we remove them.
1934    ///
1935    /// We make an exception for Schleuder mailing lists
1936    /// because they typically create messages with two text parts,
1937    /// one for headers and one for the actual contents.
1938    fn maybe_remove_inline_mailinglist_footer(&mut self) {
1939        if self.is_mailinglist_message() && !self.is_schleuder_message() {
1940            let text_part_cnt = self
1941                .parts
1942                .iter()
1943                .filter(|p| p.typ == Viewtype::Text)
1944                .count();
1945            if text_part_cnt == 2
1946                && let Some(last_part) = self.parts.last()
1947                && last_part.typ == Viewtype::Text
1948            {
1949                self.parts.pop();
1950            }
1951        }
1952    }
1953
1954    /// Some providers like GMX and Yahoo do not send standard NDNs (Non Delivery notifications).
1955    /// If you improve heuristics here you might also have to change prefetch_should_download() in imap/mod.rs.
1956    /// Also you should add a test in receive_imf.rs (there already are lots of test_parse_ndn_* tests).
1957    async fn heuristically_parse_ndn(&mut self, context: &Context) {
1958        let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1959            let from = from.to_ascii_lowercase();
1960            from.contains("mailer-daemon") || from.contains("mail-daemon")
1961        } else {
1962            false
1963        };
1964        if maybe_ndn && self.delivery_report.is_none() {
1965            for original_message_id in self
1966                .parts
1967                .iter()
1968                .filter_map(|part| part.msg_raw.as_ref())
1969                .flat_map(|part| part.lines())
1970                .filter_map(|line| line.split_once("Message-ID:"))
1971                .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1972            {
1973                if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1974                {
1975                    self.delivery_report = Some(DeliveryReport {
1976                        rfc724_mid: original_message_id,
1977                        failure: true,
1978                    })
1979                }
1980            }
1981        }
1982    }
1983
1984    /// Handle reports
1985    /// (MDNs = Message Disposition Notification, the message was read
1986    /// and NDNs = Non delivery notification, the message could not be delivered)
1987    pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1988        for report in &self.mdn_reports {
1989            for original_message_id in report
1990                .original_message_id
1991                .iter()
1992                .chain(&report.additional_message_ids)
1993            {
1994                if let Err(err) =
1995                    handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1996                {
1997                    warn!(context, "Could not handle MDN: {err:#}.");
1998                }
1999            }
2000        }
2001
2002        if let Some(delivery_report) = &self.delivery_report
2003            && delivery_report.failure
2004        {
2005            let error = parts
2006                .iter()
2007                .find(|p| p.typ == Viewtype::Text)
2008                .map(|p| p.msg.clone());
2009            if let Err(err) = handle_ndn(context, delivery_report, error).await {
2010                warn!(context, "Could not handle NDN: {err:#}.");
2011            }
2012        }
2013    }
2014
2015    /// Returns timestamp of the parent message.
2016    ///
2017    /// If there is no parent message or it is not found in the
2018    /// database, returns None.
2019    pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
2020        let parent_timestamp = if let Some(field) = self
2021            .get_header(HeaderDef::InReplyTo)
2022            .and_then(|msgid| parse_message_id(msgid).ok())
2023        {
2024            context
2025                .sql
2026                .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
2027                .await?
2028        } else {
2029            None
2030        };
2031        Ok(parent_timestamp)
2032    }
2033
2034    /// Returns parsed `Chat-Group-Member-Timestamps` header contents.
2035    ///
2036    /// Returns `None` if there is no such header.
2037    #[expect(clippy::arithmetic_side_effects)]
2038    pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
2039        let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
2040        self.get_header(HeaderDef::ChatGroupMemberTimestamps)
2041            .map(|h| {
2042                h.split_ascii_whitespace()
2043                    .filter_map(|ts| ts.parse::<i64>().ok())
2044                    .map(|ts| std::cmp::min(now, ts))
2045                    .collect()
2046            })
2047    }
2048
2049    /// Returns list of fingerprints from
2050    /// `Chat-Group-Member-Fpr` header.
2051    pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
2052        if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
2053            header
2054                .split_ascii_whitespace()
2055                .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
2056                .collect()
2057        } else {
2058            Vec::new()
2059        }
2060    }
2061}
2062
2063fn rm_legacy_display_elements(text: &str) -> String {
2064    let mut res = None;
2065    for l in text.lines() {
2066        res = res.map(|r: String| match r.is_empty() {
2067            true => l.to_string(),
2068            false => r + "\r\n" + l,
2069        });
2070        if l.is_empty() {
2071            res = Some(String::new());
2072        }
2073    }
2074    res.unwrap_or_default()
2075}
2076
2077fn remove_header(
2078    headers: &mut HashMap<String, String>,
2079    key: &str,
2080    removed: &mut HashSet<String>,
2081) -> Option<String> {
2082    if let Some((k, v)) = headers.remove_entry(key) {
2083        removed.insert(k);
2084        Some(v)
2085    } else {
2086        None
2087    }
2088}
2089
2090/// Parses `Autocrypt-Gossip` headers from the email,
2091/// saves the keys into the `public_keys` table,
2092/// and returns them in a HashMap<address, public key>.
2093///
2094/// * `from`: The address which sent the message currently being parsed
2095async fn parse_gossip_headers(
2096    context: &Context,
2097    from: &str,
2098    recipients: &[SingleInfo],
2099    gossip_headers: Vec<String>,
2100) -> Result<BTreeMap<String, GossipedKey>> {
2101    // XXX split the parsing from the modification part
2102    let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
2103
2104    for value in &gossip_headers {
2105        let header = match Aheader::from_str(value) {
2106            Ok(header) => header,
2107            Err(err) => {
2108                warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
2109                continue;
2110            }
2111        };
2112
2113        if !recipients
2114            .iter()
2115            .any(|info| addr_cmp(&info.addr, &header.addr))
2116        {
2117            warn!(
2118                context,
2119                "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
2120            );
2121            continue;
2122        }
2123        if addr_cmp(from, &header.addr) {
2124            // Non-standard, might not be necessary to have this check here
2125            warn!(
2126                context,
2127                "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
2128            );
2129            continue;
2130        }
2131
2132        import_public_key(context, &header.public_key)
2133            .await
2134            .context("Failed to import Autocrypt-Gossip key")?;
2135
2136        let gossiped_key = GossipedKey {
2137            public_key: header.public_key,
2138
2139            verified: header.verified,
2140        };
2141        gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2142    }
2143
2144    Ok(gossiped_keys)
2145}
2146
2147/// Message Disposition Notification (RFC 8098)
2148#[derive(Debug)]
2149pub(crate) struct Report {
2150    /// Original-Message-ID header
2151    ///
2152    /// It MUST be present if the original message has a Message-ID according to RFC 8098.
2153    /// In case we can't find it (shouldn't happen), this is None.
2154    pub original_message_id: Option<String>,
2155    /// Additional-Message-IDs
2156    pub additional_message_ids: Vec<String>,
2157}
2158
2159/// Delivery Status Notification (RFC 3464, RFC 6533)
2160#[derive(Debug)]
2161pub(crate) struct DeliveryReport {
2162    pub rfc724_mid: String,
2163    pub failure: bool,
2164}
2165
2166pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2167    // take care with mailparse::msgidparse() that is pretty untolerant eg. wrt missing `<` or `>`
2168    let mut msgids = Vec::new();
2169    for id in ids.split_whitespace() {
2170        let mut id = id.to_string();
2171        if let Some(id_without_prefix) = id.strip_prefix('<') {
2172            id = id_without_prefix.to_string();
2173        };
2174        if let Some(id_without_suffix) = id.strip_suffix('>') {
2175            id = id_without_suffix.to_string();
2176        };
2177        if !id.is_empty() {
2178            msgids.push(id);
2179        }
2180    }
2181    msgids
2182}
2183
2184pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2185    if let Some(id) = parse_message_ids(ids).first() {
2186        Ok(id.to_string())
2187    } else {
2188        bail!("could not parse message_id: {ids}");
2189    }
2190}
2191
2192/// Returns whether the outer header value must be ignored if the message contains a signed (and
2193/// optionally encrypted) part. This is independent from the modern Header Protection defined in
2194/// <https://www.rfc-editor.org/rfc/rfc9788.html>.
2195fn is_protected(key: &str) -> bool {
2196    key.starts_with("chat-")
2197        || matches!(
2198            key,
2199            "return-path"
2200                | "auto-submitted"
2201                | "autocrypt-setup-message"
2202                | "date"
2203                | "from"
2204                | "sender"
2205                | "reply-to"
2206                | "to"
2207                | "cc"
2208                | "bcc"
2209                | "message-id"
2210                | "in-reply-to"
2211                | "references"
2212                | "secure-join"
2213        )
2214}
2215
2216/// Returns if the header is hidden and must be ignored in the IMF section.
2217pub(crate) fn is_hidden(key: &str) -> bool {
2218    matches!(
2219        key,
2220        "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2221    )
2222}
2223
2224/// Parsed MIME part.
2225#[derive(Debug, Default, Clone)]
2226pub struct Part {
2227    /// Type of the MIME part determining how it should be displayed.
2228    pub typ: Viewtype,
2229
2230    /// MIME type.
2231    pub mimetype: Option<Mime>,
2232
2233    /// Message text to be displayed in the chat.
2234    pub msg: String,
2235
2236    /// Message text to be displayed in message info.
2237    pub msg_raw: Option<String>,
2238
2239    /// Size of the MIME part in bytes.
2240    pub bytes: usize,
2241
2242    /// Parameters.
2243    pub param: Params,
2244
2245    /// Attachment filename.
2246    pub(crate) org_filename: Option<String>,
2247
2248    /// An error detected during parsing.
2249    pub error: Option<String>,
2250
2251    /// True if conversion from HTML to plaintext failed.
2252    pub(crate) dehtml_failed: bool,
2253
2254    /// the part is a child or a descendant of multipart/related.
2255    /// typically, these are images that are referenced from text/html part
2256    /// and should not displayed inside chat.
2257    ///
2258    /// note that multipart/related may contain further multipart nestings
2259    /// and all of them needs to be marked with `is_related`.
2260    pub(crate) is_related: bool,
2261
2262    /// Part is an RFC 9078 reaction.
2263    pub(crate) is_reaction: bool,
2264}
2265
2266/// Returns the mimetype and viewtype for a parsed mail.
2267///
2268/// This only looks at the metadata, not at the content;
2269/// the viewtype may later be corrected in `do_add_single_file_part()`.
2270fn get_mime_type(
2271    mail: &mailparse::ParsedMail<'_>,
2272    filename: &Option<String>,
2273    is_chat_msg: bool,
2274) -> Result<(Mime, Viewtype)> {
2275    let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2276
2277    let viewtype = match mimetype.type_() {
2278        mime::TEXT => match mimetype.subtype() {
2279            mime::VCARD => Viewtype::Vcard,
2280            mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2281            _ => Viewtype::File,
2282        },
2283        mime::IMAGE => match mimetype.subtype() {
2284            mime::GIF => Viewtype::Gif,
2285            mime::SVG => Viewtype::File,
2286            _ => Viewtype::Image,
2287        },
2288        mime::AUDIO => Viewtype::Audio,
2289        mime::VIDEO => Viewtype::Video,
2290        mime::MULTIPART => Viewtype::Unknown,
2291        mime::MESSAGE => {
2292            if is_attachment_disposition(mail) {
2293                Viewtype::File
2294            } else {
2295                // Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
2296                // Also used as part "message/disposition-notification" of "multipart/report", which, however, will
2297                // be handled separately.
2298                // I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
2299                // which are unwanted at all).
2300                // For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
2301                // for selected and known subparts.
2302                Viewtype::Unknown
2303            }
2304        }
2305        mime::APPLICATION => match mimetype.subtype() {
2306            mime::OCTET_STREAM => match filename {
2307                Some(filename) if !is_chat_msg => {
2308                    match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2309                        Some((viewtype, _)) => viewtype,
2310                        None => Viewtype::File,
2311                    }
2312                }
2313                _ => Viewtype::File,
2314            },
2315            _ => Viewtype::File,
2316        },
2317        _ => Viewtype::Unknown,
2318    };
2319
2320    Ok((mimetype, viewtype))
2321}
2322
2323fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2324    let ct = mail.get_content_disposition();
2325    ct.disposition == DispositionType::Attachment
2326        && ct
2327            .params
2328            .iter()
2329            .any(|(key, _value)| key.starts_with("filename"))
2330}
2331
2332/// Tries to get attachment filename.
2333///
2334/// If filename is explicitly specified in Content-Disposition, it is
2335/// returned. If Content-Disposition is "attachment" but filename is
2336/// not specified, filename is guessed. If Content-Disposition cannot
2337/// be parsed, returns an error.
2338fn get_attachment_filename(
2339    context: &Context,
2340    mail: &mailparse::ParsedMail,
2341) -> Result<Option<String>> {
2342    let ct = mail.get_content_disposition();
2343
2344    // try to get file name as "encoded-words" from
2345    // `Content-Disposition: ... filename=...`
2346    let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2347
2348    if desired_filename.is_none()
2349        && let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
2350    {
2351        // be graceful and just use the original name.
2352        // some MUA, including Delta Chat up to core1.50,
2353        // use `filename*` mistakenly for simple encoded-words without following rfc2231
2354        warn!(context, "apostrophed encoding invalid: {}", name);
2355        desired_filename = Some(name);
2356    }
2357
2358    // if no filename is set, try `Content-Disposition: ... name=...`
2359    if desired_filename.is_none() {
2360        desired_filename = ct.params.get("name").map(|s| s.to_string());
2361    }
2362
2363    // MS Outlook is known to specify filename in the "name" attribute of
2364    // Content-Type and omit Content-Disposition.
2365    if desired_filename.is_none() {
2366        desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2367    }
2368
2369    // If there is no filename, but part is an attachment, guess filename
2370    if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2371        if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2372            desired_filename = Some(format!("file.{subtype}",));
2373        } else {
2374            bail!(
2375                "could not determine attachment filename: {:?}",
2376                ct.disposition
2377            );
2378        };
2379    }
2380
2381    let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2382
2383    Ok(desired_filename)
2384}
2385
2386/// Returned addresses are normalized and lowercased.
2387pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2388    let to_addresses = get_all_addresses_from_header(headers, "to");
2389    let cc_addresses = get_all_addresses_from_header(headers, "cc");
2390
2391    let mut res = to_addresses;
2392    res.extend(cc_addresses);
2393    res
2394}
2395
2396/// Returned addresses are normalized and lowercased.
2397pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2398    let all = get_all_addresses_from_header(headers, "from");
2399    tools::single_value(all)
2400}
2401
2402/// Returned addresses are normalized and lowercased.
2403pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2404    get_all_addresses_from_header(headers, "list-post")
2405        .into_iter()
2406        .next()
2407        .map(|s| s.addr)
2408}
2409
2410/// Extracts all addresses from the header named `header`.
2411///
2412/// If multiple headers with the same name are present,
2413/// the last one is taken.
2414/// This is because DKIM-Signatures apply to the last
2415/// headers, and more headers may be added
2416/// to the beginning of the messages
2417/// without invalidating the signature
2418/// unless the header is "oversigned",
2419/// i.e. included in the signature more times
2420/// than it appears in the mail.
2421fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2422    let mut result: Vec<SingleInfo> = Default::default();
2423
2424    if let Some(header) = headers
2425        .iter()
2426        .rev()
2427        .find(|h| h.get_key().to_lowercase() == header)
2428        && let Ok(addrs) = mailparse::addrparse_header(header)
2429    {
2430        for addr in addrs.iter() {
2431            match addr {
2432                mailparse::MailAddr::Single(info) => {
2433                    result.push(SingleInfo {
2434                        addr: addr_normalize(&info.addr).to_lowercase(),
2435                        display_name: info.display_name.clone(),
2436                    });
2437                }
2438                mailparse::MailAddr::Group(infos) => {
2439                    for info in &infos.addrs {
2440                        result.push(SingleInfo {
2441                            addr: addr_normalize(&info.addr).to_lowercase(),
2442                            display_name: info.display_name.clone(),
2443                        });
2444                    }
2445                }
2446            }
2447        }
2448    }
2449
2450    result
2451}
2452
2453async fn handle_mdn(
2454    context: &Context,
2455    from_id: ContactId,
2456    rfc724_mid: &str,
2457    timestamp_sent: i64,
2458) -> Result<()> {
2459    if from_id == ContactId::SELF {
2460        // MDNs to self are handled in receive_imf_inner().
2461        return Ok(());
2462    }
2463
2464    let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2465        .sql
2466        .query_row_optional(
2467            // MDN on a pre-message (message preview) references the post-message. So we can't tell
2468            // if the pre-message or fully downloaded message was seen, but this is on purpose. For
2469            // images this problem is going to be solved by showing thumbnails.
2470            "SELECT
2471                m.id AS msg_id,
2472                c.id AS chat_id,
2473                mdns.contact_id AS mdn_contact
2474             FROM msgs m 
2475             LEFT JOIN chats c ON m.chat_id=c.id
2476             LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
2477             WHERE rfc724_mid=? AND from_id=1
2478             ORDER BY msg_id DESC, mdn_contact=? DESC
2479             LIMIT 1",
2480            (&rfc724_mid, from_id),
2481            |row| {
2482                let msg_id: MsgId = row.get("msg_id")?;
2483                let chat_id: ChatId = row.get("chat_id")?;
2484                let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2485                Ok((
2486                    msg_id,
2487                    chat_id,
2488                    mdn_contact.is_some(),
2489                    mdn_contact == Some(from_id),
2490                ))
2491            },
2492        )
2493        .await?
2494    else {
2495        info!(
2496            context,
2497            "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2498        );
2499        return Ok(());
2500    };
2501
2502    if is_dup {
2503        return Ok(());
2504    }
2505    context
2506        .sql
2507        .execute(
2508            "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2509            (msg_id, from_id, timestamp_sent),
2510        )
2511        .await?;
2512    if !has_mdns {
2513        context.emit_event(EventType::MsgRead { chat_id, msg_id });
2514        // note(treefit): only matters if it is the last message in chat (but probably too expensive to check, debounce also solves it)
2515        chatlist_events::emit_chatlist_item_changed(context, chat_id);
2516    }
2517    context.emit_event(EventType::MsgReadCountChanged { chat_id, msg_id });
2518    Ok(())
2519}
2520
2521/// Marks a message as failed after an ndn (non-delivery-notification) arrived.
2522/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
2523async fn handle_ndn(
2524    context: &Context,
2525    failed: &DeliveryReport,
2526    error: Option<String>,
2527) -> Result<()> {
2528    if failed.rfc724_mid.is_empty() {
2529        return Ok(());
2530    }
2531
2532    // The NDN might be for a message-id that had attachments and was sent from a non-Delta Chat client.
2533    // In this case we need to mark multiple "msgids" as failed that all refer to the same message-id.
2534    let msg_ids = context
2535        .sql
2536        .query_map_vec(
2537            "SELECT id FROM msgs
2538                WHERE rfc724_mid=? AND from_id=1",
2539            (&failed.rfc724_mid,),
2540            |row| {
2541                let msg_id: MsgId = row.get(0)?;
2542                Ok(msg_id)
2543            },
2544        )
2545        .await?;
2546
2547    let error = if let Some(error) = error {
2548        error
2549    } else {
2550        "Delivery to at least one recipient failed.".to_string()
2551    };
2552    let err_msg = &error;
2553
2554    for msg_id in msg_ids {
2555        let mut message = Message::load_from_db(context, msg_id).await?;
2556        let chat = Chat::load_from_db(context, message.chat_id).await?;
2557        if chat.typ == constants::Chattype::OutBroadcast {
2558            continue;
2559        }
2560        let aggregated_error = message
2561            .error
2562            .as_ref()
2563            .map(|err| format!("{err}\n\n{err_msg}"));
2564        set_msg_failed(
2565            context,
2566            &mut message,
2567            aggregated_error.as_ref().unwrap_or(err_msg),
2568        )
2569        .await?;
2570    }
2571
2572    Ok(())
2573}
2574
2575#[cfg(test)]
2576mod mimeparser_tests;
2577#[cfg(test)]
2578mod shared_secret_decryption_tests;