1use 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, ensure};
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::blob::BlobObject;
18use crate::chat::{Chat, ChatId};
19use crate::config::Config;
20use crate::constants;
21use crate::contact::{ContactId, import_public_key};
22use crate::context::Context;
23use crate::decrypt::{self, validate_detached_signature};
24use crate::dehtml::dehtml;
25use crate::download::PostMsgMetadata;
26use crate::events::EventType;
27use crate::headerdef::{HeaderDef, HeaderDefMap};
28use crate::key::{self, DcKey, Fingerprint, SignedPublicKey};
29use crate::log::warn;
30use crate::message::{self, Message, MsgId, Viewtype, get_vcard_summary, set_msg_failed};
31use crate::param::{Param, Params};
32use crate::simplify::{SimplifiedText, simplify};
33use crate::sync::SyncItems;
34use crate::tools::{
35 get_filemeta, parse_receive_headers, smeared_time, time, truncate_msg_text, validate_id,
36};
37use crate::{chatlist_events, location, tools};
38
39#[derive(Debug)]
42pub struct GossipedKey {
43 pub public_key: SignedPublicKey,
45
46 pub verified: bool,
48}
49
50#[derive(Debug)]
60pub(crate) struct MimeMessage {
61 pub parts: Vec<Part>,
63
64 headers: HashMap<String, String>,
66
67 #[cfg(test)]
68 headers_removed: HashSet<String>,
70
71 pub recipients: Vec<SingleInfo>,
75
76 pub past_members: Vec<SingleInfo>,
78
79 pub from: SingleInfo,
81
82 pub incoming: bool,
84 pub list_post: Option<String>,
87 pub chat_disposition_notification_to: Option<SingleInfo>,
88
89 pub decryption_error: Option<String>,
91
92 pub signature: Option<(Fingerprint, HashSet<Fingerprint>)>,
99
100 pub gossiped_keys: BTreeMap<String, GossipedKey>,
103
104 pub autocrypt_fingerprint: Option<String>,
108
109 pub is_forwarded: bool,
111 pub is_system_message: SystemMessage,
112 pub location_kml: Option<location::Kml>,
113 pub message_kml: Option<location::Kml>,
114 pub(crate) sync_items: Option<SyncItems>,
115 pub(crate) webxdc_status_update: Option<String>,
116 pub(crate) user_avatar: Option<AvatarAction>,
117 pub(crate) group_avatar: Option<AvatarAction>,
118 pub(crate) mdn_reports: Vec<Report>,
119 pub(crate) delivery_report: Option<DeliveryReport>,
120
121 pub(crate) footer: Option<String>,
126
127 pub is_mime_modified: bool,
130
131 pub decoded_data: Vec<u8>,
133
134 pub(crate) hop_info: String,
136
137 pub(crate) is_bot: Option<bool>,
147
148 pub(crate) timestamp_rcvd: i64,
150 pub(crate) timestamp_sent: i64,
153
154 pub(crate) pre_message: PreMessageMode,
155}
156
157#[derive(Debug, Clone, PartialEq)]
158pub(crate) enum PreMessageMode {
159 Post,
163 Pre {
167 post_msg_rfc724_mid: String,
168 metadata: Option<PostMsgMetadata>,
169 },
170 None,
172}
173
174#[derive(Debug, PartialEq)]
175pub(crate) enum AvatarAction {
176 Delete,
177 Change(String),
178}
179
180#[derive(
182 Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
183)]
184#[repr(u32)]
185pub enum SystemMessage {
186 #[default]
188 Unknown = 0,
189
190 GroupNameChanged = 2,
192
193 GroupImageChanged = 3,
195
196 MemberAddedToGroup = 4,
198
199 MemberRemovedFromGroup = 5,
201
202 AutocryptSetupMessage = 6,
207
208 SecurejoinMessage = 7,
210
211 LocationStreamingEnabled = 8,
213
214 LocationOnly = 9,
216
217 EphemeralTimerChanged = 10,
219
220 ChatProtectionEnabled = 11,
222
223 ChatProtectionDisabled = 12,
225
226 InvalidUnencryptedMail = 13,
229
230 SecurejoinWait = 14,
233
234 SecurejoinWaitTimeout = 15,
237
238 MultiDeviceSync = 20,
241
242 WebxdcStatusUpdate = 30,
246
247 WebxdcInfoMessage = 32,
249
250 IrohNodeAddr = 40,
252
253 ChatE2ee = 50,
255
256 CallAccepted = 66,
258
259 CallEnded = 67,
261
262 GroupDescriptionChanged = 70,
264}
265
266impl MimeMessage {
267 pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
272 let mail = mailparse::parse_mail(body)?;
273
274 let timestamp_rcvd = smeared_time(context);
275 let mut timestamp_sent =
276 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
277 let hop_info = parse_receive_headers(&mail.get_headers());
278
279 let mut headers = Default::default();
280 let mut headers_removed = HashSet::<String>::new();
281 let mut recipients = Default::default();
282 let mut past_members = Default::default();
283 let mut from = Default::default();
284 let mut list_post = Default::default();
285 let mut chat_disposition_notification_to = None;
286
287 MimeMessage::merge_headers(
289 context,
290 &mut headers,
291 &mut headers_removed,
292 &mut recipients,
293 &mut past_members,
294 &mut from,
295 &mut list_post,
296 &mut chat_disposition_notification_to,
297 &mail,
298 );
299 headers_removed.extend(
300 headers
301 .extract_if(|k, _v| is_hidden(k))
302 .map(|(k, _v)| k.to_string()),
303 );
304
305 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
307 if mimetype.type_() == mime::MULTIPART
308 && mimetype.subtype().as_str() == "mixed"
309 && let Some(part) = mail.subparts.first()
310 {
311 for field in &part.headers {
312 let key = field.get_key().to_lowercase();
313 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
314 headers.insert(key.to_string(), field.get_value());
315 }
316 }
317 }
318
319 if let Some(microsoft_message_id) = remove_header(
323 &mut headers,
324 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
325 &mut headers_removed,
326 ) {
327 headers.insert(
328 HeaderDef::MessageId.get_headername().to_string(),
329 microsoft_message_id,
330 );
331 }
332
333 let encrypted = false;
335 Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
336
337 let mut from = from.context("No from in message")?;
338
339 let mut gossiped_keys = Default::default();
340
341 let from_is_not_self_addr = !context.is_self_addr(&from.addr).await?;
342
343 let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
344
345 let mut pre_message = if mail
346 .headers
347 .get_header_value(HeaderDef::ChatIsPostMessage)
348 .is_some()
349 {
350 PreMessageMode::Post
351 } else {
352 PreMessageMode::None
353 };
354
355 let mail_raw; let decrypted_msg; let expected_sender_fingerprint: Option<String>;
358
359 let (mail, is_encrypted) = match decrypt::decrypt(context, &mail).await {
360 Ok(Some((mut msg, expected_sender_fp))) => {
361 mail_raw = msg.as_data_vec().unwrap_or_default();
362
363 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
364 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
365 info!(
366 context,
367 "decrypted message mime-body:\n{}",
368 String::from_utf8_lossy(&mail_raw),
369 );
370 }
371
372 decrypted_msg = Some(msg);
373
374 timestamp_sent = Self::get_timestamp_sent(
375 &decrypted_mail.headers,
376 timestamp_sent,
377 timestamp_rcvd,
378 );
379
380 let protected_aheader_values = decrypted_mail
381 .headers
382 .get_all_values(HeaderDef::Autocrypt.into());
383 if !protected_aheader_values.is_empty() {
384 aheader_values = protected_aheader_values;
385 }
386
387 expected_sender_fingerprint = expected_sender_fp;
388 (Ok(decrypted_mail), true)
389 }
390 Ok(None) => {
391 mail_raw = Vec::new();
392 decrypted_msg = None;
393 expected_sender_fingerprint = None;
394 (Ok(mail), false)
395 }
396 Err(err) => {
397 mail_raw = Vec::new();
398 decrypted_msg = None;
399 expected_sender_fingerprint = None;
400 warn!(context, "decryption failed: {:#}", err);
401 (Err(err), false)
402 }
403 };
404
405 let mut autocrypt_header = None;
406 if from_is_not_self_addr {
407 for val in aheader_values.iter().rev() {
409 autocrypt_header = match Aheader::from_str(val) {
410 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
411 Ok(header) => {
412 warn!(
413 context,
414 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
415 );
416 continue;
417 }
418 Err(err) => {
419 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
420 continue;
421 }
422 };
423 break;
424 }
425 }
426
427 let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
428 let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
429 import_public_key(context, &autocrypt_header.public_key)
430 .await
431 .context("Failed to import public key from the Autocrypt header")?;
432 Some(fingerprint)
433 } else {
434 None
435 };
436
437 let mut public_keyring = if from_is_not_self_addr {
438 if let Some(autocrypt_header) = autocrypt_header {
439 vec![autocrypt_header.public_key]
440 } else {
441 vec![]
442 }
443 } else {
444 key::load_self_public_keyring(context).await?
445 };
446
447 if let Some(signature) = match &decrypted_msg {
448 Some(pgp::composed::Message::Literal { .. }) => None,
449 Some(pgp::composed::Message::Compressed { .. }) => {
450 None
453 }
454 Some(pgp::composed::Message::Signed { reader, .. }) => reader.signature(0),
455 Some(pgp::composed::Message::Encrypted { .. }) => {
456 None
458 }
459 None => None,
460 } {
461 for issuer_fingerprint in signature.issuer_fingerprint() {
462 let issuer_fingerprint =
463 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
464 if let Some(public_key_bytes) = context
465 .sql
466 .query_row_optional(
467 "SELECT public_key
468 FROM public_keys
469 WHERE fingerprint=?",
470 (&issuer_fingerprint,),
471 |row| {
472 let bytes: Vec<u8> = row.get(0)?;
473 Ok(bytes)
474 },
475 )
476 .await?
477 {
478 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
479 public_keyring.push(public_key)
480 }
481 }
482 }
483
484 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
485 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
486 } else {
487 HashMap::new()
488 };
489
490 let mail = mail.as_ref().map(|mail| {
491 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
492 .unwrap_or((mail, Default::default()));
493 if is_encrypted {
494 let signatures_detached = signatures_detached
495 .into_iter()
496 .map(|fp| (fp, Vec::new()))
497 .collect::<HashMap<_, _>>();
498 signatures.extend(signatures_detached);
499 }
500 content
501 });
502
503 if let Some(expected_sender_fingerprint) = expected_sender_fingerprint {
504 ensure!(
505 !signatures.is_empty(),
506 "Unsigned message is not allowed to be encrypted with this shared secret"
507 );
508 ensure!(
509 signatures.len() == 1,
510 "Too many signatures on symm-encrypted message"
511 );
512 ensure!(
513 signatures.contains_key(&expected_sender_fingerprint.parse()?),
514 "This sender is not allowed to encrypt with this secret key"
515 );
516 }
517
518 if let (Ok(mail), true) = (mail, is_encrypted) {
519 if !signatures.is_empty() {
520 remove_header(&mut headers, "subject", &mut headers_removed);
524 remove_header(&mut headers, "list-id", &mut headers_removed);
525 }
526
527 let mut inner_from = None;
533
534 MimeMessage::merge_headers(
535 context,
536 &mut headers,
537 &mut headers_removed,
538 &mut recipients,
539 &mut past_members,
540 &mut inner_from,
541 &mut list_post,
542 &mut chat_disposition_notification_to,
543 mail,
544 );
545
546 if !signatures.is_empty() {
547 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
552 gossiped_keys =
553 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
554 }
555
556 if let Some(inner_from) = inner_from {
557 if !addr_cmp(&inner_from.addr, &from.addr) {
558 warn!(
567 context,
568 "From header in encrypted part doesn't match the outer one",
569 );
570
571 bail!("From header is forged");
576 }
577 from = inner_from;
578 }
579 }
580 if signatures.is_empty() {
581 Self::remove_secured_headers(&mut headers, &mut headers_removed, is_encrypted);
582 }
583 if !is_encrypted {
584 signatures.clear();
585 }
586
587 if let (Ok(mail), true) = (mail, is_encrypted)
588 && let Some(post_msg_rfc724_mid) =
589 mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
590 {
591 let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
592 let metadata = if let Some(value) = mail
593 .headers
594 .get_header_value(HeaderDef::ChatPostMessageMetadata)
595 {
596 match PostMsgMetadata::try_from_header_value(&value) {
597 Ok(metadata) => Some(metadata),
598 Err(error) => {
599 error!(
600 context,
601 "Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
602 );
603 None
604 }
605 }
606 } else {
607 warn!(
608 context,
609 "Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
610 );
611 None
612 };
613
614 pre_message = PreMessageMode::Pre {
615 post_msg_rfc724_mid,
616 metadata,
617 };
618 }
619
620 let signature = signatures
621 .into_iter()
622 .last()
623 .map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
624
625 let incoming = if let Some((ref sig_fp, _)) = signature {
626 sig_fp.hex() != key::self_fingerprint(context).await?
627 } else {
628 from_is_not_self_addr
631 };
632
633 let mut parser = MimeMessage {
634 parts: Vec::new(),
635 headers,
636 #[cfg(test)]
637 headers_removed,
638
639 recipients,
640 past_members,
641 list_post,
642 from,
643 incoming,
644 chat_disposition_notification_to,
645 decryption_error: mail.err().map(|err| format!("{err:#}")),
646
647 signature,
649 autocrypt_fingerprint,
650 gossiped_keys,
651 is_forwarded: false,
652 mdn_reports: Vec::new(),
653 is_system_message: SystemMessage::Unknown,
654 location_kml: None,
655 message_kml: None,
656 sync_items: None,
657 webxdc_status_update: None,
658 user_avatar: None,
659 group_avatar: None,
660 delivery_report: None,
661 footer: None,
662 is_mime_modified: false,
663 decoded_data: Vec::new(),
664 hop_info,
665 is_bot: None,
666 timestamp_rcvd,
667 timestamp_sent,
668 pre_message,
669 };
670
671 match mail {
672 Ok(mail) => {
673 parser.parse_mime_recursive(context, mail, false).await?;
674 }
675 Err(err) => {
676 let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
677
678 let part = Part {
679 typ: Viewtype::Text,
680 msg_raw: Some(txt.to_string()),
681 msg: txt.to_string(),
682 error: Some(format!("Decrypting failed: {err:#}")),
685 ..Default::default()
686 };
687 parser.do_add_single_part(part);
688 }
689 };
690
691 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
692 if parser.mdn_reports.is_empty()
693 && !is_location_only
694 && parser.sync_items.is_none()
695 && parser.webxdc_status_update.is_none()
696 {
697 let is_bot =
698 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
699 parser.is_bot = Some(is_bot);
700 }
701 parser.maybe_remove_bad_parts();
702 parser.maybe_remove_inline_mailinglist_footer();
703 parser.heuristically_parse_ndn(context).await;
704 parser.parse_headers(context).await?;
705 parser.decoded_data = mail_raw;
706
707 Ok(parser)
708 }
709
710 #[expect(clippy::arithmetic_side_effects)]
711 fn get_timestamp_sent(
712 hdrs: &[mailparse::MailHeader<'_>],
713 default: i64,
714 timestamp_rcvd: i64,
715 ) -> i64 {
716 hdrs.get_header_value(HeaderDef::Date)
717 .and_then(|v| mailparse::dateparse(&v).ok())
718 .map_or(default, |value| {
719 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
720 })
721 }
722
723 fn parse_system_message_headers(&mut self) {
725 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
726 if value == "location-streaming-enabled" {
727 self.is_system_message = SystemMessage::LocationStreamingEnabled;
728 } else if value == "ephemeral-timer-changed" {
729 self.is_system_message = SystemMessage::EphemeralTimerChanged;
730 } else if value == "protection-enabled" {
731 self.is_system_message = SystemMessage::ChatProtectionEnabled;
732 } else if value == "protection-disabled" {
733 self.is_system_message = SystemMessage::ChatProtectionDisabled;
734 } else if value == "group-avatar-changed" {
735 self.is_system_message = SystemMessage::GroupImageChanged;
736 } else if value == "call-accepted" {
737 self.is_system_message = SystemMessage::CallAccepted;
738 } else if value == "call-ended" {
739 self.is_system_message = SystemMessage::CallEnded;
740 }
741 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
742 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
743 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
744 self.is_system_message = SystemMessage::MemberAddedToGroup;
745 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
746 self.is_system_message = SystemMessage::GroupNameChanged;
747 } else if self
748 .get_header(HeaderDef::ChatGroupDescriptionChanged)
749 .is_some()
750 {
751 self.is_system_message = SystemMessage::GroupDescriptionChanged;
752 }
753 }
754
755 fn parse_avatar_headers(&mut self, context: &Context) -> Result<()> {
757 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
758 self.group_avatar =
759 self.avatar_action_from_header(context, header_value.to_string())?;
760 }
761
762 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
763 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string())?;
764 }
765 Ok(())
766 }
767
768 fn parse_videochat_headers(&mut self) {
769 let content = self
770 .get_header(HeaderDef::ChatContent)
771 .unwrap_or_default()
772 .to_string();
773 let room = self
774 .get_header(HeaderDef::ChatWebrtcRoom)
775 .map(|s| s.to_string());
776 let accepted = self
777 .get_header(HeaderDef::ChatWebrtcAccepted)
778 .map(|s| s.to_string());
779 let has_video = self
780 .get_header(HeaderDef::ChatWebrtcHasVideoInitially)
781 .map(|s| s.to_string());
782 if let Some(part) = self.parts.first_mut() {
783 if let Some(room) = room {
784 if content == "call" {
785 part.typ = Viewtype::Call;
786 part.param.set(Param::WebrtcRoom, room);
787 }
788 } else if let Some(accepted) = accepted {
789 part.param.set(Param::WebrtcAccepted, accepted);
790 }
791 if let Some(has_video) = has_video {
792 part.param.set(Param::WebrtcHasVideoInitially, has_video);
793 }
794 }
795 }
796
797 fn squash_attachment_parts(&mut self) {
803 if self.parts.len() == 2
804 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
805 && self
806 .parts
807 .get(1)
808 .is_some_and(|filepart| match filepart.typ {
809 Viewtype::Image
810 | Viewtype::Gif
811 | Viewtype::Sticker
812 | Viewtype::Audio
813 | Viewtype::Voice
814 | Viewtype::Video
815 | Viewtype::Vcard
816 | Viewtype::File
817 | Viewtype::Webxdc => true,
818 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
819 })
820 {
821 let mut parts = std::mem::take(&mut self.parts);
822 let Some(mut filepart) = parts.pop() else {
823 return;
825 };
826 let Some(textpart) = parts.pop() else {
827 return;
829 };
830
831 filepart.msg.clone_from(&textpart.msg);
832 if let Some(quote) = textpart.param.get(Param::Quote) {
833 filepart.param.set(Param::Quote, quote);
834 }
835
836 self.parts = vec![filepart];
837 }
838 }
839
840 fn parse_attachments(&mut self) {
842 if self.parts.len() != 1 {
845 return;
846 }
847
848 if let Some(mut part) = self.parts.pop() {
849 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
850 {
851 part.typ = Viewtype::Voice;
852 }
853 if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
854 && let Some(value) = self.get_header(HeaderDef::ChatContent)
855 && value == "sticker"
856 {
857 part.typ = Viewtype::Sticker;
858 }
859 if (part.typ == Viewtype::Audio
860 || part.typ == Viewtype::Voice
861 || part.typ == Viewtype::Video)
862 && let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
863 {
864 let duration_ms = field_0.parse().unwrap_or_default();
865 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
866 part.param.set_int(Param::Duration, duration_ms);
867 }
868 }
869
870 self.parts.push(part);
871 }
872 }
873
874 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
875 self.parse_system_message_headers();
876 self.parse_avatar_headers(context)?;
877 self.parse_videochat_headers();
878 if self.delivery_report.is_none() {
879 self.squash_attachment_parts();
880 }
881
882 if !context.get_config_bool(Config::Bot).await?
883 && let Some(ref subject) = self.get_subject()
884 {
885 let mut prepend_subject = true;
886 if self.decryption_error.is_none() {
887 let colon = subject.find(':');
888 if colon == Some(2)
889 || colon == Some(3)
890 || self.has_chat_version()
891 || subject.contains("Chat:")
892 {
893 prepend_subject = false
894 }
895 }
896
897 if self.is_mailinglist_message() && !self.has_chat_version() {
900 prepend_subject = true;
901 }
902
903 if prepend_subject && !subject.is_empty() {
904 let part_with_text = self
905 .parts
906 .iter_mut()
907 .find(|part| !part.msg.is_empty() && !part.is_reaction);
908 if let Some(part) = part_with_text {
909 part.msg = format!("{} – {}", subject, part.msg);
914 }
915 }
916 }
917
918 if self.is_forwarded {
919 for part in &mut self.parts {
920 part.param.set_int(Param::Forwarded, 1);
921 }
922 }
923
924 self.parse_attachments();
925
926 if self.decryption_error.is_none()
928 && !self.parts.is_empty()
929 && let Some(ref dn_to) = self.chat_disposition_notification_to
930 {
931 let from = &self.from.addr;
933 if !context.is_self_addr(from).await? {
934 if from.to_lowercase() == dn_to.addr.to_lowercase() {
935 if let Some(part) = self.parts.last_mut() {
936 part.param.set_int(Param::WantsMdn, 1);
937 }
938 } else {
939 warn!(
940 context,
941 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
942 );
943 }
944 }
945 }
946
947 if self.parts.is_empty() && self.mdn_reports.is_empty() {
952 let mut part = Part {
953 typ: Viewtype::Text,
954 ..Default::default()
955 };
956
957 if let Some(ref subject) = self.get_subject()
958 && !self.has_chat_version()
959 && self.webxdc_status_update.is_none()
960 {
961 part.msg = subject.to_string();
962 }
963
964 self.do_add_single_part(part);
965 }
966
967 if self.is_bot == Some(true) {
968 for part in &mut self.parts {
969 part.param.set(Param::Bot, "1");
970 }
971 }
972
973 Ok(())
974 }
975
976 #[expect(clippy::arithmetic_side_effects)]
977 fn avatar_action_from_header(
978 &mut self,
979 context: &Context,
980 header_value: String,
981 ) -> Result<Option<AvatarAction>> {
982 let res = if header_value == "0" {
983 Some(AvatarAction::Delete)
984 } else if let Some(base64) = header_value
985 .split_ascii_whitespace()
986 .collect::<String>()
987 .strip_prefix("base64:")
988 {
989 match BlobObject::store_from_base64(context, base64)? {
990 Some(path) => Some(AvatarAction::Change(path)),
991 None => {
992 warn!(context, "Could not decode avatar base64");
993 None
994 }
995 }
996 } else {
997 let mut i = 0;
1000 while let Some(part) = self.parts.get_mut(i) {
1001 if let Some(part_filename) = &part.org_filename
1002 && part_filename == &header_value
1003 {
1004 if let Some(blob) = part.param.get(Param::File) {
1005 let res = Some(AvatarAction::Change(blob.to_string()));
1006 self.parts.remove(i);
1007 return Ok(res);
1008 }
1009 break;
1010 }
1011 i += 1;
1012 }
1013 None
1014 };
1015 Ok(res)
1016 }
1017
1018 pub fn was_encrypted(&self) -> bool {
1024 self.signature.is_some()
1025 }
1026
1027 pub(crate) fn has_chat_version(&self) -> bool {
1030 self.headers.contains_key("chat-version")
1031 }
1032
1033 pub(crate) fn get_subject(&self) -> Option<String> {
1034 self.get_header(HeaderDef::Subject)
1035 .map(|s| s.trim_start())
1036 .filter(|s| !s.is_empty())
1037 .map(|s| s.to_string())
1038 }
1039
1040 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
1041 self.headers
1042 .get(headerdef.get_headername())
1043 .map(|s| s.as_str())
1044 }
1045
1046 #[cfg(test)]
1047 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1052 let hname = headerdef.get_headername();
1053 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1054 }
1055
1056 #[cfg(test)]
1057 pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
1059 assert!(self.decryption_error.is_none());
1060 let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
1061 decoded_str.contains(s)
1062 }
1063
1064 pub fn get_chat_group_id(&self) -> Option<&str> {
1066 self.get_header(HeaderDef::ChatGroupId)
1067 .filter(|s| validate_id(s))
1068 }
1069
1070 async fn parse_mime_recursive<'a>(
1071 &'a mut self,
1072 context: &'a Context,
1073 mail: &'a mailparse::ParsedMail<'a>,
1074 is_related: bool,
1075 ) -> Result<bool> {
1076 enum MimeS {
1077 Multiple,
1078 Single,
1079 Message,
1080 }
1081
1082 let mimetype = mail.ctype.mimetype.to_lowercase();
1083
1084 let m = if mimetype.starts_with("multipart") {
1085 if mail.ctype.params.contains_key("boundary") {
1086 MimeS::Multiple
1087 } else {
1088 MimeS::Single
1089 }
1090 } else if mimetype.starts_with("message") {
1091 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1092 MimeS::Message
1093 } else {
1094 MimeS::Single
1095 }
1096 } else {
1097 MimeS::Single
1098 };
1099
1100 let is_related = is_related || mimetype == "multipart/related";
1101 match m {
1102 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1103 MimeS::Message => {
1104 let raw = mail.get_body_raw()?;
1105 if raw.is_empty() {
1106 return Ok(false);
1107 }
1108 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1109
1110 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1111 }
1112 MimeS::Single => {
1113 self.add_single_part_if_known(context, mail, is_related)
1114 .await
1115 }
1116 }
1117 }
1118
1119 async fn handle_multiple(
1120 &mut self,
1121 context: &Context,
1122 mail: &mailparse::ParsedMail<'_>,
1123 is_related: bool,
1124 ) -> Result<bool> {
1125 let mut any_part_added = false;
1126 let mimetype = get_mime_type(
1127 mail,
1128 &get_attachment_filename(context, mail)?,
1129 self.has_chat_version(),
1130 )?
1131 .0;
1132 match (mimetype.type_(), mimetype.subtype().as_str()) {
1133 (mime::MULTIPART, "alternative") => {
1134 for cur_data in mail.subparts.iter().rev() {
1146 let (mime_type, _viewtype) = get_mime_type(
1147 cur_data,
1148 &get_attachment_filename(context, cur_data)?,
1149 self.has_chat_version(),
1150 )?;
1151
1152 if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
1153 any_part_added = self
1154 .parse_mime_recursive(context, cur_data, is_related)
1155 .await?;
1156 break;
1157 }
1158 }
1159
1160 for cur_data in mail.subparts.iter().rev() {
1169 let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
1170 if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
1171 let filename = get_attachment_filename(context, cur_data)?
1172 .unwrap_or_else(|| "calendar.ics".to_string());
1173 self.do_add_single_file_part(
1174 context,
1175 Viewtype::File,
1176 mimetype,
1177 &mail.ctype.mimetype.to_lowercase(),
1178 &mail.get_body_raw()?,
1179 &filename,
1180 is_related,
1181 )
1182 .await?;
1183 }
1184 }
1185
1186 if !any_part_added {
1187 for cur_part in mail.subparts.iter().rev() {
1188 if self
1189 .parse_mime_recursive(context, cur_part, is_related)
1190 .await?
1191 {
1192 any_part_added = true;
1193 break;
1194 }
1195 }
1196 }
1197 if any_part_added && mail.subparts.len() > 1 {
1198 self.is_mime_modified = true;
1202 }
1203 }
1204 (mime::MULTIPART, "signed") => {
1205 if let Some(first) = mail.subparts.first() {
1214 any_part_added = self
1215 .parse_mime_recursive(context, first, is_related)
1216 .await?;
1217 }
1218 }
1219 (mime::MULTIPART, "report") => {
1220 if mail.subparts.len() >= 2 {
1222 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1223 Some("disposition-notification") => {
1224 if let Some(report) = self.process_report(context, mail)? {
1225 self.mdn_reports.push(report);
1226 }
1227
1228 let part = Part {
1233 typ: Viewtype::Unknown,
1234 ..Default::default()
1235 };
1236 self.parts.push(part);
1237
1238 any_part_added = true;
1239 }
1240 Some("delivery-status") | None => {
1242 if let Some(report) = self.process_delivery_status(context, mail)? {
1243 self.delivery_report = Some(report);
1244 }
1245
1246 for cur_data in &mail.subparts {
1248 if self
1249 .parse_mime_recursive(context, cur_data, is_related)
1250 .await?
1251 {
1252 any_part_added = true;
1253 }
1254 }
1255 }
1256 Some("multi-device-sync") => {
1257 if let Some(second) = mail.subparts.get(1) {
1258 self.add_single_part_if_known(context, second, is_related)
1259 .await?;
1260 }
1261 }
1262 Some("status-update") => {
1263 if let Some(second) = mail.subparts.get(1) {
1264 self.add_single_part_if_known(context, second, is_related)
1265 .await?;
1266 }
1267 }
1268 Some(_) => {
1269 for cur_data in &mail.subparts {
1270 if self
1271 .parse_mime_recursive(context, cur_data, is_related)
1272 .await?
1273 {
1274 any_part_added = true;
1275 }
1276 }
1277 }
1278 }
1279 }
1280 }
1281 _ => {
1282 for cur_data in &mail.subparts {
1285 if self
1286 .parse_mime_recursive(context, cur_data, is_related)
1287 .await?
1288 {
1289 any_part_added = true;
1290 }
1291 }
1292 }
1293 }
1294
1295 Ok(any_part_added)
1296 }
1297
1298 async fn add_single_part_if_known(
1300 &mut self,
1301 context: &Context,
1302 mail: &mailparse::ParsedMail<'_>,
1303 is_related: bool,
1304 ) -> Result<bool> {
1305 let filename = get_attachment_filename(context, mail)?;
1307 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1308 let raw_mime = mail.ctype.mimetype.to_lowercase();
1309
1310 let old_part_count = self.parts.len();
1311
1312 match filename {
1313 Some(filename) => {
1314 self.do_add_single_file_part(
1315 context,
1316 msg_type,
1317 mime_type,
1318 &raw_mime,
1319 &mail.get_body_raw()?,
1320 &filename,
1321 is_related,
1322 )
1323 .await?;
1324 }
1325 None => {
1326 match mime_type.type_() {
1327 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1328 warn!(context, "Missing attachment");
1329 return Ok(false);
1330 }
1331 mime::TEXT
1332 if mail.get_content_disposition().disposition
1333 == DispositionType::Extension("reaction".to_string()) =>
1334 {
1335 let decoded_data = match mail.get_body() {
1337 Ok(decoded_data) => decoded_data,
1338 Err(err) => {
1339 warn!(context, "Invalid body parsed {:#}", err);
1340 return Ok(false);
1342 }
1343 };
1344
1345 let part = Part {
1346 typ: Viewtype::Text,
1347 mimetype: Some(mime_type),
1348 msg: decoded_data,
1349 is_reaction: true,
1350 ..Default::default()
1351 };
1352 self.do_add_single_part(part);
1353 return Ok(true);
1354 }
1355 mime::TEXT | mime::HTML => {
1356 let decoded_data = match mail.get_body() {
1357 Ok(decoded_data) => decoded_data,
1358 Err(err) => {
1359 warn!(context, "Invalid body parsed {:#}", err);
1360 return Ok(false);
1362 }
1363 };
1364
1365 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1366 let mut dehtml_failed = false;
1367
1368 let SimplifiedText {
1369 text: simplified_txt,
1370 is_forwarded,
1371 is_cut,
1372 top_quote,
1373 footer,
1374 } = if decoded_data.is_empty() {
1375 Default::default()
1376 } else {
1377 let is_html = mime_type == mime::TEXT_HTML;
1378 if is_html {
1379 self.is_mime_modified = true;
1380 if let Some(text) = dehtml(&decoded_data) {
1385 text
1386 } else {
1387 dehtml_failed = true;
1388 SimplifiedText {
1389 text: decoded_data.clone(),
1390 ..Default::default()
1391 }
1392 }
1393 } else {
1394 simplify(decoded_data.clone(), self.has_chat_version())
1395 }
1396 };
1397
1398 self.is_mime_modified = self.is_mime_modified
1399 || ((is_forwarded || is_cut || top_quote.is_some())
1400 && !self.has_chat_version());
1401
1402 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1403 {
1404 format.as_str().eq_ignore_ascii_case("flowed")
1405 } else {
1406 false
1407 };
1408
1409 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1410 && mime_type.subtype() == mime::PLAIN
1411 {
1412 let simplified_txt = match mail
1415 .ctype
1416 .params
1417 .get("hp-legacy-display")
1418 .is_some_and(|v| v == "1")
1419 {
1420 false => simplified_txt,
1421 true => rm_legacy_display_elements(&simplified_txt),
1422 };
1423 if is_format_flowed {
1424 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1425 delsp.as_str().eq_ignore_ascii_case("yes")
1426 } else {
1427 false
1428 };
1429 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1430 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1431 (unflowed_text, unflowed_quote)
1432 } else {
1433 (simplified_txt, top_quote)
1434 }
1435 } else {
1436 (simplified_txt, top_quote)
1437 };
1438
1439 let (simplified_txt, was_truncated) =
1440 truncate_msg_text(context, simplified_txt).await?;
1441 if was_truncated {
1442 self.is_mime_modified = was_truncated;
1443 }
1444
1445 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1446 let mut part = Part {
1447 dehtml_failed,
1448 typ: Viewtype::Text,
1449 mimetype: Some(mime_type),
1450 msg: simplified_txt,
1451 ..Default::default()
1452 };
1453 if let Some(quote) = simplified_quote {
1454 part.param.set(Param::Quote, quote);
1455 }
1456 part.msg_raw = Some(decoded_data);
1457 self.do_add_single_part(part);
1458 }
1459
1460 if is_forwarded {
1461 self.is_forwarded = true;
1462 }
1463
1464 if self.footer.is_none() && is_plaintext {
1465 self.footer = Some(footer.unwrap_or_default());
1466 }
1467 }
1468 _ => {}
1469 }
1470 }
1471 }
1472
1473 Ok(self.parts.len() > old_part_count)
1475 }
1476
1477 #[expect(clippy::too_many_arguments)]
1478 #[expect(clippy::arithmetic_side_effects)]
1479 async fn do_add_single_file_part(
1480 &mut self,
1481 context: &Context,
1482 msg_type: Viewtype,
1483 mime_type: Mime,
1484 raw_mime: &str,
1485 decoded_data: &[u8],
1486 filename: &str,
1487 is_related: bool,
1488 ) -> Result<()> {
1489 if mime_type.type_() == mime::APPLICATION
1491 && mime_type.subtype().as_str() == "pgp-keys"
1492 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1493 {
1494 return Ok(());
1495 }
1496 let mut part = Part::default();
1497 let msg_type = if context
1498 .is_webxdc_file(filename, decoded_data)
1499 .await
1500 .unwrap_or(false)
1501 {
1502 Viewtype::Webxdc
1503 } else if filename.ends_with(".kml") {
1504 if filename.starts_with("location") || filename.starts_with("message") {
1507 let parsed = location::Kml::parse(decoded_data)
1508 .map_err(|err| {
1509 warn!(context, "failed to parse kml part: {:#}", err);
1510 })
1511 .ok();
1512 if filename.starts_with("location") {
1513 self.location_kml = parsed;
1514 } else {
1515 self.message_kml = parsed;
1516 }
1517 return Ok(());
1518 }
1519 msg_type
1520 } else if filename == "multi-device-sync.json" {
1521 if !context.get_config_bool(Config::SyncMsgs).await? {
1522 return Ok(());
1523 }
1524 let serialized = String::from_utf8_lossy(decoded_data)
1525 .parse()
1526 .unwrap_or_default();
1527 self.sync_items = context
1528 .parse_sync_items(serialized)
1529 .map_err(|err| {
1530 warn!(context, "failed to parse sync data: {:#}", err);
1531 })
1532 .ok();
1533 return Ok(());
1534 } else if filename == "status-update.json" {
1535 let serialized = String::from_utf8_lossy(decoded_data)
1536 .parse()
1537 .unwrap_or_default();
1538 self.webxdc_status_update = Some(serialized);
1539 return Ok(());
1540 } else if msg_type == Viewtype::Vcard {
1541 if let Some(summary) = get_vcard_summary(decoded_data) {
1542 part.param.set(Param::Summary1, summary);
1543 msg_type
1544 } else {
1545 Viewtype::File
1546 }
1547 } else if msg_type == Viewtype::Image
1548 || msg_type == Viewtype::Gif
1549 || msg_type == Viewtype::Sticker
1550 {
1551 match get_filemeta(decoded_data) {
1552 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1554 part.param.set_i64(Param::Width, width.into());
1555 part.param.set_i64(Param::Height, height.into());
1556 msg_type
1557 }
1558 _ => Viewtype::File,
1560 }
1561 } else {
1562 msg_type
1563 };
1564
1565 let blob =
1569 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1570 Ok(blob) => blob,
1571 Err(err) => {
1572 error!(
1573 context,
1574 "Could not add blob for mime part {}, error {:#}", filename, err
1575 );
1576 return Ok(());
1577 }
1578 };
1579 info!(context, "added blobfile: {:?}", blob.as_name());
1580
1581 part.typ = msg_type;
1582 part.org_filename = Some(filename.to_string());
1583 part.mimetype = Some(mime_type);
1584 part.bytes = decoded_data.len();
1585 part.param.set(Param::File, blob.as_name());
1586 part.param.set(Param::Filename, filename);
1587 part.param.set(Param::MimeType, raw_mime);
1588 part.is_related = is_related;
1589
1590 self.do_add_single_part(part);
1591 Ok(())
1592 }
1593
1594 async fn try_set_peer_key_from_file_part(
1596 context: &Context,
1597 decoded_data: &[u8],
1598 ) -> Result<bool> {
1599 let key = match str::from_utf8(decoded_data) {
1600 Err(err) => {
1601 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1602 return Ok(false);
1603 }
1604 Ok(key) => key,
1605 };
1606 let key = match SignedPublicKey::from_asc(key) {
1607 Err(err) => {
1608 warn!(
1609 context,
1610 "PGP key attachment is not an ASCII-armored file: {err:#}."
1611 );
1612 return Ok(false);
1613 }
1614 Ok(key) => key,
1615 };
1616 if let Err(err) = import_public_key(context, &key).await {
1617 warn!(context, "Attached PGP key import failed: {err:#}.");
1618 return Ok(false);
1619 }
1620
1621 info!(context, "Imported PGP key from attachment.");
1622 Ok(true)
1623 }
1624
1625 pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
1626 if self.was_encrypted() {
1627 part.param.set_int(Param::GuaranteeE2ee, 1);
1628 }
1629 self.parts.push(part);
1630 }
1631
1632 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1633 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1634 return Some(list_id);
1637 } else if let Some(chat_list_id) = self.get_header(HeaderDef::ChatListId) {
1638 return Some(chat_list_id);
1639 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1640 if let Some(precedence) = self.get_header(HeaderDef::Precedence)
1643 && (precedence == "list" || precedence == "bulk")
1644 {
1645 return Some(sender);
1649 }
1650 }
1651 None
1652 }
1653
1654 pub(crate) fn is_mailinglist_message(&self) -> bool {
1655 self.get_mailinglist_header().is_some()
1656 }
1657
1658 pub(crate) fn is_schleuder_message(&self) -> bool {
1660 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1661 list_help == "<https://schleuder.org/>"
1662 } else {
1663 false
1664 }
1665 }
1666
1667 pub(crate) fn is_call(&self) -> bool {
1669 self.parts
1670 .first()
1671 .is_some_and(|part| part.typ == Viewtype::Call)
1672 }
1673
1674 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1675 self.get_header(HeaderDef::MessageId)
1676 .and_then(|msgid| parse_message_id(msgid).ok())
1677 }
1678
1679 fn remove_secured_headers(
1685 headers: &mut HashMap<String, String>,
1686 removed: &mut HashSet<String>,
1687 encrypted: bool,
1688 ) {
1689 remove_header(headers, "secure-join-fingerprint", removed);
1690 remove_header(headers, "chat-verified", removed);
1691 remove_header(headers, "autocrypt-gossip", removed);
1692
1693 if headers.get("secure-join") == Some(&"vc-request-pubkey".to_string()) && encrypted {
1694 } else {
1702 remove_header(headers, "secure-join-auth", removed);
1703
1704 if let Some(secure_join) = remove_header(headers, "secure-join", removed)
1706 && (secure_join == "vc-request" || secure_join == "vg-request")
1707 {
1708 headers.insert("secure-join".to_string(), secure_join);
1709 }
1710 }
1711 }
1712
1713 #[allow(clippy::too_many_arguments)]
1719 fn merge_headers(
1720 context: &Context,
1721 headers: &mut HashMap<String, String>,
1722 headers_removed: &mut HashSet<String>,
1723 recipients: &mut Vec<SingleInfo>,
1724 past_members: &mut Vec<SingleInfo>,
1725 from: &mut Option<SingleInfo>,
1726 list_post: &mut Option<String>,
1727 chat_disposition_notification_to: &mut Option<SingleInfo>,
1728 part: &mailparse::ParsedMail,
1729 ) {
1730 let fields = &part.headers;
1731 let has_header_protection = part.ctype.params.contains_key("hp");
1733
1734 headers_removed.extend(
1735 headers
1736 .extract_if(|k, _v| has_header_protection || is_protected(k))
1737 .map(|(k, _v)| k.to_string()),
1738 );
1739 for field in fields {
1740 let key = field.get_key().to_lowercase();
1742 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1743 match addrparse_header(field) {
1744 Ok(addrlist) => {
1745 *chat_disposition_notification_to = addrlist.extract_single_info();
1746 }
1747 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1748 }
1749 } else {
1750 let value = field.get_value();
1751 headers.insert(key.to_string(), value);
1752 }
1753 }
1754 let recipients_new = get_recipients(fields);
1755 if !recipients_new.is_empty() {
1756 *recipients = recipients_new;
1757 }
1758 let past_members_addresses =
1759 get_all_addresses_from_header(fields, "chat-group-past-members");
1760 if !past_members_addresses.is_empty() {
1761 *past_members = past_members_addresses;
1762 }
1763 let from_new = get_from(fields);
1764 if from_new.is_some() {
1765 *from = from_new;
1766 }
1767 let list_post_new = get_list_post(fields);
1768 if list_post_new.is_some() {
1769 *list_post = list_post_new;
1770 }
1771 }
1772
1773 fn process_report(
1774 &self,
1775 context: &Context,
1776 report: &mailparse::ParsedMail<'_>,
1777 ) -> Result<Option<Report>> {
1778 let report_body = if let Some(subpart) = report.subparts.get(1) {
1780 subpart.get_body_raw()?
1781 } else {
1782 bail!("Report does not have second MIME part");
1783 };
1784 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1785
1786 if report_fields
1788 .get_header_value(HeaderDef::Disposition)
1789 .is_none()
1790 {
1791 warn!(
1792 context,
1793 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1794 report_fields.get_header_value(HeaderDef::MessageId)
1795 );
1796 return Ok(None);
1797 };
1798
1799 let original_message_id = report_fields
1800 .get_header_value(HeaderDef::OriginalMessageId)
1801 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1804 .and_then(|v| parse_message_id(&v).ok());
1805 let additional_message_ids = report_fields
1806 .get_header_value(HeaderDef::AdditionalMessageIds)
1807 .map_or_else(Vec::new, |v| {
1808 v.split(' ')
1809 .filter_map(|s| parse_message_id(s).ok())
1810 .collect()
1811 });
1812
1813 Ok(Some(Report {
1814 original_message_id,
1815 additional_message_ids,
1816 }))
1817 }
1818
1819 fn process_delivery_status(
1820 &self,
1821 context: &Context,
1822 report: &mailparse::ParsedMail<'_>,
1823 ) -> Result<Option<DeliveryReport>> {
1824 let mut failure = true;
1826
1827 if let Some(status_part) = report.subparts.get(1) {
1828 if status_part.ctype.mimetype != "message/delivery-status"
1831 && status_part.ctype.mimetype != "message/global-delivery-status"
1832 {
1833 warn!(
1834 context,
1835 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1836 );
1837 return Ok(None);
1838 }
1839
1840 let status_body = status_part.get_body_raw()?;
1841
1842 let (_, sz) = mailparse::parse_headers(&status_body)?;
1844
1845 if let Some(status_body) = status_body.get(sz..) {
1847 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1848 if let Some(action) = status_fields.get_first_value("action") {
1849 if action != "failed" {
1850 info!(context, "DSN with {:?} action", action);
1851 failure = false;
1852 }
1853 } else {
1854 warn!(context, "DSN without action");
1855 }
1856 } else {
1857 warn!(context, "DSN without per-recipient fields");
1858 }
1859 } else {
1860 return Ok(None);
1862 }
1863
1864 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1866 p.ctype.mimetype.contains("rfc822")
1867 || p.ctype.mimetype == "message/global"
1868 || p.ctype.mimetype == "message/global-headers"
1869 }) {
1870 let report_body = original_msg.get_body_raw()?;
1871 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1872
1873 if let Some(original_message_id) = report_fields
1874 .get_header_value(HeaderDef::MessageId)
1875 .and_then(|v| parse_message_id(&v).ok())
1876 {
1877 return Ok(Some(DeliveryReport {
1878 rfc724_mid: original_message_id,
1879 failure,
1880 }));
1881 }
1882
1883 warn!(
1884 context,
1885 "ignoring unknown ndn-notification, Message-Id: {:?}",
1886 report_fields.get_header_value(HeaderDef::MessageId)
1887 );
1888 }
1889
1890 Ok(None)
1891 }
1892
1893 fn maybe_remove_bad_parts(&mut self) {
1894 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1895 if good_parts == 0 {
1896 self.parts.truncate(1);
1898 } else if good_parts < self.parts.len() {
1899 self.parts.retain(|p| !p.dehtml_failed);
1900 }
1901
1902 if !self.has_chat_version() && self.is_mime_modified {
1910 fn is_related_image(p: &&Part) -> bool {
1911 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1912 }
1913 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1914 if related_image_cnt > 1 {
1915 let mut is_first_image = true;
1916 self.parts.retain(|p| {
1917 let retain = is_first_image || !is_related_image(&p);
1918 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1919 is_first_image = false;
1920 }
1921 retain
1922 });
1923 }
1924 }
1925 }
1926
1927 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1937 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1938 let text_part_cnt = self
1939 .parts
1940 .iter()
1941 .filter(|p| p.typ == Viewtype::Text)
1942 .count();
1943 if text_part_cnt == 2
1944 && let Some(last_part) = self.parts.last()
1945 && last_part.typ == Viewtype::Text
1946 {
1947 self.parts.pop();
1948 }
1949 }
1950 }
1951
1952 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1956 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1957 let from = from.to_ascii_lowercase();
1958 from.contains("mailer-daemon") || from.contains("mail-daemon")
1959 } else {
1960 false
1961 };
1962 if maybe_ndn && self.delivery_report.is_none() {
1963 for original_message_id in self
1964 .parts
1965 .iter()
1966 .filter_map(|part| part.msg_raw.as_ref())
1967 .flat_map(|part| part.lines())
1968 .filter_map(|line| line.split_once("Message-ID:"))
1969 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1970 {
1971 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1972 {
1973 self.delivery_report = Some(DeliveryReport {
1974 rfc724_mid: original_message_id,
1975 failure: true,
1976 })
1977 }
1978 }
1979 }
1980 }
1981
1982 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1986 for report in &self.mdn_reports {
1987 for original_message_id in report
1988 .original_message_id
1989 .iter()
1990 .chain(&report.additional_message_ids)
1991 {
1992 if let Err(err) =
1993 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1994 {
1995 warn!(context, "Could not handle MDN: {err:#}.");
1996 }
1997 }
1998 }
1999
2000 if let Some(delivery_report) = &self.delivery_report
2001 && delivery_report.failure
2002 {
2003 let error = parts
2004 .iter()
2005 .find(|p| p.typ == Viewtype::Text)
2006 .map(|p| p.msg.clone());
2007 if let Err(err) = handle_ndn(context, delivery_report, error).await {
2008 warn!(context, "Could not handle NDN: {err:#}.");
2009 }
2010 }
2011 }
2012
2013 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
2018 let parent_timestamp = if let Some(field) = self
2019 .get_header(HeaderDef::InReplyTo)
2020 .and_then(|msgid| parse_message_id(msgid).ok())
2021 {
2022 context
2023 .sql
2024 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
2025 .await?
2026 } else {
2027 None
2028 };
2029 Ok(parent_timestamp)
2030 }
2031
2032 #[expect(clippy::arithmetic_side_effects)]
2036 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
2037 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
2038 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
2039 .map(|h| {
2040 h.split_ascii_whitespace()
2041 .filter_map(|ts| ts.parse::<i64>().ok())
2042 .map(|ts| std::cmp::min(now, ts))
2043 .collect()
2044 })
2045 }
2046
2047 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
2050 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
2051 header
2052 .split_ascii_whitespace()
2053 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
2054 .collect()
2055 } else {
2056 Vec::new()
2057 }
2058 }
2059}
2060
2061fn rm_legacy_display_elements(text: &str) -> String {
2062 let mut res = None;
2063 for l in text.lines() {
2064 res = res.map(|r: String| match r.is_empty() {
2065 true => l.to_string(),
2066 false => r + "\r\n" + l,
2067 });
2068 if l.is_empty() {
2069 res = Some(String::new());
2070 }
2071 }
2072 res.unwrap_or_default()
2073}
2074
2075fn remove_header(
2076 headers: &mut HashMap<String, String>,
2077 key: &str,
2078 removed: &mut HashSet<String>,
2079) -> Option<String> {
2080 if let Some((k, v)) = headers.remove_entry(key) {
2081 removed.insert(k);
2082 Some(v)
2083 } else {
2084 None
2085 }
2086}
2087
2088async fn parse_gossip_headers(
2094 context: &Context,
2095 from: &str,
2096 recipients: &[SingleInfo],
2097 gossip_headers: Vec<String>,
2098) -> Result<BTreeMap<String, GossipedKey>> {
2099 let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
2101
2102 for value in &gossip_headers {
2103 let header = match Aheader::from_str(value) {
2104 Ok(header) => header,
2105 Err(err) => {
2106 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
2107 continue;
2108 }
2109 };
2110
2111 if !recipients
2112 .iter()
2113 .any(|info| addr_cmp(&info.addr, &header.addr))
2114 {
2115 warn!(
2116 context,
2117 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
2118 );
2119 continue;
2120 }
2121 if addr_cmp(from, &header.addr) {
2122 warn!(
2124 context,
2125 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
2126 );
2127 continue;
2128 }
2129
2130 import_public_key(context, &header.public_key)
2131 .await
2132 .context("Failed to import Autocrypt-Gossip key")?;
2133
2134 let gossiped_key = GossipedKey {
2135 public_key: header.public_key,
2136
2137 verified: header.verified,
2138 };
2139 gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2140 }
2141
2142 Ok(gossiped_keys)
2143}
2144
2145#[derive(Debug)]
2147pub(crate) struct Report {
2148 pub original_message_id: Option<String>,
2153 pub additional_message_ids: Vec<String>,
2155}
2156
2157#[derive(Debug)]
2159pub(crate) struct DeliveryReport {
2160 pub rfc724_mid: String,
2161 pub failure: bool,
2162}
2163
2164pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2165 let mut msgids = Vec::new();
2167 for id in ids.split_whitespace() {
2168 let mut id = id.to_string();
2169 if let Some(id_without_prefix) = id.strip_prefix('<') {
2170 id = id_without_prefix.to_string();
2171 };
2172 if let Some(id_without_suffix) = id.strip_suffix('>') {
2173 id = id_without_suffix.to_string();
2174 };
2175 if !id.is_empty() {
2176 msgids.push(id);
2177 }
2178 }
2179 msgids
2180}
2181
2182pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2183 if let Some(id) = parse_message_ids(ids).first() {
2184 Ok(id.to_string())
2185 } else {
2186 bail!("could not parse message_id: {ids}");
2187 }
2188}
2189
2190fn is_protected(key: &str) -> bool {
2194 key.starts_with("chat-")
2195 || matches!(
2196 key,
2197 "return-path"
2198 | "auto-submitted"
2199 | "autocrypt-setup-message"
2200 | "date"
2201 | "from"
2202 | "sender"
2203 | "reply-to"
2204 | "to"
2205 | "cc"
2206 | "bcc"
2207 | "message-id"
2208 | "in-reply-to"
2209 | "references"
2210 | "secure-join"
2211 )
2212}
2213
2214pub(crate) fn is_hidden(key: &str) -> bool {
2216 matches!(
2217 key,
2218 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2219 )
2220}
2221
2222#[derive(Debug, Default, Clone)]
2224pub struct Part {
2225 pub typ: Viewtype,
2227
2228 pub mimetype: Option<Mime>,
2230
2231 pub msg: String,
2233
2234 pub msg_raw: Option<String>,
2236
2237 pub bytes: usize,
2239
2240 pub param: Params,
2242
2243 pub(crate) org_filename: Option<String>,
2245
2246 pub error: Option<String>,
2248
2249 pub(crate) dehtml_failed: bool,
2251
2252 pub(crate) is_related: bool,
2259
2260 pub(crate) is_reaction: bool,
2262}
2263
2264fn get_mime_type(
2269 mail: &mailparse::ParsedMail<'_>,
2270 filename: &Option<String>,
2271 is_chat_msg: bool,
2272) -> Result<(Mime, Viewtype)> {
2273 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2274
2275 let viewtype = match mimetype.type_() {
2276 mime::TEXT => match mimetype.subtype() {
2277 mime::VCARD => Viewtype::Vcard,
2278 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2279 _ => Viewtype::File,
2280 },
2281 mime::IMAGE => match mimetype.subtype() {
2282 mime::GIF => Viewtype::Gif,
2283 mime::SVG => Viewtype::File,
2284 _ => Viewtype::Image,
2285 },
2286 mime::AUDIO => Viewtype::Audio,
2287 mime::VIDEO => Viewtype::Video,
2288 mime::MULTIPART => Viewtype::Unknown,
2289 mime::MESSAGE => {
2290 if is_attachment_disposition(mail) {
2291 Viewtype::File
2292 } else {
2293 Viewtype::Unknown
2301 }
2302 }
2303 mime::APPLICATION => match mimetype.subtype() {
2304 mime::OCTET_STREAM => match filename {
2305 Some(filename) if !is_chat_msg => {
2306 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2307 Some((viewtype, _)) => viewtype,
2308 None => Viewtype::File,
2309 }
2310 }
2311 _ => Viewtype::File,
2312 },
2313 _ => Viewtype::File,
2314 },
2315 _ => Viewtype::Unknown,
2316 };
2317
2318 Ok((mimetype, viewtype))
2319}
2320
2321fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2322 let ct = mail.get_content_disposition();
2323 ct.disposition == DispositionType::Attachment
2324 && ct
2325 .params
2326 .iter()
2327 .any(|(key, _value)| key.starts_with("filename"))
2328}
2329
2330fn get_attachment_filename(
2337 context: &Context,
2338 mail: &mailparse::ParsedMail,
2339) -> Result<Option<String>> {
2340 let ct = mail.get_content_disposition();
2341
2342 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2345
2346 if desired_filename.is_none()
2347 && let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
2348 {
2349 warn!(context, "apostrophed encoding invalid: {}", name);
2353 desired_filename = Some(name);
2354 }
2355
2356 if desired_filename.is_none() {
2358 desired_filename = ct.params.get("name").map(|s| s.to_string());
2359 }
2360
2361 if desired_filename.is_none() {
2364 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2365 }
2366
2367 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2369 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2370 desired_filename = Some(format!("file.{subtype}",));
2371 } else {
2372 bail!(
2373 "could not determine attachment filename: {:?}",
2374 ct.disposition
2375 );
2376 };
2377 }
2378
2379 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2380
2381 Ok(desired_filename)
2382}
2383
2384pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2386 let to_addresses = get_all_addresses_from_header(headers, "to");
2387 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2388
2389 let mut res = to_addresses;
2390 res.extend(cc_addresses);
2391 res
2392}
2393
2394pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2396 let all = get_all_addresses_from_header(headers, "from");
2397 tools::single_value(all)
2398}
2399
2400pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2402 get_all_addresses_from_header(headers, "list-post")
2403 .into_iter()
2404 .next()
2405 .map(|s| s.addr)
2406}
2407
2408fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2420 let mut result: Vec<SingleInfo> = Default::default();
2421
2422 if let Some(header) = headers
2423 .iter()
2424 .rev()
2425 .find(|h| h.get_key().to_lowercase() == header)
2426 && let Ok(addrs) = mailparse::addrparse_header(header)
2427 {
2428 for addr in addrs.iter() {
2429 match addr {
2430 mailparse::MailAddr::Single(info) => {
2431 result.push(SingleInfo {
2432 addr: addr_normalize(&info.addr).to_lowercase(),
2433 display_name: info.display_name.clone(),
2434 });
2435 }
2436 mailparse::MailAddr::Group(infos) => {
2437 for info in &infos.addrs {
2438 result.push(SingleInfo {
2439 addr: addr_normalize(&info.addr).to_lowercase(),
2440 display_name: info.display_name.clone(),
2441 });
2442 }
2443 }
2444 }
2445 }
2446 }
2447
2448 result
2449}
2450
2451async fn handle_mdn(
2452 context: &Context,
2453 from_id: ContactId,
2454 rfc724_mid: &str,
2455 timestamp_sent: i64,
2456) -> Result<()> {
2457 if from_id == ContactId::SELF {
2458 return Ok(());
2460 }
2461
2462 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2463 .sql
2464 .query_row_optional(
2465 "SELECT
2466 m.id AS msg_id,
2467 c.id AS chat_id,
2468 mdns.contact_id AS mdn_contact
2469 FROM msgs m
2470 LEFT JOIN chats c ON m.chat_id=c.id
2471 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
2472 WHERE rfc724_mid=? AND from_id=1
2473 ORDER BY msg_id DESC, mdn_contact=? DESC
2474 LIMIT 1",
2475 (&rfc724_mid, from_id),
2476 |row| {
2477 let msg_id: MsgId = row.get("msg_id")?;
2478 let chat_id: ChatId = row.get("chat_id")?;
2479 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2480 Ok((
2481 msg_id,
2482 chat_id,
2483 mdn_contact.is_some(),
2484 mdn_contact == Some(from_id),
2485 ))
2486 },
2487 )
2488 .await?
2489 else {
2490 info!(
2491 context,
2492 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2493 );
2494 return Ok(());
2495 };
2496
2497 if is_dup {
2498 return Ok(());
2499 }
2500 context
2501 .sql
2502 .execute(
2503 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2504 (msg_id, from_id, timestamp_sent),
2505 )
2506 .await?;
2507 if !has_mdns {
2508 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2509 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2511 }
2512 Ok(())
2513}
2514
2515async fn handle_ndn(
2518 context: &Context,
2519 failed: &DeliveryReport,
2520 error: Option<String>,
2521) -> Result<()> {
2522 if failed.rfc724_mid.is_empty() {
2523 return Ok(());
2524 }
2525
2526 let msg_ids = context
2529 .sql
2530 .query_map_vec(
2531 "SELECT id FROM msgs
2532 WHERE rfc724_mid=? AND from_id=1",
2533 (&failed.rfc724_mid,),
2534 |row| {
2535 let msg_id: MsgId = row.get(0)?;
2536 Ok(msg_id)
2537 },
2538 )
2539 .await?;
2540
2541 let error = if let Some(error) = error {
2542 error
2543 } else {
2544 "Delivery to at least one recipient failed.".to_string()
2545 };
2546 let err_msg = &error;
2547
2548 for msg_id in msg_ids {
2549 let mut message = Message::load_from_db(context, msg_id).await?;
2550 let chat = Chat::load_from_db(context, message.chat_id).await?;
2551 if chat.typ == constants::Chattype::OutBroadcast {
2552 continue;
2553 }
2554 let aggregated_error = message
2555 .error
2556 .as_ref()
2557 .map(|err| format!("{err}\n\n{err_msg}"));
2558 set_msg_failed(
2559 context,
2560 &mut message,
2561 aggregated_error.as_ref().unwrap_or(err_msg),
2562 )
2563 .await?;
2564 }
2565
2566 Ok(())
2567}
2568
2569#[cfg(test)]
2570mod mimeparser_tests;
2571#[cfg(test)]
2572mod shared_secret_decryption_tests;