deltachat/
mimeparser.rs

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