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, 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,
203
204 SecurejoinMessage = 7,
206
207 LocationStreamingEnabled = 8,
209
210 LocationOnly = 9,
212
213 EphemeralTimerChanged = 10,
215
216 ChatProtectionEnabled = 11,
218
219 ChatProtectionDisabled = 12,
221
222 InvalidUnencryptedMail = 13,
225
226 SecurejoinWait = 14,
229
230 SecurejoinWaitTimeout = 15,
233
234 MultiDeviceSync = 20,
237
238 WebxdcStatusUpdate = 30,
242
243 WebxdcInfoMessage = 32,
245
246 IrohNodeAddr = 40,
248
249 ChatE2ee = 50,
251
252 CallAccepted = 66,
254
255 CallEnded = 67,
257
258 GroupDescriptionChanged = 70,
260}
261
262const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
263
264impl MimeMessage {
265 pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
270 let mail = mailparse::parse_mail(body)?;
271
272 let timestamp_rcvd = smeared_time(context);
273 let mut timestamp_sent =
274 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
275 let mut hop_info = parse_receive_headers(&mail.get_headers());
276
277 let mut headers = Default::default();
278 let mut headers_removed = HashSet::<String>::new();
279 let mut recipients = Default::default();
280 let mut past_members = Default::default();
281 let mut from = Default::default();
282 let mut list_post = Default::default();
283 let mut chat_disposition_notification_to = None;
284
285 MimeMessage::merge_headers(
287 context,
288 &mut headers,
289 &mut headers_removed,
290 &mut recipients,
291 &mut past_members,
292 &mut from,
293 &mut list_post,
294 &mut chat_disposition_notification_to,
295 &mail,
296 );
297 headers_removed.extend(
298 headers
299 .extract_if(|k, _v| is_hidden(k))
300 .map(|(k, _v)| k.to_string()),
301 );
302
303 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
305 let (part, mimetype) =
306 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
307 if let Some(part) = mail.subparts.first() {
308 timestamp_sent =
312 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
313 MimeMessage::merge_headers(
314 context,
315 &mut headers,
316 &mut headers_removed,
317 &mut recipients,
318 &mut past_members,
319 &mut from,
320 &mut list_post,
321 &mut chat_disposition_notification_to,
322 part,
323 );
324 (part, part.ctype.mimetype.parse::<Mime>()?)
325 } else {
326 (&mail, mimetype)
328 }
329 } else {
330 (&mail, mimetype)
332 };
333 if mimetype.type_() == mime::MULTIPART
334 && mimetype.subtype().as_str() == "mixed"
335 && let Some(part) = part.subparts.first()
336 {
337 for field in &part.headers {
338 let key = field.get_key().to_lowercase();
339 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
340 headers.insert(key.to_string(), field.get_value());
341 }
342 }
343 }
344
345 if let Some(microsoft_message_id) = remove_header(
349 &mut headers,
350 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
351 &mut headers_removed,
352 ) {
353 headers.insert(
354 HeaderDef::MessageId.get_headername().to_string(),
355 microsoft_message_id,
356 );
357 }
358
359 Self::remove_secured_headers(&mut headers, &mut headers_removed);
362
363 let mut from = from.context("No from in message")?;
364 let private_keyring = load_self_secret_keyring(context).await?;
365
366 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
367
368 let mut gossiped_keys = Default::default();
369 hop_info += "\n\n";
370 hop_info += &dkim_results.to_string();
371
372 let incoming = !context.is_self_addr(&from.addr).await?;
373
374 let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
375
376 let mut pre_message = if mail
377 .headers
378 .get_header_value(HeaderDef::ChatIsPostMessage)
379 .is_some()
380 {
381 PreMessageMode::Post
382 } else {
383 PreMessageMode::None
384 };
385
386 let mail_raw; let decrypted_msg; let secrets: Vec<String> = context
389 .sql
390 .query_map_vec("SELECT secret FROM broadcast_secrets", (), |row| {
391 let secret: String = row.get(0)?;
392 Ok(secret)
393 })
394 .await?;
395
396 let (mail, is_encrypted) =
397 match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring, &secrets)) {
398 Ok(Some(mut msg)) => {
399 mail_raw = msg.as_data_vec().unwrap_or_default();
400
401 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
402 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
403 info!(
404 context,
405 "decrypted message mime-body:\n{}",
406 String::from_utf8_lossy(&mail_raw),
407 );
408 }
409
410 decrypted_msg = Some(msg);
411
412 timestamp_sent = Self::get_timestamp_sent(
413 &decrypted_mail.headers,
414 timestamp_sent,
415 timestamp_rcvd,
416 );
417
418 let protected_aheader_values = decrypted_mail
419 .headers
420 .get_all_values(HeaderDef::Autocrypt.into());
421 if !protected_aheader_values.is_empty() {
422 aheader_values = protected_aheader_values;
423 }
424
425 (Ok(decrypted_mail), true)
426 }
427 Ok(None) => {
428 mail_raw = Vec::new();
429 decrypted_msg = None;
430 (Ok(mail), false)
431 }
432 Err(err) => {
433 mail_raw = Vec::new();
434 decrypted_msg = 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 let inserted = context
465 .sql
466 .execute(
467 "INSERT INTO public_keys (fingerprint, public_key)
468 VALUES (?, ?)
469 ON CONFLICT (fingerprint)
470 DO NOTHING",
471 (&fingerprint, autocrypt_header.public_key.to_bytes()),
472 )
473 .await?;
474 if inserted > 0 {
475 info!(
476 context,
477 "Saved key with fingerprint {fingerprint} from the Autocrypt header"
478 );
479 }
480 Some(fingerprint)
481 } else {
482 None
483 };
484
485 let mut public_keyring = if incoming {
486 if let Some(autocrypt_header) = autocrypt_header {
487 vec![autocrypt_header.public_key]
488 } else {
489 vec![]
490 }
491 } else {
492 key::load_self_public_keyring(context).await?
493 };
494
495 if let Some(signature) = match &decrypted_msg {
496 Some(pgp::composed::Message::Literal { .. }) => None,
497 Some(pgp::composed::Message::Compressed { .. }) => {
498 None
501 }
502 Some(pgp::composed::Message::Signed { reader, .. }) => reader.signature(0),
503 Some(pgp::composed::Message::Encrypted { .. }) => {
504 None
506 }
507 None => None,
508 } {
509 for issuer_fingerprint in signature.issuer_fingerprint() {
510 let issuer_fingerprint =
511 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
512 if let Some(public_key_bytes) = context
513 .sql
514 .query_row_optional(
515 "SELECT public_key
516 FROM public_keys
517 WHERE fingerprint=?",
518 (&issuer_fingerprint,),
519 |row| {
520 let bytes: Vec<u8> = row.get(0)?;
521 Ok(bytes)
522 },
523 )
524 .await?
525 {
526 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
527 public_keyring.push(public_key)
528 }
529 }
530 }
531
532 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
533 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
534 } else {
535 HashMap::new()
536 };
537
538 let mail = mail.as_ref().map(|mail| {
539 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
540 .unwrap_or((mail, Default::default()));
541 let signatures_detached = signatures_detached
542 .into_iter()
543 .map(|fp| (fp, Vec::new()))
544 .collect::<HashMap<_, _>>();
545 signatures.extend(signatures_detached);
546 content
547 });
548 if let (Ok(mail), true) = (mail, is_encrypted) {
549 if !signatures.is_empty() {
550 remove_header(&mut headers, "subject", &mut headers_removed);
554 remove_header(&mut headers, "list-id", &mut headers_removed);
555 }
556
557 let mut inner_from = None;
563
564 MimeMessage::merge_headers(
565 context,
566 &mut headers,
567 &mut headers_removed,
568 &mut recipients,
569 &mut past_members,
570 &mut inner_from,
571 &mut list_post,
572 &mut chat_disposition_notification_to,
573 mail,
574 );
575
576 if !signatures.is_empty() {
577 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
582 gossiped_keys =
583 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
584 }
585
586 if let Some(inner_from) = inner_from {
587 if !addr_cmp(&inner_from.addr, &from.addr) {
588 warn!(
597 context,
598 "From header in encrypted part doesn't match the outer one",
599 );
600
601 bail!("From header is forged");
606 }
607 from = inner_from;
608 }
609 }
610 if signatures.is_empty() {
611 Self::remove_secured_headers(&mut headers, &mut headers_removed);
612 }
613 if !is_encrypted {
614 signatures.clear();
615 }
616
617 if let (Ok(mail), true) = (mail, is_encrypted)
618 && let Some(post_msg_rfc724_mid) =
619 mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
620 {
621 let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
622 let metadata = if let Some(value) = mail
623 .headers
624 .get_header_value(HeaderDef::ChatPostMessageMetadata)
625 {
626 match PostMsgMetadata::try_from_header_value(&value) {
627 Ok(metadata) => Some(metadata),
628 Err(error) => {
629 error!(
630 context,
631 "Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
632 );
633 None
634 }
635 }
636 } else {
637 warn!(
638 context,
639 "Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
640 );
641 None
642 };
643
644 pre_message = PreMessageMode::Pre {
645 post_msg_rfc724_mid,
646 metadata,
647 };
648 }
649
650 let signature = signatures
651 .into_iter()
652 .last()
653 .map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
654 let mut parser = MimeMessage {
655 parts: Vec::new(),
656 headers,
657 #[cfg(test)]
658 headers_removed,
659
660 recipients,
661 past_members,
662 list_post,
663 from,
664 incoming,
665 chat_disposition_notification_to,
666 decrypting_failed: mail.is_err(),
667
668 signature,
670 autocrypt_fingerprint,
671 gossiped_keys,
672 is_forwarded: false,
673 mdn_reports: Vec::new(),
674 is_system_message: SystemMessage::Unknown,
675 location_kml: None,
676 message_kml: None,
677 sync_items: None,
678 webxdc_status_update: None,
679 user_avatar: None,
680 group_avatar: None,
681 delivery_report: None,
682 footer: None,
683 is_mime_modified: false,
684 decoded_data: Vec::new(),
685 hop_info,
686 is_bot: None,
687 timestamp_rcvd,
688 timestamp_sent,
689 pre_message,
690 };
691
692 match mail {
693 Ok(mail) => {
694 parser.parse_mime_recursive(context, mail, false).await?;
695 }
696 Err(err) => {
697 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.]";
698
699 let part = Part {
700 typ: Viewtype::Text,
701 msg_raw: Some(txt.to_string()),
702 msg: txt.to_string(),
703 error: Some(format!("Decrypting failed: {err:#}")),
706 ..Default::default()
707 };
708 parser.do_add_single_part(part);
709 }
710 };
711
712 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
713 if parser.mdn_reports.is_empty()
714 && !is_location_only
715 && parser.sync_items.is_none()
716 && parser.webxdc_status_update.is_none()
717 {
718 let is_bot =
719 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
720 parser.is_bot = Some(is_bot);
721 }
722 parser.maybe_remove_bad_parts();
723 parser.maybe_remove_inline_mailinglist_footer();
724 parser.heuristically_parse_ndn(context).await;
725 parser.parse_headers(context).await?;
726 parser.decoded_data = mail_raw;
727
728 Ok(parser)
729 }
730
731 fn get_timestamp_sent(
732 hdrs: &[mailparse::MailHeader<'_>],
733 default: i64,
734 timestamp_rcvd: i64,
735 ) -> i64 {
736 hdrs.get_header_value(HeaderDef::Date)
737 .and_then(|v| mailparse::dateparse(&v).ok())
738 .map_or(default, |value| {
739 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
740 })
741 }
742
743 fn parse_system_message_headers(&mut self, context: &Context) {
745 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
746 self.parts.retain(|part| {
747 part.mimetype
748 .as_ref()
749 .is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
750 });
751
752 if self.parts.len() == 1 {
753 self.is_system_message = SystemMessage::AutocryptSetupMessage;
754 } else {
755 warn!(context, "could not determine ASM mime-part");
756 }
757 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
758 if value == "location-streaming-enabled" {
759 self.is_system_message = SystemMessage::LocationStreamingEnabled;
760 } else if value == "ephemeral-timer-changed" {
761 self.is_system_message = SystemMessage::EphemeralTimerChanged;
762 } else if value == "protection-enabled" {
763 self.is_system_message = SystemMessage::ChatProtectionEnabled;
764 } else if value == "protection-disabled" {
765 self.is_system_message = SystemMessage::ChatProtectionDisabled;
766 } else if value == "group-avatar-changed" {
767 self.is_system_message = SystemMessage::GroupImageChanged;
768 } else if value == "call-accepted" {
769 self.is_system_message = SystemMessage::CallAccepted;
770 } else if value == "call-ended" {
771 self.is_system_message = SystemMessage::CallEnded;
772 }
773 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
774 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
775 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
776 self.is_system_message = SystemMessage::MemberAddedToGroup;
777 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
778 self.is_system_message = SystemMessage::GroupNameChanged;
779 } else if self
780 .get_header(HeaderDef::ChatGroupDescriptionChanged)
781 .is_some()
782 {
783 self.is_system_message = SystemMessage::GroupDescriptionChanged;
784 }
785 }
786
787 fn parse_avatar_headers(&mut self, context: &Context) -> Result<()> {
789 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
790 self.group_avatar =
791 self.avatar_action_from_header(context, header_value.to_string())?;
792 }
793
794 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
795 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string())?;
796 }
797 Ok(())
798 }
799
800 fn parse_videochat_headers(&mut self) {
801 let content = self
802 .get_header(HeaderDef::ChatContent)
803 .unwrap_or_default()
804 .to_string();
805 let room = self
806 .get_header(HeaderDef::ChatWebrtcRoom)
807 .map(|s| s.to_string());
808 let accepted = self
809 .get_header(HeaderDef::ChatWebrtcAccepted)
810 .map(|s| s.to_string());
811 let has_video = self
812 .get_header(HeaderDef::ChatWebrtcHasVideoInitially)
813 .map(|s| s.to_string());
814 if let Some(part) = self.parts.first_mut() {
815 if let Some(room) = room {
816 if content == "call" {
817 part.typ = Viewtype::Call;
818 part.param.set(Param::WebrtcRoom, room);
819 }
820 } else if let Some(accepted) = accepted {
821 part.param.set(Param::WebrtcAccepted, accepted);
822 }
823 if let Some(has_video) = has_video {
824 part.param.set(Param::WebrtcHasVideoInitially, has_video);
825 }
826 }
827 }
828
829 fn squash_attachment_parts(&mut self) {
835 if self.parts.len() == 2
836 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
837 && self
838 .parts
839 .get(1)
840 .is_some_and(|filepart| match filepart.typ {
841 Viewtype::Image
842 | Viewtype::Gif
843 | Viewtype::Sticker
844 | Viewtype::Audio
845 | Viewtype::Voice
846 | Viewtype::Video
847 | Viewtype::Vcard
848 | Viewtype::File
849 | Viewtype::Webxdc => true,
850 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
851 })
852 {
853 let mut parts = std::mem::take(&mut self.parts);
854 let Some(mut filepart) = parts.pop() else {
855 return;
857 };
858 let Some(textpart) = parts.pop() else {
859 return;
861 };
862
863 filepart.msg.clone_from(&textpart.msg);
864 if let Some(quote) = textpart.param.get(Param::Quote) {
865 filepart.param.set(Param::Quote, quote);
866 }
867
868 self.parts = vec![filepart];
869 }
870 }
871
872 fn parse_attachments(&mut self) {
874 if self.parts.len() != 1 {
877 return;
878 }
879
880 if let Some(mut part) = self.parts.pop() {
881 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
882 {
883 part.typ = Viewtype::Voice;
884 }
885 if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
886 && let Some(value) = self.get_header(HeaderDef::ChatContent)
887 && value == "sticker"
888 {
889 part.typ = Viewtype::Sticker;
890 }
891 if (part.typ == Viewtype::Audio
892 || part.typ == Viewtype::Voice
893 || part.typ == Viewtype::Video)
894 && let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
895 {
896 let duration_ms = field_0.parse().unwrap_or_default();
897 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
898 part.param.set_int(Param::Duration, duration_ms);
899 }
900 }
901
902 self.parts.push(part);
903 }
904 }
905
906 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
907 self.parse_system_message_headers(context);
908 self.parse_avatar_headers(context)?;
909 self.parse_videochat_headers();
910 if self.delivery_report.is_none() {
911 self.squash_attachment_parts();
912 }
913
914 if !context.get_config_bool(Config::Bot).await?
915 && let Some(ref subject) = self.get_subject()
916 {
917 let mut prepend_subject = true;
918 if !self.decrypting_failed {
919 let colon = subject.find(':');
920 if colon == Some(2)
921 || colon == Some(3)
922 || self.has_chat_version()
923 || subject.contains("Chat:")
924 {
925 prepend_subject = false
926 }
927 }
928
929 if self.is_mailinglist_message() && !self.has_chat_version() {
932 prepend_subject = true;
933 }
934
935 if prepend_subject && !subject.is_empty() {
936 let part_with_text = self
937 .parts
938 .iter_mut()
939 .find(|part| !part.msg.is_empty() && !part.is_reaction);
940 if let Some(part) = part_with_text {
941 part.msg = format!("{} – {}", subject, part.msg);
946 }
947 }
948 }
949
950 if self.is_forwarded {
951 for part in &mut self.parts {
952 part.param.set_int(Param::Forwarded, 1);
953 }
954 }
955
956 self.parse_attachments();
957
958 if !self.decrypting_failed
960 && !self.parts.is_empty()
961 && let Some(ref dn_to) = self.chat_disposition_notification_to
962 {
963 let from = &self.from.addr;
965 if !context.is_self_addr(from).await? {
966 if from.to_lowercase() == dn_to.addr.to_lowercase() {
967 if let Some(part) = self.parts.last_mut() {
968 part.param.set_int(Param::WantsMdn, 1);
969 }
970 } else {
971 warn!(
972 context,
973 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
974 );
975 }
976 }
977 }
978
979 if self.parts.is_empty() && self.mdn_reports.is_empty() {
984 let mut part = Part {
985 typ: Viewtype::Text,
986 ..Default::default()
987 };
988
989 if let Some(ref subject) = self.get_subject()
990 && !self.has_chat_version()
991 && self.webxdc_status_update.is_none()
992 {
993 part.msg = subject.to_string();
994 }
995
996 self.do_add_single_part(part);
997 }
998
999 if self.is_bot == Some(true) {
1000 for part in &mut self.parts {
1001 part.param.set(Param::Bot, "1");
1002 }
1003 }
1004
1005 Ok(())
1006 }
1007
1008 fn avatar_action_from_header(
1009 &mut self,
1010 context: &Context,
1011 header_value: String,
1012 ) -> Result<Option<AvatarAction>> {
1013 let res = if header_value == "0" {
1014 Some(AvatarAction::Delete)
1015 } else if let Some(base64) = header_value
1016 .split_ascii_whitespace()
1017 .collect::<String>()
1018 .strip_prefix("base64:")
1019 {
1020 match BlobObject::store_from_base64(context, base64)? {
1021 Some(path) => Some(AvatarAction::Change(path)),
1022 None => {
1023 warn!(context, "Could not decode avatar base64");
1024 None
1025 }
1026 }
1027 } else {
1028 let mut i = 0;
1031 while let Some(part) = self.parts.get_mut(i) {
1032 if let Some(part_filename) = &part.org_filename
1033 && part_filename == &header_value
1034 {
1035 if let Some(blob) = part.param.get(Param::File) {
1036 let res = Some(AvatarAction::Change(blob.to_string()));
1037 self.parts.remove(i);
1038 return Ok(res);
1039 }
1040 break;
1041 }
1042 i += 1;
1043 }
1044 None
1045 };
1046 Ok(res)
1047 }
1048
1049 pub fn was_encrypted(&self) -> bool {
1055 self.signature.is_some()
1056 }
1057
1058 pub(crate) fn has_chat_version(&self) -> bool {
1061 self.headers.contains_key("chat-version")
1062 }
1063
1064 pub(crate) fn get_subject(&self) -> Option<String> {
1065 self.get_header(HeaderDef::Subject)
1066 .map(|s| s.trim_start())
1067 .filter(|s| !s.is_empty())
1068 .map(|s| s.to_string())
1069 }
1070
1071 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
1072 self.headers
1073 .get(headerdef.get_headername())
1074 .map(|s| s.as_str())
1075 }
1076
1077 #[cfg(test)]
1078 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1083 let hname = headerdef.get_headername();
1084 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1085 }
1086
1087 #[cfg(test)]
1088 pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
1090 assert!(!self.decrypting_failed);
1091 let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
1092 decoded_str.contains(s)
1093 }
1094
1095 pub fn get_chat_group_id(&self) -> Option<&str> {
1097 self.get_header(HeaderDef::ChatGroupId)
1098 .filter(|s| validate_id(s))
1099 }
1100
1101 async fn parse_mime_recursive<'a>(
1102 &'a mut self,
1103 context: &'a Context,
1104 mail: &'a mailparse::ParsedMail<'a>,
1105 is_related: bool,
1106 ) -> Result<bool> {
1107 enum MimeS {
1108 Multiple,
1109 Single,
1110 Message,
1111 }
1112
1113 let mimetype = mail.ctype.mimetype.to_lowercase();
1114
1115 let m = if mimetype.starts_with("multipart") {
1116 if mail.ctype.params.contains_key("boundary") {
1117 MimeS::Multiple
1118 } else {
1119 MimeS::Single
1120 }
1121 } else if mimetype.starts_with("message") {
1122 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1123 MimeS::Message
1124 } else {
1125 MimeS::Single
1126 }
1127 } else {
1128 MimeS::Single
1129 };
1130
1131 let is_related = is_related || mimetype == "multipart/related";
1132 match m {
1133 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1134 MimeS::Message => {
1135 let raw = mail.get_body_raw()?;
1136 if raw.is_empty() {
1137 return Ok(false);
1138 }
1139 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1140
1141 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1142 }
1143 MimeS::Single => {
1144 self.add_single_part_if_known(context, mail, is_related)
1145 .await
1146 }
1147 }
1148 }
1149
1150 async fn handle_multiple(
1151 &mut self,
1152 context: &Context,
1153 mail: &mailparse::ParsedMail<'_>,
1154 is_related: bool,
1155 ) -> Result<bool> {
1156 let mut any_part_added = false;
1157 let mimetype = get_mime_type(
1158 mail,
1159 &get_attachment_filename(context, mail)?,
1160 self.has_chat_version(),
1161 )?
1162 .0;
1163 match (mimetype.type_(), mimetype.subtype().as_str()) {
1164 (mime::MULTIPART, "alternative") => {
1165 for cur_data in mail.subparts.iter().rev() {
1177 let (mime_type, _viewtype) = get_mime_type(
1178 cur_data,
1179 &get_attachment_filename(context, cur_data)?,
1180 self.has_chat_version(),
1181 )?;
1182
1183 if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
1184 any_part_added = self
1185 .parse_mime_recursive(context, cur_data, is_related)
1186 .await?;
1187 break;
1188 }
1189 }
1190
1191 for cur_data in mail.subparts.iter().rev() {
1200 let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
1201 if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
1202 let filename = get_attachment_filename(context, cur_data)?
1203 .unwrap_or_else(|| "calendar.ics".to_string());
1204 self.do_add_single_file_part(
1205 context,
1206 Viewtype::File,
1207 mimetype,
1208 &mail.ctype.mimetype.to_lowercase(),
1209 &mail.get_body_raw()?,
1210 &filename,
1211 is_related,
1212 )
1213 .await?;
1214 }
1215 }
1216
1217 if !any_part_added {
1218 for cur_part in mail.subparts.iter().rev() {
1219 if self
1220 .parse_mime_recursive(context, cur_part, is_related)
1221 .await?
1222 {
1223 any_part_added = true;
1224 break;
1225 }
1226 }
1227 }
1228 if any_part_added && mail.subparts.len() > 1 {
1229 self.is_mime_modified = true;
1233 }
1234 }
1235 (mime::MULTIPART, "signed") => {
1236 if let Some(first) = mail.subparts.first() {
1245 any_part_added = self
1246 .parse_mime_recursive(context, first, is_related)
1247 .await?;
1248 }
1249 }
1250 (mime::MULTIPART, "report") => {
1251 if mail.subparts.len() >= 2 {
1253 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1254 Some("disposition-notification") => {
1255 if let Some(report) = self.process_report(context, mail)? {
1256 self.mdn_reports.push(report);
1257 }
1258
1259 let part = Part {
1264 typ: Viewtype::Unknown,
1265 ..Default::default()
1266 };
1267 self.parts.push(part);
1268
1269 any_part_added = true;
1270 }
1271 Some("delivery-status") | None => {
1273 if let Some(report) = self.process_delivery_status(context, mail)? {
1274 self.delivery_report = Some(report);
1275 }
1276
1277 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 Some("multi-device-sync") => {
1288 if let Some(second) = mail.subparts.get(1) {
1289 self.add_single_part_if_known(context, second, is_related)
1290 .await?;
1291 }
1292 }
1293 Some("status-update") => {
1294 if let Some(second) = mail.subparts.get(1) {
1295 self.add_single_part_if_known(context, second, is_related)
1296 .await?;
1297 }
1298 }
1299 Some(_) => {
1300 for cur_data in &mail.subparts {
1301 if self
1302 .parse_mime_recursive(context, cur_data, is_related)
1303 .await?
1304 {
1305 any_part_added = true;
1306 }
1307 }
1308 }
1309 }
1310 }
1311 }
1312 _ => {
1313 for cur_data in &mail.subparts {
1316 if self
1317 .parse_mime_recursive(context, cur_data, is_related)
1318 .await?
1319 {
1320 any_part_added = true;
1321 }
1322 }
1323 }
1324 }
1325
1326 Ok(any_part_added)
1327 }
1328
1329 async fn add_single_part_if_known(
1331 &mut self,
1332 context: &Context,
1333 mail: &mailparse::ParsedMail<'_>,
1334 is_related: bool,
1335 ) -> Result<bool> {
1336 let filename = get_attachment_filename(context, mail)?;
1338 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1339 let raw_mime = mail.ctype.mimetype.to_lowercase();
1340
1341 let old_part_count = self.parts.len();
1342
1343 match filename {
1344 Some(filename) => {
1345 self.do_add_single_file_part(
1346 context,
1347 msg_type,
1348 mime_type,
1349 &raw_mime,
1350 &mail.get_body_raw()?,
1351 &filename,
1352 is_related,
1353 )
1354 .await?;
1355 }
1356 None => {
1357 match mime_type.type_() {
1358 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1359 warn!(context, "Missing attachment");
1360 return Ok(false);
1361 }
1362 mime::TEXT
1363 if mail.get_content_disposition().disposition
1364 == DispositionType::Extension("reaction".to_string()) =>
1365 {
1366 let decoded_data = match mail.get_body() {
1368 Ok(decoded_data) => decoded_data,
1369 Err(err) => {
1370 warn!(context, "Invalid body parsed {:#}", err);
1371 return Ok(false);
1373 }
1374 };
1375
1376 let part = Part {
1377 typ: Viewtype::Text,
1378 mimetype: Some(mime_type),
1379 msg: decoded_data,
1380 is_reaction: true,
1381 ..Default::default()
1382 };
1383 self.do_add_single_part(part);
1384 return Ok(true);
1385 }
1386 mime::TEXT | mime::HTML => {
1387 let decoded_data = match mail.get_body() {
1388 Ok(decoded_data) => decoded_data,
1389 Err(err) => {
1390 warn!(context, "Invalid body parsed {:#}", err);
1391 return Ok(false);
1393 }
1394 };
1395
1396 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1397 let mut dehtml_failed = false;
1398
1399 let SimplifiedText {
1400 text: simplified_txt,
1401 is_forwarded,
1402 is_cut,
1403 top_quote,
1404 footer,
1405 } = if decoded_data.is_empty() {
1406 Default::default()
1407 } else {
1408 let is_html = mime_type == mime::TEXT_HTML;
1409 if is_html {
1410 self.is_mime_modified = true;
1411 if let Some(text) = dehtml(&decoded_data) {
1416 text
1417 } else {
1418 dehtml_failed = true;
1419 SimplifiedText {
1420 text: decoded_data.clone(),
1421 ..Default::default()
1422 }
1423 }
1424 } else {
1425 simplify(decoded_data.clone(), self.has_chat_version())
1426 }
1427 };
1428
1429 self.is_mime_modified = self.is_mime_modified
1430 || ((is_forwarded || is_cut || top_quote.is_some())
1431 && !self.has_chat_version());
1432
1433 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1434 {
1435 format.as_str().eq_ignore_ascii_case("flowed")
1436 } else {
1437 false
1438 };
1439
1440 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1441 && mime_type.subtype() == mime::PLAIN
1442 {
1443 let simplified_txt = match mail
1446 .ctype
1447 .params
1448 .get("hp-legacy-display")
1449 .is_some_and(|v| v == "1")
1450 {
1451 false => simplified_txt,
1452 true => rm_legacy_display_elements(&simplified_txt),
1453 };
1454 if is_format_flowed {
1455 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1456 delsp.as_str().eq_ignore_ascii_case("yes")
1457 } else {
1458 false
1459 };
1460 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1461 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1462 (unflowed_text, unflowed_quote)
1463 } else {
1464 (simplified_txt, top_quote)
1465 }
1466 } else {
1467 (simplified_txt, top_quote)
1468 };
1469
1470 let (simplified_txt, was_truncated) =
1471 truncate_msg_text(context, simplified_txt).await?;
1472 if was_truncated {
1473 self.is_mime_modified = was_truncated;
1474 }
1475
1476 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1477 let mut part = Part {
1478 dehtml_failed,
1479 typ: Viewtype::Text,
1480 mimetype: Some(mime_type),
1481 msg: simplified_txt,
1482 ..Default::default()
1483 };
1484 if let Some(quote) = simplified_quote {
1485 part.param.set(Param::Quote, quote);
1486 }
1487 part.msg_raw = Some(decoded_data);
1488 self.do_add_single_part(part);
1489 }
1490
1491 if is_forwarded {
1492 self.is_forwarded = true;
1493 }
1494
1495 if self.footer.is_none() && is_plaintext {
1496 self.footer = Some(footer.unwrap_or_default());
1497 }
1498 }
1499 _ => {}
1500 }
1501 }
1502 }
1503
1504 Ok(self.parts.len() > old_part_count)
1506 }
1507
1508 #[expect(clippy::too_many_arguments)]
1509 async fn do_add_single_file_part(
1510 &mut self,
1511 context: &Context,
1512 msg_type: Viewtype,
1513 mime_type: Mime,
1514 raw_mime: &str,
1515 decoded_data: &[u8],
1516 filename: &str,
1517 is_related: bool,
1518 ) -> Result<()> {
1519 if mime_type.type_() == mime::APPLICATION
1521 && mime_type.subtype().as_str() == "pgp-keys"
1522 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1523 {
1524 return Ok(());
1525 }
1526 let mut part = Part::default();
1527 let msg_type = if context
1528 .is_webxdc_file(filename, decoded_data)
1529 .await
1530 .unwrap_or(false)
1531 {
1532 Viewtype::Webxdc
1533 } else if filename.ends_with(".kml") {
1534 if filename.starts_with("location") || filename.starts_with("message") {
1537 let parsed = location::Kml::parse(decoded_data)
1538 .map_err(|err| {
1539 warn!(context, "failed to parse kml part: {:#}", err);
1540 })
1541 .ok();
1542 if filename.starts_with("location") {
1543 self.location_kml = parsed;
1544 } else {
1545 self.message_kml = parsed;
1546 }
1547 return Ok(());
1548 }
1549 msg_type
1550 } else if filename == "multi-device-sync.json" {
1551 if !context.get_config_bool(Config::SyncMsgs).await? {
1552 return Ok(());
1553 }
1554 let serialized = String::from_utf8_lossy(decoded_data)
1555 .parse()
1556 .unwrap_or_default();
1557 self.sync_items = context
1558 .parse_sync_items(serialized)
1559 .map_err(|err| {
1560 warn!(context, "failed to parse sync data: {:#}", err);
1561 })
1562 .ok();
1563 return Ok(());
1564 } else if filename == "status-update.json" {
1565 let serialized = String::from_utf8_lossy(decoded_data)
1566 .parse()
1567 .unwrap_or_default();
1568 self.webxdc_status_update = Some(serialized);
1569 return Ok(());
1570 } else if msg_type == Viewtype::Vcard {
1571 if let Some(summary) = get_vcard_summary(decoded_data) {
1572 part.param.set(Param::Summary1, summary);
1573 msg_type
1574 } else {
1575 Viewtype::File
1576 }
1577 } else if msg_type == Viewtype::Image
1578 || msg_type == Viewtype::Gif
1579 || msg_type == Viewtype::Sticker
1580 {
1581 match get_filemeta(decoded_data) {
1582 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1584 part.param.set_i64(Param::Width, width.into());
1585 part.param.set_i64(Param::Height, height.into());
1586 msg_type
1587 }
1588 _ => Viewtype::File,
1590 }
1591 } else {
1592 msg_type
1593 };
1594
1595 let blob =
1599 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1600 Ok(blob) => blob,
1601 Err(err) => {
1602 error!(
1603 context,
1604 "Could not add blob for mime part {}, error {:#}", filename, err
1605 );
1606 return Ok(());
1607 }
1608 };
1609 info!(context, "added blobfile: {:?}", blob.as_name());
1610
1611 part.typ = msg_type;
1612 part.org_filename = Some(filename.to_string());
1613 part.mimetype = Some(mime_type);
1614 part.bytes = decoded_data.len();
1615 part.param.set(Param::File, blob.as_name());
1616 part.param.set(Param::Filename, filename);
1617 part.param.set(Param::MimeType, raw_mime);
1618 part.is_related = is_related;
1619
1620 self.do_add_single_part(part);
1621 Ok(())
1622 }
1623
1624 async fn try_set_peer_key_from_file_part(
1626 context: &Context,
1627 decoded_data: &[u8],
1628 ) -> Result<bool> {
1629 let key = match str::from_utf8(decoded_data) {
1630 Err(err) => {
1631 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1632 return Ok(false);
1633 }
1634 Ok(key) => key,
1635 };
1636 let key = match SignedPublicKey::from_asc(key) {
1637 Err(err) => {
1638 warn!(
1639 context,
1640 "PGP key attachment is not an ASCII-armored file: {err:#}."
1641 );
1642 return Ok(false);
1643 }
1644 Ok(key) => key,
1645 };
1646 if let Err(err) = key.verify_bindings() {
1647 warn!(context, "Attached PGP key verification failed: {err:#}.");
1648 return Ok(false);
1649 }
1650
1651 let fingerprint = key.dc_fingerprint().hex();
1652 context
1653 .sql
1654 .execute(
1655 "INSERT INTO public_keys (fingerprint, public_key)
1656 VALUES (?, ?)
1657 ON CONFLICT (fingerprint)
1658 DO NOTHING",
1659 (&fingerprint, key.to_bytes()),
1660 )
1661 .await?;
1662
1663 info!(context, "Imported PGP key {fingerprint} from attachment.");
1664 Ok(true)
1665 }
1666
1667 pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
1668 if self.was_encrypted() {
1669 part.param.set_int(Param::GuaranteeE2ee, 1);
1670 }
1671 self.parts.push(part);
1672 }
1673
1674 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1675 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1676 return Some(list_id);
1679 } else if let Some(chat_list_id) = self.get_header(HeaderDef::ChatListId) {
1680 return Some(chat_list_id);
1681 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1682 if let Some(precedence) = self.get_header(HeaderDef::Precedence)
1685 && (precedence == "list" || precedence == "bulk")
1686 {
1687 return Some(sender);
1691 }
1692 }
1693 None
1694 }
1695
1696 pub(crate) fn is_mailinglist_message(&self) -> bool {
1697 self.get_mailinglist_header().is_some()
1698 }
1699
1700 pub(crate) fn is_schleuder_message(&self) -> bool {
1702 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1703 list_help == "<https://schleuder.org/>"
1704 } else {
1705 false
1706 }
1707 }
1708
1709 pub(crate) fn is_call(&self) -> bool {
1711 self.parts
1712 .first()
1713 .is_some_and(|part| part.typ == Viewtype::Call)
1714 }
1715
1716 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1717 self.get_header(HeaderDef::MessageId)
1718 .and_then(|msgid| parse_message_id(msgid).ok())
1719 }
1720
1721 fn remove_secured_headers(
1722 headers: &mut HashMap<String, String>,
1723 removed: &mut HashSet<String>,
1724 ) {
1725 remove_header(headers, "secure-join-fingerprint", removed);
1726 remove_header(headers, "secure-join-auth", removed);
1727 remove_header(headers, "chat-verified", removed);
1728 remove_header(headers, "autocrypt-gossip", removed);
1729
1730 if let Some(secure_join) = remove_header(headers, "secure-join", removed)
1732 && (secure_join == "vc-request" || secure_join == "vg-request")
1733 {
1734 headers.insert("secure-join".to_string(), secure_join);
1735 }
1736 }
1737
1738 #[allow(clippy::too_many_arguments)]
1744 fn merge_headers(
1745 context: &Context,
1746 headers: &mut HashMap<String, String>,
1747 headers_removed: &mut HashSet<String>,
1748 recipients: &mut Vec<SingleInfo>,
1749 past_members: &mut Vec<SingleInfo>,
1750 from: &mut Option<SingleInfo>,
1751 list_post: &mut Option<String>,
1752 chat_disposition_notification_to: &mut Option<SingleInfo>,
1753 part: &mailparse::ParsedMail,
1754 ) {
1755 let fields = &part.headers;
1756 let has_header_protection = part.ctype.params.contains_key("hp");
1758
1759 headers_removed.extend(
1760 headers
1761 .extract_if(|k, _v| has_header_protection || is_protected(k))
1762 .map(|(k, _v)| k.to_string()),
1763 );
1764 for field in fields {
1765 let key = field.get_key().to_lowercase();
1767 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1768 match addrparse_header(field) {
1769 Ok(addrlist) => {
1770 *chat_disposition_notification_to = addrlist.extract_single_info();
1771 }
1772 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1773 }
1774 } else {
1775 let value = field.get_value();
1776 headers.insert(key.to_string(), value);
1777 }
1778 }
1779 let recipients_new = get_recipients(fields);
1780 if !recipients_new.is_empty() {
1781 *recipients = recipients_new;
1782 }
1783 let past_members_addresses =
1784 get_all_addresses_from_header(fields, "chat-group-past-members");
1785 if !past_members_addresses.is_empty() {
1786 *past_members = past_members_addresses;
1787 }
1788 let from_new = get_from(fields);
1789 if from_new.is_some() {
1790 *from = from_new;
1791 }
1792 let list_post_new = get_list_post(fields);
1793 if list_post_new.is_some() {
1794 *list_post = list_post_new;
1795 }
1796 }
1797
1798 fn process_report(
1799 &self,
1800 context: &Context,
1801 report: &mailparse::ParsedMail<'_>,
1802 ) -> Result<Option<Report>> {
1803 let report_body = if let Some(subpart) = report.subparts.get(1) {
1805 subpart.get_body_raw()?
1806 } else {
1807 bail!("Report does not have second MIME part");
1808 };
1809 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1810
1811 if report_fields
1813 .get_header_value(HeaderDef::Disposition)
1814 .is_none()
1815 {
1816 warn!(
1817 context,
1818 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1819 report_fields.get_header_value(HeaderDef::MessageId)
1820 );
1821 return Ok(None);
1822 };
1823
1824 let original_message_id = report_fields
1825 .get_header_value(HeaderDef::OriginalMessageId)
1826 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1829 .and_then(|v| parse_message_id(&v).ok());
1830 let additional_message_ids = report_fields
1831 .get_header_value(HeaderDef::AdditionalMessageIds)
1832 .map_or_else(Vec::new, |v| {
1833 v.split(' ')
1834 .filter_map(|s| parse_message_id(s).ok())
1835 .collect()
1836 });
1837
1838 Ok(Some(Report {
1839 original_message_id,
1840 additional_message_ids,
1841 }))
1842 }
1843
1844 fn process_delivery_status(
1845 &self,
1846 context: &Context,
1847 report: &mailparse::ParsedMail<'_>,
1848 ) -> Result<Option<DeliveryReport>> {
1849 let mut failure = true;
1851
1852 if let Some(status_part) = report.subparts.get(1) {
1853 if status_part.ctype.mimetype != "message/delivery-status"
1856 && status_part.ctype.mimetype != "message/global-delivery-status"
1857 {
1858 warn!(
1859 context,
1860 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1861 );
1862 return Ok(None);
1863 }
1864
1865 let status_body = status_part.get_body_raw()?;
1866
1867 let (_, sz) = mailparse::parse_headers(&status_body)?;
1869
1870 if let Some(status_body) = status_body.get(sz..) {
1872 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1873 if let Some(action) = status_fields.get_first_value("action") {
1874 if action != "failed" {
1875 info!(context, "DSN with {:?} action", action);
1876 failure = false;
1877 }
1878 } else {
1879 warn!(context, "DSN without action");
1880 }
1881 } else {
1882 warn!(context, "DSN without per-recipient fields");
1883 }
1884 } else {
1885 return Ok(None);
1887 }
1888
1889 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1891 p.ctype.mimetype.contains("rfc822")
1892 || p.ctype.mimetype == "message/global"
1893 || p.ctype.mimetype == "message/global-headers"
1894 }) {
1895 let report_body = original_msg.get_body_raw()?;
1896 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1897
1898 if let Some(original_message_id) = report_fields
1899 .get_header_value(HeaderDef::MessageId)
1900 .and_then(|v| parse_message_id(&v).ok())
1901 {
1902 return Ok(Some(DeliveryReport {
1903 rfc724_mid: original_message_id,
1904 failure,
1905 }));
1906 }
1907
1908 warn!(
1909 context,
1910 "ignoring unknown ndn-notification, Message-Id: {:?}",
1911 report_fields.get_header_value(HeaderDef::MessageId)
1912 );
1913 }
1914
1915 Ok(None)
1916 }
1917
1918 fn maybe_remove_bad_parts(&mut self) {
1919 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1920 if good_parts == 0 {
1921 self.parts.truncate(1);
1923 } else if good_parts < self.parts.len() {
1924 self.parts.retain(|p| !p.dehtml_failed);
1925 }
1926
1927 if !self.has_chat_version() && self.is_mime_modified {
1935 fn is_related_image(p: &&Part) -> bool {
1936 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1937 }
1938 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1939 if related_image_cnt > 1 {
1940 let mut is_first_image = true;
1941 self.parts.retain(|p| {
1942 let retain = is_first_image || !is_related_image(&p);
1943 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1944 is_first_image = false;
1945 }
1946 retain
1947 });
1948 }
1949 }
1950 }
1951
1952 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1962 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1963 let text_part_cnt = self
1964 .parts
1965 .iter()
1966 .filter(|p| p.typ == Viewtype::Text)
1967 .count();
1968 if text_part_cnt == 2
1969 && let Some(last_part) = self.parts.last()
1970 && last_part.typ == Viewtype::Text
1971 {
1972 self.parts.pop();
1973 }
1974 }
1975 }
1976
1977 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1981 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1982 let from = from.to_ascii_lowercase();
1983 from.contains("mailer-daemon") || from.contains("mail-daemon")
1984 } else {
1985 false
1986 };
1987 if maybe_ndn && self.delivery_report.is_none() {
1988 for original_message_id in self
1989 .parts
1990 .iter()
1991 .filter_map(|part| part.msg_raw.as_ref())
1992 .flat_map(|part| part.lines())
1993 .filter_map(|line| line.split_once("Message-ID:"))
1994 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1995 {
1996 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1997 {
1998 self.delivery_report = Some(DeliveryReport {
1999 rfc724_mid: original_message_id,
2000 failure: true,
2001 })
2002 }
2003 }
2004 }
2005 }
2006
2007 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
2011 for report in &self.mdn_reports {
2012 for original_message_id in report
2013 .original_message_id
2014 .iter()
2015 .chain(&report.additional_message_ids)
2016 {
2017 if let Err(err) =
2018 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
2019 {
2020 warn!(context, "Could not handle MDN: {err:#}.");
2021 }
2022 }
2023 }
2024
2025 if let Some(delivery_report) = &self.delivery_report
2026 && delivery_report.failure
2027 {
2028 let error = parts
2029 .iter()
2030 .find(|p| p.typ == Viewtype::Text)
2031 .map(|p| p.msg.clone());
2032 if let Err(err) = handle_ndn(context, delivery_report, error).await {
2033 warn!(context, "Could not handle NDN: {err:#}.");
2034 }
2035 }
2036 }
2037
2038 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
2043 let parent_timestamp = if let Some(field) = self
2044 .get_header(HeaderDef::InReplyTo)
2045 .and_then(|msgid| parse_message_id(msgid).ok())
2046 {
2047 context
2048 .sql
2049 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
2050 .await?
2051 } else {
2052 None
2053 };
2054 Ok(parent_timestamp)
2055 }
2056
2057 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
2061 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
2062 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
2063 .map(|h| {
2064 h.split_ascii_whitespace()
2065 .filter_map(|ts| ts.parse::<i64>().ok())
2066 .map(|ts| std::cmp::min(now, ts))
2067 .collect()
2068 })
2069 }
2070
2071 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
2074 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
2075 header
2076 .split_ascii_whitespace()
2077 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
2078 .collect()
2079 } else {
2080 Vec::new()
2081 }
2082 }
2083}
2084
2085fn rm_legacy_display_elements(text: &str) -> String {
2086 let mut res = None;
2087 for l in text.lines() {
2088 res = res.map(|r: String| match r.is_empty() {
2089 true => l.to_string(),
2090 false => r + "\r\n" + l,
2091 });
2092 if l.is_empty() {
2093 res = Some(String::new());
2094 }
2095 }
2096 res.unwrap_or_default()
2097}
2098
2099fn remove_header(
2100 headers: &mut HashMap<String, String>,
2101 key: &str,
2102 removed: &mut HashSet<String>,
2103) -> Option<String> {
2104 if let Some((k, v)) = headers.remove_entry(key) {
2105 removed.insert(k);
2106 Some(v)
2107 } else {
2108 None
2109 }
2110}
2111
2112async fn parse_gossip_headers(
2118 context: &Context,
2119 from: &str,
2120 recipients: &[SingleInfo],
2121 gossip_headers: Vec<String>,
2122) -> Result<BTreeMap<String, GossipedKey>> {
2123 let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
2125
2126 for value in &gossip_headers {
2127 let header = match value.parse::<Aheader>() {
2128 Ok(header) => header,
2129 Err(err) => {
2130 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
2131 continue;
2132 }
2133 };
2134
2135 if !recipients
2136 .iter()
2137 .any(|info| addr_cmp(&info.addr, &header.addr))
2138 {
2139 warn!(
2140 context,
2141 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
2142 );
2143 continue;
2144 }
2145 if addr_cmp(from, &header.addr) {
2146 warn!(
2148 context,
2149 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
2150 );
2151 continue;
2152 }
2153
2154 let fingerprint = header.public_key.dc_fingerprint().hex();
2155 context
2156 .sql
2157 .execute(
2158 "INSERT INTO public_keys (fingerprint, public_key)
2159 VALUES (?, ?)
2160 ON CONFLICT (fingerprint)
2161 DO NOTHING",
2162 (&fingerprint, header.public_key.to_bytes()),
2163 )
2164 .await?;
2165
2166 let gossiped_key = GossipedKey {
2167 public_key: header.public_key,
2168
2169 verified: header.verified,
2170 };
2171 gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2172 }
2173
2174 Ok(gossiped_keys)
2175}
2176
2177#[derive(Debug)]
2179pub(crate) struct Report {
2180 pub original_message_id: Option<String>,
2185 pub additional_message_ids: Vec<String>,
2187}
2188
2189#[derive(Debug)]
2191pub(crate) struct DeliveryReport {
2192 pub rfc724_mid: String,
2193 pub failure: bool,
2194}
2195
2196pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2197 let mut msgids = Vec::new();
2199 for id in ids.split_whitespace() {
2200 let mut id = id.to_string();
2201 if let Some(id_without_prefix) = id.strip_prefix('<') {
2202 id = id_without_prefix.to_string();
2203 };
2204 if let Some(id_without_suffix) = id.strip_suffix('>') {
2205 id = id_without_suffix.to_string();
2206 };
2207 if !id.is_empty() {
2208 msgids.push(id);
2209 }
2210 }
2211 msgids
2212}
2213
2214pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2215 if let Some(id) = parse_message_ids(ids).first() {
2216 Ok(id.to_string())
2217 } else {
2218 bail!("could not parse message_id: {ids}");
2219 }
2220}
2221
2222fn is_protected(key: &str) -> bool {
2229 key.starts_with("chat-")
2230 || matches!(
2231 key,
2232 "return-path"
2233 | "auto-submitted"
2234 | "autocrypt-setup-message"
2235 | "date"
2236 | "from"
2237 | "sender"
2238 | "reply-to"
2239 | "to"
2240 | "cc"
2241 | "bcc"
2242 | "message-id"
2243 | "in-reply-to"
2244 | "references"
2245 | "secure-join"
2246 )
2247}
2248
2249pub(crate) fn is_hidden(key: &str) -> bool {
2251 matches!(
2252 key,
2253 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2254 )
2255}
2256
2257#[derive(Debug, Default, Clone)]
2259pub struct Part {
2260 pub typ: Viewtype,
2262
2263 pub mimetype: Option<Mime>,
2265
2266 pub msg: String,
2268
2269 pub msg_raw: Option<String>,
2271
2272 pub bytes: usize,
2274
2275 pub param: Params,
2277
2278 pub(crate) org_filename: Option<String>,
2280
2281 pub error: Option<String>,
2283
2284 pub(crate) dehtml_failed: bool,
2286
2287 pub(crate) is_related: bool,
2294
2295 pub(crate) is_reaction: bool,
2297}
2298
2299fn get_mime_type(
2304 mail: &mailparse::ParsedMail<'_>,
2305 filename: &Option<String>,
2306 is_chat_msg: bool,
2307) -> Result<(Mime, Viewtype)> {
2308 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2309
2310 let viewtype = match mimetype.type_() {
2311 mime::TEXT => match mimetype.subtype() {
2312 mime::VCARD => Viewtype::Vcard,
2313 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2314 _ => Viewtype::File,
2315 },
2316 mime::IMAGE => match mimetype.subtype() {
2317 mime::GIF => Viewtype::Gif,
2318 mime::SVG => Viewtype::File,
2319 _ => Viewtype::Image,
2320 },
2321 mime::AUDIO => Viewtype::Audio,
2322 mime::VIDEO => Viewtype::Video,
2323 mime::MULTIPART => Viewtype::Unknown,
2324 mime::MESSAGE => {
2325 if is_attachment_disposition(mail) {
2326 Viewtype::File
2327 } else {
2328 Viewtype::Unknown
2336 }
2337 }
2338 mime::APPLICATION => match mimetype.subtype() {
2339 mime::OCTET_STREAM => match filename {
2340 Some(filename) if !is_chat_msg => {
2341 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2342 Some((viewtype, _)) => viewtype,
2343 None => Viewtype::File,
2344 }
2345 }
2346 _ => Viewtype::File,
2347 },
2348 _ => Viewtype::File,
2349 },
2350 _ => Viewtype::Unknown,
2351 };
2352
2353 Ok((mimetype, viewtype))
2354}
2355
2356fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2357 let ct = mail.get_content_disposition();
2358 ct.disposition == DispositionType::Attachment
2359 && ct
2360 .params
2361 .iter()
2362 .any(|(key, _value)| key.starts_with("filename"))
2363}
2364
2365fn get_attachment_filename(
2372 context: &Context,
2373 mail: &mailparse::ParsedMail,
2374) -> Result<Option<String>> {
2375 let ct = mail.get_content_disposition();
2376
2377 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2380
2381 if desired_filename.is_none()
2382 && let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
2383 {
2384 warn!(context, "apostrophed encoding invalid: {}", name);
2388 desired_filename = Some(name);
2389 }
2390
2391 if desired_filename.is_none() {
2393 desired_filename = ct.params.get("name").map(|s| s.to_string());
2394 }
2395
2396 if desired_filename.is_none() {
2399 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2400 }
2401
2402 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2404 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2405 desired_filename = Some(format!("file.{subtype}",));
2406 } else {
2407 bail!(
2408 "could not determine attachment filename: {:?}",
2409 ct.disposition
2410 );
2411 };
2412 }
2413
2414 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2415
2416 Ok(desired_filename)
2417}
2418
2419pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2421 let to_addresses = get_all_addresses_from_header(headers, "to");
2422 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2423
2424 let mut res = to_addresses;
2425 res.extend(cc_addresses);
2426 res
2427}
2428
2429pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2431 let all = get_all_addresses_from_header(headers, "from");
2432 tools::single_value(all)
2433}
2434
2435pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2437 get_all_addresses_from_header(headers, "list-post")
2438 .into_iter()
2439 .next()
2440 .map(|s| s.addr)
2441}
2442
2443fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2455 let mut result: Vec<SingleInfo> = Default::default();
2456
2457 if let Some(header) = headers
2458 .iter()
2459 .rev()
2460 .find(|h| h.get_key().to_lowercase() == header)
2461 && let Ok(addrs) = mailparse::addrparse_header(header)
2462 {
2463 for addr in addrs.iter() {
2464 match addr {
2465 mailparse::MailAddr::Single(info) => {
2466 result.push(SingleInfo {
2467 addr: addr_normalize(&info.addr).to_lowercase(),
2468 display_name: info.display_name.clone(),
2469 });
2470 }
2471 mailparse::MailAddr::Group(infos) => {
2472 for info in &infos.addrs {
2473 result.push(SingleInfo {
2474 addr: addr_normalize(&info.addr).to_lowercase(),
2475 display_name: info.display_name.clone(),
2476 });
2477 }
2478 }
2479 }
2480 }
2481 }
2482
2483 result
2484}
2485
2486async fn handle_mdn(
2487 context: &Context,
2488 from_id: ContactId,
2489 rfc724_mid: &str,
2490 timestamp_sent: i64,
2491) -> Result<()> {
2492 if from_id == ContactId::SELF {
2493 return Ok(());
2495 }
2496
2497 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2498 .sql
2499 .query_row_optional(
2500 "SELECT
2501 m.id AS msg_id,
2502 c.id AS chat_id,
2503 mdns.contact_id AS mdn_contact
2504 FROM msgs m
2505 LEFT JOIN chats c ON m.chat_id=c.id
2506 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
2507 WHERE rfc724_mid=? AND from_id=1
2508 ORDER BY msg_id DESC, mdn_contact=? DESC
2509 LIMIT 1",
2510 (&rfc724_mid, from_id),
2511 |row| {
2512 let msg_id: MsgId = row.get("msg_id")?;
2513 let chat_id: ChatId = row.get("chat_id")?;
2514 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2515 Ok((
2516 msg_id,
2517 chat_id,
2518 mdn_contact.is_some(),
2519 mdn_contact == Some(from_id),
2520 ))
2521 },
2522 )
2523 .await?
2524 else {
2525 info!(
2526 context,
2527 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2528 );
2529 return Ok(());
2530 };
2531
2532 if is_dup {
2533 return Ok(());
2534 }
2535 context
2536 .sql
2537 .execute(
2538 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2539 (msg_id, from_id, timestamp_sent),
2540 )
2541 .await?;
2542 if !has_mdns {
2543 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2544 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2546 }
2547 Ok(())
2548}
2549
2550async fn handle_ndn(
2553 context: &Context,
2554 failed: &DeliveryReport,
2555 error: Option<String>,
2556) -> Result<()> {
2557 if failed.rfc724_mid.is_empty() {
2558 return Ok(());
2559 }
2560
2561 let msg_ids = context
2564 .sql
2565 .query_map_vec(
2566 "SELECT id FROM msgs
2567 WHERE rfc724_mid=? AND from_id=1",
2568 (&failed.rfc724_mid,),
2569 |row| {
2570 let msg_id: MsgId = row.get(0)?;
2571 Ok(msg_id)
2572 },
2573 )
2574 .await?;
2575
2576 let error = if let Some(error) = error {
2577 error
2578 } else {
2579 "Delivery to at least one recipient failed.".to_string()
2580 };
2581 let err_msg = &error;
2582
2583 for msg_id in msg_ids {
2584 let mut message = Message::load_from_db(context, msg_id).await?;
2585 let aggregated_error = message
2586 .error
2587 .as_ref()
2588 .map(|err| format!("{err}\n\n{err_msg}"));
2589 set_msg_failed(
2590 context,
2591 &mut message,
2592 aggregated_error.as_ref().unwrap_or(err_msg),
2593 )
2594 .await?;
2595 }
2596
2597 Ok(())
2598}
2599
2600#[cfg(test)]
2601mod mimeparser_tests;