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