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