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