deltachat/
mimeparser.rs

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