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};
10use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
11use deltachat_derive::{FromSql, ToSql};
12use format_flowed::unformat_flowed;
13use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrparse_header};
14use mime::Mime;
15
16use crate::aheader::Aheader;
17use crate::authres::handle_authres;
18use crate::blob::BlobObject;
19use crate::chat::ChatId;
20use crate::config::Config;
21use crate::constants;
22use crate::contact::ContactId;
23use crate::context::Context;
24use crate::decrypt::{try_decrypt, validate_detached_signature};
25use crate::dehtml::dehtml;
26use crate::download::PostMsgMetadata;
27use crate::events::EventType;
28use crate::headerdef::{HeaderDef, HeaderDefMap};
29use crate::key::{self, DcKey, Fingerprint, SignedPublicKey, load_self_secret_keyring};
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>,
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,
202
203 SecurejoinMessage = 7,
205
206 LocationStreamingEnabled = 8,
208
209 LocationOnly = 9,
211
212 EphemeralTimerChanged = 10,
214
215 ChatProtectionEnabled = 11,
217
218 ChatProtectionDisabled = 12,
220
221 InvalidUnencryptedMail = 13,
224
225 SecurejoinWait = 14,
228
229 SecurejoinWaitTimeout = 15,
232
233 MultiDeviceSync = 20,
236
237 WebxdcStatusUpdate = 30,
241
242 WebxdcInfoMessage = 32,
244
245 IrohNodeAddr = 40,
247
248 ChatE2ee = 50,
250
251 CallAccepted = 66,
253
254 CallEnded = 67,
256}
257
258const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
259
260impl MimeMessage {
261 pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
266 let mail = mailparse::parse_mail(body)?;
267
268 let timestamp_rcvd = smeared_time(context);
269 let mut timestamp_sent =
270 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
271 let mut hop_info = parse_receive_headers(&mail.get_headers());
272
273 let mut headers = Default::default();
274 let mut headers_removed = HashSet::<String>::new();
275 let mut recipients = Default::default();
276 let mut past_members = Default::default();
277 let mut from = Default::default();
278 let mut list_post = Default::default();
279 let mut chat_disposition_notification_to = None;
280
281 MimeMessage::merge_headers(
283 context,
284 &mut headers,
285 &mut headers_removed,
286 &mut recipients,
287 &mut past_members,
288 &mut from,
289 &mut list_post,
290 &mut chat_disposition_notification_to,
291 &mail,
292 );
293 headers_removed.extend(
294 headers
295 .extract_if(|k, _v| is_hidden(k))
296 .map(|(k, _v)| k.to_string()),
297 );
298
299 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
301 let (part, mimetype) =
302 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
303 if let Some(part) = mail.subparts.first() {
304 timestamp_sent =
308 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
309 MimeMessage::merge_headers(
310 context,
311 &mut headers,
312 &mut headers_removed,
313 &mut recipients,
314 &mut past_members,
315 &mut from,
316 &mut list_post,
317 &mut chat_disposition_notification_to,
318 part,
319 );
320 (part, part.ctype.mimetype.parse::<Mime>()?)
321 } else {
322 (&mail, mimetype)
324 }
325 } else {
326 (&mail, mimetype)
328 };
329 if mimetype.type_() == mime::MULTIPART
330 && mimetype.subtype().as_str() == "mixed"
331 && let Some(part) = part.subparts.first()
332 {
333 for field in &part.headers {
334 let key = field.get_key().to_lowercase();
335 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
336 headers.insert(key.to_string(), field.get_value());
337 }
338 }
339 }
340
341 if let Some(microsoft_message_id) = remove_header(
345 &mut headers,
346 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
347 &mut headers_removed,
348 ) {
349 headers.insert(
350 HeaderDef::MessageId.get_headername().to_string(),
351 microsoft_message_id,
352 );
353 }
354
355 Self::remove_secured_headers(&mut headers, &mut headers_removed);
358
359 let mut from = from.context("No from in message")?;
360 let private_keyring = load_self_secret_keyring(context).await?;
361
362 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
363
364 let mut gossiped_keys = Default::default();
365 hop_info += "\n\n";
366 hop_info += &dkim_results.to_string();
367
368 let incoming = !context.is_self_addr(&from.addr).await?;
369
370 let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
371
372 let mut pre_message = if mail
373 .headers
374 .get_header_value(HeaderDef::ChatIsPostMessage)
375 .is_some()
376 {
377 PreMessageMode::Post
378 } else {
379 PreMessageMode::None
380 };
381
382 let mail_raw; let decrypted_msg; let secrets: Vec<String> = context
385 .sql
386 .query_map_vec("SELECT secret FROM broadcast_secrets", (), |row| {
387 let secret: String = row.get(0)?;
388 Ok(secret)
389 })
390 .await?;
391
392 let (mail, is_encrypted) =
393 match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring, &secrets)) {
394 Ok(Some(mut msg)) => {
395 mail_raw = msg.as_data_vec().unwrap_or_default();
396
397 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
398 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
399 info!(
400 context,
401 "decrypted message mime-body:\n{}",
402 String::from_utf8_lossy(&mail_raw),
403 );
404 }
405
406 decrypted_msg = Some(msg);
407
408 timestamp_sent = Self::get_timestamp_sent(
409 &decrypted_mail.headers,
410 timestamp_sent,
411 timestamp_rcvd,
412 );
413
414 let protected_aheader_values = decrypted_mail
415 .headers
416 .get_all_values(HeaderDef::Autocrypt.into());
417 if !protected_aheader_values.is_empty() {
418 aheader_values = protected_aheader_values;
419 }
420
421 (Ok(decrypted_mail), true)
422 }
423 Ok(None) => {
424 mail_raw = Vec::new();
425 decrypted_msg = None;
426 (Ok(mail), false)
427 }
428 Err(err) => {
429 mail_raw = Vec::new();
430 decrypted_msg = None;
431 warn!(context, "decryption failed: {:#}", err);
432 (Err(err), false)
433 }
434 };
435
436 let mut autocrypt_header = None;
437 if incoming {
438 for val in aheader_values.iter().rev() {
440 autocrypt_header = match Aheader::from_str(val) {
441 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
442 Ok(header) => {
443 warn!(
444 context,
445 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
446 );
447 continue;
448 }
449 Err(err) => {
450 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
451 continue;
452 }
453 };
454 break;
455 }
456 }
457
458 let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
459 let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
460 let inserted = context
461 .sql
462 .execute(
463 "INSERT INTO public_keys (fingerprint, public_key)
464 VALUES (?, ?)
465 ON CONFLICT (fingerprint)
466 DO NOTHING",
467 (&fingerprint, autocrypt_header.public_key.to_bytes()),
468 )
469 .await?;
470 if inserted > 0 {
471 info!(
472 context,
473 "Saved key with fingerprint {fingerprint} from the Autocrypt header"
474 );
475 }
476 Some(fingerprint)
477 } else {
478 None
479 };
480
481 let mut public_keyring = if incoming {
482 if let Some(autocrypt_header) = autocrypt_header {
483 vec![autocrypt_header.public_key]
484 } else {
485 vec![]
486 }
487 } else {
488 key::load_self_public_keyring(context).await?
489 };
490
491 if let Some(signature) = match &decrypted_msg {
492 Some(pgp::composed::Message::Literal { .. }) => None,
493 Some(pgp::composed::Message::Compressed { .. }) => {
494 None
497 }
498 Some(pgp::composed::Message::SignedOnePass { reader, .. }) => reader.signature(),
499 Some(pgp::composed::Message::Signed { reader, .. }) => Some(reader.signature()),
500 Some(pgp::composed::Message::Encrypted { .. }) => {
501 None
503 }
504 None => None,
505 } {
506 for issuer_fingerprint in signature.issuer_fingerprint() {
507 let issuer_fingerprint =
508 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
509 if let Some(public_key_bytes) = context
510 .sql
511 .query_row_optional(
512 "SELECT public_key
513 FROM public_keys
514 WHERE fingerprint=?",
515 (&issuer_fingerprint,),
516 |row| {
517 let bytes: Vec<u8> = row.get(0)?;
518 Ok(bytes)
519 },
520 )
521 .await?
522 {
523 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
524 public_keyring.push(public_key)
525 }
526 }
527 }
528
529 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
530 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
531 } else {
532 HashSet::new()
533 };
534
535 let mail = mail.as_ref().map(|mail| {
536 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
537 .unwrap_or((mail, Default::default()));
538 signatures.extend(signatures_detached);
539 content
540 });
541 if let (Ok(mail), true) = (mail, is_encrypted) {
542 if !signatures.is_empty() {
543 remove_header(&mut headers, "subject", &mut headers_removed);
547 remove_header(&mut headers, "list-id", &mut headers_removed);
548 }
549
550 let mut inner_from = None;
556
557 MimeMessage::merge_headers(
558 context,
559 &mut headers,
560 &mut headers_removed,
561 &mut recipients,
562 &mut past_members,
563 &mut inner_from,
564 &mut list_post,
565 &mut chat_disposition_notification_to,
566 mail,
567 );
568
569 if !signatures.is_empty() {
570 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
575 gossiped_keys =
576 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
577 }
578
579 if let Some(inner_from) = inner_from {
580 if !addr_cmp(&inner_from.addr, &from.addr) {
581 warn!(
590 context,
591 "From header in encrypted part doesn't match the outer one",
592 );
593
594 bail!("From header is forged");
599 }
600 from = inner_from;
601 }
602 }
603 if signatures.is_empty() {
604 Self::remove_secured_headers(&mut headers, &mut headers_removed);
605 }
606 if !is_encrypted {
607 signatures.clear();
608 }
609
610 if let (Ok(mail), true) = (mail, is_encrypted)
611 && let Some(post_msg_rfc724_mid) =
612 mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
613 {
614 let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
615 let metadata = if let Some(value) = mail
616 .headers
617 .get_header_value(HeaderDef::ChatPostMessageMetadata)
618 {
619 match PostMsgMetadata::try_from_header_value(&value) {
620 Ok(metadata) => Some(metadata),
621 Err(error) => {
622 error!(
623 context,
624 "Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
625 );
626 None
627 }
628 }
629 } else {
630 warn!(
631 context,
632 "Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
633 );
634 None
635 };
636
637 pre_message = PreMessageMode::Pre {
638 post_msg_rfc724_mid,
639 metadata,
640 };
641 }
642
643 let mut parser = MimeMessage {
644 parts: Vec::new(),
645 headers,
646 #[cfg(test)]
647 headers_removed,
648
649 recipients,
650 past_members,
651 list_post,
652 from,
653 incoming,
654 chat_disposition_notification_to,
655 decrypting_failed: mail.is_err(),
656
657 signature: signatures.into_iter().last(),
659 autocrypt_fingerprint,
660 gossiped_keys,
661 is_forwarded: false,
662 mdn_reports: Vec::new(),
663 is_system_message: SystemMessage::Unknown,
664 location_kml: None,
665 message_kml: None,
666 sync_items: None,
667 webxdc_status_update: None,
668 user_avatar: None,
669 group_avatar: None,
670 delivery_report: None,
671 footer: None,
672 is_mime_modified: false,
673 decoded_data: Vec::new(),
674 hop_info,
675 is_bot: None,
676 timestamp_rcvd,
677 timestamp_sent,
678 pre_message,
679 };
680
681 match mail {
682 Ok(mail) => {
683 parser.parse_mime_recursive(context, mail, false).await?;
684 }
685 Err(err) => {
686 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.]";
687
688 let part = Part {
689 typ: Viewtype::Text,
690 msg_raw: Some(txt.to_string()),
691 msg: txt.to_string(),
692 error: Some(format!("Decrypting failed: {err:#}")),
695 ..Default::default()
696 };
697 parser.do_add_single_part(part);
698 }
699 };
700
701 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
702 if parser.mdn_reports.is_empty()
703 && !is_location_only
704 && parser.sync_items.is_none()
705 && parser.webxdc_status_update.is_none()
706 {
707 let is_bot =
708 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
709 parser.is_bot = Some(is_bot);
710 }
711 parser.maybe_remove_bad_parts();
712 parser.maybe_remove_inline_mailinglist_footer();
713 parser.heuristically_parse_ndn(context).await;
714 parser.parse_headers(context).await?;
715 parser.decoded_data = mail_raw;
716
717 Ok(parser)
718 }
719
720 fn get_timestamp_sent(
721 hdrs: &[mailparse::MailHeader<'_>],
722 default: i64,
723 timestamp_rcvd: i64,
724 ) -> i64 {
725 hdrs.get_header_value(HeaderDef::Date)
726 .and_then(|v| mailparse::dateparse(&v).ok())
727 .map_or(default, |value| {
728 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
729 })
730 }
731
732 fn parse_system_message_headers(&mut self, context: &Context) {
734 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
735 self.parts.retain(|part| {
736 part.mimetype
737 .as_ref()
738 .is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
739 });
740
741 if self.parts.len() == 1 {
742 self.is_system_message = SystemMessage::AutocryptSetupMessage;
743 } else {
744 warn!(context, "could not determine ASM mime-part");
745 }
746 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
747 if value == "location-streaming-enabled" {
748 self.is_system_message = SystemMessage::LocationStreamingEnabled;
749 } else if value == "ephemeral-timer-changed" {
750 self.is_system_message = SystemMessage::EphemeralTimerChanged;
751 } else if value == "protection-enabled" {
752 self.is_system_message = SystemMessage::ChatProtectionEnabled;
753 } else if value == "protection-disabled" {
754 self.is_system_message = SystemMessage::ChatProtectionDisabled;
755 } else if value == "group-avatar-changed" {
756 self.is_system_message = SystemMessage::GroupImageChanged;
757 } else if value == "call-accepted" {
758 self.is_system_message = SystemMessage::CallAccepted;
759 } else if value == "call-ended" {
760 self.is_system_message = SystemMessage::CallEnded;
761 }
762 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
763 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
764 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
765 self.is_system_message = SystemMessage::MemberAddedToGroup;
766 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
767 self.is_system_message = SystemMessage::GroupNameChanged;
768 }
769 }
770
771 fn parse_avatar_headers(&mut self, context: &Context) -> Result<()> {
773 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
774 self.group_avatar =
775 self.avatar_action_from_header(context, header_value.to_string())?;
776 }
777
778 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
779 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string())?;
780 }
781 Ok(())
782 }
783
784 fn parse_videochat_headers(&mut self) {
785 let content = self
786 .get_header(HeaderDef::ChatContent)
787 .unwrap_or_default()
788 .to_string();
789 let room = self
790 .get_header(HeaderDef::ChatWebrtcRoom)
791 .map(|s| s.to_string());
792 let accepted = self
793 .get_header(HeaderDef::ChatWebrtcAccepted)
794 .map(|s| s.to_string());
795 if let Some(part) = self.parts.first_mut() {
796 if let Some(room) = room {
797 if content == "call" {
798 part.typ = Viewtype::Call;
799 part.param.set(Param::WebrtcRoom, room);
800 }
801 } else if let Some(accepted) = accepted {
802 part.param.set(Param::WebrtcAccepted, accepted);
803 }
804 }
805 }
806
807 fn squash_attachment_parts(&mut self) {
813 if self.parts.len() == 2
814 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
815 && self
816 .parts
817 .get(1)
818 .is_some_and(|filepart| match filepart.typ {
819 Viewtype::Image
820 | Viewtype::Gif
821 | Viewtype::Sticker
822 | Viewtype::Audio
823 | Viewtype::Voice
824 | Viewtype::Video
825 | Viewtype::Vcard
826 | Viewtype::File
827 | Viewtype::Webxdc => true,
828 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
829 })
830 {
831 let mut parts = std::mem::take(&mut self.parts);
832 let Some(mut filepart) = parts.pop() else {
833 return;
835 };
836 let Some(textpart) = parts.pop() else {
837 return;
839 };
840
841 filepart.msg.clone_from(&textpart.msg);
842 if let Some(quote) = textpart.param.get(Param::Quote) {
843 filepart.param.set(Param::Quote, quote);
844 }
845
846 self.parts = vec![filepart];
847 }
848 }
849
850 fn parse_attachments(&mut self) {
852 if self.parts.len() != 1 {
855 return;
856 }
857
858 if let Some(mut part) = self.parts.pop() {
859 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
860 {
861 part.typ = Viewtype::Voice;
862 }
863 if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
864 && let Some(value) = self.get_header(HeaderDef::ChatContent)
865 && value == "sticker"
866 {
867 part.typ = Viewtype::Sticker;
868 }
869 if (part.typ == Viewtype::Audio
870 || part.typ == Viewtype::Voice
871 || part.typ == Viewtype::Video)
872 && let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
873 {
874 let duration_ms = field_0.parse().unwrap_or_default();
875 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
876 part.param.set_int(Param::Duration, duration_ms);
877 }
878 }
879
880 self.parts.push(part);
881 }
882 }
883
884 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
885 self.parse_system_message_headers(context);
886 self.parse_avatar_headers(context)?;
887 self.parse_videochat_headers();
888 if self.delivery_report.is_none() {
889 self.squash_attachment_parts();
890 }
891
892 if !context.get_config_bool(Config::Bot).await?
893 && let Some(ref subject) = self.get_subject()
894 {
895 let mut prepend_subject = true;
896 if !self.decrypting_failed {
897 let colon = subject.find(':');
898 if colon == Some(2)
899 || colon == Some(3)
900 || self.has_chat_version()
901 || subject.contains("Chat:")
902 {
903 prepend_subject = false
904 }
905 }
906
907 if self.is_mailinglist_message() && !self.has_chat_version() {
910 prepend_subject = true;
911 }
912
913 if prepend_subject && !subject.is_empty() {
914 let part_with_text = self
915 .parts
916 .iter_mut()
917 .find(|part| !part.msg.is_empty() && !part.is_reaction);
918 if let Some(part) = part_with_text {
919 part.msg = format!("{} – {}", subject, part.msg);
924 }
925 }
926 }
927
928 if self.is_forwarded {
929 for part in &mut self.parts {
930 part.param.set_int(Param::Forwarded, 1);
931 }
932 }
933
934 self.parse_attachments();
935
936 if !self.decrypting_failed
938 && !self.parts.is_empty()
939 && let Some(ref dn_to) = self.chat_disposition_notification_to
940 {
941 let from = &self.from.addr;
943 if !context.is_self_addr(from).await? {
944 if from.to_lowercase() == dn_to.addr.to_lowercase() {
945 if let Some(part) = self.parts.last_mut() {
946 part.param.set_int(Param::WantsMdn, 1);
947 }
948 } else {
949 warn!(
950 context,
951 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
952 );
953 }
954 }
955 }
956
957 if self.parts.is_empty() && self.mdn_reports.is_empty() {
962 let mut part = Part {
963 typ: Viewtype::Text,
964 ..Default::default()
965 };
966
967 if let Some(ref subject) = self.get_subject()
968 && !self.has_chat_version()
969 && self.webxdc_status_update.is_none()
970 {
971 part.msg = subject.to_string();
972 }
973
974 self.do_add_single_part(part);
975 }
976
977 if self.is_bot == Some(true) {
978 for part in &mut self.parts {
979 part.param.set(Param::Bot, "1");
980 }
981 }
982
983 Ok(())
984 }
985
986 fn avatar_action_from_header(
987 &mut self,
988 context: &Context,
989 header_value: String,
990 ) -> Result<Option<AvatarAction>> {
991 let res = if header_value == "0" {
992 Some(AvatarAction::Delete)
993 } else if let Some(base64) = header_value
994 .split_ascii_whitespace()
995 .collect::<String>()
996 .strip_prefix("base64:")
997 {
998 match BlobObject::store_from_base64(context, base64)? {
999 Some(path) => Some(AvatarAction::Change(path)),
1000 None => {
1001 warn!(context, "Could not decode avatar base64");
1002 None
1003 }
1004 }
1005 } else {
1006 let mut i = 0;
1009 while let Some(part) = self.parts.get_mut(i) {
1010 if let Some(part_filename) = &part.org_filename
1011 && part_filename == &header_value
1012 {
1013 if let Some(blob) = part.param.get(Param::File) {
1014 let res = Some(AvatarAction::Change(blob.to_string()));
1015 self.parts.remove(i);
1016 return Ok(res);
1017 }
1018 break;
1019 }
1020 i += 1;
1021 }
1022 None
1023 };
1024 Ok(res)
1025 }
1026
1027 pub fn was_encrypted(&self) -> bool {
1033 self.signature.is_some()
1034 }
1035
1036 pub(crate) fn has_chat_version(&self) -> bool {
1039 self.headers.contains_key("chat-version")
1040 }
1041
1042 pub(crate) fn get_subject(&self) -> Option<String> {
1043 self.get_header(HeaderDef::Subject)
1044 .map(|s| s.trim_start())
1045 .filter(|s| !s.is_empty())
1046 .map(|s| s.to_string())
1047 }
1048
1049 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
1050 self.headers
1051 .get(headerdef.get_headername())
1052 .map(|s| s.as_str())
1053 }
1054
1055 #[cfg(test)]
1056 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1061 let hname = headerdef.get_headername();
1062 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1063 }
1064
1065 #[cfg(test)]
1066 pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
1068 assert!(!self.decrypting_failed);
1069 let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
1070 decoded_str.contains(s)
1071 }
1072
1073 pub fn get_chat_group_id(&self) -> Option<&str> {
1075 self.get_header(HeaderDef::ChatGroupId)
1076 .filter(|s| validate_id(s))
1077 }
1078
1079 async fn parse_mime_recursive<'a>(
1080 &'a mut self,
1081 context: &'a Context,
1082 mail: &'a mailparse::ParsedMail<'a>,
1083 is_related: bool,
1084 ) -> Result<bool> {
1085 enum MimeS {
1086 Multiple,
1087 Single,
1088 Message,
1089 }
1090
1091 let mimetype = mail.ctype.mimetype.to_lowercase();
1092
1093 let m = if mimetype.starts_with("multipart") {
1094 if mail.ctype.params.contains_key("boundary") {
1095 MimeS::Multiple
1096 } else {
1097 MimeS::Single
1098 }
1099 } else if mimetype.starts_with("message") {
1100 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1101 MimeS::Message
1102 } else {
1103 MimeS::Single
1104 }
1105 } else {
1106 MimeS::Single
1107 };
1108
1109 let is_related = is_related || mimetype == "multipart/related";
1110 match m {
1111 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1112 MimeS::Message => {
1113 let raw = mail.get_body_raw()?;
1114 if raw.is_empty() {
1115 return Ok(false);
1116 }
1117 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1118
1119 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1120 }
1121 MimeS::Single => {
1122 self.add_single_part_if_known(context, mail, is_related)
1123 .await
1124 }
1125 }
1126 }
1127
1128 async fn handle_multiple(
1129 &mut self,
1130 context: &Context,
1131 mail: &mailparse::ParsedMail<'_>,
1132 is_related: bool,
1133 ) -> Result<bool> {
1134 let mut any_part_added = false;
1135 let mimetype = get_mime_type(
1136 mail,
1137 &get_attachment_filename(context, mail)?,
1138 self.has_chat_version(),
1139 )?
1140 .0;
1141 match (mimetype.type_(), mimetype.subtype().as_str()) {
1142 (mime::MULTIPART, "alternative") => {
1143 for cur_data in mail.subparts.iter().rev() {
1155 let (mime_type, _viewtype) = get_mime_type(
1156 cur_data,
1157 &get_attachment_filename(context, cur_data)?,
1158 self.has_chat_version(),
1159 )?;
1160
1161 if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
1162 any_part_added = self
1163 .parse_mime_recursive(context, cur_data, is_related)
1164 .await?;
1165 break;
1166 }
1167 }
1168
1169 for cur_data in mail.subparts.iter().rev() {
1178 let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
1179 if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
1180 let filename = get_attachment_filename(context, cur_data)?
1181 .unwrap_or_else(|| "calendar.ics".to_string());
1182 self.do_add_single_file_part(
1183 context,
1184 Viewtype::File,
1185 mimetype,
1186 &mail.ctype.mimetype.to_lowercase(),
1187 &mail.get_body_raw()?,
1188 &filename,
1189 is_related,
1190 )
1191 .await?;
1192 }
1193 }
1194
1195 if !any_part_added {
1196 for cur_part in mail.subparts.iter().rev() {
1197 if self
1198 .parse_mime_recursive(context, cur_part, is_related)
1199 .await?
1200 {
1201 any_part_added = true;
1202 break;
1203 }
1204 }
1205 }
1206 if any_part_added && mail.subparts.len() > 1 {
1207 self.is_mime_modified = true;
1211 }
1212 }
1213 (mime::MULTIPART, "signed") => {
1214 if let Some(first) = mail.subparts.first() {
1223 any_part_added = self
1224 .parse_mime_recursive(context, first, is_related)
1225 .await?;
1226 }
1227 }
1228 (mime::MULTIPART, "report") => {
1229 if mail.subparts.len() >= 2 {
1231 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1232 Some("disposition-notification") => {
1233 if let Some(report) = self.process_report(context, mail)? {
1234 self.mdn_reports.push(report);
1235 }
1236
1237 let part = Part {
1242 typ: Viewtype::Unknown,
1243 ..Default::default()
1244 };
1245 self.parts.push(part);
1246
1247 any_part_added = true;
1248 }
1249 Some("delivery-status") | None => {
1251 if let Some(report) = self.process_delivery_status(context, mail)? {
1252 self.delivery_report = Some(report);
1253 }
1254
1255 for cur_data in &mail.subparts {
1257 if self
1258 .parse_mime_recursive(context, cur_data, is_related)
1259 .await?
1260 {
1261 any_part_added = true;
1262 }
1263 }
1264 }
1265 Some("multi-device-sync") => {
1266 if let Some(second) = mail.subparts.get(1) {
1267 self.add_single_part_if_known(context, second, is_related)
1268 .await?;
1269 }
1270 }
1271 Some("status-update") => {
1272 if let Some(second) = mail.subparts.get(1) {
1273 self.add_single_part_if_known(context, second, is_related)
1274 .await?;
1275 }
1276 }
1277 Some(_) => {
1278 for cur_data in &mail.subparts {
1279 if self
1280 .parse_mime_recursive(context, cur_data, is_related)
1281 .await?
1282 {
1283 any_part_added = true;
1284 }
1285 }
1286 }
1287 }
1288 }
1289 }
1290 _ => {
1291 for cur_data in &mail.subparts {
1294 if self
1295 .parse_mime_recursive(context, cur_data, is_related)
1296 .await?
1297 {
1298 any_part_added = true;
1299 }
1300 }
1301 }
1302 }
1303
1304 Ok(any_part_added)
1305 }
1306
1307 async fn add_single_part_if_known(
1309 &mut self,
1310 context: &Context,
1311 mail: &mailparse::ParsedMail<'_>,
1312 is_related: bool,
1313 ) -> Result<bool> {
1314 let filename = get_attachment_filename(context, mail)?;
1316 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1317 let raw_mime = mail.ctype.mimetype.to_lowercase();
1318
1319 let old_part_count = self.parts.len();
1320
1321 match filename {
1322 Some(filename) => {
1323 self.do_add_single_file_part(
1324 context,
1325 msg_type,
1326 mime_type,
1327 &raw_mime,
1328 &mail.get_body_raw()?,
1329 &filename,
1330 is_related,
1331 )
1332 .await?;
1333 }
1334 None => {
1335 match mime_type.type_() {
1336 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1337 warn!(context, "Missing attachment");
1338 return Ok(false);
1339 }
1340 mime::TEXT
1341 if mail.get_content_disposition().disposition
1342 == DispositionType::Extension("reaction".to_string()) =>
1343 {
1344 let decoded_data = match mail.get_body() {
1346 Ok(decoded_data) => decoded_data,
1347 Err(err) => {
1348 warn!(context, "Invalid body parsed {:#}", err);
1349 return Ok(false);
1351 }
1352 };
1353
1354 let part = Part {
1355 typ: Viewtype::Text,
1356 mimetype: Some(mime_type),
1357 msg: decoded_data,
1358 is_reaction: true,
1359 ..Default::default()
1360 };
1361 self.do_add_single_part(part);
1362 return Ok(true);
1363 }
1364 mime::TEXT | mime::HTML => {
1365 let decoded_data = match mail.get_body() {
1366 Ok(decoded_data) => decoded_data,
1367 Err(err) => {
1368 warn!(context, "Invalid body parsed {:#}", err);
1369 return Ok(false);
1371 }
1372 };
1373
1374 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1375 let mut dehtml_failed = false;
1376
1377 let SimplifiedText {
1378 text: simplified_txt,
1379 is_forwarded,
1380 is_cut,
1381 top_quote,
1382 footer,
1383 } = if decoded_data.is_empty() {
1384 Default::default()
1385 } else {
1386 let is_html = mime_type == mime::TEXT_HTML;
1387 if is_html {
1388 self.is_mime_modified = true;
1389 if let Some(text) = dehtml(&decoded_data) {
1394 text
1395 } else {
1396 dehtml_failed = true;
1397 SimplifiedText {
1398 text: decoded_data.clone(),
1399 ..Default::default()
1400 }
1401 }
1402 } else {
1403 simplify(decoded_data.clone(), self.has_chat_version())
1404 }
1405 };
1406
1407 self.is_mime_modified = self.is_mime_modified
1408 || ((is_forwarded || is_cut || top_quote.is_some())
1409 && !self.has_chat_version());
1410
1411 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1412 {
1413 format.as_str().eq_ignore_ascii_case("flowed")
1414 } else {
1415 false
1416 };
1417
1418 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1419 && mime_type.subtype() == mime::PLAIN
1420 {
1421 let simplified_txt = match mail
1424 .ctype
1425 .params
1426 .get("hp-legacy-display")
1427 .is_some_and(|v| v == "1")
1428 {
1429 false => simplified_txt,
1430 true => rm_legacy_display_elements(&simplified_txt),
1431 };
1432 if is_format_flowed {
1433 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1434 delsp.as_str().eq_ignore_ascii_case("yes")
1435 } else {
1436 false
1437 };
1438 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1439 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1440 (unflowed_text, unflowed_quote)
1441 } else {
1442 (simplified_txt, top_quote)
1443 }
1444 } else {
1445 (simplified_txt, top_quote)
1446 };
1447
1448 let (simplified_txt, was_truncated) =
1449 truncate_msg_text(context, simplified_txt).await?;
1450 if was_truncated {
1451 self.is_mime_modified = was_truncated;
1452 }
1453
1454 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1455 let mut part = Part {
1456 dehtml_failed,
1457 typ: Viewtype::Text,
1458 mimetype: Some(mime_type),
1459 msg: simplified_txt,
1460 ..Default::default()
1461 };
1462 if let Some(quote) = simplified_quote {
1463 part.param.set(Param::Quote, quote);
1464 }
1465 part.msg_raw = Some(decoded_data);
1466 self.do_add_single_part(part);
1467 }
1468
1469 if is_forwarded {
1470 self.is_forwarded = true;
1471 }
1472
1473 if self.footer.is_none() && is_plaintext {
1474 self.footer = Some(footer.unwrap_or_default());
1475 }
1476 }
1477 _ => {}
1478 }
1479 }
1480 }
1481
1482 Ok(self.parts.len() > old_part_count)
1484 }
1485
1486 #[expect(clippy::too_many_arguments)]
1487 async fn do_add_single_file_part(
1488 &mut self,
1489 context: &Context,
1490 msg_type: Viewtype,
1491 mime_type: Mime,
1492 raw_mime: &str,
1493 decoded_data: &[u8],
1494 filename: &str,
1495 is_related: bool,
1496 ) -> Result<()> {
1497 if mime_type.type_() == mime::APPLICATION
1499 && mime_type.subtype().as_str() == "pgp-keys"
1500 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1501 {
1502 return Ok(());
1503 }
1504 let mut part = Part::default();
1505 let msg_type = if context
1506 .is_webxdc_file(filename, decoded_data)
1507 .await
1508 .unwrap_or(false)
1509 {
1510 Viewtype::Webxdc
1511 } else if filename.ends_with(".kml") {
1512 if filename.starts_with("location") || filename.starts_with("message") {
1515 let parsed = location::Kml::parse(decoded_data)
1516 .map_err(|err| {
1517 warn!(context, "failed to parse kml part: {:#}", err);
1518 })
1519 .ok();
1520 if filename.starts_with("location") {
1521 self.location_kml = parsed;
1522 } else {
1523 self.message_kml = parsed;
1524 }
1525 return Ok(());
1526 }
1527 msg_type
1528 } else if filename == "multi-device-sync.json" {
1529 if !context.get_config_bool(Config::SyncMsgs).await? {
1530 return Ok(());
1531 }
1532 let serialized = String::from_utf8_lossy(decoded_data)
1533 .parse()
1534 .unwrap_or_default();
1535 self.sync_items = context
1536 .parse_sync_items(serialized)
1537 .map_err(|err| {
1538 warn!(context, "failed to parse sync data: {:#}", err);
1539 })
1540 .ok();
1541 return Ok(());
1542 } else if filename == "status-update.json" {
1543 let serialized = String::from_utf8_lossy(decoded_data)
1544 .parse()
1545 .unwrap_or_default();
1546 self.webxdc_status_update = Some(serialized);
1547 return Ok(());
1548 } else if msg_type == Viewtype::Vcard {
1549 if let Some(summary) = get_vcard_summary(decoded_data) {
1550 part.param.set(Param::Summary1, summary);
1551 msg_type
1552 } else {
1553 Viewtype::File
1554 }
1555 } else if msg_type == Viewtype::Image
1556 || msg_type == Viewtype::Gif
1557 || msg_type == Viewtype::Sticker
1558 {
1559 match get_filemeta(decoded_data) {
1560 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1562 part.param.set_i64(Param::Width, width.into());
1563 part.param.set_i64(Param::Height, height.into());
1564 msg_type
1565 }
1566 _ => Viewtype::File,
1568 }
1569 } else {
1570 msg_type
1571 };
1572
1573 let blob =
1577 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1578 Ok(blob) => blob,
1579 Err(err) => {
1580 error!(
1581 context,
1582 "Could not add blob for mime part {}, error {:#}", filename, err
1583 );
1584 return Ok(());
1585 }
1586 };
1587 info!(context, "added blobfile: {:?}", blob.as_name());
1588
1589 part.typ = msg_type;
1590 part.org_filename = Some(filename.to_string());
1591 part.mimetype = Some(mime_type);
1592 part.bytes = decoded_data.len();
1593 part.param.set(Param::File, blob.as_name());
1594 part.param.set(Param::Filename, filename);
1595 part.param.set(Param::MimeType, raw_mime);
1596 part.is_related = is_related;
1597
1598 self.do_add_single_part(part);
1599 Ok(())
1600 }
1601
1602 async fn try_set_peer_key_from_file_part(
1604 context: &Context,
1605 decoded_data: &[u8],
1606 ) -> Result<bool> {
1607 let key = match str::from_utf8(decoded_data) {
1608 Err(err) => {
1609 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1610 return Ok(false);
1611 }
1612 Ok(key) => key,
1613 };
1614 let key = match SignedPublicKey::from_asc(key) {
1615 Err(err) => {
1616 warn!(
1617 context,
1618 "PGP key attachment is not an ASCII-armored file: {err:#}."
1619 );
1620 return Ok(false);
1621 }
1622 Ok(key) => key,
1623 };
1624 if let Err(err) = key.verify() {
1625 warn!(context, "Attached PGP key verification failed: {err:#}.");
1626 return Ok(false);
1627 }
1628
1629 let fingerprint = key.dc_fingerprint().hex();
1630 context
1631 .sql
1632 .execute(
1633 "INSERT INTO public_keys (fingerprint, public_key)
1634 VALUES (?, ?)
1635 ON CONFLICT (fingerprint)
1636 DO NOTHING",
1637 (&fingerprint, key.to_bytes()),
1638 )
1639 .await?;
1640
1641 info!(context, "Imported PGP key {fingerprint} from attachment.");
1642 Ok(true)
1643 }
1644
1645 pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
1646 if self.was_encrypted() {
1647 part.param.set_int(Param::GuaranteeE2ee, 1);
1648 }
1649 self.parts.push(part);
1650 }
1651
1652 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1653 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1654 return Some(list_id);
1657 } else if let Some(chat_list_id) = self.get_header(HeaderDef::ChatListId) {
1658 return Some(chat_list_id);
1659 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1660 if let Some(precedence) = self.get_header(HeaderDef::Precedence)
1663 && (precedence == "list" || precedence == "bulk")
1664 {
1665 return Some(sender);
1669 }
1670 }
1671 None
1672 }
1673
1674 pub(crate) fn is_mailinglist_message(&self) -> bool {
1675 self.get_mailinglist_header().is_some()
1676 }
1677
1678 pub(crate) fn is_schleuder_message(&self) -> bool {
1680 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1681 list_help == "<https://schleuder.org/>"
1682 } else {
1683 false
1684 }
1685 }
1686
1687 pub(crate) fn is_call(&self) -> bool {
1689 self.parts
1690 .first()
1691 .is_some_and(|part| part.typ == Viewtype::Call)
1692 }
1693
1694 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1695 self.get_header(HeaderDef::MessageId)
1696 .and_then(|msgid| parse_message_id(msgid).ok())
1697 }
1698
1699 fn remove_secured_headers(
1700 headers: &mut HashMap<String, String>,
1701 removed: &mut HashSet<String>,
1702 ) {
1703 remove_header(headers, "secure-join-fingerprint", removed);
1704 remove_header(headers, "secure-join-auth", removed);
1705 remove_header(headers, "chat-verified", removed);
1706 remove_header(headers, "autocrypt-gossip", removed);
1707
1708 if let Some(secure_join) = remove_header(headers, "secure-join", removed)
1710 && (secure_join == "vc-request" || secure_join == "vg-request")
1711 {
1712 headers.insert("secure-join".to_string(), secure_join);
1713 }
1714 }
1715
1716 #[allow(clippy::too_many_arguments)]
1722 fn merge_headers(
1723 context: &Context,
1724 headers: &mut HashMap<String, String>,
1725 headers_removed: &mut HashSet<String>,
1726 recipients: &mut Vec<SingleInfo>,
1727 past_members: &mut Vec<SingleInfo>,
1728 from: &mut Option<SingleInfo>,
1729 list_post: &mut Option<String>,
1730 chat_disposition_notification_to: &mut Option<SingleInfo>,
1731 part: &mailparse::ParsedMail,
1732 ) {
1733 let fields = &part.headers;
1734 let has_header_protection = part.ctype.params.contains_key("hp");
1736
1737 headers_removed.extend(
1738 headers
1739 .extract_if(|k, _v| has_header_protection || is_protected(k))
1740 .map(|(k, _v)| k.to_string()),
1741 );
1742 for field in fields {
1743 let key = field.get_key().to_lowercase();
1745 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1746 match addrparse_header(field) {
1747 Ok(addrlist) => {
1748 *chat_disposition_notification_to = addrlist.extract_single_info();
1749 }
1750 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1751 }
1752 } else {
1753 let value = field.get_value();
1754 headers.insert(key.to_string(), value);
1755 }
1756 }
1757 let recipients_new = get_recipients(fields);
1758 if !recipients_new.is_empty() {
1759 *recipients = recipients_new;
1760 }
1761 let past_members_addresses =
1762 get_all_addresses_from_header(fields, "chat-group-past-members");
1763 if !past_members_addresses.is_empty() {
1764 *past_members = past_members_addresses;
1765 }
1766 let from_new = get_from(fields);
1767 if from_new.is_some() {
1768 *from = from_new;
1769 }
1770 let list_post_new = get_list_post(fields);
1771 if list_post_new.is_some() {
1772 *list_post = list_post_new;
1773 }
1774 }
1775
1776 fn process_report(
1777 &self,
1778 context: &Context,
1779 report: &mailparse::ParsedMail<'_>,
1780 ) -> Result<Option<Report>> {
1781 let report_body = if let Some(subpart) = report.subparts.get(1) {
1783 subpart.get_body_raw()?
1784 } else {
1785 bail!("Report does not have second MIME part");
1786 };
1787 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1788
1789 if report_fields
1791 .get_header_value(HeaderDef::Disposition)
1792 .is_none()
1793 {
1794 warn!(
1795 context,
1796 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1797 report_fields.get_header_value(HeaderDef::MessageId)
1798 );
1799 return Ok(None);
1800 };
1801
1802 let original_message_id = report_fields
1803 .get_header_value(HeaderDef::OriginalMessageId)
1804 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1807 .and_then(|v| parse_message_id(&v).ok());
1808 let additional_message_ids = report_fields
1809 .get_header_value(HeaderDef::AdditionalMessageIds)
1810 .map_or_else(Vec::new, |v| {
1811 v.split(' ')
1812 .filter_map(|s| parse_message_id(s).ok())
1813 .collect()
1814 });
1815
1816 Ok(Some(Report {
1817 original_message_id,
1818 additional_message_ids,
1819 }))
1820 }
1821
1822 fn process_delivery_status(
1823 &self,
1824 context: &Context,
1825 report: &mailparse::ParsedMail<'_>,
1826 ) -> Result<Option<DeliveryReport>> {
1827 let mut failure = true;
1829
1830 if let Some(status_part) = report.subparts.get(1) {
1831 if status_part.ctype.mimetype != "message/delivery-status"
1834 && status_part.ctype.mimetype != "message/global-delivery-status"
1835 {
1836 warn!(
1837 context,
1838 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1839 );
1840 return Ok(None);
1841 }
1842
1843 let status_body = status_part.get_body_raw()?;
1844
1845 let (_, sz) = mailparse::parse_headers(&status_body)?;
1847
1848 if let Some(status_body) = status_body.get(sz..) {
1850 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1851 if let Some(action) = status_fields.get_first_value("action") {
1852 if action != "failed" {
1853 info!(context, "DSN with {:?} action", action);
1854 failure = false;
1855 }
1856 } else {
1857 warn!(context, "DSN without action");
1858 }
1859 } else {
1860 warn!(context, "DSN without per-recipient fields");
1861 }
1862 } else {
1863 return Ok(None);
1865 }
1866
1867 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1869 p.ctype.mimetype.contains("rfc822")
1870 || p.ctype.mimetype == "message/global"
1871 || p.ctype.mimetype == "message/global-headers"
1872 }) {
1873 let report_body = original_msg.get_body_raw()?;
1874 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1875
1876 if let Some(original_message_id) = report_fields
1877 .get_header_value(HeaderDef::MessageId)
1878 .and_then(|v| parse_message_id(&v).ok())
1879 {
1880 return Ok(Some(DeliveryReport {
1881 rfc724_mid: original_message_id,
1882 failure,
1883 }));
1884 }
1885
1886 warn!(
1887 context,
1888 "ignoring unknown ndn-notification, Message-Id: {:?}",
1889 report_fields.get_header_value(HeaderDef::MessageId)
1890 );
1891 }
1892
1893 Ok(None)
1894 }
1895
1896 fn maybe_remove_bad_parts(&mut self) {
1897 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1898 if good_parts == 0 {
1899 self.parts.truncate(1);
1901 } else if good_parts < self.parts.len() {
1902 self.parts.retain(|p| !p.dehtml_failed);
1903 }
1904
1905 if !self.has_chat_version() && self.is_mime_modified {
1913 fn is_related_image(p: &&Part) -> bool {
1914 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1915 }
1916 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1917 if related_image_cnt > 1 {
1918 let mut is_first_image = true;
1919 self.parts.retain(|p| {
1920 let retain = is_first_image || !is_related_image(&p);
1921 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1922 is_first_image = false;
1923 }
1924 retain
1925 });
1926 }
1927 }
1928 }
1929
1930 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1940 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1941 let text_part_cnt = self
1942 .parts
1943 .iter()
1944 .filter(|p| p.typ == Viewtype::Text)
1945 .count();
1946 if text_part_cnt == 2
1947 && let Some(last_part) = self.parts.last()
1948 && last_part.typ == Viewtype::Text
1949 {
1950 self.parts.pop();
1951 }
1952 }
1953 }
1954
1955 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1959 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1960 let from = from.to_ascii_lowercase();
1961 from.contains("mailer-daemon") || from.contains("mail-daemon")
1962 } else {
1963 false
1964 };
1965 if maybe_ndn && self.delivery_report.is_none() {
1966 for original_message_id in self
1967 .parts
1968 .iter()
1969 .filter_map(|part| part.msg_raw.as_ref())
1970 .flat_map(|part| part.lines())
1971 .filter_map(|line| line.split_once("Message-ID:"))
1972 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1973 {
1974 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1975 {
1976 self.delivery_report = Some(DeliveryReport {
1977 rfc724_mid: original_message_id,
1978 failure: true,
1979 })
1980 }
1981 }
1982 }
1983 }
1984
1985 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1989 for report in &self.mdn_reports {
1990 for original_message_id in report
1991 .original_message_id
1992 .iter()
1993 .chain(&report.additional_message_ids)
1994 {
1995 if let Err(err) =
1996 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1997 {
1998 warn!(context, "Could not handle MDN: {err:#}.");
1999 }
2000 }
2001 }
2002
2003 if let Some(delivery_report) = &self.delivery_report
2004 && delivery_report.failure
2005 {
2006 let error = parts
2007 .iter()
2008 .find(|p| p.typ == Viewtype::Text)
2009 .map(|p| p.msg.clone());
2010 if let Err(err) = handle_ndn(context, delivery_report, error).await {
2011 warn!(context, "Could not handle NDN: {err:#}.");
2012 }
2013 }
2014 }
2015
2016 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
2021 let parent_timestamp = if let Some(field) = self
2022 .get_header(HeaderDef::InReplyTo)
2023 .and_then(|msgid| parse_message_id(msgid).ok())
2024 {
2025 context
2026 .sql
2027 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
2028 .await?
2029 } else {
2030 None
2031 };
2032 Ok(parent_timestamp)
2033 }
2034
2035 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 value.parse::<Aheader>() {
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 let fingerprint = header.public_key.dc_fingerprint().hex();
2133 context
2134 .sql
2135 .execute(
2136 "INSERT INTO public_keys (fingerprint, public_key)
2137 VALUES (?, ?)
2138 ON CONFLICT (fingerprint)
2139 DO NOTHING",
2140 (&fingerprint, header.public_key.to_bytes()),
2141 )
2142 .await?;
2143
2144 let gossiped_key = GossipedKey {
2145 public_key: header.public_key,
2146
2147 verified: header.verified,
2148 };
2149 gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2150 }
2151
2152 Ok(gossiped_keys)
2153}
2154
2155#[derive(Debug)]
2157pub(crate) struct Report {
2158 pub original_message_id: Option<String>,
2163 pub additional_message_ids: Vec<String>,
2165}
2166
2167#[derive(Debug)]
2169pub(crate) struct DeliveryReport {
2170 pub rfc724_mid: String,
2171 pub failure: bool,
2172}
2173
2174pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2175 let mut msgids = Vec::new();
2177 for id in ids.split_whitespace() {
2178 let mut id = id.to_string();
2179 if let Some(id_without_prefix) = id.strip_prefix('<') {
2180 id = id_without_prefix.to_string();
2181 };
2182 if let Some(id_without_suffix) = id.strip_suffix('>') {
2183 id = id_without_suffix.to_string();
2184 };
2185 if !id.is_empty() {
2186 msgids.push(id);
2187 }
2188 }
2189 msgids
2190}
2191
2192pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2193 if let Some(id) = parse_message_ids(ids).first() {
2194 Ok(id.to_string())
2195 } else {
2196 bail!("could not parse message_id: {ids}");
2197 }
2198}
2199
2200fn is_protected(key: &str) -> bool {
2207 key.starts_with("chat-")
2208 || matches!(
2209 key,
2210 "return-path"
2211 | "auto-submitted"
2212 | "autocrypt-setup-message"
2213 | "date"
2214 | "from"
2215 | "sender"
2216 | "reply-to"
2217 | "to"
2218 | "cc"
2219 | "bcc"
2220 | "message-id"
2221 | "in-reply-to"
2222 | "references"
2223 | "secure-join"
2224 )
2225}
2226
2227pub(crate) fn is_hidden(key: &str) -> bool {
2229 matches!(
2230 key,
2231 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2232 )
2233}
2234
2235#[derive(Debug, Default, Clone)]
2237pub struct Part {
2238 pub typ: Viewtype,
2240
2241 pub mimetype: Option<Mime>,
2243
2244 pub msg: String,
2246
2247 pub msg_raw: Option<String>,
2249
2250 pub bytes: usize,
2252
2253 pub param: Params,
2255
2256 pub(crate) org_filename: Option<String>,
2258
2259 pub error: Option<String>,
2261
2262 pub(crate) dehtml_failed: bool,
2264
2265 pub(crate) is_related: bool,
2272
2273 pub(crate) is_reaction: bool,
2275}
2276
2277fn get_mime_type(
2282 mail: &mailparse::ParsedMail<'_>,
2283 filename: &Option<String>,
2284 is_chat_msg: bool,
2285) -> Result<(Mime, Viewtype)> {
2286 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2287
2288 let viewtype = match mimetype.type_() {
2289 mime::TEXT => match mimetype.subtype() {
2290 mime::VCARD => Viewtype::Vcard,
2291 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2292 _ => Viewtype::File,
2293 },
2294 mime::IMAGE => match mimetype.subtype() {
2295 mime::GIF => Viewtype::Gif,
2296 mime::SVG => Viewtype::File,
2297 _ => Viewtype::Image,
2298 },
2299 mime::AUDIO => Viewtype::Audio,
2300 mime::VIDEO => Viewtype::Video,
2301 mime::MULTIPART => Viewtype::Unknown,
2302 mime::MESSAGE => {
2303 if is_attachment_disposition(mail) {
2304 Viewtype::File
2305 } else {
2306 Viewtype::Unknown
2314 }
2315 }
2316 mime::APPLICATION => match mimetype.subtype() {
2317 mime::OCTET_STREAM => match filename {
2318 Some(filename) if !is_chat_msg => {
2319 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2320 Some((viewtype, _)) => viewtype,
2321 None => Viewtype::File,
2322 }
2323 }
2324 _ => Viewtype::File,
2325 },
2326 _ => Viewtype::File,
2327 },
2328 _ => Viewtype::Unknown,
2329 };
2330
2331 Ok((mimetype, viewtype))
2332}
2333
2334fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2335 let ct = mail.get_content_disposition();
2336 ct.disposition == DispositionType::Attachment
2337 && ct
2338 .params
2339 .iter()
2340 .any(|(key, _value)| key.starts_with("filename"))
2341}
2342
2343fn get_attachment_filename(
2350 context: &Context,
2351 mail: &mailparse::ParsedMail,
2352) -> Result<Option<String>> {
2353 let ct = mail.get_content_disposition();
2354
2355 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2358
2359 if desired_filename.is_none()
2360 && let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
2361 {
2362 warn!(context, "apostrophed encoding invalid: {}", name);
2366 desired_filename = Some(name);
2367 }
2368
2369 if desired_filename.is_none() {
2371 desired_filename = ct.params.get("name").map(|s| s.to_string());
2372 }
2373
2374 if desired_filename.is_none() {
2377 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2378 }
2379
2380 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2382 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2383 desired_filename = Some(format!("file.{subtype}",));
2384 } else {
2385 bail!(
2386 "could not determine attachment filename: {:?}",
2387 ct.disposition
2388 );
2389 };
2390 }
2391
2392 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2393
2394 Ok(desired_filename)
2395}
2396
2397pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2399 let to_addresses = get_all_addresses_from_header(headers, "to");
2400 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2401
2402 let mut res = to_addresses;
2403 res.extend(cc_addresses);
2404 res
2405}
2406
2407pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2409 let all = get_all_addresses_from_header(headers, "from");
2410 tools::single_value(all)
2411}
2412
2413pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2415 get_all_addresses_from_header(headers, "list-post")
2416 .into_iter()
2417 .next()
2418 .map(|s| s.addr)
2419}
2420
2421fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2433 let mut result: Vec<SingleInfo> = Default::default();
2434
2435 if let Some(header) = headers
2436 .iter()
2437 .rev()
2438 .find(|h| h.get_key().to_lowercase() == header)
2439 && let Ok(addrs) = mailparse::addrparse_header(header)
2440 {
2441 for addr in addrs.iter() {
2442 match addr {
2443 mailparse::MailAddr::Single(info) => {
2444 result.push(SingleInfo {
2445 addr: addr_normalize(&info.addr).to_lowercase(),
2446 display_name: info.display_name.clone(),
2447 });
2448 }
2449 mailparse::MailAddr::Group(infos) => {
2450 for info in &infos.addrs {
2451 result.push(SingleInfo {
2452 addr: addr_normalize(&info.addr).to_lowercase(),
2453 display_name: info.display_name.clone(),
2454 });
2455 }
2456 }
2457 }
2458 }
2459 }
2460
2461 result
2462}
2463
2464async fn handle_mdn(
2465 context: &Context,
2466 from_id: ContactId,
2467 rfc724_mid: &str,
2468 timestamp_sent: i64,
2469) -> Result<()> {
2470 if from_id == ContactId::SELF {
2471 return Ok(());
2473 }
2474
2475 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2476 .sql
2477 .query_row_optional(
2478 "SELECT
2479 m.id AS msg_id,
2480 c.id AS chat_id,
2481 mdns.contact_id AS mdn_contact
2482 FROM msgs m
2483 LEFT JOIN chats c ON m.chat_id=c.id
2484 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
2485 WHERE rfc724_mid=? AND from_id=1
2486 ORDER BY msg_id DESC, mdn_contact=? DESC
2487 LIMIT 1",
2488 (&rfc724_mid, from_id),
2489 |row| {
2490 let msg_id: MsgId = row.get("msg_id")?;
2491 let chat_id: ChatId = row.get("chat_id")?;
2492 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2493 Ok((
2494 msg_id,
2495 chat_id,
2496 mdn_contact.is_some(),
2497 mdn_contact == Some(from_id),
2498 ))
2499 },
2500 )
2501 .await?
2502 else {
2503 info!(
2504 context,
2505 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2506 );
2507 return Ok(());
2508 };
2509
2510 if is_dup {
2511 return Ok(());
2512 }
2513 context
2514 .sql
2515 .execute(
2516 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2517 (msg_id, from_id, timestamp_sent),
2518 )
2519 .await?;
2520 if !has_mdns {
2521 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2522 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2524 }
2525 Ok(())
2526}
2527
2528async fn handle_ndn(
2531 context: &Context,
2532 failed: &DeliveryReport,
2533 error: Option<String>,
2534) -> Result<()> {
2535 if failed.rfc724_mid.is_empty() {
2536 return Ok(());
2537 }
2538
2539 let msg_ids = context
2542 .sql
2543 .query_map_vec(
2544 "SELECT id FROM msgs
2545 WHERE rfc724_mid=? AND from_id=1",
2546 (&failed.rfc724_mid,),
2547 |row| {
2548 let msg_id: MsgId = row.get(0)?;
2549 Ok(msg_id)
2550 },
2551 )
2552 .await?;
2553
2554 let error = if let Some(error) = error {
2555 error
2556 } else {
2557 "Delivery to at least one recipient failed.".to_string()
2558 };
2559 let err_msg = &error;
2560
2561 for msg_id in msg_ids {
2562 let mut message = Message::load_from_db(context, msg_id).await?;
2563 let aggregated_error = message
2564 .error
2565 .as_ref()
2566 .map(|err| format!("{err}\n\n{err_msg}"));
2567 set_msg_failed(
2568 context,
2569 &mut message,
2570 aggregated_error.as_ref().unwrap_or(err_msg),
2571 )
2572 .await?;
2573 }
2574
2575 Ok(())
2576}
2577
2578#[cfg(test)]
2579mod mimeparser_tests;