1use std::cmp::min;
4use std::collections::{HashMap, HashSet};
5use std::path::Path;
6use std::str;
7use std::str::FromStr;
8
9use anyhow::{bail, Context as _, Result};
10use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
11use deltachat_derive::{FromSql, ToSql};
12use format_flowed::unformat_flowed;
13use mailparse::{addrparse_header, DispositionType, MailHeader, MailHeaderMap, SingleInfo};
14use mime::Mime;
15
16use crate::aheader::{Aheader, EncryptPreference};
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::{
25 get_autocrypt_peerstate, get_encrypted_mime, keyring_from_peerstate, try_decrypt,
26 validate_detached_signature,
27};
28use crate::dehtml::dehtml;
29use crate::events::EventType;
30use crate::headerdef::{HeaderDef, HeaderDefMap};
31use crate::key::{self, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
32use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
33use crate::param::{Param, Params};
34use crate::peerstate::Peerstate;
35use crate::simplify::{simplify, SimplifiedText};
36use crate::sync::SyncItems;
37use crate::tools::time;
38use crate::tools::{
39 get_filemeta, parse_receive_headers, smeared_time, truncate_msg_text, validate_id,
40};
41use crate::{chatlist_events, location, stock_str, tools};
42
43#[derive(Debug)]
53pub(crate) struct MimeMessage {
54 pub parts: Vec<Part>,
56
57 headers: HashMap<String, String>,
59
60 #[cfg(test)]
61 headers_removed: HashSet<String>,
63
64 pub recipients: Vec<SingleInfo>,
68
69 pub past_members: Vec<SingleInfo>,
71
72 pub from: SingleInfo,
74
75 pub from_is_signed: bool,
78 pub incoming: bool,
80 pub list_post: Option<String>,
83 pub chat_disposition_notification_to: Option<SingleInfo>,
84 pub autocrypt_header: Option<Aheader>,
85 pub peerstate: Option<Peerstate>,
86 pub decrypting_failed: bool,
87
88 pub signatures: HashSet<Fingerprint>,
94 pub gossiped_keys: HashMap<String, SignedPublicKey>,
98
99 pub is_forwarded: bool,
101 pub is_system_message: SystemMessage,
102 pub location_kml: Option<location::Kml>,
103 pub message_kml: Option<location::Kml>,
104 pub(crate) sync_items: Option<SyncItems>,
105 pub(crate) webxdc_status_update: Option<String>,
106 pub(crate) user_avatar: Option<AvatarAction>,
107 pub(crate) group_avatar: Option<AvatarAction>,
108 pub(crate) mdn_reports: Vec<Report>,
109 pub(crate) delivery_report: Option<DeliveryReport>,
110
111 pub(crate) footer: Option<String>,
116
117 pub is_mime_modified: bool,
120
121 pub decoded_data: Vec<u8>,
124
125 pub(crate) hop_info: String,
127
128 pub(crate) is_bot: Option<bool>,
138
139 pub(crate) timestamp_rcvd: i64,
141 pub(crate) timestamp_sent: i64,
144}
145
146#[derive(Debug, PartialEq)]
147pub(crate) enum AvatarAction {
148 Delete,
149 Change(String),
150}
151
152#[derive(
154 Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
155)]
156#[repr(u32)]
157pub enum SystemMessage {
158 #[default]
160 Unknown = 0,
161
162 GroupNameChanged = 2,
164
165 GroupImageChanged = 3,
167
168 MemberAddedToGroup = 4,
170
171 MemberRemovedFromGroup = 5,
173
174 AutocryptSetupMessage = 6,
176
177 SecurejoinMessage = 7,
179
180 LocationStreamingEnabled = 8,
182
183 LocationOnly = 9,
185
186 EphemeralTimerChanged = 10,
188
189 ChatProtectionEnabled = 11,
191
192 ChatProtectionDisabled = 12,
194
195 InvalidUnencryptedMail = 13,
198
199 SecurejoinWait = 14,
202
203 SecurejoinWaitTimeout = 15,
206
207 MultiDeviceSync = 20,
210
211 WebxdcStatusUpdate = 30,
215
216 WebxdcInfoMessage = 32,
218
219 IrohNodeAddr = 40,
221}
222
223const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
224
225impl MimeMessage {
226 pub(crate) async fn from_bytes(
231 context: &Context,
232 body: &[u8],
233 partial: Option<u32>,
234 ) -> Result<Self> {
235 let mail = mailparse::parse_mail(body)?;
236
237 let timestamp_rcvd = smeared_time(context);
238 let mut timestamp_sent =
239 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
240 let mut hop_info = parse_receive_headers(&mail.get_headers());
241
242 let mut headers = Default::default();
243 let mut headers_removed = HashSet::<String>::new();
244 let mut recipients = Default::default();
245 let mut past_members = Default::default();
246 let mut from = Default::default();
247 let mut list_post = Default::default();
248 let mut chat_disposition_notification_to = None;
249
250 MimeMessage::merge_headers(
252 context,
253 &mut headers,
254 &mut recipients,
255 &mut past_members,
256 &mut from,
257 &mut list_post,
258 &mut chat_disposition_notification_to,
259 &mail.headers,
260 );
261 headers.retain(|k, _| {
262 !is_hidden(k) || {
263 headers_removed.insert(k.clone());
264 false
265 }
266 });
267
268 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
270 let (part, mimetype) =
271 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
272 if let Some(part) = mail.subparts.first() {
273 timestamp_sent =
277 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
278 MimeMessage::merge_headers(
279 context,
280 &mut headers,
281 &mut recipients,
282 &mut past_members,
283 &mut from,
284 &mut list_post,
285 &mut chat_disposition_notification_to,
286 &part.headers,
287 );
288 (part, part.ctype.mimetype.parse::<Mime>()?)
289 } else {
290 (&mail, mimetype)
292 }
293 } else {
294 (&mail, mimetype)
296 };
297 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
298 if let Some(part) = part.subparts.first() {
299 for field in &part.headers {
300 let key = field.get_key().to_lowercase();
301 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
302 headers.insert(key.to_string(), field.get_value());
303 }
304 }
305 }
306 }
307
308 if let Some(microsoft_message_id) = remove_header(
312 &mut headers,
313 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
314 &mut headers_removed,
315 ) {
316 headers.insert(
317 HeaderDef::MessageId.get_headername().to_string(),
318 microsoft_message_id,
319 );
320 }
321
322 Self::remove_secured_headers(&mut headers, &mut headers_removed);
325
326 let mut from = from.context("No from in message")?;
327 let private_keyring = load_self_secret_keyring(context).await?;
328
329 let allow_aeap = get_encrypted_mime(&mail).is_some();
330
331 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
332
333 let mut gossiped_keys = Default::default();
334 let mut from_is_signed = false;
335 hop_info += "\n\n";
336 hop_info += &dkim_results.to_string();
337
338 let incoming = !context.is_self_addr(&from.addr).await?;
339
340 let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
341
342 let mail_raw; let decrypted_msg; let (mail, encrypted) =
346 match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
347 Ok(Some(msg)) => {
348 mail_raw = msg.get_content()?.unwrap_or_default();
349
350 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
351 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
352 info!(
353 context,
354 "decrypted message mime-body:\n{}",
355 String::from_utf8_lossy(&mail_raw),
356 );
357 }
358
359 decrypted_msg = Some(msg);
360
361 timestamp_sent = Self::get_timestamp_sent(
362 &decrypted_mail.headers,
363 timestamp_sent,
364 timestamp_rcvd,
365 );
366
367 if let Some(protected_aheader_value) = decrypted_mail
368 .headers
369 .get_header_value(HeaderDef::Autocrypt)
370 {
371 aheader_value = Some(protected_aheader_value);
372 }
373
374 (Ok(decrypted_mail), true)
375 }
376 Ok(None) => {
377 mail_raw = Vec::new();
378 decrypted_msg = None;
379 (Ok(mail), false)
380 }
381 Err(err) => {
382 mail_raw = Vec::new();
383 decrypted_msg = None;
384 warn!(context, "decryption failed: {:#}", err);
385 (Err(err), false)
386 }
387 };
388
389 let autocrypt_header = if !incoming {
390 None
391 } else if let Some(aheader_value) = aheader_value {
392 match Aheader::from_str(&aheader_value) {
393 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
394 Ok(header) => {
395 warn!(
396 context,
397 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
398 );
399 None
400 }
401 Err(err) => {
402 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
403 None
404 }
405 }
406 } else {
407 None
408 };
409
410 let mut peerstate = get_autocrypt_peerstate(
412 context,
413 &from.addr,
414 autocrypt_header.as_ref(),
415 timestamp_sent,
416 allow_aeap,
417 )
418 .await?;
419
420 let public_keyring = match peerstate.is_none() && !incoming {
421 true => key::load_self_public_keyring(context).await?,
422 false => keyring_from_peerstate(peerstate.as_ref()),
423 };
424
425 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
426 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
427 } else {
428 HashSet::new()
429 };
430
431 let mail = mail.as_ref().map(|mail| {
432 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
433 .unwrap_or((mail, Default::default()));
434 signatures.extend(signatures_detached);
435 content
436 });
437 if let (Ok(mail), true) = (mail, encrypted) {
438 if !signatures.is_empty() {
439 for h in [
443 HeaderDef::Subject,
444 HeaderDef::ChatGroupId,
445 HeaderDef::ChatGroupName,
446 HeaderDef::ChatGroupNameChanged,
447 HeaderDef::ChatGroupNameTimestamp,
448 HeaderDef::ChatGroupAvatar,
449 HeaderDef::ChatGroupMemberRemoved,
450 HeaderDef::ChatGroupMemberAdded,
451 HeaderDef::ChatGroupMemberTimestamps,
452 HeaderDef::ChatGroupPastMembers,
453 HeaderDef::ChatDelete,
454 HeaderDef::ChatEdit,
455 HeaderDef::ChatUserAvatar,
456 ] {
457 remove_header(&mut headers, h.get_headername(), &mut headers_removed);
458 }
459 }
460
461 let mut inner_from = None;
467
468 MimeMessage::merge_headers(
469 context,
470 &mut headers,
471 &mut recipients,
472 &mut past_members,
473 &mut inner_from,
474 &mut list_post,
475 &mut chat_disposition_notification_to,
476 &mail.headers,
477 );
478
479 if !signatures.is_empty() {
480 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
485 gossiped_keys = update_gossip_peerstates(
486 context,
487 timestamp_sent,
488 &from.addr,
489 &recipients,
490 gossip_headers,
491 )
492 .await?;
493 }
494
495 if let Some(inner_from) = inner_from {
496 if !addr_cmp(&inner_from.addr, &from.addr) {
497 warn!(
506 context,
507 "From header in encrypted part doesn't match the outer one",
508 );
509
510 bail!("From header is forged");
515 }
516 from = inner_from;
517 from_is_signed = !signatures.is_empty();
518 }
519 }
520 if signatures.is_empty() {
521 Self::remove_secured_headers(&mut headers, &mut headers_removed);
522
523 if let (Some(peerstate), Ok(mail)) = (&mut peerstate, mail) {
525 if timestamp_sent > peerstate.last_seen_autocrypt
526 && mail.ctype.mimetype != "multipart/report"
527 {
528 peerstate.degrade_encryption(timestamp_sent);
529 }
530 }
531 }
532 if !encrypted {
533 signatures.clear();
534 }
535 if let Some(peerstate) = &mut peerstate {
536 if peerstate.prefer_encrypt != EncryptPreference::Mutual && !signatures.is_empty() {
537 peerstate.prefer_encrypt = EncryptPreference::Mutual;
538 peerstate.save_to_db(&context.sql).await?;
539 }
540 }
541
542 let mut parser = MimeMessage {
543 parts: Vec::new(),
544 headers,
545 #[cfg(test)]
546 headers_removed,
547
548 recipients,
549 past_members,
550 list_post,
551 from,
552 from_is_signed,
553 incoming,
554 chat_disposition_notification_to,
555 autocrypt_header,
556 peerstate,
557 decrypting_failed: mail.is_err(),
558
559 signatures,
561 gossiped_keys,
562 is_forwarded: false,
563 mdn_reports: Vec::new(),
564 is_system_message: SystemMessage::Unknown,
565 location_kml: None,
566 message_kml: None,
567 sync_items: None,
568 webxdc_status_update: None,
569 user_avatar: None,
570 group_avatar: None,
571 delivery_report: None,
572 footer: None,
573 is_mime_modified: false,
574 decoded_data: Vec::new(),
575 hop_info,
576 is_bot: None,
577 timestamp_rcvd,
578 timestamp_sent,
579 };
580
581 match partial {
582 Some(org_bytes) => {
583 parser
584 .create_stub_from_partial_download(context, org_bytes)
585 .await?;
586 }
587 None => match mail {
588 Ok(mail) => {
589 parser.parse_mime_recursive(context, mail, false).await?;
590 }
591 Err(err) => {
592 let msg_body = stock_str::cant_decrypt_msg_body(context).await;
593 let txt = format!("[{msg_body}]");
594
595 let part = Part {
596 typ: Viewtype::Text,
597 msg_raw: Some(txt.clone()),
598 msg: txt,
599 error: Some(format!("Decrypting failed: {err:#}")),
602 ..Default::default()
603 };
604 parser.parts.push(part);
605 }
606 },
607 };
608
609 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
610 if parser.mdn_reports.is_empty()
611 && !is_location_only
612 && parser.sync_items.is_none()
613 && parser.webxdc_status_update.is_none()
614 {
615 let is_bot =
616 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
617 parser.is_bot = Some(is_bot);
618 }
619 parser.maybe_remove_bad_parts();
620 parser.maybe_remove_inline_mailinglist_footer();
621 parser.heuristically_parse_ndn(context).await;
622 parser.parse_headers(context).await?;
623
624 if parser.is_mime_modified {
625 parser.decoded_data = mail_raw;
626 }
627
628 Ok(parser)
629 }
630
631 fn get_timestamp_sent(
632 hdrs: &[mailparse::MailHeader<'_>],
633 default: i64,
634 timestamp_rcvd: i64,
635 ) -> i64 {
636 hdrs.get_header_value(HeaderDef::Date)
637 .and_then(|v| mailparse::dateparse(&v).ok())
638 .map_or(default, |value| {
639 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
640 })
641 }
642
643 fn parse_system_message_headers(&mut self, context: &Context) {
645 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
646 self.parts.retain(|part| {
647 part.mimetype.is_none()
648 || part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
649 });
650
651 if self.parts.len() == 1 {
652 self.is_system_message = SystemMessage::AutocryptSetupMessage;
653 } else {
654 warn!(context, "could not determine ASM mime-part");
655 }
656 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
657 if value == "location-streaming-enabled" {
658 self.is_system_message = SystemMessage::LocationStreamingEnabled;
659 } else if value == "ephemeral-timer-changed" {
660 self.is_system_message = SystemMessage::EphemeralTimerChanged;
661 } else if value == "protection-enabled" {
662 self.is_system_message = SystemMessage::ChatProtectionEnabled;
663 } else if value == "protection-disabled" {
664 self.is_system_message = SystemMessage::ChatProtectionDisabled;
665 } else if value == "group-avatar-changed" {
666 self.is_system_message = SystemMessage::GroupImageChanged;
667 }
668 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
669 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
670 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
671 self.is_system_message = SystemMessage::MemberAddedToGroup;
672 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
673 self.is_system_message = SystemMessage::GroupNameChanged;
674 }
675 }
676
677 fn parse_avatar_headers(&mut self, context: &Context) {
679 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
680 self.group_avatar = self.avatar_action_from_header(context, header_value.to_string());
681 }
682
683 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
684 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string());
685 }
686 }
687
688 fn parse_videochat_headers(&mut self) {
689 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
690 if value == "videochat-invitation" {
691 let instance = self
692 .get_header(HeaderDef::ChatWebrtcRoom)
693 .map(|s| s.to_string());
694 if let Some(part) = self.parts.first_mut() {
695 part.typ = Viewtype::VideochatInvitation;
696 part.param
697 .set(Param::WebrtcRoom, instance.unwrap_or_default());
698 }
699 }
700 }
701 }
702
703 fn squash_attachment_parts(&mut self) {
709 if self.parts.len() == 2
710 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
711 && self
712 .parts
713 .get(1)
714 .is_some_and(|filepart| match filepart.typ {
715 Viewtype::Image
716 | Viewtype::Gif
717 | Viewtype::Sticker
718 | Viewtype::Audio
719 | Viewtype::Voice
720 | Viewtype::Video
721 | Viewtype::Vcard
722 | Viewtype::File
723 | Viewtype::Webxdc => true,
724 Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
725 })
726 {
727 let mut parts = std::mem::take(&mut self.parts);
728 let Some(mut filepart) = parts.pop() else {
729 return;
731 };
732 let Some(textpart) = parts.pop() else {
733 return;
735 };
736
737 filepart.msg.clone_from(&textpart.msg);
738 if let Some(quote) = textpart.param.get(Param::Quote) {
739 filepart.param.set(Param::Quote, quote);
740 }
741
742 self.parts = vec![filepart];
743 }
744 }
745
746 fn parse_attachments(&mut self) {
748 if self.parts.len() != 1 {
751 return;
752 }
753
754 if let Some(mut part) = self.parts.pop() {
755 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
756 {
757 part.typ = Viewtype::Voice;
758 }
759 if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
760 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
761 if value == "sticker" {
762 part.typ = Viewtype::Sticker;
763 }
764 }
765 }
766 if part.typ == Viewtype::Audio
767 || part.typ == Viewtype::Voice
768 || part.typ == Viewtype::Video
769 {
770 if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
771 let duration_ms = field_0.parse().unwrap_or_default();
772 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
773 part.param.set_int(Param::Duration, duration_ms);
774 }
775 }
776 }
777
778 self.parts.push(part);
779 }
780 }
781
782 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
783 self.parse_system_message_headers(context);
784 self.parse_avatar_headers(context);
785 self.parse_videochat_headers();
786 if self.delivery_report.is_none() {
787 self.squash_attachment_parts();
788 }
789
790 if !context.get_config_bool(Config::Bot).await? {
791 if let Some(ref subject) = self.get_subject() {
792 let mut prepend_subject = true;
793 if !self.decrypting_failed {
794 let colon = subject.find(':');
795 if colon == Some(2)
796 || colon == Some(3)
797 || self.has_chat_version()
798 || subject.contains("Chat:")
799 {
800 prepend_subject = false
801 }
802 }
803
804 if self.is_mailinglist_message() && !self.has_chat_version() {
807 prepend_subject = true;
808 }
809
810 if prepend_subject && !subject.is_empty() {
811 let part_with_text = self
812 .parts
813 .iter_mut()
814 .find(|part| !part.msg.is_empty() && !part.is_reaction);
815 if let Some(part) = part_with_text {
816 part.msg = format!("{} – {}", subject, part.msg);
817 }
818 }
819 }
820 }
821
822 if self.is_forwarded {
823 for part in &mut self.parts {
824 part.param.set_int(Param::Forwarded, 1);
825 }
826 }
827
828 self.parse_attachments();
829
830 if !self.decrypting_failed && !self.parts.is_empty() {
832 if let Some(ref dn_to) = self.chat_disposition_notification_to {
833 let from = &self.from.addr;
835 if !context.is_self_addr(from).await? {
836 if from.to_lowercase() == dn_to.addr.to_lowercase() {
837 if let Some(part) = self.parts.last_mut() {
838 part.param.set_int(Param::WantsMdn, 1);
839 }
840 } else {
841 warn!(
842 context,
843 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
844 );
845 }
846 }
847 }
848 }
849
850 if self.parts.is_empty() && self.mdn_reports.is_empty() {
855 let mut part = Part {
856 typ: Viewtype::Text,
857 ..Default::default()
858 };
859
860 if let Some(ref subject) = self.get_subject() {
861 if !self.has_chat_version() && self.webxdc_status_update.is_none() {
862 part.msg = subject.to_string();
863 }
864 }
865
866 self.do_add_single_part(part);
867 }
868
869 if self.is_bot == Some(true) {
870 for part in &mut self.parts {
871 part.param.set(Param::Bot, "1");
872 }
873 }
874
875 Ok(())
876 }
877
878 fn avatar_action_from_header(
879 &mut self,
880 context: &Context,
881 header_value: String,
882 ) -> Option<AvatarAction> {
883 if header_value == "0" {
884 Some(AvatarAction::Delete)
885 } else if let Some(base64) = header_value
886 .split_ascii_whitespace()
887 .collect::<String>()
888 .strip_prefix("base64:")
889 {
890 match BlobObject::store_from_base64(context, base64) {
891 Ok(path) => Some(AvatarAction::Change(path)),
892 Err(err) => {
893 warn!(
894 context,
895 "Could not decode and save avatar to blob file: {:#}", err,
896 );
897 None
898 }
899 }
900 } else {
901 let mut i = 0;
904 while let Some(part) = self.parts.get_mut(i) {
905 if let Some(part_filename) = &part.org_filename {
906 if part_filename == &header_value {
907 if let Some(blob) = part.param.get(Param::File) {
908 let res = Some(AvatarAction::Change(blob.to_string()));
909 self.parts.remove(i);
910 return res;
911 }
912 break;
913 }
914 }
915 i += 1;
916 }
917 None
918 }
919 }
920
921 pub fn was_encrypted(&self) -> bool {
927 !self.signatures.is_empty()
928 }
929
930 pub(crate) fn has_chat_version(&self) -> bool {
933 self.headers.contains_key("chat-version")
934 }
935
936 pub(crate) fn get_subject(&self) -> Option<String> {
937 self.get_header(HeaderDef::Subject)
938 .map(|s| s.trim_start())
939 .filter(|s| !s.is_empty())
940 .map(|s| s.to_string())
941 }
942
943 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
944 self.headers
945 .get(headerdef.get_headername())
946 .map(|s| s.as_str())
947 }
948
949 #[cfg(test)]
950 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
955 let hname = headerdef.get_headername();
956 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
957 }
958
959 pub fn get_chat_group_id(&self) -> Option<&str> {
961 self.get_header(HeaderDef::ChatGroupId)
962 .filter(|s| validate_id(s))
963 }
964
965 async fn parse_mime_recursive<'a>(
966 &'a mut self,
967 context: &'a Context,
968 mail: &'a mailparse::ParsedMail<'a>,
969 is_related: bool,
970 ) -> Result<bool> {
971 enum MimeS {
972 Multiple,
973 Single,
974 Message,
975 }
976
977 let mimetype = mail.ctype.mimetype.to_lowercase();
978
979 let m = if mimetype.starts_with("multipart") {
980 if mail.ctype.params.contains_key("boundary") {
981 MimeS::Multiple
982 } else {
983 MimeS::Single
984 }
985 } else if mimetype.starts_with("message") {
986 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
987 MimeS::Message
988 } else {
989 MimeS::Single
990 }
991 } else {
992 MimeS::Single
993 };
994
995 let is_related = is_related || mimetype == "multipart/related";
996 match m {
997 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
998 MimeS::Message => {
999 let raw = mail.get_body_raw()?;
1000 if raw.is_empty() {
1001 return Ok(false);
1002 }
1003 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1004
1005 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1006 }
1007 MimeS::Single => {
1008 self.add_single_part_if_known(context, mail, is_related)
1009 .await
1010 }
1011 }
1012 }
1013
1014 async fn handle_multiple(
1015 &mut self,
1016 context: &Context,
1017 mail: &mailparse::ParsedMail<'_>,
1018 is_related: bool,
1019 ) -> Result<bool> {
1020 let mut any_part_added = false;
1021 let mimetype = get_mime_type(mail, &get_attachment_filename(context, mail)?)?.0;
1022 match (mimetype.type_(), mimetype.subtype().as_str()) {
1023 (mime::MULTIPART, "alternative") => {
1028 for cur_data in &mail.subparts {
1029 let mime_type =
1030 get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
1031 if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
1032 any_part_added = self
1033 .parse_mime_recursive(context, cur_data, is_related)
1034 .await?;
1035 break;
1036 }
1037 }
1038 if !any_part_added {
1039 for cur_data in &mail.subparts {
1041 if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
1042 .0
1043 .type_()
1044 == mime::TEXT
1045 {
1046 any_part_added = self
1047 .parse_mime_recursive(context, cur_data, is_related)
1048 .await?;
1049 break;
1050 }
1051 }
1052 }
1053 if !any_part_added {
1054 for cur_part in &mail.subparts {
1056 if self
1057 .parse_mime_recursive(context, cur_part, is_related)
1058 .await?
1059 {
1060 any_part_added = true;
1061 break;
1062 }
1063 }
1064 }
1065 if any_part_added && mail.subparts.len() > 1 {
1066 self.is_mime_modified = true;
1070 }
1071 }
1072 (mime::MULTIPART, "signed") => {
1073 if let Some(first) = mail.subparts.first() {
1082 any_part_added = self
1083 .parse_mime_recursive(context, first, is_related)
1084 .await?;
1085 }
1086 }
1087 (mime::MULTIPART, "report") => {
1088 if mail.subparts.len() >= 2 {
1090 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1091 Some("disposition-notification") => {
1092 if let Some(report) = self.process_report(context, mail)? {
1093 self.mdn_reports.push(report);
1094 }
1095
1096 let part = Part {
1101 typ: Viewtype::Unknown,
1102 ..Default::default()
1103 };
1104 self.parts.push(part);
1105
1106 any_part_added = true;
1107 }
1108 Some("delivery-status") | None => {
1110 if let Some(report) = self.process_delivery_status(context, mail)? {
1111 self.delivery_report = Some(report);
1112 }
1113
1114 for cur_data in &mail.subparts {
1116 if self
1117 .parse_mime_recursive(context, cur_data, is_related)
1118 .await?
1119 {
1120 any_part_added = true;
1121 }
1122 }
1123 }
1124 Some("multi-device-sync") => {
1125 if let Some(second) = mail.subparts.get(1) {
1126 self.add_single_part_if_known(context, second, is_related)
1127 .await?;
1128 }
1129 }
1130 Some("status-update") => {
1131 if let Some(second) = mail.subparts.get(1) {
1132 self.add_single_part_if_known(context, second, is_related)
1133 .await?;
1134 }
1135 }
1136 Some(_) => {
1137 for cur_data in &mail.subparts {
1138 if self
1139 .parse_mime_recursive(context, cur_data, is_related)
1140 .await?
1141 {
1142 any_part_added = true;
1143 }
1144 }
1145 }
1146 }
1147 }
1148 }
1149 _ => {
1150 for cur_data in &mail.subparts {
1153 if self
1154 .parse_mime_recursive(context, cur_data, is_related)
1155 .await?
1156 {
1157 any_part_added = true;
1158 }
1159 }
1160 }
1161 }
1162
1163 Ok(any_part_added)
1164 }
1165
1166 async fn add_single_part_if_known(
1168 &mut self,
1169 context: &Context,
1170 mail: &mailparse::ParsedMail<'_>,
1171 is_related: bool,
1172 ) -> Result<bool> {
1173 let filename = get_attachment_filename(context, mail)?;
1175 let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
1176 let raw_mime = mail.ctype.mimetype.to_lowercase();
1177
1178 let old_part_count = self.parts.len();
1179
1180 match filename {
1181 Some(filename) => {
1182 self.do_add_single_file_part(
1183 context,
1184 msg_type,
1185 mime_type,
1186 &raw_mime,
1187 &mail.get_body_raw()?,
1188 &filename,
1189 is_related,
1190 )
1191 .await?;
1192 }
1193 None => {
1194 match mime_type.type_() {
1195 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1196 warn!(context, "Missing attachment");
1197 return Ok(false);
1198 }
1199 mime::TEXT
1200 if mail.get_content_disposition().disposition
1201 == DispositionType::Extension("reaction".to_string()) =>
1202 {
1203 let decoded_data = match mail.get_body() {
1205 Ok(decoded_data) => decoded_data,
1206 Err(err) => {
1207 warn!(context, "Invalid body parsed {:#}", err);
1208 return Ok(false);
1210 }
1211 };
1212
1213 let part = Part {
1214 typ: Viewtype::Text,
1215 mimetype: Some(mime_type),
1216 msg: decoded_data,
1217 is_reaction: true,
1218 ..Default::default()
1219 };
1220 self.do_add_single_part(part);
1221 return Ok(true);
1222 }
1223 mime::TEXT | mime::HTML => {
1224 let decoded_data = match mail.get_body() {
1225 Ok(decoded_data) => decoded_data,
1226 Err(err) => {
1227 warn!(context, "Invalid body parsed {:#}", err);
1228 return Ok(false);
1230 }
1231 };
1232
1233 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1234 let mut dehtml_failed = false;
1235
1236 let SimplifiedText {
1237 text: simplified_txt,
1238 is_forwarded,
1239 is_cut,
1240 top_quote,
1241 footer,
1242 } = if decoded_data.is_empty() {
1243 Default::default()
1244 } else {
1245 let is_html = mime_type == mime::TEXT_HTML;
1246 if is_html {
1247 self.is_mime_modified = true;
1248 if let Some(text) = dehtml(&decoded_data) {
1249 text
1250 } else {
1251 dehtml_failed = true;
1252 SimplifiedText {
1253 text: decoded_data.clone(),
1254 ..Default::default()
1255 }
1256 }
1257 } else {
1258 simplify(decoded_data.clone(), self.has_chat_version())
1259 }
1260 };
1261
1262 self.is_mime_modified = self.is_mime_modified
1263 || ((is_forwarded || is_cut || top_quote.is_some())
1264 && !self.has_chat_version());
1265
1266 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1267 {
1268 format.as_str().eq_ignore_ascii_case("flowed")
1269 } else {
1270 false
1271 };
1272
1273 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1274 && mime_type.subtype() == mime::PLAIN
1275 && is_format_flowed
1276 {
1277 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1278 delsp.as_str().eq_ignore_ascii_case("yes")
1279 } else {
1280 false
1281 };
1282 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1283 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1284 (unflowed_text, unflowed_quote)
1285 } else {
1286 (simplified_txt, top_quote)
1287 };
1288
1289 let (simplified_txt, was_truncated) =
1290 truncate_msg_text(context, simplified_txt).await?;
1291 if was_truncated {
1292 self.is_mime_modified = was_truncated;
1293 }
1294
1295 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1296 let mut part = Part {
1297 dehtml_failed,
1298 typ: Viewtype::Text,
1299 mimetype: Some(mime_type),
1300 msg: simplified_txt,
1301 ..Default::default()
1302 };
1303 if let Some(quote) = simplified_quote {
1304 part.param.set(Param::Quote, quote);
1305 }
1306 part.msg_raw = Some(decoded_data);
1307 self.do_add_single_part(part);
1308 }
1309
1310 if is_forwarded {
1311 self.is_forwarded = true;
1312 }
1313
1314 if self.footer.is_none() && is_plaintext {
1315 self.footer = Some(footer.unwrap_or_default());
1316 }
1317 }
1318 _ => {}
1319 }
1320 }
1321 }
1322
1323 Ok(self.parts.len() > old_part_count)
1325 }
1326
1327 #[expect(clippy::too_many_arguments)]
1328 async fn do_add_single_file_part(
1329 &mut self,
1330 context: &Context,
1331 msg_type: Viewtype,
1332 mime_type: Mime,
1333 raw_mime: &str,
1334 decoded_data: &[u8],
1335 filename: &str,
1336 is_related: bool,
1337 ) -> Result<()> {
1338 if decoded_data.is_empty() {
1339 return Ok(());
1340 }
1341 if let Some(peerstate) = &mut self.peerstate {
1342 if peerstate.prefer_encrypt != EncryptPreference::Mutual
1343 && mime_type.type_() == mime::APPLICATION
1344 && mime_type.subtype().as_str() == "pgp-keys"
1345 && Self::try_set_peer_key_from_file_part(context, peerstate, decoded_data).await?
1346 {
1347 return Ok(());
1348 }
1349 }
1350 let mut part = Part::default();
1351 let msg_type = if context
1352 .is_webxdc_file(filename, decoded_data)
1353 .await
1354 .unwrap_or(false)
1355 {
1356 Viewtype::Webxdc
1357 } else if filename.ends_with(".kml") {
1358 if filename.starts_with("location") || filename.starts_with("message") {
1361 let parsed = location::Kml::parse(decoded_data)
1362 .map_err(|err| {
1363 warn!(context, "failed to parse kml part: {:#}", err);
1364 })
1365 .ok();
1366 if filename.starts_with("location") {
1367 self.location_kml = parsed;
1368 } else {
1369 self.message_kml = parsed;
1370 }
1371 return Ok(());
1372 }
1373 msg_type
1374 } else if filename == "multi-device-sync.json" {
1375 if !context.get_config_bool(Config::SyncMsgs).await? {
1376 return Ok(());
1377 }
1378 let serialized = String::from_utf8_lossy(decoded_data)
1379 .parse()
1380 .unwrap_or_default();
1381 self.sync_items = context
1382 .parse_sync_items(serialized)
1383 .map_err(|err| {
1384 warn!(context, "failed to parse sync data: {:#}", err);
1385 })
1386 .ok();
1387 return Ok(());
1388 } else if filename == "status-update.json" {
1389 let serialized = String::from_utf8_lossy(decoded_data)
1390 .parse()
1391 .unwrap_or_default();
1392 self.webxdc_status_update = Some(serialized);
1393 return Ok(());
1394 } else if msg_type == Viewtype::Vcard {
1395 if let Some(summary) = get_vcard_summary(decoded_data) {
1396 part.param.set(Param::Summary1, summary);
1397 msg_type
1398 } else {
1399 Viewtype::File
1400 }
1401 } else {
1402 msg_type
1403 };
1404
1405 let blob =
1409 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1410 Ok(blob) => blob,
1411 Err(err) => {
1412 error!(
1413 context,
1414 "Could not add blob for mime part {}, error {:#}", filename, err
1415 );
1416 return Ok(());
1417 }
1418 };
1419 info!(context, "added blobfile: {:?}", blob.as_name());
1420
1421 if mime_type.type_() == mime::IMAGE {
1422 if let Ok((width, height)) = get_filemeta(decoded_data) {
1423 part.param.set_int(Param::Width, width as i32);
1424 part.param.set_int(Param::Height, height as i32);
1425 }
1426 }
1427
1428 part.typ = msg_type;
1429 part.org_filename = Some(filename.to_string());
1430 part.mimetype = Some(mime_type);
1431 part.bytes = decoded_data.len();
1432 part.param.set(Param::File, blob.as_name());
1433 part.param.set(Param::Filename, filename);
1434 part.param.set(Param::MimeType, raw_mime);
1435 part.is_related = is_related;
1436
1437 self.do_add_single_part(part);
1438 Ok(())
1439 }
1440
1441 async fn try_set_peer_key_from_file_part(
1443 context: &Context,
1444 peerstate: &mut Peerstate,
1445 decoded_data: &[u8],
1446 ) -> Result<bool> {
1447 let key = match str::from_utf8(decoded_data) {
1448 Err(err) => {
1449 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1450 return Ok(false);
1451 }
1452 Ok(key) => key,
1453 };
1454 let key = match SignedPublicKey::from_asc(key) {
1455 Err(err) => {
1456 warn!(
1457 context,
1458 "PGP key attachment is not an ASCII-armored file: {:#}", err
1459 );
1460 return Ok(false);
1461 }
1462 Ok((key, _)) => key,
1463 };
1464 if let Err(err) = key.verify() {
1465 warn!(context, "attached PGP key verification failed: {}", err);
1466 return Ok(false);
1467 }
1468 if !key.details.users.iter().any(|user| {
1469 user.id
1470 .id()
1471 .ends_with((String::from("<") + &peerstate.addr + ">").as_bytes())
1472 }) {
1473 return Ok(false);
1474 }
1475 if let Some(curr_key) = &peerstate.public_key {
1476 if key != *curr_key && peerstate.prefer_encrypt != EncryptPreference::Reset {
1477 warn!(
1481 context,
1482 "not using attached PGP key for peer '{}' because another one is already set \
1483 with prefer-encrypt={}",
1484 peerstate.addr,
1485 peerstate.prefer_encrypt,
1486 );
1487 return Ok(false);
1488 }
1489 }
1490 peerstate.public_key = Some(key);
1491 info!(
1492 context,
1493 "using attached PGP key for peer '{}' with prefer-encrypt=mutual", peerstate.addr,
1494 );
1495 peerstate.prefer_encrypt = EncryptPreference::Mutual;
1496 peerstate.save_to_db(&context.sql).await?;
1497 Ok(true)
1498 }
1499
1500 fn do_add_single_part(&mut self, mut part: Part) {
1501 if self.was_encrypted() {
1502 part.param.set_int(Param::GuaranteeE2ee, 1);
1503 }
1504 self.parts.push(part);
1505 }
1506
1507 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1508 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1509 return Some(list_id);
1512 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1513 if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
1516 if precedence == "list" || precedence == "bulk" {
1517 return Some(sender);
1521 }
1522 }
1523 }
1524 None
1525 }
1526
1527 pub(crate) fn is_mailinglist_message(&self) -> bool {
1528 self.get_mailinglist_header().is_some()
1529 }
1530
1531 pub(crate) fn is_schleuder_message(&self) -> bool {
1533 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1534 list_help == "<https://schleuder.org/>"
1535 } else {
1536 false
1537 }
1538 }
1539
1540 pub fn replace_msg_by_error(&mut self, error_msg: &str) {
1541 self.is_system_message = SystemMessage::Unknown;
1542 if let Some(part) = self.parts.first_mut() {
1543 part.typ = Viewtype::Text;
1544 part.msg = format!("[{error_msg}]");
1545 self.parts.truncate(1);
1546 }
1547 }
1548
1549 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1550 self.get_header(HeaderDef::MessageId)
1551 .and_then(|msgid| parse_message_id(msgid).ok())
1552 }
1553
1554 fn remove_secured_headers(
1555 headers: &mut HashMap<String, String>,
1556 removed: &mut HashSet<String>,
1557 ) {
1558 remove_header(headers, "secure-join-fingerprint", removed);
1559 remove_header(headers, "secure-join-auth", removed);
1560 remove_header(headers, "chat-verified", removed);
1561 remove_header(headers, "autocrypt-gossip", removed);
1562
1563 if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
1565 if secure_join == "vc-request" || secure_join == "vg-request" {
1566 headers.insert("secure-join".to_string(), secure_join);
1567 }
1568 }
1569 }
1570
1571 #[allow(clippy::too_many_arguments)]
1572 fn merge_headers(
1573 context: &Context,
1574 headers: &mut HashMap<String, String>,
1575 recipients: &mut Vec<SingleInfo>,
1576 past_members: &mut Vec<SingleInfo>,
1577 from: &mut Option<SingleInfo>,
1578 list_post: &mut Option<String>,
1579 chat_disposition_notification_to: &mut Option<SingleInfo>,
1580 fields: &[mailparse::MailHeader<'_>],
1581 ) {
1582 for field in fields {
1583 let key = field.get_key().to_lowercase();
1585 if !headers.contains_key(&key) || is_known(&key) || key.starts_with("chat-")
1587 {
1588 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1589 match addrparse_header(field) {
1590 Ok(addrlist) => {
1591 *chat_disposition_notification_to = addrlist.extract_single_info();
1592 }
1593 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1594 }
1595 } else {
1596 let value = field.get_value();
1597 headers.insert(key.to_string(), value);
1598 }
1599 }
1600 }
1601 let recipients_new = get_recipients(fields);
1602 if !recipients_new.is_empty() {
1603 *recipients = recipients_new;
1604 }
1605 let past_members_addresses =
1606 get_all_addresses_from_header(fields, "chat-group-past-members");
1607 if !past_members_addresses.is_empty() {
1608 *past_members = past_members_addresses;
1609 }
1610 let from_new = get_from(fields);
1611 if from_new.is_some() {
1612 *from = from_new;
1613 }
1614 let list_post_new = get_list_post(fields);
1615 if list_post_new.is_some() {
1616 *list_post = list_post_new;
1617 }
1618 }
1619
1620 fn process_report(
1621 &self,
1622 context: &Context,
1623 report: &mailparse::ParsedMail<'_>,
1624 ) -> Result<Option<Report>> {
1625 let report_body = if let Some(subpart) = report.subparts.get(1) {
1627 subpart.get_body_raw()?
1628 } else {
1629 bail!("Report does not have second MIME part");
1630 };
1631 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1632
1633 if report_fields
1635 .get_header_value(HeaderDef::Disposition)
1636 .is_none()
1637 {
1638 warn!(
1639 context,
1640 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1641 report_fields.get_header_value(HeaderDef::MessageId)
1642 );
1643 return Ok(None);
1644 };
1645
1646 let original_message_id = report_fields
1647 .get_header_value(HeaderDef::OriginalMessageId)
1648 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1651 .and_then(|v| parse_message_id(&v).ok());
1652 let additional_message_ids = report_fields
1653 .get_header_value(HeaderDef::AdditionalMessageIds)
1654 .map_or_else(Vec::new, |v| {
1655 v.split(' ')
1656 .filter_map(|s| parse_message_id(s).ok())
1657 .collect()
1658 });
1659
1660 Ok(Some(Report {
1661 original_message_id,
1662 additional_message_ids,
1663 }))
1664 }
1665
1666 fn process_delivery_status(
1667 &self,
1668 context: &Context,
1669 report: &mailparse::ParsedMail<'_>,
1670 ) -> Result<Option<DeliveryReport>> {
1671 let mut failure = true;
1673
1674 if let Some(status_part) = report.subparts.get(1) {
1675 if status_part.ctype.mimetype != "message/delivery-status"
1678 && status_part.ctype.mimetype != "message/global-delivery-status"
1679 {
1680 warn!(context, "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring");
1681 return Ok(None);
1682 }
1683
1684 let status_body = status_part.get_body_raw()?;
1685
1686 let (_, sz) = mailparse::parse_headers(&status_body)?;
1688
1689 if let Some(status_body) = status_body.get(sz..) {
1691 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1692 if let Some(action) = status_fields.get_first_value("action") {
1693 if action != "failed" {
1694 info!(context, "DSN with {:?} action", action);
1695 failure = false;
1696 }
1697 } else {
1698 warn!(context, "DSN without action");
1699 }
1700 } else {
1701 warn!(context, "DSN without per-recipient fields");
1702 }
1703 } else {
1704 return Ok(None);
1706 }
1707
1708 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1710 p.ctype.mimetype.contains("rfc822")
1711 || p.ctype.mimetype == "message/global"
1712 || p.ctype.mimetype == "message/global-headers"
1713 }) {
1714 let report_body = original_msg.get_body_raw()?;
1715 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1716
1717 if let Some(original_message_id) = report_fields
1718 .get_header_value(HeaderDef::MessageId)
1719 .and_then(|v| parse_message_id(&v).ok())
1720 {
1721 return Ok(Some(DeliveryReport {
1722 rfc724_mid: original_message_id,
1723 failure,
1724 }));
1725 }
1726
1727 warn!(
1728 context,
1729 "ignoring unknown ndn-notification, Message-Id: {:?}",
1730 report_fields.get_header_value(HeaderDef::MessageId)
1731 );
1732 }
1733
1734 Ok(None)
1735 }
1736
1737 fn maybe_remove_bad_parts(&mut self) {
1738 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1739 if good_parts == 0 {
1740 self.parts.truncate(1);
1742 } else if good_parts < self.parts.len() {
1743 self.parts.retain(|p| !p.dehtml_failed);
1744 }
1745
1746 if !self.has_chat_version() && self.is_mime_modified {
1754 fn is_related_image(p: &&Part) -> bool {
1755 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1756 }
1757 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1758 if related_image_cnt > 1 {
1759 let mut is_first_image = true;
1760 self.parts.retain(|p| {
1761 let retain = is_first_image || !is_related_image(&p);
1762 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1763 is_first_image = false;
1764 }
1765 retain
1766 });
1767 }
1768 }
1769 }
1770
1771 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1781 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1782 let text_part_cnt = self
1783 .parts
1784 .iter()
1785 .filter(|p| p.typ == Viewtype::Text)
1786 .count();
1787 if text_part_cnt == 2 {
1788 if let Some(last_part) = self.parts.last() {
1789 if last_part.typ == Viewtype::Text {
1790 self.parts.pop();
1791 }
1792 }
1793 }
1794 }
1795 }
1796
1797 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1801 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1802 let from = from.to_ascii_lowercase();
1803 from.contains("mailer-daemon") || from.contains("mail-daemon")
1804 } else {
1805 false
1806 };
1807 if maybe_ndn && self.delivery_report.is_none() {
1808 for original_message_id in self
1809 .parts
1810 .iter()
1811 .filter_map(|part| part.msg_raw.as_ref())
1812 .flat_map(|part| part.lines())
1813 .filter_map(|line| line.split_once("Message-ID:"))
1814 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1815 {
1816 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1817 {
1818 self.delivery_report = Some(DeliveryReport {
1819 rfc724_mid: original_message_id,
1820 failure: true,
1821 })
1822 }
1823 }
1824 }
1825 }
1826
1827 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1831 for report in &self.mdn_reports {
1832 for original_message_id in report
1833 .original_message_id
1834 .iter()
1835 .chain(&report.additional_message_ids)
1836 {
1837 if let Err(err) =
1838 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1839 {
1840 warn!(context, "Could not handle MDN: {err:#}.");
1841 }
1842 }
1843 }
1844
1845 if let Some(delivery_report) = &self.delivery_report {
1846 if delivery_report.failure {
1847 let error = parts
1848 .iter()
1849 .find(|p| p.typ == Viewtype::Text)
1850 .map(|p| p.msg.clone());
1851 if let Err(err) = handle_ndn(context, delivery_report, error).await {
1852 warn!(context, "Could not handle NDN: {err:#}.");
1853 }
1854 }
1855 }
1856 }
1857
1858 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
1863 let parent_timestamp = if let Some(field) = self
1864 .get_header(HeaderDef::InReplyTo)
1865 .and_then(|msgid| parse_message_id(msgid).ok())
1866 {
1867 context
1868 .sql
1869 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
1870 .await?
1871 } else {
1872 None
1873 };
1874 Ok(parent_timestamp)
1875 }
1876
1877 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
1881 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
1882 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
1883 .map(|h| {
1884 h.split_ascii_whitespace()
1885 .filter_map(|ts| ts.parse::<i64>().ok())
1886 .map(|ts| std::cmp::min(now, ts))
1887 .collect()
1888 })
1889 }
1890}
1891
1892fn remove_header(
1893 headers: &mut HashMap<String, String>,
1894 key: &str,
1895 removed: &mut HashSet<String>,
1896) -> Option<String> {
1897 if let Some((k, v)) = headers.remove_entry(key) {
1898 removed.insert(k);
1899 Some(v)
1900 } else {
1901 None
1902 }
1903}
1904
1905async fn update_gossip_peerstates(
1911 context: &Context,
1912 message_time: i64,
1913 from: &str,
1914 recipients: &[SingleInfo],
1915 gossip_headers: Vec<String>,
1916) -> Result<HashMap<String, SignedPublicKey>> {
1917 let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
1919
1920 for value in &gossip_headers {
1921 let header = match value.parse::<Aheader>() {
1922 Ok(header) => header,
1923 Err(err) => {
1924 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
1925 continue;
1926 }
1927 };
1928
1929 if !recipients
1930 .iter()
1931 .any(|info| addr_cmp(&info.addr, &header.addr))
1932 {
1933 warn!(
1934 context,
1935 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
1936 );
1937 continue;
1938 }
1939 if addr_cmp(from, &header.addr) {
1940 warn!(
1942 context,
1943 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
1944 );
1945 continue;
1946 }
1947
1948 let peerstate;
1949 if let Some(mut p) = Peerstate::from_addr(context, &header.addr).await? {
1950 p.apply_gossip(&header, message_time);
1951 p.save_to_db(&context.sql).await?;
1952 peerstate = p;
1953 } else {
1954 let p = Peerstate::from_gossip(&header, message_time);
1955 p.save_to_db(&context.sql).await?;
1956 peerstate = p;
1957 };
1958 peerstate
1959 .handle_fingerprint_change(context, message_time)
1960 .await?;
1961
1962 gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
1963 }
1964
1965 Ok(gossiped_keys)
1966}
1967
1968#[derive(Debug)]
1970pub(crate) struct Report {
1971 original_message_id: Option<String>,
1976 additional_message_ids: Vec<String>,
1978}
1979
1980#[derive(Debug)]
1982pub(crate) struct DeliveryReport {
1983 pub rfc724_mid: String,
1984 pub failure: bool,
1985}
1986
1987pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
1988 let mut msgids = Vec::new();
1990 for id in ids.split_whitespace() {
1991 let mut id = id.to_string();
1992 if let Some(id_without_prefix) = id.strip_prefix('<') {
1993 id = id_without_prefix.to_string();
1994 };
1995 if let Some(id_without_suffix) = id.strip_suffix('>') {
1996 id = id_without_suffix.to_string();
1997 };
1998 if !id.is_empty() {
1999 msgids.push(id);
2000 }
2001 }
2002 msgids
2003}
2004
2005pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2006 if let Some(id) = parse_message_ids(ids).first() {
2007 Ok(id.to_string())
2008 } else {
2009 bail!("could not parse message_id: {}", ids);
2010 }
2011}
2012
2013fn is_known(key: &str) -> bool {
2016 matches!(
2017 key,
2018 "return-path"
2019 | "date"
2020 | "from"
2021 | "sender"
2022 | "reply-to"
2023 | "to"
2024 | "cc"
2025 | "bcc"
2026 | "message-id"
2027 | "in-reply-to"
2028 | "references"
2029 | "subject"
2030 | "secure-join"
2031 )
2032}
2033
2034pub(crate) fn is_hidden(key: &str) -> bool {
2036 matches!(
2037 key,
2038 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2039 )
2040}
2041
2042#[derive(Debug, Default, Clone)]
2044pub struct Part {
2045 pub typ: Viewtype,
2047
2048 pub mimetype: Option<Mime>,
2050
2051 pub msg: String,
2053
2054 pub msg_raw: Option<String>,
2056
2057 pub bytes: usize,
2059
2060 pub param: Params,
2062
2063 pub(crate) org_filename: Option<String>,
2065
2066 pub error: Option<String>,
2068
2069 pub(crate) dehtml_failed: bool,
2071
2072 pub(crate) is_related: bool,
2079
2080 pub(crate) is_reaction: bool,
2082}
2083
2084fn get_mime_type(
2089 mail: &mailparse::ParsedMail<'_>,
2090 filename: &Option<String>,
2091) -> Result<(Mime, Viewtype)> {
2092 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2093
2094 let viewtype = match mimetype.type_() {
2095 mime::TEXT => match mimetype.subtype() {
2096 mime::VCARD => Viewtype::Vcard,
2097 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2098 _ => Viewtype::File,
2099 },
2100 mime::IMAGE => match mimetype.subtype() {
2101 mime::GIF => Viewtype::Gif,
2102 mime::SVG => Viewtype::File,
2103 _ => Viewtype::Image,
2104 },
2105 mime::AUDIO => Viewtype::Audio,
2106 mime::VIDEO => Viewtype::Video,
2107 mime::MULTIPART => Viewtype::Unknown,
2108 mime::MESSAGE => {
2109 if is_attachment_disposition(mail) {
2110 Viewtype::File
2111 } else {
2112 Viewtype::Unknown
2120 }
2121 }
2122 mime::APPLICATION => match mimetype.subtype() {
2123 mime::OCTET_STREAM => match filename {
2124 Some(filename) => {
2125 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2126 Some((viewtype, _)) => viewtype,
2127 None => Viewtype::File,
2128 }
2129 }
2130 None => Viewtype::File,
2131 },
2132 _ => Viewtype::File,
2133 },
2134 _ => Viewtype::Unknown,
2135 };
2136
2137 Ok((mimetype, viewtype))
2138}
2139
2140fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2141 let ct = mail.get_content_disposition();
2142 ct.disposition == DispositionType::Attachment
2143 && ct
2144 .params
2145 .iter()
2146 .any(|(key, _value)| key.starts_with("filename"))
2147}
2148
2149fn get_attachment_filename(
2156 context: &Context,
2157 mail: &mailparse::ParsedMail,
2158) -> Result<Option<String>> {
2159 let ct = mail.get_content_disposition();
2160
2161 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2164
2165 if desired_filename.is_none() {
2166 if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
2167 warn!(context, "apostrophed encoding invalid: {}", name);
2171 desired_filename = Some(name);
2172 }
2173 }
2174
2175 if desired_filename.is_none() {
2177 desired_filename = ct.params.get("name").map(|s| s.to_string());
2178 }
2179
2180 if desired_filename.is_none() {
2183 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2184 }
2185
2186 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2188 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2189 desired_filename = Some(format!("file.{subtype}",));
2190 } else {
2191 bail!(
2192 "could not determine attachment filename: {:?}",
2193 ct.disposition
2194 );
2195 };
2196 }
2197
2198 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2199
2200 Ok(desired_filename)
2201}
2202
2203pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2205 let to_addresses = get_all_addresses_from_header(headers, "to");
2206 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2207
2208 let mut res = to_addresses;
2209 res.extend(cc_addresses);
2210 res
2211}
2212
2213pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2215 let all = get_all_addresses_from_header(headers, "from");
2216 tools::single_value(all)
2217}
2218
2219pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2221 get_all_addresses_from_header(headers, "list-post")
2222 .into_iter()
2223 .next()
2224 .map(|s| s.addr)
2225}
2226
2227fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2239 let mut result: Vec<SingleInfo> = Default::default();
2240
2241 if let Some(header) = headers
2242 .iter()
2243 .rev()
2244 .find(|h| h.get_key().to_lowercase() == header)
2245 {
2246 if let Ok(addrs) = mailparse::addrparse_header(header) {
2247 for addr in addrs.iter() {
2248 match addr {
2249 mailparse::MailAddr::Single(ref info) => {
2250 result.push(SingleInfo {
2251 addr: addr_normalize(&info.addr).to_lowercase(),
2252 display_name: info.display_name.clone(),
2253 });
2254 }
2255 mailparse::MailAddr::Group(ref infos) => {
2256 for info in &infos.addrs {
2257 result.push(SingleInfo {
2258 addr: addr_normalize(&info.addr).to_lowercase(),
2259 display_name: info.display_name.clone(),
2260 });
2261 }
2262 }
2263 }
2264 }
2265 }
2266 }
2267
2268 result
2269}
2270
2271async fn handle_mdn(
2272 context: &Context,
2273 from_id: ContactId,
2274 rfc724_mid: &str,
2275 timestamp_sent: i64,
2276) -> Result<()> {
2277 if from_id == ContactId::SELF {
2278 warn!(
2279 context,
2280 "Ignoring MDN sent to self, this is a bug on the sender device."
2281 );
2282
2283 return Ok(());
2286 }
2287
2288 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2289 .sql
2290 .query_row_optional(
2291 concat!(
2292 "SELECT",
2293 " m.id AS msg_id,",
2294 " c.id AS chat_id,",
2295 " mdns.contact_id AS mdn_contact",
2296 " FROM msgs m ",
2297 " LEFT JOIN chats c ON m.chat_id=c.id",
2298 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
2299 " WHERE rfc724_mid=? AND from_id=1",
2300 " ORDER BY msg_id DESC, mdn_contact=? DESC",
2301 " LIMIT 1",
2302 ),
2303 (&rfc724_mid, from_id),
2304 |row| {
2305 let msg_id: MsgId = row.get("msg_id")?;
2306 let chat_id: ChatId = row.get("chat_id")?;
2307 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2308 Ok((
2309 msg_id,
2310 chat_id,
2311 mdn_contact.is_some(),
2312 mdn_contact == Some(from_id),
2313 ))
2314 },
2315 )
2316 .await?
2317 else {
2318 info!(
2319 context,
2320 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2321 );
2322 return Ok(());
2323 };
2324
2325 if is_dup {
2326 return Ok(());
2327 }
2328 context
2329 .sql
2330 .execute(
2331 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2332 (msg_id, from_id, timestamp_sent),
2333 )
2334 .await?;
2335 if !has_mdns {
2336 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2337 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2339 }
2340 Ok(())
2341}
2342
2343async fn handle_ndn(
2346 context: &Context,
2347 failed: &DeliveryReport,
2348 error: Option<String>,
2349) -> Result<()> {
2350 if failed.rfc724_mid.is_empty() {
2351 return Ok(());
2352 }
2353
2354 let msgs: Vec<_> = context
2357 .sql
2358 .query_map(
2359 "SELECT id FROM msgs
2360 WHERE rfc724_mid=? AND from_id=1",
2361 (&failed.rfc724_mid,),
2362 |row| {
2363 let msg_id: MsgId = row.get(0)?;
2364 Ok(msg_id)
2365 },
2366 |rows| Ok(rows.collect::<Vec<_>>()),
2367 )
2368 .await?;
2369
2370 let error = if let Some(error) = error {
2371 error
2372 } else {
2373 "Delivery to at least one recipient failed.".to_string()
2374 };
2375 let err_msg = &error;
2376
2377 for msg in msgs {
2378 let msg_id = msg?;
2379 let mut message = Message::load_from_db(context, msg_id).await?;
2380 let aggregated_error = message
2381 .error
2382 .as_ref()
2383 .map(|err| format!("{}\n\n{}", err, err_msg));
2384 set_msg_failed(
2385 context,
2386 &mut message,
2387 aggregated_error.as_ref().unwrap_or(err_msg),
2388 )
2389 .await?;
2390 }
2391
2392 Ok(())
2393}
2394
2395#[cfg(test)]
2396mod mimeparser_tests;