deltachat/
mimeparser.rs

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