deltachat/
mimeparser.rs

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