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