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