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