deltachat/
mimeparser.rs

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