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 ChatE2ee = 50,
219}
220
221const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
222
223impl MimeMessage {
224 pub(crate) async fn from_bytes(
229 context: &Context,
230 body: &[u8],
231 partial: Option<u32>,
232 ) -> Result<Self> {
233 let mail = mailparse::parse_mail(body)?;
234
235 let timestamp_rcvd = smeared_time(context);
236 let mut timestamp_sent =
237 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
238 let mut hop_info = parse_receive_headers(&mail.get_headers());
239
240 let mut headers = Default::default();
241 let mut headers_removed = HashSet::<String>::new();
242 let mut recipients = Default::default();
243 let mut past_members = Default::default();
244 let mut from = Default::default();
245 let mut list_post = Default::default();
246 let mut chat_disposition_notification_to = None;
247
248 MimeMessage::merge_headers(
250 context,
251 &mut headers,
252 &mut headers_removed,
253 &mut recipients,
254 &mut past_members,
255 &mut from,
256 &mut list_post,
257 &mut chat_disposition_notification_to,
258 &mail.headers,
259 );
260 headers.retain(|k, _| {
261 !is_hidden(k) || {
262 headers_removed.insert(k.to_string());
263 false
264 }
265 });
266
267 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
269 let (part, mimetype) =
270 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
271 if let Some(part) = mail.subparts.first() {
272 timestamp_sent =
276 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
277 MimeMessage::merge_headers(
278 context,
279 &mut headers,
280 &mut headers_removed,
281 &mut recipients,
282 &mut past_members,
283 &mut from,
284 &mut list_post,
285 &mut chat_disposition_notification_to,
286 &part.headers,
287 );
288 (part, part.ctype.mimetype.parse::<Mime>()?)
289 } else {
290 (&mail, mimetype)
292 }
293 } else {
294 (&mail, mimetype)
296 };
297 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
298 if let Some(part) = part.subparts.first() {
299 for field in &part.headers {
300 let key = field.get_key().to_lowercase();
301 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
302 headers.insert(key.to_string(), field.get_value());
303 }
304 }
305 }
306 }
307
308 if let Some(microsoft_message_id) = remove_header(
312 &mut headers,
313 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
314 &mut headers_removed,
315 ) {
316 headers.insert(
317 HeaderDef::MessageId.get_headername().to_string(),
318 microsoft_message_id,
319 );
320 }
321
322 Self::remove_secured_headers(&mut headers, &mut headers_removed);
325
326 let mut from = from.context("No from in message")?;
327 let private_keyring = load_self_secret_keyring(context).await?;
328
329 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
330
331 let mut gossiped_keys = Default::default();
332 hop_info += "\n\n";
333 hop_info += &dkim_results.to_string();
334
335 let incoming = !context.is_self_addr(&from.addr).await?;
336
337 let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
338
339 let mail_raw; let decrypted_msg; let (mail, is_encrypted) =
343 match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
344 Ok(Some(mut msg)) => {
345 mail_raw = msg.as_data_vec().unwrap_or_default();
346
347 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
348 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
349 info!(
350 context,
351 "decrypted message mime-body:\n{}",
352 String::from_utf8_lossy(&mail_raw),
353 );
354 }
355
356 decrypted_msg = Some(msg);
357
358 timestamp_sent = Self::get_timestamp_sent(
359 &decrypted_mail.headers,
360 timestamp_sent,
361 timestamp_rcvd,
362 );
363
364 if let Some(protected_aheader_value) = decrypted_mail
365 .headers
366 .get_header_value(HeaderDef::Autocrypt)
367 {
368 aheader_value = Some(protected_aheader_value);
369 }
370
371 (Ok(decrypted_mail), true)
372 }
373 Ok(None) => {
374 mail_raw = Vec::new();
375 decrypted_msg = None;
376 (Ok(mail), false)
377 }
378 Err(err) => {
379 mail_raw = Vec::new();
380 decrypted_msg = None;
381 warn!(context, "decryption failed: {:#}", err);
382 (Err(err), false)
383 }
384 };
385
386 let autocrypt_header = if !incoming {
387 None
388 } else if let Some(aheader_value) = aheader_value {
389 match Aheader::from_str(&aheader_value) {
390 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
391 Ok(header) => {
392 warn!(
393 context,
394 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
395 );
396 None
397 }
398 Err(err) => {
399 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
400 None
401 }
402 }
403 } else {
404 None
405 };
406
407 let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
408 let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
409 let inserted = context
410 .sql
411 .execute(
412 "INSERT INTO public_keys (fingerprint, public_key)
413 VALUES (?, ?)
414 ON CONFLICT (fingerprint)
415 DO NOTHING",
416 (&fingerprint, autocrypt_header.public_key.to_bytes()),
417 )
418 .await?;
419 if inserted > 0 {
420 info!(
421 context,
422 "Saved key with fingerprint {fingerprint} from the Autocrypt header"
423 );
424 }
425 Some(fingerprint)
426 } else {
427 None
428 };
429
430 let mut public_keyring = if incoming {
431 if let Some(autocrypt_header) = autocrypt_header {
432 vec![autocrypt_header.public_key]
433 } else {
434 vec![]
435 }
436 } else {
437 key::load_self_public_keyring(context).await?
438 };
439
440 if let Some(signature) = match &decrypted_msg {
441 Some(pgp::composed::Message::Literal { .. }) => None,
442 Some(pgp::composed::Message::Compressed { .. }) => {
443 None
446 }
447 Some(pgp::composed::Message::SignedOnePass { reader, .. }) => reader.signature(),
448 Some(pgp::composed::Message::Signed { reader, .. }) => Some(reader.signature()),
449 Some(pgp::composed::Message::Encrypted { .. }) => {
450 None
452 }
453 None => None,
454 } {
455 for issuer_fingerprint in signature.issuer_fingerprint() {
456 let issuer_fingerprint =
457 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
458 if let Some(public_key_bytes) = context
459 .sql
460 .query_row_optional(
461 "SELECT public_key
462 FROM public_keys
463 WHERE fingerprint=?",
464 (&issuer_fingerprint,),
465 |row| {
466 let bytes: Vec<u8> = row.get(0)?;
467 Ok(bytes)
468 },
469 )
470 .await?
471 {
472 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
473 public_keyring.push(public_key)
474 }
475 }
476 }
477
478 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
479 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
480 } else {
481 HashSet::new()
482 };
483
484 let mail = mail.as_ref().map(|mail| {
485 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
486 .unwrap_or((mail, Default::default()));
487 signatures.extend(signatures_detached);
488 content
489 });
490 if let (Ok(mail), true) = (mail, is_encrypted) {
491 if !signatures.is_empty() {
492 remove_header(&mut headers, "subject", &mut headers_removed);
496 remove_header(&mut headers, "list-id", &mut headers_removed);
497 }
498
499 let mut inner_from = None;
505
506 MimeMessage::merge_headers(
507 context,
508 &mut headers,
509 &mut headers_removed,
510 &mut recipients,
511 &mut past_members,
512 &mut inner_from,
513 &mut list_post,
514 &mut chat_disposition_notification_to,
515 &mail.headers,
516 );
517
518 if !signatures.is_empty() {
519 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
524 gossiped_keys =
525 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
526 }
527
528 if let Some(inner_from) = inner_from {
529 if !addr_cmp(&inner_from.addr, &from.addr) {
530 warn!(
539 context,
540 "From header in encrypted part doesn't match the outer one",
541 );
542
543 bail!("From header is forged");
548 }
549 from = inner_from;
550 }
551 }
552 if signatures.is_empty() {
553 Self::remove_secured_headers(&mut headers, &mut headers_removed);
554 }
555 if !is_encrypted {
556 signatures.clear();
557 }
558
559 let mut parser = MimeMessage {
560 parts: Vec::new(),
561 headers,
562 #[cfg(test)]
563 headers_removed,
564
565 recipients,
566 past_members,
567 list_post,
568 from,
569 incoming,
570 chat_disposition_notification_to,
571 decrypting_failed: mail.is_err(),
572
573 signatures,
575 autocrypt_fingerprint,
576 gossiped_keys,
577 is_forwarded: false,
578 mdn_reports: Vec::new(),
579 is_system_message: SystemMessage::Unknown,
580 location_kml: None,
581 message_kml: None,
582 sync_items: None,
583 webxdc_status_update: None,
584 user_avatar: None,
585 group_avatar: None,
586 delivery_report: None,
587 footer: None,
588 is_mime_modified: false,
589 decoded_data: Vec::new(),
590 hop_info,
591 is_bot: None,
592 timestamp_rcvd,
593 timestamp_sent,
594 };
595
596 match partial {
597 Some(org_bytes) => {
598 parser
599 .create_stub_from_partial_download(context, org_bytes)
600 .await?;
601 }
602 None => match mail {
603 Ok(mail) => {
604 parser.parse_mime_recursive(context, mail, false).await?;
605 }
606 Err(err) => {
607 let msg_body = stock_str::cant_decrypt_msg_body(context).await;
608 let txt = format!("[{msg_body}]");
609
610 let part = Part {
611 typ: Viewtype::Text,
612 msg_raw: Some(txt.clone()),
613 msg: txt,
614 error: Some(format!("Decrypting failed: {err:#}")),
617 ..Default::default()
618 };
619 parser.parts.push(part);
620 }
621 },
622 };
623
624 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
625 if parser.mdn_reports.is_empty()
626 && !is_location_only
627 && parser.sync_items.is_none()
628 && parser.webxdc_status_update.is_none()
629 {
630 let is_bot =
631 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
632 parser.is_bot = Some(is_bot);
633 }
634 parser.maybe_remove_bad_parts();
635 parser.maybe_remove_inline_mailinglist_footer();
636 parser.heuristically_parse_ndn(context).await;
637 parser.parse_headers(context).await?;
638 parser.decoded_data = mail_raw;
639
640 Ok(parser)
641 }
642
643 fn get_timestamp_sent(
644 hdrs: &[mailparse::MailHeader<'_>],
645 default: i64,
646 timestamp_rcvd: i64,
647 ) -> i64 {
648 hdrs.get_header_value(HeaderDef::Date)
649 .and_then(|v| mailparse::dateparse(&v).ok())
650 .map_or(default, |value| {
651 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
652 })
653 }
654
655 fn parse_system_message_headers(&mut self, context: &Context) {
657 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
658 self.parts.retain(|part| {
659 part.mimetype.is_none()
660 || part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
661 });
662
663 if self.parts.len() == 1 {
664 self.is_system_message = SystemMessage::AutocryptSetupMessage;
665 } else {
666 warn!(context, "could not determine ASM mime-part");
667 }
668 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
669 if value == "location-streaming-enabled" {
670 self.is_system_message = SystemMessage::LocationStreamingEnabled;
671 } else if value == "ephemeral-timer-changed" {
672 self.is_system_message = SystemMessage::EphemeralTimerChanged;
673 } else if value == "protection-enabled" {
674 self.is_system_message = SystemMessage::ChatProtectionEnabled;
675 } else if value == "protection-disabled" {
676 self.is_system_message = SystemMessage::ChatProtectionDisabled;
677 } else if value == "group-avatar-changed" {
678 self.is_system_message = SystemMessage::GroupImageChanged;
679 }
680 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
681 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
682 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
683 self.is_system_message = SystemMessage::MemberAddedToGroup;
684 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
685 self.is_system_message = SystemMessage::GroupNameChanged;
686 }
687 }
688
689 fn parse_avatar_headers(&mut self, context: &Context) {
691 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
692 self.group_avatar = self.avatar_action_from_header(context, header_value.to_string());
693 }
694
695 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
696 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string());
697 }
698 }
699
700 fn parse_videochat_headers(&mut self) {
701 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
702 if value == "videochat-invitation" {
703 let instance = self
704 .get_header(HeaderDef::ChatWebrtcRoom)
705 .map(|s| s.to_string());
706 if let Some(part) = self.parts.first_mut() {
707 part.typ = Viewtype::VideochatInvitation;
708 part.param
709 .set(Param::WebrtcRoom, instance.unwrap_or_default());
710 }
711 }
712 }
713 }
714
715 fn squash_attachment_parts(&mut self) {
721 if self.parts.len() == 2
722 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
723 && self
724 .parts
725 .get(1)
726 .is_some_and(|filepart| match filepart.typ {
727 Viewtype::Image
728 | Viewtype::Gif
729 | Viewtype::Sticker
730 | Viewtype::Audio
731 | Viewtype::Voice
732 | Viewtype::Video
733 | Viewtype::Vcard
734 | Viewtype::File
735 | Viewtype::Webxdc => true,
736 Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
737 })
738 {
739 let mut parts = std::mem::take(&mut self.parts);
740 let Some(mut filepart) = parts.pop() else {
741 return;
743 };
744 let Some(textpart) = parts.pop() else {
745 return;
747 };
748
749 filepart.msg.clone_from(&textpart.msg);
750 if let Some(quote) = textpart.param.get(Param::Quote) {
751 filepart.param.set(Param::Quote, quote);
752 }
753
754 self.parts = vec![filepart];
755 }
756 }
757
758 fn parse_attachments(&mut self) {
760 if self.parts.len() != 1 {
763 return;
764 }
765
766 if let Some(mut part) = self.parts.pop() {
767 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
768 {
769 part.typ = Viewtype::Voice;
770 }
771 if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
772 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
773 if value == "sticker" {
774 part.typ = Viewtype::Sticker;
775 }
776 }
777 }
778 if part.typ == Viewtype::Audio
779 || part.typ == Viewtype::Voice
780 || part.typ == Viewtype::Video
781 {
782 if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
783 let duration_ms = field_0.parse().unwrap_or_default();
784 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
785 part.param.set_int(Param::Duration, duration_ms);
786 }
787 }
788 }
789
790 self.parts.push(part);
791 }
792 }
793
794 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
795 self.parse_system_message_headers(context);
796 self.parse_avatar_headers(context);
797 self.parse_videochat_headers();
798 if self.delivery_report.is_none() {
799 self.squash_attachment_parts();
800 }
801
802 if !context.get_config_bool(Config::Bot).await? {
803 if let Some(ref subject) = self.get_subject() {
804 let mut prepend_subject = true;
805 if !self.decrypting_failed {
806 let colon = subject.find(':');
807 if colon == Some(2)
808 || colon == Some(3)
809 || self.has_chat_version()
810 || subject.contains("Chat:")
811 {
812 prepend_subject = false
813 }
814 }
815
816 if self.is_mailinglist_message() && !self.has_chat_version() {
819 prepend_subject = true;
820 }
821
822 if prepend_subject && !subject.is_empty() {
823 let part_with_text = self
824 .parts
825 .iter_mut()
826 .find(|part| !part.msg.is_empty() && !part.is_reaction);
827 if let Some(part) = part_with_text {
828 part.msg = format!("{} – {}", subject, part.msg);
829 }
830 }
831 }
832 }
833
834 if self.is_forwarded {
835 for part in &mut self.parts {
836 part.param.set_int(Param::Forwarded, 1);
837 }
838 }
839
840 self.parse_attachments();
841
842 if !self.decrypting_failed && !self.parts.is_empty() {
844 if let Some(ref dn_to) = self.chat_disposition_notification_to {
845 let from = &self.from.addr;
847 if !context.is_self_addr(from).await? {
848 if from.to_lowercase() == dn_to.addr.to_lowercase() {
849 if let Some(part) = self.parts.last_mut() {
850 part.param.set_int(Param::WantsMdn, 1);
851 }
852 } else {
853 warn!(
854 context,
855 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
856 );
857 }
858 }
859 }
860 }
861
862 if self.parts.is_empty() && self.mdn_reports.is_empty() {
867 let mut part = Part {
868 typ: Viewtype::Text,
869 ..Default::default()
870 };
871
872 if let Some(ref subject) = self.get_subject() {
873 if !self.has_chat_version() && self.webxdc_status_update.is_none() {
874 part.msg = subject.to_string();
875 }
876 }
877
878 self.do_add_single_part(part);
879 }
880
881 if self.is_bot == Some(true) {
882 for part in &mut self.parts {
883 part.param.set(Param::Bot, "1");
884 }
885 }
886
887 Ok(())
888 }
889
890 fn avatar_action_from_header(
891 &mut self,
892 context: &Context,
893 header_value: String,
894 ) -> Option<AvatarAction> {
895 if header_value == "0" {
896 Some(AvatarAction::Delete)
897 } else if let Some(base64) = header_value
898 .split_ascii_whitespace()
899 .collect::<String>()
900 .strip_prefix("base64:")
901 {
902 match BlobObject::store_from_base64(context, base64) {
903 Ok(path) => Some(AvatarAction::Change(path)),
904 Err(err) => {
905 warn!(
906 context,
907 "Could not decode and save avatar to blob file: {:#}", err,
908 );
909 None
910 }
911 }
912 } else {
913 let mut i = 0;
916 while let Some(part) = self.parts.get_mut(i) {
917 if let Some(part_filename) = &part.org_filename {
918 if part_filename == &header_value {
919 if let Some(blob) = part.param.get(Param::File) {
920 let res = Some(AvatarAction::Change(blob.to_string()));
921 self.parts.remove(i);
922 return res;
923 }
924 break;
925 }
926 }
927 i += 1;
928 }
929 None
930 }
931 }
932
933 pub fn was_encrypted(&self) -> bool {
939 !self.signatures.is_empty()
940 }
941
942 pub(crate) fn has_chat_version(&self) -> bool {
945 self.headers.contains_key("chat-version")
946 }
947
948 pub(crate) fn get_subject(&self) -> Option<String> {
949 self.get_header(HeaderDef::Subject)
950 .map(|s| s.trim_start())
951 .filter(|s| !s.is_empty())
952 .map(|s| s.to_string())
953 }
954
955 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
956 self.headers
957 .get(headerdef.get_headername())
958 .map(|s| s.as_str())
959 }
960
961 #[cfg(test)]
962 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
967 let hname = headerdef.get_headername();
968 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
969 }
970
971 pub fn get_chat_group_id(&self) -> Option<&str> {
973 self.get_header(HeaderDef::ChatGroupId)
974 .filter(|s| validate_id(s))
975 }
976
977 async fn parse_mime_recursive<'a>(
978 &'a mut self,
979 context: &'a Context,
980 mail: &'a mailparse::ParsedMail<'a>,
981 is_related: bool,
982 ) -> Result<bool> {
983 enum MimeS {
984 Multiple,
985 Single,
986 Message,
987 }
988
989 let mimetype = mail.ctype.mimetype.to_lowercase();
990
991 let m = if mimetype.starts_with("multipart") {
992 if mail.ctype.params.contains_key("boundary") {
993 MimeS::Multiple
994 } else {
995 MimeS::Single
996 }
997 } else if mimetype.starts_with("message") {
998 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
999 MimeS::Message
1000 } else {
1001 MimeS::Single
1002 }
1003 } else {
1004 MimeS::Single
1005 };
1006
1007 let is_related = is_related || mimetype == "multipart/related";
1008 match m {
1009 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1010 MimeS::Message => {
1011 let raw = mail.get_body_raw()?;
1012 if raw.is_empty() {
1013 return Ok(false);
1014 }
1015 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1016
1017 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1018 }
1019 MimeS::Single => {
1020 self.add_single_part_if_known(context, mail, is_related)
1021 .await
1022 }
1023 }
1024 }
1025
1026 async fn handle_multiple(
1027 &mut self,
1028 context: &Context,
1029 mail: &mailparse::ParsedMail<'_>,
1030 is_related: bool,
1031 ) -> Result<bool> {
1032 let mut any_part_added = false;
1033 let mimetype = get_mime_type(
1034 mail,
1035 &get_attachment_filename(context, mail)?,
1036 self.has_chat_version(),
1037 )?
1038 .0;
1039 match (mimetype.type_(), mimetype.subtype().as_str()) {
1040 (mime::MULTIPART, "alternative") => {
1045 for cur_data in &mail.subparts {
1046 let mime_type = get_mime_type(
1047 cur_data,
1048 &get_attachment_filename(context, cur_data)?,
1049 self.has_chat_version(),
1050 )?
1051 .0;
1052 if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
1053 any_part_added = self
1054 .parse_mime_recursive(context, cur_data, is_related)
1055 .await?;
1056 break;
1057 }
1058 }
1059 if !any_part_added {
1060 for cur_data in &mail.subparts {
1062 if get_mime_type(
1063 cur_data,
1064 &get_attachment_filename(context, cur_data)?,
1065 self.has_chat_version(),
1066 )?
1067 .0
1068 .type_()
1069 == mime::TEXT
1070 {
1071 any_part_added = self
1072 .parse_mime_recursive(context, cur_data, is_related)
1073 .await?;
1074 break;
1075 }
1076 }
1077 }
1078 if !any_part_added {
1079 for cur_part in &mail.subparts {
1081 if self
1082 .parse_mime_recursive(context, cur_part, is_related)
1083 .await?
1084 {
1085 any_part_added = true;
1086 break;
1087 }
1088 }
1089 }
1090 if any_part_added && mail.subparts.len() > 1 {
1091 self.is_mime_modified = true;
1095 }
1096 }
1097 (mime::MULTIPART, "signed") => {
1098 if let Some(first) = mail.subparts.first() {
1107 any_part_added = self
1108 .parse_mime_recursive(context, first, is_related)
1109 .await?;
1110 }
1111 }
1112 (mime::MULTIPART, "report") => {
1113 if mail.subparts.len() >= 2 {
1115 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1116 Some("disposition-notification") => {
1117 if let Some(report) = self.process_report(context, mail)? {
1118 self.mdn_reports.push(report);
1119 }
1120
1121 let part = Part {
1126 typ: Viewtype::Unknown,
1127 ..Default::default()
1128 };
1129 self.parts.push(part);
1130
1131 any_part_added = true;
1132 }
1133 Some("delivery-status") | None => {
1135 if let Some(report) = self.process_delivery_status(context, mail)? {
1136 self.delivery_report = Some(report);
1137 }
1138
1139 for cur_data in &mail.subparts {
1141 if self
1142 .parse_mime_recursive(context, cur_data, is_related)
1143 .await?
1144 {
1145 any_part_added = true;
1146 }
1147 }
1148 }
1149 Some("multi-device-sync") => {
1150 if let Some(second) = mail.subparts.get(1) {
1151 self.add_single_part_if_known(context, second, is_related)
1152 .await?;
1153 }
1154 }
1155 Some("status-update") => {
1156 if let Some(second) = mail.subparts.get(1) {
1157 self.add_single_part_if_known(context, second, is_related)
1158 .await?;
1159 }
1160 }
1161 Some(_) => {
1162 for cur_data in &mail.subparts {
1163 if self
1164 .parse_mime_recursive(context, cur_data, is_related)
1165 .await?
1166 {
1167 any_part_added = true;
1168 }
1169 }
1170 }
1171 }
1172 }
1173 }
1174 _ => {
1175 for cur_data in &mail.subparts {
1178 if self
1179 .parse_mime_recursive(context, cur_data, is_related)
1180 .await?
1181 {
1182 any_part_added = true;
1183 }
1184 }
1185 }
1186 }
1187
1188 Ok(any_part_added)
1189 }
1190
1191 async fn add_single_part_if_known(
1193 &mut self,
1194 context: &Context,
1195 mail: &mailparse::ParsedMail<'_>,
1196 is_related: bool,
1197 ) -> Result<bool> {
1198 let filename = get_attachment_filename(context, mail)?;
1200 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1201 let raw_mime = mail.ctype.mimetype.to_lowercase();
1202
1203 let old_part_count = self.parts.len();
1204
1205 match filename {
1206 Some(filename) => {
1207 self.do_add_single_file_part(
1208 context,
1209 msg_type,
1210 mime_type,
1211 &raw_mime,
1212 &mail.get_body_raw()?,
1213 &filename,
1214 is_related,
1215 )
1216 .await?;
1217 }
1218 None => {
1219 match mime_type.type_() {
1220 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1221 warn!(context, "Missing attachment");
1222 return Ok(false);
1223 }
1224 mime::TEXT
1225 if mail.get_content_disposition().disposition
1226 == DispositionType::Extension("reaction".to_string()) =>
1227 {
1228 let decoded_data = match mail.get_body() {
1230 Ok(decoded_data) => decoded_data,
1231 Err(err) => {
1232 warn!(context, "Invalid body parsed {:#}", err);
1233 return Ok(false);
1235 }
1236 };
1237
1238 let part = Part {
1239 typ: Viewtype::Text,
1240 mimetype: Some(mime_type),
1241 msg: decoded_data,
1242 is_reaction: true,
1243 ..Default::default()
1244 };
1245 self.do_add_single_part(part);
1246 return Ok(true);
1247 }
1248 mime::TEXT | mime::HTML => {
1249 let decoded_data = match mail.get_body() {
1250 Ok(decoded_data) => decoded_data,
1251 Err(err) => {
1252 warn!(context, "Invalid body parsed {:#}", err);
1253 return Ok(false);
1255 }
1256 };
1257
1258 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1259 let mut dehtml_failed = false;
1260
1261 let SimplifiedText {
1262 text: simplified_txt,
1263 is_forwarded,
1264 is_cut,
1265 top_quote,
1266 footer,
1267 } = if decoded_data.is_empty() {
1268 Default::default()
1269 } else {
1270 let is_html = mime_type == mime::TEXT_HTML;
1271 if is_html {
1272 self.is_mime_modified = true;
1273 if let Some(text) = dehtml(&decoded_data) {
1274 text
1275 } else {
1276 dehtml_failed = true;
1277 SimplifiedText {
1278 text: decoded_data.clone(),
1279 ..Default::default()
1280 }
1281 }
1282 } else {
1283 simplify(decoded_data.clone(), self.has_chat_version())
1284 }
1285 };
1286
1287 self.is_mime_modified = self.is_mime_modified
1288 || ((is_forwarded || is_cut || top_quote.is_some())
1289 && !self.has_chat_version());
1290
1291 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1292 {
1293 format.as_str().eq_ignore_ascii_case("flowed")
1294 } else {
1295 false
1296 };
1297
1298 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1299 && mime_type.subtype() == mime::PLAIN
1300 && is_format_flowed
1301 {
1302 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1303 delsp.as_str().eq_ignore_ascii_case("yes")
1304 } else {
1305 false
1306 };
1307 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1308 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1309 (unflowed_text, unflowed_quote)
1310 } else {
1311 (simplified_txt, top_quote)
1312 };
1313
1314 let (simplified_txt, was_truncated) =
1315 truncate_msg_text(context, simplified_txt).await?;
1316 if was_truncated {
1317 self.is_mime_modified = was_truncated;
1318 }
1319
1320 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1321 let mut part = Part {
1322 dehtml_failed,
1323 typ: Viewtype::Text,
1324 mimetype: Some(mime_type),
1325 msg: simplified_txt,
1326 ..Default::default()
1327 };
1328 if let Some(quote) = simplified_quote {
1329 part.param.set(Param::Quote, quote);
1330 }
1331 part.msg_raw = Some(decoded_data);
1332 self.do_add_single_part(part);
1333 }
1334
1335 if is_forwarded {
1336 self.is_forwarded = true;
1337 }
1338
1339 if self.footer.is_none() && is_plaintext {
1340 self.footer = Some(footer.unwrap_or_default());
1341 }
1342 }
1343 _ => {}
1344 }
1345 }
1346 }
1347
1348 Ok(self.parts.len() > old_part_count)
1350 }
1351
1352 #[expect(clippy::too_many_arguments)]
1353 async fn do_add_single_file_part(
1354 &mut self,
1355 context: &Context,
1356 msg_type: Viewtype,
1357 mime_type: Mime,
1358 raw_mime: &str,
1359 decoded_data: &[u8],
1360 filename: &str,
1361 is_related: bool,
1362 ) -> Result<()> {
1363 if mime_type.type_() == mime::APPLICATION
1365 && mime_type.subtype().as_str() == "pgp-keys"
1366 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1367 {
1368 return Ok(());
1369 }
1370 let mut part = Part::default();
1371 let msg_type = if context
1372 .is_webxdc_file(filename, decoded_data)
1373 .await
1374 .unwrap_or(false)
1375 {
1376 Viewtype::Webxdc
1377 } else if filename.ends_with(".kml") {
1378 if filename.starts_with("location") || filename.starts_with("message") {
1381 let parsed = location::Kml::parse(decoded_data)
1382 .map_err(|err| {
1383 warn!(context, "failed to parse kml part: {:#}", err);
1384 })
1385 .ok();
1386 if filename.starts_with("location") {
1387 self.location_kml = parsed;
1388 } else {
1389 self.message_kml = parsed;
1390 }
1391 return Ok(());
1392 }
1393 msg_type
1394 } else if filename == "multi-device-sync.json" {
1395 if !context.get_config_bool(Config::SyncMsgs).await? {
1396 return Ok(());
1397 }
1398 let serialized = String::from_utf8_lossy(decoded_data)
1399 .parse()
1400 .unwrap_or_default();
1401 self.sync_items = context
1402 .parse_sync_items(serialized)
1403 .map_err(|err| {
1404 warn!(context, "failed to parse sync data: {:#}", err);
1405 })
1406 .ok();
1407 return Ok(());
1408 } else if filename == "status-update.json" {
1409 let serialized = String::from_utf8_lossy(decoded_data)
1410 .parse()
1411 .unwrap_or_default();
1412 self.webxdc_status_update = Some(serialized);
1413 return Ok(());
1414 } else if msg_type == Viewtype::Vcard {
1415 if let Some(summary) = get_vcard_summary(decoded_data) {
1416 part.param.set(Param::Summary1, summary);
1417 msg_type
1418 } else {
1419 Viewtype::File
1420 }
1421 } else if msg_type == Viewtype::Image
1422 || msg_type == Viewtype::Gif
1423 || msg_type == Viewtype::Sticker
1424 {
1425 match get_filemeta(decoded_data) {
1426 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1428 part.param.set_i64(Param::Width, width.into());
1429 part.param.set_i64(Param::Height, height.into());
1430 msg_type
1431 }
1432 _ => Viewtype::File,
1434 }
1435 } else {
1436 msg_type
1437 };
1438
1439 let blob =
1443 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1444 Ok(blob) => blob,
1445 Err(err) => {
1446 error!(
1447 context,
1448 "Could not add blob for mime part {}, error {:#}", filename, err
1449 );
1450 return Ok(());
1451 }
1452 };
1453 info!(context, "added blobfile: {:?}", blob.as_name());
1454
1455 part.typ = msg_type;
1456 part.org_filename = Some(filename.to_string());
1457 part.mimetype = Some(mime_type);
1458 part.bytes = decoded_data.len();
1459 part.param.set(Param::File, blob.as_name());
1460 part.param.set(Param::Filename, filename);
1461 part.param.set(Param::MimeType, raw_mime);
1462 part.is_related = is_related;
1463
1464 self.do_add_single_part(part);
1465 Ok(())
1466 }
1467
1468 async fn try_set_peer_key_from_file_part(
1470 context: &Context,
1471 decoded_data: &[u8],
1472 ) -> Result<bool> {
1473 let key = match str::from_utf8(decoded_data) {
1474 Err(err) => {
1475 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1476 return Ok(false);
1477 }
1478 Ok(key) => key,
1479 };
1480 let key = match SignedPublicKey::from_asc(key) {
1481 Err(err) => {
1482 warn!(
1483 context,
1484 "PGP key attachment is not an ASCII-armored file: {err:#}."
1485 );
1486 return Ok(false);
1487 }
1488 Ok((key, _)) => key,
1489 };
1490 if let Err(err) = key.verify() {
1491 warn!(context, "Attached PGP key verification failed: {err:#}.");
1492 return Ok(false);
1493 }
1494
1495 let fingerprint = key.dc_fingerprint().hex();
1496 context
1497 .sql
1498 .execute(
1499 "INSERT INTO public_keys (fingerprint, public_key)
1500 VALUES (?, ?)
1501 ON CONFLICT (fingerprint)
1502 DO NOTHING",
1503 (&fingerprint, key.to_bytes()),
1504 )
1505 .await?;
1506
1507 info!(context, "Imported PGP key {fingerprint} from attachment.");
1508 Ok(true)
1509 }
1510
1511 fn do_add_single_part(&mut self, mut part: Part) {
1512 if self.was_encrypted() {
1513 part.param.set_int(Param::GuaranteeE2ee, 1);
1514 }
1515 self.parts.push(part);
1516 }
1517
1518 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1519 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1520 return Some(list_id);
1523 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1524 if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
1527 if precedence == "list" || precedence == "bulk" {
1528 return Some(sender);
1532 }
1533 }
1534 }
1535 None
1536 }
1537
1538 pub(crate) fn is_mailinglist_message(&self) -> bool {
1539 self.get_mailinglist_header().is_some()
1540 }
1541
1542 pub(crate) fn is_schleuder_message(&self) -> bool {
1544 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1545 list_help == "<https://schleuder.org/>"
1546 } else {
1547 false
1548 }
1549 }
1550
1551 pub fn replace_msg_by_error(&mut self, error_msg: &str) {
1552 self.is_system_message = SystemMessage::Unknown;
1553 if let Some(part) = self.parts.first_mut() {
1554 part.typ = Viewtype::Text;
1555 part.msg = format!("[{error_msg}]");
1556 self.parts.truncate(1);
1557 }
1558 }
1559
1560 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1561 self.get_header(HeaderDef::MessageId)
1562 .and_then(|msgid| parse_message_id(msgid).ok())
1563 }
1564
1565 fn remove_secured_headers(
1566 headers: &mut HashMap<String, String>,
1567 removed: &mut HashSet<String>,
1568 ) {
1569 remove_header(headers, "secure-join-fingerprint", removed);
1570 remove_header(headers, "secure-join-auth", removed);
1571 remove_header(headers, "chat-verified", removed);
1572 remove_header(headers, "autocrypt-gossip", removed);
1573
1574 if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
1576 if secure_join == "vc-request" || secure_join == "vg-request" {
1577 headers.insert("secure-join".to_string(), secure_join);
1578 }
1579 }
1580 }
1581
1582 #[allow(clippy::too_many_arguments)]
1583 fn merge_headers(
1584 context: &Context,
1585 headers: &mut HashMap<String, String>,
1586 headers_removed: &mut HashSet<String>,
1587 recipients: &mut Vec<SingleInfo>,
1588 past_members: &mut Vec<SingleInfo>,
1589 from: &mut Option<SingleInfo>,
1590 list_post: &mut Option<String>,
1591 chat_disposition_notification_to: &mut Option<SingleInfo>,
1592 fields: &[mailparse::MailHeader<'_>],
1593 ) {
1594 headers.retain(|k, _| {
1595 !is_protected(k) || {
1596 headers_removed.insert(k.to_string());
1597 false
1598 }
1599 });
1600 for field in fields {
1601 let key = field.get_key().to_lowercase();
1603 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1604 match addrparse_header(field) {
1605 Ok(addrlist) => {
1606 *chat_disposition_notification_to = addrlist.extract_single_info();
1607 }
1608 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1609 }
1610 } else {
1611 let value = field.get_value();
1612 headers.insert(key.to_string(), value);
1613 }
1614 }
1615 let recipients_new = get_recipients(fields);
1616 if !recipients_new.is_empty() {
1617 *recipients = recipients_new;
1618 }
1619 let past_members_addresses =
1620 get_all_addresses_from_header(fields, "chat-group-past-members");
1621 if !past_members_addresses.is_empty() {
1622 *past_members = past_members_addresses;
1623 }
1624 let from_new = get_from(fields);
1625 if from_new.is_some() {
1626 *from = from_new;
1627 }
1628 let list_post_new = get_list_post(fields);
1629 if list_post_new.is_some() {
1630 *list_post = list_post_new;
1631 }
1632 }
1633
1634 fn process_report(
1635 &self,
1636 context: &Context,
1637 report: &mailparse::ParsedMail<'_>,
1638 ) -> Result<Option<Report>> {
1639 let report_body = if let Some(subpart) = report.subparts.get(1) {
1641 subpart.get_body_raw()?
1642 } else {
1643 bail!("Report does not have second MIME part");
1644 };
1645 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1646
1647 if report_fields
1649 .get_header_value(HeaderDef::Disposition)
1650 .is_none()
1651 {
1652 warn!(
1653 context,
1654 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1655 report_fields.get_header_value(HeaderDef::MessageId)
1656 );
1657 return Ok(None);
1658 };
1659
1660 let original_message_id = report_fields
1661 .get_header_value(HeaderDef::OriginalMessageId)
1662 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1665 .and_then(|v| parse_message_id(&v).ok());
1666 let additional_message_ids = report_fields
1667 .get_header_value(HeaderDef::AdditionalMessageIds)
1668 .map_or_else(Vec::new, |v| {
1669 v.split(' ')
1670 .filter_map(|s| parse_message_id(s).ok())
1671 .collect()
1672 });
1673
1674 Ok(Some(Report {
1675 original_message_id,
1676 additional_message_ids,
1677 }))
1678 }
1679
1680 fn process_delivery_status(
1681 &self,
1682 context: &Context,
1683 report: &mailparse::ParsedMail<'_>,
1684 ) -> Result<Option<DeliveryReport>> {
1685 let mut failure = true;
1687
1688 if let Some(status_part) = report.subparts.get(1) {
1689 if status_part.ctype.mimetype != "message/delivery-status"
1692 && status_part.ctype.mimetype != "message/global-delivery-status"
1693 {
1694 warn!(
1695 context,
1696 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1697 );
1698 return Ok(None);
1699 }
1700
1701 let status_body = status_part.get_body_raw()?;
1702
1703 let (_, sz) = mailparse::parse_headers(&status_body)?;
1705
1706 if let Some(status_body) = status_body.get(sz..) {
1708 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1709 if let Some(action) = status_fields.get_first_value("action") {
1710 if action != "failed" {
1711 info!(context, "DSN with {:?} action", action);
1712 failure = false;
1713 }
1714 } else {
1715 warn!(context, "DSN without action");
1716 }
1717 } else {
1718 warn!(context, "DSN without per-recipient fields");
1719 }
1720 } else {
1721 return Ok(None);
1723 }
1724
1725 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1727 p.ctype.mimetype.contains("rfc822")
1728 || p.ctype.mimetype == "message/global"
1729 || p.ctype.mimetype == "message/global-headers"
1730 }) {
1731 let report_body = original_msg.get_body_raw()?;
1732 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1733
1734 if let Some(original_message_id) = report_fields
1735 .get_header_value(HeaderDef::MessageId)
1736 .and_then(|v| parse_message_id(&v).ok())
1737 {
1738 return Ok(Some(DeliveryReport {
1739 rfc724_mid: original_message_id,
1740 failure,
1741 }));
1742 }
1743
1744 warn!(
1745 context,
1746 "ignoring unknown ndn-notification, Message-Id: {:?}",
1747 report_fields.get_header_value(HeaderDef::MessageId)
1748 );
1749 }
1750
1751 Ok(None)
1752 }
1753
1754 fn maybe_remove_bad_parts(&mut self) {
1755 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1756 if good_parts == 0 {
1757 self.parts.truncate(1);
1759 } else if good_parts < self.parts.len() {
1760 self.parts.retain(|p| !p.dehtml_failed);
1761 }
1762
1763 if !self.has_chat_version() && self.is_mime_modified {
1771 fn is_related_image(p: &&Part) -> bool {
1772 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1773 }
1774 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1775 if related_image_cnt > 1 {
1776 let mut is_first_image = true;
1777 self.parts.retain(|p| {
1778 let retain = is_first_image || !is_related_image(&p);
1779 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1780 is_first_image = false;
1781 }
1782 retain
1783 });
1784 }
1785 }
1786 }
1787
1788 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1798 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1799 let text_part_cnt = self
1800 .parts
1801 .iter()
1802 .filter(|p| p.typ == Viewtype::Text)
1803 .count();
1804 if text_part_cnt == 2 {
1805 if let Some(last_part) = self.parts.last() {
1806 if last_part.typ == Viewtype::Text {
1807 self.parts.pop();
1808 }
1809 }
1810 }
1811 }
1812 }
1813
1814 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1818 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1819 let from = from.to_ascii_lowercase();
1820 from.contains("mailer-daemon") || from.contains("mail-daemon")
1821 } else {
1822 false
1823 };
1824 if maybe_ndn && self.delivery_report.is_none() {
1825 for original_message_id in self
1826 .parts
1827 .iter()
1828 .filter_map(|part| part.msg_raw.as_ref())
1829 .flat_map(|part| part.lines())
1830 .filter_map(|line| line.split_once("Message-ID:"))
1831 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1832 {
1833 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1834 {
1835 self.delivery_report = Some(DeliveryReport {
1836 rfc724_mid: original_message_id,
1837 failure: true,
1838 })
1839 }
1840 }
1841 }
1842 }
1843
1844 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1848 for report in &self.mdn_reports {
1849 for original_message_id in report
1850 .original_message_id
1851 .iter()
1852 .chain(&report.additional_message_ids)
1853 {
1854 if let Err(err) =
1855 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1856 {
1857 warn!(context, "Could not handle MDN: {err:#}.");
1858 }
1859 }
1860 }
1861
1862 if let Some(delivery_report) = &self.delivery_report {
1863 if delivery_report.failure {
1864 let error = parts
1865 .iter()
1866 .find(|p| p.typ == Viewtype::Text)
1867 .map(|p| p.msg.clone());
1868 if let Err(err) = handle_ndn(context, delivery_report, error).await {
1869 warn!(context, "Could not handle NDN: {err:#}.");
1870 }
1871 }
1872 }
1873 }
1874
1875 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
1880 let parent_timestamp = if let Some(field) = self
1881 .get_header(HeaderDef::InReplyTo)
1882 .and_then(|msgid| parse_message_id(msgid).ok())
1883 {
1884 context
1885 .sql
1886 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
1887 .await?
1888 } else {
1889 None
1890 };
1891 Ok(parent_timestamp)
1892 }
1893
1894 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
1898 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
1899 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
1900 .map(|h| {
1901 h.split_ascii_whitespace()
1902 .filter_map(|ts| ts.parse::<i64>().ok())
1903 .map(|ts| std::cmp::min(now, ts))
1904 .collect()
1905 })
1906 }
1907
1908 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
1911 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
1912 header
1913 .split_ascii_whitespace()
1914 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
1915 .collect()
1916 } else {
1917 Vec::new()
1918 }
1919 }
1920}
1921
1922fn remove_header(
1923 headers: &mut HashMap<String, String>,
1924 key: &str,
1925 removed: &mut HashSet<String>,
1926) -> Option<String> {
1927 if let Some((k, v)) = headers.remove_entry(key) {
1928 removed.insert(k);
1929 Some(v)
1930 } else {
1931 None
1932 }
1933}
1934
1935async fn parse_gossip_headers(
1941 context: &Context,
1942 from: &str,
1943 recipients: &[SingleInfo],
1944 gossip_headers: Vec<String>,
1945) -> Result<HashMap<String, SignedPublicKey>> {
1946 let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
1948
1949 for value in &gossip_headers {
1950 let header = match value.parse::<Aheader>() {
1951 Ok(header) => header,
1952 Err(err) => {
1953 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
1954 continue;
1955 }
1956 };
1957
1958 if !recipients
1959 .iter()
1960 .any(|info| addr_cmp(&info.addr, &header.addr))
1961 {
1962 warn!(
1963 context,
1964 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
1965 );
1966 continue;
1967 }
1968 if addr_cmp(from, &header.addr) {
1969 warn!(
1971 context,
1972 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
1973 );
1974 continue;
1975 }
1976
1977 let fingerprint = header.public_key.dc_fingerprint().hex();
1978 context
1979 .sql
1980 .execute(
1981 "INSERT INTO public_keys (fingerprint, public_key)
1982 VALUES (?, ?)
1983 ON CONFLICT (fingerprint)
1984 DO NOTHING",
1985 (&fingerprint, header.public_key.to_bytes()),
1986 )
1987 .await?;
1988
1989 gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
1990 }
1991
1992 Ok(gossiped_keys)
1993}
1994
1995#[derive(Debug)]
1997pub(crate) struct Report {
1998 original_message_id: Option<String>,
2003 additional_message_ids: Vec<String>,
2005}
2006
2007#[derive(Debug)]
2009pub(crate) struct DeliveryReport {
2010 pub rfc724_mid: String,
2011 pub failure: bool,
2012}
2013
2014pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2015 let mut msgids = Vec::new();
2017 for id in ids.split_whitespace() {
2018 let mut id = id.to_string();
2019 if let Some(id_without_prefix) = id.strip_prefix('<') {
2020 id = id_without_prefix.to_string();
2021 };
2022 if let Some(id_without_suffix) = id.strip_suffix('>') {
2023 id = id_without_suffix.to_string();
2024 };
2025 if !id.is_empty() {
2026 msgids.push(id);
2027 }
2028 }
2029 msgids
2030}
2031
2032pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2033 if let Some(id) = parse_message_ids(ids).first() {
2034 Ok(id.to_string())
2035 } else {
2036 bail!("could not parse message_id: {}", ids);
2037 }
2038}
2039
2040fn is_protected(key: &str) -> bool {
2046 key.starts_with("chat-")
2047 || matches!(
2048 key,
2049 "return-path"
2050 | "auto-submitted"
2051 | "autocrypt-setup-message"
2052 | "date"
2053 | "from"
2054 | "sender"
2055 | "reply-to"
2056 | "to"
2057 | "cc"
2058 | "bcc"
2059 | "message-id"
2060 | "in-reply-to"
2061 | "references"
2062 | "secure-join"
2063 )
2064}
2065
2066pub(crate) fn is_hidden(key: &str) -> bool {
2068 matches!(
2069 key,
2070 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2071 )
2072}
2073
2074#[derive(Debug, Default, Clone)]
2076pub struct Part {
2077 pub typ: Viewtype,
2079
2080 pub mimetype: Option<Mime>,
2082
2083 pub msg: String,
2085
2086 pub msg_raw: Option<String>,
2088
2089 pub bytes: usize,
2091
2092 pub param: Params,
2094
2095 pub(crate) org_filename: Option<String>,
2097
2098 pub error: Option<String>,
2100
2101 pub(crate) dehtml_failed: bool,
2103
2104 pub(crate) is_related: bool,
2111
2112 pub(crate) is_reaction: bool,
2114}
2115
2116fn get_mime_type(
2121 mail: &mailparse::ParsedMail<'_>,
2122 filename: &Option<String>,
2123 is_chat_msg: bool,
2124) -> Result<(Mime, Viewtype)> {
2125 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2126
2127 let viewtype = match mimetype.type_() {
2128 mime::TEXT => match mimetype.subtype() {
2129 mime::VCARD => Viewtype::Vcard,
2130 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2131 _ => Viewtype::File,
2132 },
2133 mime::IMAGE => match mimetype.subtype() {
2134 mime::GIF => Viewtype::Gif,
2135 mime::SVG => Viewtype::File,
2136 _ => Viewtype::Image,
2137 },
2138 mime::AUDIO => Viewtype::Audio,
2139 mime::VIDEO => Viewtype::Video,
2140 mime::MULTIPART => Viewtype::Unknown,
2141 mime::MESSAGE => {
2142 if is_attachment_disposition(mail) {
2143 Viewtype::File
2144 } else {
2145 Viewtype::Unknown
2153 }
2154 }
2155 mime::APPLICATION => match mimetype.subtype() {
2156 mime::OCTET_STREAM => match filename {
2157 Some(filename) if !is_chat_msg => {
2158 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2159 Some((viewtype, _)) => viewtype,
2160 None => Viewtype::File,
2161 }
2162 }
2163 _ => Viewtype::File,
2164 },
2165 _ => Viewtype::File,
2166 },
2167 _ => Viewtype::Unknown,
2168 };
2169
2170 Ok((mimetype, viewtype))
2171}
2172
2173fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2174 let ct = mail.get_content_disposition();
2175 ct.disposition == DispositionType::Attachment
2176 && ct
2177 .params
2178 .iter()
2179 .any(|(key, _value)| key.starts_with("filename"))
2180}
2181
2182fn get_attachment_filename(
2189 context: &Context,
2190 mail: &mailparse::ParsedMail,
2191) -> Result<Option<String>> {
2192 let ct = mail.get_content_disposition();
2193
2194 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2197
2198 if desired_filename.is_none() {
2199 if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
2200 warn!(context, "apostrophed encoding invalid: {}", name);
2204 desired_filename = Some(name);
2205 }
2206 }
2207
2208 if desired_filename.is_none() {
2210 desired_filename = ct.params.get("name").map(|s| s.to_string());
2211 }
2212
2213 if desired_filename.is_none() {
2216 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2217 }
2218
2219 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2221 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2222 desired_filename = Some(format!("file.{subtype}",));
2223 } else {
2224 bail!(
2225 "could not determine attachment filename: {:?}",
2226 ct.disposition
2227 );
2228 };
2229 }
2230
2231 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2232
2233 Ok(desired_filename)
2234}
2235
2236pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2238 let to_addresses = get_all_addresses_from_header(headers, "to");
2239 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2240
2241 let mut res = to_addresses;
2242 res.extend(cc_addresses);
2243 res
2244}
2245
2246pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2248 let all = get_all_addresses_from_header(headers, "from");
2249 tools::single_value(all)
2250}
2251
2252pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2254 get_all_addresses_from_header(headers, "list-post")
2255 .into_iter()
2256 .next()
2257 .map(|s| s.addr)
2258}
2259
2260fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2272 let mut result: Vec<SingleInfo> = Default::default();
2273
2274 if let Some(header) = headers
2275 .iter()
2276 .rev()
2277 .find(|h| h.get_key().to_lowercase() == header)
2278 {
2279 if let Ok(addrs) = mailparse::addrparse_header(header) {
2280 for addr in addrs.iter() {
2281 match addr {
2282 mailparse::MailAddr::Single(info) => {
2283 result.push(SingleInfo {
2284 addr: addr_normalize(&info.addr).to_lowercase(),
2285 display_name: info.display_name.clone(),
2286 });
2287 }
2288 mailparse::MailAddr::Group(infos) => {
2289 for info in &infos.addrs {
2290 result.push(SingleInfo {
2291 addr: addr_normalize(&info.addr).to_lowercase(),
2292 display_name: info.display_name.clone(),
2293 });
2294 }
2295 }
2296 }
2297 }
2298 }
2299 }
2300
2301 result
2302}
2303
2304async fn handle_mdn(
2305 context: &Context,
2306 from_id: ContactId,
2307 rfc724_mid: &str,
2308 timestamp_sent: i64,
2309) -> Result<()> {
2310 if from_id == ContactId::SELF {
2311 warn!(
2312 context,
2313 "Ignoring MDN sent to self, this is a bug on the sender device."
2314 );
2315
2316 return Ok(());
2319 }
2320
2321 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2322 .sql
2323 .query_row_optional(
2324 concat!(
2325 "SELECT",
2326 " m.id AS msg_id,",
2327 " c.id AS chat_id,",
2328 " mdns.contact_id AS mdn_contact",
2329 " FROM msgs m ",
2330 " LEFT JOIN chats c ON m.chat_id=c.id",
2331 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
2332 " WHERE rfc724_mid=? AND from_id=1",
2333 " ORDER BY msg_id DESC, mdn_contact=? DESC",
2334 " LIMIT 1",
2335 ),
2336 (&rfc724_mid, from_id),
2337 |row| {
2338 let msg_id: MsgId = row.get("msg_id")?;
2339 let chat_id: ChatId = row.get("chat_id")?;
2340 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2341 Ok((
2342 msg_id,
2343 chat_id,
2344 mdn_contact.is_some(),
2345 mdn_contact == Some(from_id),
2346 ))
2347 },
2348 )
2349 .await?
2350 else {
2351 info!(
2352 context,
2353 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2354 );
2355 return Ok(());
2356 };
2357
2358 if is_dup {
2359 return Ok(());
2360 }
2361 context
2362 .sql
2363 .execute(
2364 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2365 (msg_id, from_id, timestamp_sent),
2366 )
2367 .await?;
2368 if !has_mdns {
2369 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2370 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2372 }
2373 Ok(())
2374}
2375
2376async fn handle_ndn(
2379 context: &Context,
2380 failed: &DeliveryReport,
2381 error: Option<String>,
2382) -> Result<()> {
2383 if failed.rfc724_mid.is_empty() {
2384 return Ok(());
2385 }
2386
2387 let msgs: Vec<_> = context
2390 .sql
2391 .query_map(
2392 "SELECT id FROM msgs
2393 WHERE rfc724_mid=? AND from_id=1",
2394 (&failed.rfc724_mid,),
2395 |row| {
2396 let msg_id: MsgId = row.get(0)?;
2397 Ok(msg_id)
2398 },
2399 |rows| Ok(rows.collect::<Vec<_>>()),
2400 )
2401 .await?;
2402
2403 let error = if let Some(error) = error {
2404 error
2405 } else {
2406 "Delivery to at least one recipient failed.".to_string()
2407 };
2408 let err_msg = &error;
2409
2410 for msg in msgs {
2411 let msg_id = msg?;
2412 let mut message = Message::load_from_db(context, msg_id).await?;
2413 let aggregated_error = message
2414 .error
2415 .as_ref()
2416 .map(|err| format!("{err}\n\n{err_msg}"));
2417 set_msg_failed(
2418 context,
2419 &mut message,
2420 aggregated_error.as_ref().unwrap_or(err_msg),
2421 )
2422 .await?;
2423 }
2424
2425 Ok(())
2426}
2427
2428#[cfg(test)]
2429mod mimeparser_tests;