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