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;
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, load_self_secret_keyring, DcKey, Fingerprint, SignedPublicKey};
29use crate::log::{error, info, warn};
30use crate::message::{self, get_vcard_summary, set_msg_failed, Message, MsgId, Viewtype};
31use crate::param::{Param, Params};
32use crate::simplify::{simplify, SimplifiedText};
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(mail, &get_attachment_filename(context, mail)?)?.0;
1005 match (mimetype.type_(), mimetype.subtype().as_str()) {
1006 (mime::MULTIPART, "alternative") => {
1011 for cur_data in &mail.subparts {
1012 let mime_type =
1013 get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?.0;
1014 if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
1015 any_part_added = self
1016 .parse_mime_recursive(context, cur_data, is_related)
1017 .await?;
1018 break;
1019 }
1020 }
1021 if !any_part_added {
1022 for cur_data in &mail.subparts {
1024 if get_mime_type(cur_data, &get_attachment_filename(context, cur_data)?)?
1025 .0
1026 .type_()
1027 == mime::TEXT
1028 {
1029 any_part_added = self
1030 .parse_mime_recursive(context, cur_data, is_related)
1031 .await?;
1032 break;
1033 }
1034 }
1035 }
1036 if !any_part_added {
1037 for cur_part in &mail.subparts {
1039 if self
1040 .parse_mime_recursive(context, cur_part, is_related)
1041 .await?
1042 {
1043 any_part_added = true;
1044 break;
1045 }
1046 }
1047 }
1048 if any_part_added && mail.subparts.len() > 1 {
1049 self.is_mime_modified = true;
1053 }
1054 }
1055 (mime::MULTIPART, "signed") => {
1056 if let Some(first) = mail.subparts.first() {
1065 any_part_added = self
1066 .parse_mime_recursive(context, first, is_related)
1067 .await?;
1068 }
1069 }
1070 (mime::MULTIPART, "report") => {
1071 if mail.subparts.len() >= 2 {
1073 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1074 Some("disposition-notification") => {
1075 if let Some(report) = self.process_report(context, mail)? {
1076 self.mdn_reports.push(report);
1077 }
1078
1079 let part = Part {
1084 typ: Viewtype::Unknown,
1085 ..Default::default()
1086 };
1087 self.parts.push(part);
1088
1089 any_part_added = true;
1090 }
1091 Some("delivery-status") | None => {
1093 if let Some(report) = self.process_delivery_status(context, mail)? {
1094 self.delivery_report = Some(report);
1095 }
1096
1097 for cur_data in &mail.subparts {
1099 if self
1100 .parse_mime_recursive(context, cur_data, is_related)
1101 .await?
1102 {
1103 any_part_added = true;
1104 }
1105 }
1106 }
1107 Some("multi-device-sync") => {
1108 if let Some(second) = mail.subparts.get(1) {
1109 self.add_single_part_if_known(context, second, is_related)
1110 .await?;
1111 }
1112 }
1113 Some("status-update") => {
1114 if let Some(second) = mail.subparts.get(1) {
1115 self.add_single_part_if_known(context, second, is_related)
1116 .await?;
1117 }
1118 }
1119 Some(_) => {
1120 for cur_data in &mail.subparts {
1121 if self
1122 .parse_mime_recursive(context, cur_data, is_related)
1123 .await?
1124 {
1125 any_part_added = true;
1126 }
1127 }
1128 }
1129 }
1130 }
1131 }
1132 _ => {
1133 for cur_data in &mail.subparts {
1136 if self
1137 .parse_mime_recursive(context, cur_data, is_related)
1138 .await?
1139 {
1140 any_part_added = true;
1141 }
1142 }
1143 }
1144 }
1145
1146 Ok(any_part_added)
1147 }
1148
1149 async fn add_single_part_if_known(
1151 &mut self,
1152 context: &Context,
1153 mail: &mailparse::ParsedMail<'_>,
1154 is_related: bool,
1155 ) -> Result<bool> {
1156 let filename = get_attachment_filename(context, mail)?;
1158 let (mime_type, msg_type) = get_mime_type(mail, &filename)?;
1159 let raw_mime = mail.ctype.mimetype.to_lowercase();
1160
1161 let old_part_count = self.parts.len();
1162
1163 match filename {
1164 Some(filename) => {
1165 self.do_add_single_file_part(
1166 context,
1167 msg_type,
1168 mime_type,
1169 &raw_mime,
1170 &mail.get_body_raw()?,
1171 &filename,
1172 is_related,
1173 )
1174 .await?;
1175 }
1176 None => {
1177 match mime_type.type_() {
1178 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1179 warn!(context, "Missing attachment");
1180 return Ok(false);
1181 }
1182 mime::TEXT
1183 if mail.get_content_disposition().disposition
1184 == DispositionType::Extension("reaction".to_string()) =>
1185 {
1186 let decoded_data = match mail.get_body() {
1188 Ok(decoded_data) => decoded_data,
1189 Err(err) => {
1190 warn!(context, "Invalid body parsed {:#}", err);
1191 return Ok(false);
1193 }
1194 };
1195
1196 let part = Part {
1197 typ: Viewtype::Text,
1198 mimetype: Some(mime_type),
1199 msg: decoded_data,
1200 is_reaction: true,
1201 ..Default::default()
1202 };
1203 self.do_add_single_part(part);
1204 return Ok(true);
1205 }
1206 mime::TEXT | mime::HTML => {
1207 let decoded_data = match mail.get_body() {
1208 Ok(decoded_data) => decoded_data,
1209 Err(err) => {
1210 warn!(context, "Invalid body parsed {:#}", err);
1211 return Ok(false);
1213 }
1214 };
1215
1216 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1217 let mut dehtml_failed = false;
1218
1219 let SimplifiedText {
1220 text: simplified_txt,
1221 is_forwarded,
1222 is_cut,
1223 top_quote,
1224 footer,
1225 } = if decoded_data.is_empty() {
1226 Default::default()
1227 } else {
1228 let is_html = mime_type == mime::TEXT_HTML;
1229 if is_html {
1230 self.is_mime_modified = true;
1231 if let Some(text) = dehtml(&decoded_data) {
1232 text
1233 } else {
1234 dehtml_failed = true;
1235 SimplifiedText {
1236 text: decoded_data.clone(),
1237 ..Default::default()
1238 }
1239 }
1240 } else {
1241 simplify(decoded_data.clone(), self.has_chat_version())
1242 }
1243 };
1244
1245 self.is_mime_modified = self.is_mime_modified
1246 || ((is_forwarded || is_cut || top_quote.is_some())
1247 && !self.has_chat_version());
1248
1249 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1250 {
1251 format.as_str().eq_ignore_ascii_case("flowed")
1252 } else {
1253 false
1254 };
1255
1256 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1257 && mime_type.subtype() == mime::PLAIN
1258 && is_format_flowed
1259 {
1260 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1261 delsp.as_str().eq_ignore_ascii_case("yes")
1262 } else {
1263 false
1264 };
1265 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1266 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1267 (unflowed_text, unflowed_quote)
1268 } else {
1269 (simplified_txt, top_quote)
1270 };
1271
1272 let (simplified_txt, was_truncated) =
1273 truncate_msg_text(context, simplified_txt).await?;
1274 if was_truncated {
1275 self.is_mime_modified = was_truncated;
1276 }
1277
1278 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1279 let mut part = Part {
1280 dehtml_failed,
1281 typ: Viewtype::Text,
1282 mimetype: Some(mime_type),
1283 msg: simplified_txt,
1284 ..Default::default()
1285 };
1286 if let Some(quote) = simplified_quote {
1287 part.param.set(Param::Quote, quote);
1288 }
1289 part.msg_raw = Some(decoded_data);
1290 self.do_add_single_part(part);
1291 }
1292
1293 if is_forwarded {
1294 self.is_forwarded = true;
1295 }
1296
1297 if self.footer.is_none() && is_plaintext {
1298 self.footer = Some(footer.unwrap_or_default());
1299 }
1300 }
1301 _ => {}
1302 }
1303 }
1304 }
1305
1306 Ok(self.parts.len() > old_part_count)
1308 }
1309
1310 #[expect(clippy::too_many_arguments)]
1311 async fn do_add_single_file_part(
1312 &mut self,
1313 context: &Context,
1314 msg_type: Viewtype,
1315 mime_type: Mime,
1316 raw_mime: &str,
1317 decoded_data: &[u8],
1318 filename: &str,
1319 is_related: bool,
1320 ) -> Result<()> {
1321 if decoded_data.is_empty() {
1322 return Ok(());
1323 }
1324
1325 if mime_type.type_() == mime::APPLICATION
1327 && mime_type.subtype().as_str() == "pgp-keys"
1328 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1329 {
1330 return Ok(());
1331 }
1332 let mut part = Part::default();
1333 let msg_type = if context
1334 .is_webxdc_file(filename, decoded_data)
1335 .await
1336 .unwrap_or(false)
1337 {
1338 Viewtype::Webxdc
1339 } else if filename.ends_with(".kml") {
1340 if filename.starts_with("location") || filename.starts_with("message") {
1343 let parsed = location::Kml::parse(decoded_data)
1344 .map_err(|err| {
1345 warn!(context, "failed to parse kml part: {:#}", err);
1346 })
1347 .ok();
1348 if filename.starts_with("location") {
1349 self.location_kml = parsed;
1350 } else {
1351 self.message_kml = parsed;
1352 }
1353 return Ok(());
1354 }
1355 msg_type
1356 } else if filename == "multi-device-sync.json" {
1357 if !context.get_config_bool(Config::SyncMsgs).await? {
1358 return Ok(());
1359 }
1360 let serialized = String::from_utf8_lossy(decoded_data)
1361 .parse()
1362 .unwrap_or_default();
1363 self.sync_items = context
1364 .parse_sync_items(serialized)
1365 .map_err(|err| {
1366 warn!(context, "failed to parse sync data: {:#}", err);
1367 })
1368 .ok();
1369 return Ok(());
1370 } else if filename == "status-update.json" {
1371 let serialized = String::from_utf8_lossy(decoded_data)
1372 .parse()
1373 .unwrap_or_default();
1374 self.webxdc_status_update = Some(serialized);
1375 return Ok(());
1376 } else if msg_type == Viewtype::Vcard {
1377 if let Some(summary) = get_vcard_summary(decoded_data) {
1378 part.param.set(Param::Summary1, summary);
1379 msg_type
1380 } else {
1381 Viewtype::File
1382 }
1383 } else {
1384 msg_type
1385 };
1386
1387 let blob =
1391 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1392 Ok(blob) => blob,
1393 Err(err) => {
1394 error!(
1395 context,
1396 "Could not add blob for mime part {}, error {:#}", filename, err
1397 );
1398 return Ok(());
1399 }
1400 };
1401 info!(context, "added blobfile: {:?}", blob.as_name());
1402
1403 if mime_type.type_() == mime::IMAGE {
1404 if let Ok((width, height)) = get_filemeta(decoded_data) {
1405 part.param.set_int(Param::Width, width as i32);
1406 part.param.set_int(Param::Height, height as i32);
1407 }
1408 }
1409
1410 part.typ = msg_type;
1411 part.org_filename = Some(filename.to_string());
1412 part.mimetype = Some(mime_type);
1413 part.bytes = decoded_data.len();
1414 part.param.set(Param::File, blob.as_name());
1415 part.param.set(Param::Filename, filename);
1416 part.param.set(Param::MimeType, raw_mime);
1417 part.is_related = is_related;
1418
1419 self.do_add_single_part(part);
1420 Ok(())
1421 }
1422
1423 async fn try_set_peer_key_from_file_part(
1425 context: &Context,
1426 decoded_data: &[u8],
1427 ) -> Result<bool> {
1428 let key = match str::from_utf8(decoded_data) {
1429 Err(err) => {
1430 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1431 return Ok(false);
1432 }
1433 Ok(key) => key,
1434 };
1435 let key = match SignedPublicKey::from_asc(key) {
1436 Err(err) => {
1437 warn!(
1438 context,
1439 "PGP key attachment is not an ASCII-armored file: {err:#}."
1440 );
1441 return Ok(false);
1442 }
1443 Ok((key, _)) => key,
1444 };
1445 if let Err(err) = key.verify() {
1446 warn!(context, "Attached PGP key verification failed: {err:#}.");
1447 return Ok(false);
1448 }
1449
1450 let fingerprint = key.dc_fingerprint().hex();
1451 context
1452 .sql
1453 .execute(
1454 "INSERT INTO public_keys (fingerprint, public_key)
1455 VALUES (?, ?)
1456 ON CONFLICT (fingerprint)
1457 DO NOTHING",
1458 (&fingerprint, key.to_bytes()),
1459 )
1460 .await?;
1461
1462 info!(context, "Imported PGP key {fingerprint} from attachment.");
1463 Ok(true)
1464 }
1465
1466 fn do_add_single_part(&mut self, mut part: Part) {
1467 if self.was_encrypted() {
1468 part.param.set_int(Param::GuaranteeE2ee, 1);
1469 }
1470 self.parts.push(part);
1471 }
1472
1473 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1474 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1475 return Some(list_id);
1478 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1479 if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
1482 if precedence == "list" || precedence == "bulk" {
1483 return Some(sender);
1487 }
1488 }
1489 }
1490 None
1491 }
1492
1493 pub(crate) fn is_mailinglist_message(&self) -> bool {
1494 self.get_mailinglist_header().is_some()
1495 }
1496
1497 pub(crate) fn is_schleuder_message(&self) -> bool {
1499 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1500 list_help == "<https://schleuder.org/>"
1501 } else {
1502 false
1503 }
1504 }
1505
1506 pub fn replace_msg_by_error(&mut self, error_msg: &str) {
1507 self.is_system_message = SystemMessage::Unknown;
1508 if let Some(part) = self.parts.first_mut() {
1509 part.typ = Viewtype::Text;
1510 part.msg = format!("[{error_msg}]");
1511 self.parts.truncate(1);
1512 }
1513 }
1514
1515 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1516 self.get_header(HeaderDef::MessageId)
1517 .and_then(|msgid| parse_message_id(msgid).ok())
1518 }
1519
1520 fn remove_secured_headers(
1521 headers: &mut HashMap<String, String>,
1522 removed: &mut HashSet<String>,
1523 ) {
1524 remove_header(headers, "secure-join-fingerprint", removed);
1525 remove_header(headers, "secure-join-auth", removed);
1526 remove_header(headers, "chat-verified", removed);
1527 remove_header(headers, "autocrypt-gossip", removed);
1528
1529 if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
1531 if secure_join == "vc-request" || secure_join == "vg-request" {
1532 headers.insert("secure-join".to_string(), secure_join);
1533 }
1534 }
1535 }
1536
1537 #[allow(clippy::too_many_arguments)]
1538 fn merge_headers(
1539 context: &Context,
1540 headers: &mut HashMap<String, String>,
1541 recipients: &mut Vec<SingleInfo>,
1542 past_members: &mut Vec<SingleInfo>,
1543 from: &mut Option<SingleInfo>,
1544 list_post: &mut Option<String>,
1545 chat_disposition_notification_to: &mut Option<SingleInfo>,
1546 fields: &[mailparse::MailHeader<'_>],
1547 ) {
1548 for field in fields {
1549 let key = field.get_key().to_lowercase();
1551 if !headers.contains_key(&key) || is_known(&key) || key.starts_with("chat-")
1553 {
1554 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1555 match addrparse_header(field) {
1556 Ok(addrlist) => {
1557 *chat_disposition_notification_to = addrlist.extract_single_info();
1558 }
1559 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1560 }
1561 } else {
1562 let value = field.get_value();
1563 headers.insert(key.to_string(), value);
1564 }
1565 }
1566 }
1567 let recipients_new = get_recipients(fields);
1568 if !recipients_new.is_empty() {
1569 *recipients = recipients_new;
1570 }
1571 let past_members_addresses =
1572 get_all_addresses_from_header(fields, "chat-group-past-members");
1573 if !past_members_addresses.is_empty() {
1574 *past_members = past_members_addresses;
1575 }
1576 let from_new = get_from(fields);
1577 if from_new.is_some() {
1578 *from = from_new;
1579 }
1580 let list_post_new = get_list_post(fields);
1581 if list_post_new.is_some() {
1582 *list_post = list_post_new;
1583 }
1584 }
1585
1586 fn process_report(
1587 &self,
1588 context: &Context,
1589 report: &mailparse::ParsedMail<'_>,
1590 ) -> Result<Option<Report>> {
1591 let report_body = if let Some(subpart) = report.subparts.get(1) {
1593 subpart.get_body_raw()?
1594 } else {
1595 bail!("Report does not have second MIME part");
1596 };
1597 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1598
1599 if report_fields
1601 .get_header_value(HeaderDef::Disposition)
1602 .is_none()
1603 {
1604 warn!(
1605 context,
1606 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1607 report_fields.get_header_value(HeaderDef::MessageId)
1608 );
1609 return Ok(None);
1610 };
1611
1612 let original_message_id = report_fields
1613 .get_header_value(HeaderDef::OriginalMessageId)
1614 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1617 .and_then(|v| parse_message_id(&v).ok());
1618 let additional_message_ids = report_fields
1619 .get_header_value(HeaderDef::AdditionalMessageIds)
1620 .map_or_else(Vec::new, |v| {
1621 v.split(' ')
1622 .filter_map(|s| parse_message_id(s).ok())
1623 .collect()
1624 });
1625
1626 Ok(Some(Report {
1627 original_message_id,
1628 additional_message_ids,
1629 }))
1630 }
1631
1632 fn process_delivery_status(
1633 &self,
1634 context: &Context,
1635 report: &mailparse::ParsedMail<'_>,
1636 ) -> Result<Option<DeliveryReport>> {
1637 let mut failure = true;
1639
1640 if let Some(status_part) = report.subparts.get(1) {
1641 if status_part.ctype.mimetype != "message/delivery-status"
1644 && status_part.ctype.mimetype != "message/global-delivery-status"
1645 {
1646 warn!(context, "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring");
1647 return Ok(None);
1648 }
1649
1650 let status_body = status_part.get_body_raw()?;
1651
1652 let (_, sz) = mailparse::parse_headers(&status_body)?;
1654
1655 if let Some(status_body) = status_body.get(sz..) {
1657 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1658 if let Some(action) = status_fields.get_first_value("action") {
1659 if action != "failed" {
1660 info!(context, "DSN with {:?} action", action);
1661 failure = false;
1662 }
1663 } else {
1664 warn!(context, "DSN without action");
1665 }
1666 } else {
1667 warn!(context, "DSN without per-recipient fields");
1668 }
1669 } else {
1670 return Ok(None);
1672 }
1673
1674 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1676 p.ctype.mimetype.contains("rfc822")
1677 || p.ctype.mimetype == "message/global"
1678 || p.ctype.mimetype == "message/global-headers"
1679 }) {
1680 let report_body = original_msg.get_body_raw()?;
1681 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1682
1683 if let Some(original_message_id) = report_fields
1684 .get_header_value(HeaderDef::MessageId)
1685 .and_then(|v| parse_message_id(&v).ok())
1686 {
1687 return Ok(Some(DeliveryReport {
1688 rfc724_mid: original_message_id,
1689 failure,
1690 }));
1691 }
1692
1693 warn!(
1694 context,
1695 "ignoring unknown ndn-notification, Message-Id: {:?}",
1696 report_fields.get_header_value(HeaderDef::MessageId)
1697 );
1698 }
1699
1700 Ok(None)
1701 }
1702
1703 fn maybe_remove_bad_parts(&mut self) {
1704 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1705 if good_parts == 0 {
1706 self.parts.truncate(1);
1708 } else if good_parts < self.parts.len() {
1709 self.parts.retain(|p| !p.dehtml_failed);
1710 }
1711
1712 if !self.has_chat_version() && self.is_mime_modified {
1720 fn is_related_image(p: &&Part) -> bool {
1721 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1722 }
1723 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1724 if related_image_cnt > 1 {
1725 let mut is_first_image = true;
1726 self.parts.retain(|p| {
1727 let retain = is_first_image || !is_related_image(&p);
1728 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1729 is_first_image = false;
1730 }
1731 retain
1732 });
1733 }
1734 }
1735 }
1736
1737 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1747 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1748 let text_part_cnt = self
1749 .parts
1750 .iter()
1751 .filter(|p| p.typ == Viewtype::Text)
1752 .count();
1753 if text_part_cnt == 2 {
1754 if let Some(last_part) = self.parts.last() {
1755 if last_part.typ == Viewtype::Text {
1756 self.parts.pop();
1757 }
1758 }
1759 }
1760 }
1761 }
1762
1763 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1767 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1768 let from = from.to_ascii_lowercase();
1769 from.contains("mailer-daemon") || from.contains("mail-daemon")
1770 } else {
1771 false
1772 };
1773 if maybe_ndn && self.delivery_report.is_none() {
1774 for original_message_id in self
1775 .parts
1776 .iter()
1777 .filter_map(|part| part.msg_raw.as_ref())
1778 .flat_map(|part| part.lines())
1779 .filter_map(|line| line.split_once("Message-ID:"))
1780 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1781 {
1782 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1783 {
1784 self.delivery_report = Some(DeliveryReport {
1785 rfc724_mid: original_message_id,
1786 failure: true,
1787 })
1788 }
1789 }
1790 }
1791 }
1792
1793 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1797 for report in &self.mdn_reports {
1798 for original_message_id in report
1799 .original_message_id
1800 .iter()
1801 .chain(&report.additional_message_ids)
1802 {
1803 if let Err(err) =
1804 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1805 {
1806 warn!(context, "Could not handle MDN: {err:#}.");
1807 }
1808 }
1809 }
1810
1811 if let Some(delivery_report) = &self.delivery_report {
1812 if delivery_report.failure {
1813 let error = parts
1814 .iter()
1815 .find(|p| p.typ == Viewtype::Text)
1816 .map(|p| p.msg.clone());
1817 if let Err(err) = handle_ndn(context, delivery_report, error).await {
1818 warn!(context, "Could not handle NDN: {err:#}.");
1819 }
1820 }
1821 }
1822 }
1823
1824 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
1829 let parent_timestamp = if let Some(field) = self
1830 .get_header(HeaderDef::InReplyTo)
1831 .and_then(|msgid| parse_message_id(msgid).ok())
1832 {
1833 context
1834 .sql
1835 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
1836 .await?
1837 } else {
1838 None
1839 };
1840 Ok(parent_timestamp)
1841 }
1842
1843 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
1847 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
1848 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
1849 .map(|h| {
1850 h.split_ascii_whitespace()
1851 .filter_map(|ts| ts.parse::<i64>().ok())
1852 .map(|ts| std::cmp::min(now, ts))
1853 .collect()
1854 })
1855 }
1856
1857 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
1860 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
1861 header
1862 .split_ascii_whitespace()
1863 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
1864 .collect()
1865 } else {
1866 Vec::new()
1867 }
1868 }
1869}
1870
1871fn remove_header(
1872 headers: &mut HashMap<String, String>,
1873 key: &str,
1874 removed: &mut HashSet<String>,
1875) -> Option<String> {
1876 if let Some((k, v)) = headers.remove_entry(key) {
1877 removed.insert(k);
1878 Some(v)
1879 } else {
1880 None
1881 }
1882}
1883
1884async fn parse_gossip_headers(
1890 context: &Context,
1891 from: &str,
1892 recipients: &[SingleInfo],
1893 gossip_headers: Vec<String>,
1894) -> Result<HashMap<String, SignedPublicKey>> {
1895 let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
1897
1898 for value in &gossip_headers {
1899 let header = match value.parse::<Aheader>() {
1900 Ok(header) => header,
1901 Err(err) => {
1902 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
1903 continue;
1904 }
1905 };
1906
1907 if !recipients
1908 .iter()
1909 .any(|info| addr_cmp(&info.addr, &header.addr))
1910 {
1911 warn!(
1912 context,
1913 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
1914 );
1915 continue;
1916 }
1917 if addr_cmp(from, &header.addr) {
1918 warn!(
1920 context,
1921 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
1922 );
1923 continue;
1924 }
1925
1926 let fingerprint = header.public_key.dc_fingerprint().hex();
1927 context
1928 .sql
1929 .execute(
1930 "INSERT INTO public_keys (fingerprint, public_key)
1931 VALUES (?, ?)
1932 ON CONFLICT (fingerprint)
1933 DO NOTHING",
1934 (&fingerprint, header.public_key.to_bytes()),
1935 )
1936 .await?;
1937
1938 gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
1939 }
1940
1941 Ok(gossiped_keys)
1942}
1943
1944#[derive(Debug)]
1946pub(crate) struct Report {
1947 original_message_id: Option<String>,
1952 additional_message_ids: Vec<String>,
1954}
1955
1956#[derive(Debug)]
1958pub(crate) struct DeliveryReport {
1959 pub rfc724_mid: String,
1960 pub failure: bool,
1961}
1962
1963pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
1964 let mut msgids = Vec::new();
1966 for id in ids.split_whitespace() {
1967 let mut id = id.to_string();
1968 if let Some(id_without_prefix) = id.strip_prefix('<') {
1969 id = id_without_prefix.to_string();
1970 };
1971 if let Some(id_without_suffix) = id.strip_suffix('>') {
1972 id = id_without_suffix.to_string();
1973 };
1974 if !id.is_empty() {
1975 msgids.push(id);
1976 }
1977 }
1978 msgids
1979}
1980
1981pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
1982 if let Some(id) = parse_message_ids(ids).first() {
1983 Ok(id.to_string())
1984 } else {
1985 bail!("could not parse message_id: {}", ids);
1986 }
1987}
1988
1989fn is_known(key: &str) -> bool {
1992 matches!(
1993 key,
1994 "return-path"
1995 | "date"
1996 | "from"
1997 | "sender"
1998 | "reply-to"
1999 | "to"
2000 | "cc"
2001 | "bcc"
2002 | "message-id"
2003 | "in-reply-to"
2004 | "references"
2005 | "subject"
2006 | "secure-join"
2007 )
2008}
2009
2010pub(crate) fn is_hidden(key: &str) -> bool {
2012 matches!(
2013 key,
2014 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2015 )
2016}
2017
2018#[derive(Debug, Default, Clone)]
2020pub struct Part {
2021 pub typ: Viewtype,
2023
2024 pub mimetype: Option<Mime>,
2026
2027 pub msg: String,
2029
2030 pub msg_raw: Option<String>,
2032
2033 pub bytes: usize,
2035
2036 pub param: Params,
2038
2039 pub(crate) org_filename: Option<String>,
2041
2042 pub error: Option<String>,
2044
2045 pub(crate) dehtml_failed: bool,
2047
2048 pub(crate) is_related: bool,
2055
2056 pub(crate) is_reaction: bool,
2058}
2059
2060fn get_mime_type(
2065 mail: &mailparse::ParsedMail<'_>,
2066 filename: &Option<String>,
2067) -> Result<(Mime, Viewtype)> {
2068 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2069
2070 let viewtype = match mimetype.type_() {
2071 mime::TEXT => match mimetype.subtype() {
2072 mime::VCARD => Viewtype::Vcard,
2073 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2074 _ => Viewtype::File,
2075 },
2076 mime::IMAGE => match mimetype.subtype() {
2077 mime::GIF => Viewtype::Gif,
2078 mime::SVG => Viewtype::File,
2079 _ => Viewtype::Image,
2080 },
2081 mime::AUDIO => Viewtype::Audio,
2082 mime::VIDEO => Viewtype::Video,
2083 mime::MULTIPART => Viewtype::Unknown,
2084 mime::MESSAGE => {
2085 if is_attachment_disposition(mail) {
2086 Viewtype::File
2087 } else {
2088 Viewtype::Unknown
2096 }
2097 }
2098 mime::APPLICATION => match mimetype.subtype() {
2099 mime::OCTET_STREAM => match filename {
2100 Some(filename) => {
2101 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2102 Some((viewtype, _)) => viewtype,
2103 None => Viewtype::File,
2104 }
2105 }
2106 None => Viewtype::File,
2107 },
2108 _ => Viewtype::File,
2109 },
2110 _ => Viewtype::Unknown,
2111 };
2112
2113 Ok((mimetype, viewtype))
2114}
2115
2116fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2117 let ct = mail.get_content_disposition();
2118 ct.disposition == DispositionType::Attachment
2119 && ct
2120 .params
2121 .iter()
2122 .any(|(key, _value)| key.starts_with("filename"))
2123}
2124
2125fn get_attachment_filename(
2132 context: &Context,
2133 mail: &mailparse::ParsedMail,
2134) -> Result<Option<String>> {
2135 let ct = mail.get_content_disposition();
2136
2137 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2140
2141 if desired_filename.is_none() {
2142 if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
2143 warn!(context, "apostrophed encoding invalid: {}", name);
2147 desired_filename = Some(name);
2148 }
2149 }
2150
2151 if desired_filename.is_none() {
2153 desired_filename = ct.params.get("name").map(|s| s.to_string());
2154 }
2155
2156 if desired_filename.is_none() {
2159 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2160 }
2161
2162 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2164 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2165 desired_filename = Some(format!("file.{subtype}",));
2166 } else {
2167 bail!(
2168 "could not determine attachment filename: {:?}",
2169 ct.disposition
2170 );
2171 };
2172 }
2173
2174 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2175
2176 Ok(desired_filename)
2177}
2178
2179pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2181 let to_addresses = get_all_addresses_from_header(headers, "to");
2182 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2183
2184 let mut res = to_addresses;
2185 res.extend(cc_addresses);
2186 res
2187}
2188
2189pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2191 let all = get_all_addresses_from_header(headers, "from");
2192 tools::single_value(all)
2193}
2194
2195pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2197 get_all_addresses_from_header(headers, "list-post")
2198 .into_iter()
2199 .next()
2200 .map(|s| s.addr)
2201}
2202
2203fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2215 let mut result: Vec<SingleInfo> = Default::default();
2216
2217 if let Some(header) = headers
2218 .iter()
2219 .rev()
2220 .find(|h| h.get_key().to_lowercase() == header)
2221 {
2222 if let Ok(addrs) = mailparse::addrparse_header(header) {
2223 for addr in addrs.iter() {
2224 match addr {
2225 mailparse::MailAddr::Single(ref info) => {
2226 result.push(SingleInfo {
2227 addr: addr_normalize(&info.addr).to_lowercase(),
2228 display_name: info.display_name.clone(),
2229 });
2230 }
2231 mailparse::MailAddr::Group(ref infos) => {
2232 for info in &infos.addrs {
2233 result.push(SingleInfo {
2234 addr: addr_normalize(&info.addr).to_lowercase(),
2235 display_name: info.display_name.clone(),
2236 });
2237 }
2238 }
2239 }
2240 }
2241 }
2242 }
2243
2244 result
2245}
2246
2247async fn handle_mdn(
2248 context: &Context,
2249 from_id: ContactId,
2250 rfc724_mid: &str,
2251 timestamp_sent: i64,
2252) -> Result<()> {
2253 if from_id == ContactId::SELF {
2254 warn!(
2255 context,
2256 "Ignoring MDN sent to self, this is a bug on the sender device."
2257 );
2258
2259 return Ok(());
2262 }
2263
2264 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2265 .sql
2266 .query_row_optional(
2267 concat!(
2268 "SELECT",
2269 " m.id AS msg_id,",
2270 " c.id AS chat_id,",
2271 " mdns.contact_id AS mdn_contact",
2272 " FROM msgs m ",
2273 " LEFT JOIN chats c ON m.chat_id=c.id",
2274 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
2275 " WHERE rfc724_mid=? AND from_id=1",
2276 " ORDER BY msg_id DESC, mdn_contact=? DESC",
2277 " LIMIT 1",
2278 ),
2279 (&rfc724_mid, from_id),
2280 |row| {
2281 let msg_id: MsgId = row.get("msg_id")?;
2282 let chat_id: ChatId = row.get("chat_id")?;
2283 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2284 Ok((
2285 msg_id,
2286 chat_id,
2287 mdn_contact.is_some(),
2288 mdn_contact == Some(from_id),
2289 ))
2290 },
2291 )
2292 .await?
2293 else {
2294 info!(
2295 context,
2296 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2297 );
2298 return Ok(());
2299 };
2300
2301 if is_dup {
2302 return Ok(());
2303 }
2304 context
2305 .sql
2306 .execute(
2307 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2308 (msg_id, from_id, timestamp_sent),
2309 )
2310 .await?;
2311 if !has_mdns {
2312 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2313 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2315 }
2316 Ok(())
2317}
2318
2319async fn handle_ndn(
2322 context: &Context,
2323 failed: &DeliveryReport,
2324 error: Option<String>,
2325) -> Result<()> {
2326 if failed.rfc724_mid.is_empty() {
2327 return Ok(());
2328 }
2329
2330 let msgs: Vec<_> = context
2333 .sql
2334 .query_map(
2335 "SELECT id FROM msgs
2336 WHERE rfc724_mid=? AND from_id=1",
2337 (&failed.rfc724_mid,),
2338 |row| {
2339 let msg_id: MsgId = row.get(0)?;
2340 Ok(msg_id)
2341 },
2342 |rows| Ok(rows.collect::<Vec<_>>()),
2343 )
2344 .await?;
2345
2346 let error = if let Some(error) = error {
2347 error
2348 } else {
2349 "Delivery to at least one recipient failed.".to_string()
2350 };
2351 let err_msg = &error;
2352
2353 for msg in msgs {
2354 let msg_id = msg?;
2355 let mut message = Message::load_from_db(context, msg_id).await?;
2356 let aggregated_error = message
2357 .error
2358 .as_ref()
2359 .map(|err| format!("{err}\n\n{err_msg}"));
2360 set_msg_failed(
2361 context,
2362 &mut message,
2363 aggregated_error.as_ref().unwrap_or(err_msg),
2364 )
2365 .await?;
2366 }
2367
2368 Ok(())
2369}
2370
2371#[cfg(test)]
2372mod mimeparser_tests;