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