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