1use std::cmp::min;
4use std::collections::{BTreeMap, HashMap, HashSet};
5use std::path::Path;
6use std::str;
7use std::str::FromStr;
8
9use anyhow::{Context as _, Result, bail};
10use deltachat_contact_tools::{addr_cmp, addr_normalize, sanitize_bidi_characters};
11use deltachat_derive::{FromSql, ToSql};
12use format_flowed::unformat_flowed;
13use mailparse::{DispositionType, MailHeader, MailHeaderMap, SingleInfo, addrparse_header};
14use mime::Mime;
15
16use crate::aheader::Aheader;
17use crate::authres::handle_authres;
18use crate::blob::BlobObject;
19use crate::chat::ChatId;
20use crate::config::Config;
21use crate::constants;
22use crate::contact::ContactId;
23use crate::context::Context;
24use crate::decrypt::{try_decrypt, validate_detached_signature};
25use crate::dehtml::dehtml;
26use crate::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)]
42pub struct GossipedKey {
43 pub public_key: SignedPublicKey,
45
46 pub verified: bool,
48}
49
50#[derive(Debug)]
60pub(crate) struct MimeMessage {
61 pub parts: Vec<Part>,
63
64 headers: HashMap<String, String>,
66
67 #[cfg(test)]
68 headers_removed: HashSet<String>,
70
71 pub recipients: Vec<SingleInfo>,
75
76 pub past_members: Vec<SingleInfo>,
78
79 pub from: SingleInfo,
81
82 pub incoming: bool,
84 pub list_post: Option<String>,
87 pub chat_disposition_notification_to: Option<SingleInfo>,
88 pub decrypting_failed: bool,
89
90 pub signatures: HashSet<Fingerprint>,
96
97 pub gossiped_keys: BTreeMap<String, GossipedKey>,
100
101 pub autocrypt_fingerprint: Option<String>,
105
106 pub is_forwarded: bool,
108 pub is_system_message: SystemMessage,
109 pub location_kml: Option<location::Kml>,
110 pub message_kml: Option<location::Kml>,
111 pub(crate) sync_items: Option<SyncItems>,
112 pub(crate) webxdc_status_update: Option<String>,
113 pub(crate) user_avatar: Option<AvatarAction>,
114 pub(crate) group_avatar: Option<AvatarAction>,
115 pub(crate) mdn_reports: Vec<Report>,
116 pub(crate) delivery_report: Option<DeliveryReport>,
117
118 pub(crate) footer: Option<String>,
123
124 pub is_mime_modified: bool,
127
128 pub decoded_data: Vec<u8>,
130
131 pub(crate) hop_info: String,
133
134 pub(crate) is_bot: Option<bool>,
144
145 pub(crate) timestamp_rcvd: i64,
147 pub(crate) timestamp_sent: i64,
150}
151
152#[derive(Debug, PartialEq)]
153pub(crate) enum AvatarAction {
154 Delete,
155 Change(String),
156}
157
158#[derive(
160 Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
161)]
162#[repr(u32)]
163pub enum SystemMessage {
164 #[default]
166 Unknown = 0,
167
168 GroupNameChanged = 2,
170
171 GroupImageChanged = 3,
173
174 MemberAddedToGroup = 4,
176
177 MemberRemovedFromGroup = 5,
179
180 AutocryptSetupMessage = 6,
182
183 SecurejoinMessage = 7,
185
186 LocationStreamingEnabled = 8,
188
189 LocationOnly = 9,
191
192 EphemeralTimerChanged = 10,
194
195 ChatProtectionEnabled = 11,
197
198 ChatProtectionDisabled = 12,
200
201 InvalidUnencryptedMail = 13,
204
205 SecurejoinWait = 14,
208
209 SecurejoinWaitTimeout = 15,
212
213 MultiDeviceSync = 20,
216
217 WebxdcStatusUpdate = 30,
221
222 WebxdcInfoMessage = 32,
224
225 IrohNodeAddr = 40,
227
228 ChatE2ee = 50,
230
231 CallAccepted = 66,
233
234 CallEnded = 67,
236}
237
238const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
239
240impl MimeMessage {
241 pub(crate) async fn from_bytes(
246 context: &Context,
247 body: &[u8],
248 partial: Option<u32>,
249 ) -> Result<Self> {
250 let mail = mailparse::parse_mail(body)?;
251
252 let timestamp_rcvd = smeared_time(context);
253 let mut timestamp_sent =
254 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
255 let mut hop_info = parse_receive_headers(&mail.get_headers());
256
257 let mut headers = Default::default();
258 let mut headers_removed = HashSet::<String>::new();
259 let mut recipients = Default::default();
260 let mut past_members = Default::default();
261 let mut from = Default::default();
262 let mut list_post = Default::default();
263 let mut chat_disposition_notification_to = None;
264
265 MimeMessage::merge_headers(
267 context,
268 &mut headers,
269 &mut headers_removed,
270 &mut recipients,
271 &mut past_members,
272 &mut from,
273 &mut list_post,
274 &mut chat_disposition_notification_to,
275 &mail.headers,
276 );
277 headers.retain(|k, _| {
278 !is_hidden(k) || {
279 headers_removed.insert(k.to_string());
280 false
281 }
282 });
283
284 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
286 let (part, mimetype) =
287 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
288 if let Some(part) = mail.subparts.first() {
289 timestamp_sent =
293 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
294 MimeMessage::merge_headers(
295 context,
296 &mut headers,
297 &mut headers_removed,
298 &mut recipients,
299 &mut past_members,
300 &mut from,
301 &mut list_post,
302 &mut chat_disposition_notification_to,
303 &part.headers,
304 );
305 (part, part.ctype.mimetype.parse::<Mime>()?)
306 } else {
307 (&mail, mimetype)
309 }
310 } else {
311 (&mail, mimetype)
313 };
314 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "mixed" {
315 if let Some(part) = part.subparts.first() {
316 for field in &part.headers {
317 let key = field.get_key().to_lowercase();
318 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
319 headers.insert(key.to_string(), field.get_value());
320 }
321 }
322 }
323 }
324
325 if let Some(microsoft_message_id) = remove_header(
329 &mut headers,
330 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
331 &mut headers_removed,
332 ) {
333 headers.insert(
334 HeaderDef::MessageId.get_headername().to_string(),
335 microsoft_message_id,
336 );
337 }
338
339 Self::remove_secured_headers(&mut headers, &mut headers_removed);
342
343 let mut from = from.context("No from in message")?;
344 let private_keyring = load_self_secret_keyring(context).await?;
345
346 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
347
348 let mut gossiped_keys = Default::default();
349 hop_info += "\n\n";
350 hop_info += &dkim_results.to_string();
351
352 let incoming = !context.is_self_addr(&from.addr).await?;
353
354 let mut aheader_value: Option<String> = mail.headers.get_header_value(HeaderDef::Autocrypt);
355
356 let mail_raw; let decrypted_msg; let (mail, is_encrypted) =
360 match tokio::task::block_in_place(|| try_decrypt(&mail, &private_keyring)) {
361 Ok(Some(mut msg)) => {
362 mail_raw = msg.as_data_vec().unwrap_or_default();
363
364 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
365 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
366 info!(
367 context,
368 "decrypted message mime-body:\n{}",
369 String::from_utf8_lossy(&mail_raw),
370 );
371 }
372
373 decrypted_msg = Some(msg);
374
375 timestamp_sent = Self::get_timestamp_sent(
376 &decrypted_mail.headers,
377 timestamp_sent,
378 timestamp_rcvd,
379 );
380
381 if let Some(protected_aheader_value) = decrypted_mail
382 .headers
383 .get_header_value(HeaderDef::Autocrypt)
384 {
385 aheader_value = Some(protected_aheader_value);
386 }
387
388 (Ok(decrypted_mail), true)
389 }
390 Ok(None) => {
391 mail_raw = Vec::new();
392 decrypted_msg = None;
393 (Ok(mail), false)
394 }
395 Err(err) => {
396 mail_raw = Vec::new();
397 decrypted_msg = None;
398 warn!(context, "decryption failed: {:#}", err);
399 (Err(err), false)
400 }
401 };
402
403 let autocrypt_header = if !incoming {
404 None
405 } else if let Some(aheader_value) = aheader_value {
406 match Aheader::from_str(&aheader_value) {
407 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
408 Ok(header) => {
409 warn!(
410 context,
411 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
412 );
413 None
414 }
415 Err(err) => {
416 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
417 None
418 }
419 }
420 } else {
421 None
422 };
423
424 let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
425 let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
426 let inserted = context
427 .sql
428 .execute(
429 "INSERT INTO public_keys (fingerprint, public_key)
430 VALUES (?, ?)
431 ON CONFLICT (fingerprint)
432 DO NOTHING",
433 (&fingerprint, autocrypt_header.public_key.to_bytes()),
434 )
435 .await?;
436 if inserted > 0 {
437 info!(
438 context,
439 "Saved key with fingerprint {fingerprint} from the Autocrypt header"
440 );
441 }
442 Some(fingerprint)
443 } else {
444 None
445 };
446
447 let mut public_keyring = if incoming {
448 if let Some(autocrypt_header) = autocrypt_header {
449 vec![autocrypt_header.public_key]
450 } else {
451 vec![]
452 }
453 } else {
454 key::load_self_public_keyring(context).await?
455 };
456
457 if let Some(signature) = match &decrypted_msg {
458 Some(pgp::composed::Message::Literal { .. }) => None,
459 Some(pgp::composed::Message::Compressed { .. }) => {
460 None
463 }
464 Some(pgp::composed::Message::SignedOnePass { reader, .. }) => reader.signature(),
465 Some(pgp::composed::Message::Signed { reader, .. }) => Some(reader.signature()),
466 Some(pgp::composed::Message::Encrypted { .. }) => {
467 None
469 }
470 None => None,
471 } {
472 for issuer_fingerprint in signature.issuer_fingerprint() {
473 let issuer_fingerprint =
474 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
475 if let Some(public_key_bytes) = context
476 .sql
477 .query_row_optional(
478 "SELECT public_key
479 FROM public_keys
480 WHERE fingerprint=?",
481 (&issuer_fingerprint,),
482 |row| {
483 let bytes: Vec<u8> = row.get(0)?;
484 Ok(bytes)
485 },
486 )
487 .await?
488 {
489 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
490 public_keyring.push(public_key)
491 }
492 }
493 }
494
495 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
496 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
497 } else {
498 HashSet::new()
499 };
500
501 let mail = mail.as_ref().map(|mail| {
502 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
503 .unwrap_or((mail, Default::default()));
504 signatures.extend(signatures_detached);
505 content
506 });
507 if let (Ok(mail), true) = (mail, is_encrypted) {
508 if !signatures.is_empty() {
509 remove_header(&mut headers, "subject", &mut headers_removed);
513 remove_header(&mut headers, "list-id", &mut headers_removed);
514 }
515
516 let mut inner_from = None;
522
523 MimeMessage::merge_headers(
524 context,
525 &mut headers,
526 &mut headers_removed,
527 &mut recipients,
528 &mut past_members,
529 &mut inner_from,
530 &mut list_post,
531 &mut chat_disposition_notification_to,
532 &mail.headers,
533 );
534
535 if !signatures.is_empty() {
536 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
541 gossiped_keys =
542 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
543 }
544
545 if let Some(inner_from) = inner_from {
546 if !addr_cmp(&inner_from.addr, &from.addr) {
547 warn!(
556 context,
557 "From header in encrypted part doesn't match the outer one",
558 );
559
560 bail!("From header is forged");
565 }
566 from = inner_from;
567 }
568 }
569 if signatures.is_empty() {
570 Self::remove_secured_headers(&mut headers, &mut headers_removed);
571 }
572 if !is_encrypted {
573 signatures.clear();
574 }
575
576 let mut parser = MimeMessage {
577 parts: Vec::new(),
578 headers,
579 #[cfg(test)]
580 headers_removed,
581
582 recipients,
583 past_members,
584 list_post,
585 from,
586 incoming,
587 chat_disposition_notification_to,
588 decrypting_failed: mail.is_err(),
589
590 signatures,
592 autocrypt_fingerprint,
593 gossiped_keys,
594 is_forwarded: false,
595 mdn_reports: Vec::new(),
596 is_system_message: SystemMessage::Unknown,
597 location_kml: None,
598 message_kml: None,
599 sync_items: None,
600 webxdc_status_update: None,
601 user_avatar: None,
602 group_avatar: None,
603 delivery_report: None,
604 footer: None,
605 is_mime_modified: false,
606 decoded_data: Vec::new(),
607 hop_info,
608 is_bot: None,
609 timestamp_rcvd,
610 timestamp_sent,
611 };
612
613 match partial {
614 Some(org_bytes) => {
615 parser
616 .create_stub_from_partial_download(context, org_bytes)
617 .await?;
618 }
619 None => match mail {
620 Ok(mail) => {
621 parser.parse_mime_recursive(context, mail, false).await?;
622 }
623 Err(err) => {
624 let msg_body = stock_str::cant_decrypt_msg_body(context).await;
625 let txt = format!("[{msg_body}]");
626
627 let part = Part {
628 typ: Viewtype::Text,
629 msg_raw: Some(txt.clone()),
630 msg: txt,
631 error: Some(format!("Decrypting failed: {err:#}")),
634 ..Default::default()
635 };
636 parser.parts.push(part);
637 }
638 },
639 };
640
641 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
642 if parser.mdn_reports.is_empty()
643 && !is_location_only
644 && parser.sync_items.is_none()
645 && parser.webxdc_status_update.is_none()
646 {
647 let is_bot =
648 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
649 parser.is_bot = Some(is_bot);
650 }
651 parser.maybe_remove_bad_parts();
652 parser.maybe_remove_inline_mailinglist_footer();
653 parser.heuristically_parse_ndn(context).await;
654 parser.parse_headers(context).await?;
655 parser.decoded_data = mail_raw;
656
657 Ok(parser)
658 }
659
660 fn get_timestamp_sent(
661 hdrs: &[mailparse::MailHeader<'_>],
662 default: i64,
663 timestamp_rcvd: i64,
664 ) -> i64 {
665 hdrs.get_header_value(HeaderDef::Date)
666 .and_then(|v| mailparse::dateparse(&v).ok())
667 .map_or(default, |value| {
668 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
669 })
670 }
671
672 fn parse_system_message_headers(&mut self, context: &Context) {
674 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
675 self.parts.retain(|part| {
676 part.mimetype.is_none()
677 || part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
678 });
679
680 if self.parts.len() == 1 {
681 self.is_system_message = SystemMessage::AutocryptSetupMessage;
682 } else {
683 warn!(context, "could not determine ASM mime-part");
684 }
685 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
686 if value == "location-streaming-enabled" {
687 self.is_system_message = SystemMessage::LocationStreamingEnabled;
688 } else if value == "ephemeral-timer-changed" {
689 self.is_system_message = SystemMessage::EphemeralTimerChanged;
690 } else if value == "protection-enabled" {
691 self.is_system_message = SystemMessage::ChatProtectionEnabled;
692 } else if value == "protection-disabled" {
693 self.is_system_message = SystemMessage::ChatProtectionDisabled;
694 } else if value == "group-avatar-changed" {
695 self.is_system_message = SystemMessage::GroupImageChanged;
696 } else if value == "call-accepted" {
697 self.is_system_message = SystemMessage::CallAccepted;
698 } else if value == "call-ended" {
699 self.is_system_message = SystemMessage::CallEnded;
700 }
701 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
702 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
703 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
704 self.is_system_message = SystemMessage::MemberAddedToGroup;
705 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
706 self.is_system_message = SystemMessage::GroupNameChanged;
707 }
708 }
709
710 fn parse_avatar_headers(&mut self, context: &Context) {
712 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
713 self.group_avatar = self.avatar_action_from_header(context, header_value.to_string());
714 }
715
716 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
717 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string());
718 }
719 }
720
721 fn parse_videochat_headers(&mut self) {
722 let content = self
723 .get_header(HeaderDef::ChatContent)
724 .unwrap_or_default()
725 .to_string();
726 let room = self
727 .get_header(HeaderDef::ChatWebrtcRoom)
728 .map(|s| s.to_string());
729 let accepted = self
730 .get_header(HeaderDef::ChatWebrtcAccepted)
731 .map(|s| s.to_string());
732 if let Some(part) = self.parts.first_mut() {
733 if let Some(room) = room {
734 if content == "videochat-invitation" {
735 part.typ = Viewtype::VideochatInvitation;
736 } else if content == "call" {
737 part.typ = Viewtype::Call
738 }
739 part.param.set(Param::WebrtcRoom, room);
740 } else if let Some(accepted) = accepted {
741 part.param.set(Param::WebrtcAccepted, accepted);
742 }
743 }
744 }
745
746 fn squash_attachment_parts(&mut self) {
752 if self.parts.len() == 2
753 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
754 && self
755 .parts
756 .get(1)
757 .is_some_and(|filepart| match filepart.typ {
758 Viewtype::Image
759 | Viewtype::Gif
760 | Viewtype::Sticker
761 | Viewtype::Audio
762 | Viewtype::Voice
763 | Viewtype::Video
764 | Viewtype::Vcard
765 | Viewtype::File
766 | Viewtype::Webxdc => true,
767 Viewtype::Unknown
768 | Viewtype::Text
769 | Viewtype::VideochatInvitation
770 | Viewtype::Call => false,
771 })
772 {
773 let mut parts = std::mem::take(&mut self.parts);
774 let Some(mut filepart) = parts.pop() else {
775 return;
777 };
778 let Some(textpart) = parts.pop() else {
779 return;
781 };
782
783 filepart.msg.clone_from(&textpart.msg);
784 if let Some(quote) = textpart.param.get(Param::Quote) {
785 filepart.param.set(Param::Quote, quote);
786 }
787
788 self.parts = vec![filepart];
789 }
790 }
791
792 fn parse_attachments(&mut self) {
794 if self.parts.len() != 1 {
797 return;
798 }
799
800 if let Some(mut part) = self.parts.pop() {
801 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
802 {
803 part.typ = Viewtype::Voice;
804 }
805 if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
806 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
807 if value == "sticker" {
808 part.typ = Viewtype::Sticker;
809 }
810 }
811 }
812 if part.typ == Viewtype::Audio
813 || part.typ == Viewtype::Voice
814 || part.typ == Viewtype::Video
815 {
816 if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
817 let duration_ms = field_0.parse().unwrap_or_default();
818 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
819 part.param.set_int(Param::Duration, duration_ms);
820 }
821 }
822 }
823
824 self.parts.push(part);
825 }
826 }
827
828 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
829 self.parse_system_message_headers(context);
830 self.parse_avatar_headers(context);
831 self.parse_videochat_headers();
832 if self.delivery_report.is_none() {
833 self.squash_attachment_parts();
834 }
835
836 if !context.get_config_bool(Config::Bot).await? {
837 if let Some(ref subject) = self.get_subject() {
838 let mut prepend_subject = true;
839 if !self.decrypting_failed {
840 let colon = subject.find(':');
841 if colon == Some(2)
842 || colon == Some(3)
843 || self.has_chat_version()
844 || subject.contains("Chat:")
845 {
846 prepend_subject = false
847 }
848 }
849
850 if self.is_mailinglist_message() && !self.has_chat_version() {
853 prepend_subject = true;
854 }
855
856 if prepend_subject && !subject.is_empty() {
857 let part_with_text = self
858 .parts
859 .iter_mut()
860 .find(|part| !part.msg.is_empty() && !part.is_reaction);
861 if let Some(part) = part_with_text {
862 part.msg = format!("{} – {}", subject, part.msg);
863 }
864 }
865 }
866 }
867
868 if self.is_forwarded {
869 for part in &mut self.parts {
870 part.param.set_int(Param::Forwarded, 1);
871 }
872 }
873
874 self.parse_attachments();
875
876 if !self.decrypting_failed && !self.parts.is_empty() {
878 if let Some(ref dn_to) = self.chat_disposition_notification_to {
879 let from = &self.from.addr;
881 if !context.is_self_addr(from).await? {
882 if from.to_lowercase() == dn_to.addr.to_lowercase() {
883 if let Some(part) = self.parts.last_mut() {
884 part.param.set_int(Param::WantsMdn, 1);
885 }
886 } else {
887 warn!(
888 context,
889 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
890 );
891 }
892 }
893 }
894 }
895
896 if self.parts.is_empty() && self.mdn_reports.is_empty() {
901 let mut part = Part {
902 typ: Viewtype::Text,
903 ..Default::default()
904 };
905
906 if let Some(ref subject) = self.get_subject() {
907 if !self.has_chat_version() && self.webxdc_status_update.is_none() {
908 part.msg = subject.to_string();
909 }
910 }
911
912 self.do_add_single_part(part);
913 }
914
915 if self.is_bot == Some(true) {
916 for part in &mut self.parts {
917 part.param.set(Param::Bot, "1");
918 }
919 }
920
921 Ok(())
922 }
923
924 fn avatar_action_from_header(
925 &mut self,
926 context: &Context,
927 header_value: String,
928 ) -> Option<AvatarAction> {
929 if header_value == "0" {
930 Some(AvatarAction::Delete)
931 } else if let Some(base64) = header_value
932 .split_ascii_whitespace()
933 .collect::<String>()
934 .strip_prefix("base64:")
935 {
936 match BlobObject::store_from_base64(context, base64) {
937 Ok(path) => Some(AvatarAction::Change(path)),
938 Err(err) => {
939 warn!(
940 context,
941 "Could not decode and save avatar to blob file: {:#}", err,
942 );
943 None
944 }
945 }
946 } else {
947 let mut i = 0;
950 while let Some(part) = self.parts.get_mut(i) {
951 if let Some(part_filename) = &part.org_filename {
952 if part_filename == &header_value {
953 if let Some(blob) = part.param.get(Param::File) {
954 let res = Some(AvatarAction::Change(blob.to_string()));
955 self.parts.remove(i);
956 return res;
957 }
958 break;
959 }
960 }
961 i += 1;
962 }
963 None
964 }
965 }
966
967 pub fn was_encrypted(&self) -> bool {
973 !self.signatures.is_empty()
974 }
975
976 pub(crate) fn has_chat_version(&self) -> bool {
979 self.headers.contains_key("chat-version")
980 }
981
982 pub(crate) fn get_subject(&self) -> Option<String> {
983 self.get_header(HeaderDef::Subject)
984 .map(|s| s.trim_start())
985 .filter(|s| !s.is_empty())
986 .map(|s| s.to_string())
987 }
988
989 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
990 self.headers
991 .get(headerdef.get_headername())
992 .map(|s| s.as_str())
993 }
994
995 #[cfg(test)]
996 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1001 let hname = headerdef.get_headername();
1002 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1003 }
1004
1005 pub fn get_chat_group_id(&self) -> Option<&str> {
1007 self.get_header(HeaderDef::ChatGroupId)
1008 .filter(|s| validate_id(s))
1009 }
1010
1011 async fn parse_mime_recursive<'a>(
1012 &'a mut self,
1013 context: &'a Context,
1014 mail: &'a mailparse::ParsedMail<'a>,
1015 is_related: bool,
1016 ) -> Result<bool> {
1017 enum MimeS {
1018 Multiple,
1019 Single,
1020 Message,
1021 }
1022
1023 let mimetype = mail.ctype.mimetype.to_lowercase();
1024
1025 let m = if mimetype.starts_with("multipart") {
1026 if mail.ctype.params.contains_key("boundary") {
1027 MimeS::Multiple
1028 } else {
1029 MimeS::Single
1030 }
1031 } else if mimetype.starts_with("message") {
1032 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1033 MimeS::Message
1034 } else {
1035 MimeS::Single
1036 }
1037 } else {
1038 MimeS::Single
1039 };
1040
1041 let is_related = is_related || mimetype == "multipart/related";
1042 match m {
1043 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1044 MimeS::Message => {
1045 let raw = mail.get_body_raw()?;
1046 if raw.is_empty() {
1047 return Ok(false);
1048 }
1049 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1050
1051 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1052 }
1053 MimeS::Single => {
1054 self.add_single_part_if_known(context, mail, is_related)
1055 .await
1056 }
1057 }
1058 }
1059
1060 async fn handle_multiple(
1061 &mut self,
1062 context: &Context,
1063 mail: &mailparse::ParsedMail<'_>,
1064 is_related: bool,
1065 ) -> Result<bool> {
1066 let mut any_part_added = false;
1067 let mimetype = get_mime_type(
1068 mail,
1069 &get_attachment_filename(context, mail)?,
1070 self.has_chat_version(),
1071 )?
1072 .0;
1073 match (mimetype.type_(), mimetype.subtype().as_str()) {
1074 (mime::MULTIPART, "alternative") => {
1079 for cur_data in &mail.subparts {
1080 let mime_type = get_mime_type(
1081 cur_data,
1082 &get_attachment_filename(context, cur_data)?,
1083 self.has_chat_version(),
1084 )?
1085 .0;
1086 if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
1087 any_part_added = self
1088 .parse_mime_recursive(context, cur_data, is_related)
1089 .await?;
1090 break;
1091 }
1092 }
1093 if !any_part_added {
1094 for cur_data in &mail.subparts {
1096 if get_mime_type(
1097 cur_data,
1098 &get_attachment_filename(context, cur_data)?,
1099 self.has_chat_version(),
1100 )?
1101 .0
1102 .type_()
1103 == mime::TEXT
1104 {
1105 any_part_added = self
1106 .parse_mime_recursive(context, cur_data, is_related)
1107 .await?;
1108 break;
1109 }
1110 }
1111 }
1112 if !any_part_added {
1113 for cur_part in &mail.subparts {
1115 if self
1116 .parse_mime_recursive(context, cur_part, is_related)
1117 .await?
1118 {
1119 any_part_added = true;
1120 break;
1121 }
1122 }
1123 }
1124 if any_part_added && mail.subparts.len() > 1 {
1125 self.is_mime_modified = true;
1129 }
1130 }
1131 (mime::MULTIPART, "signed") => {
1132 if let Some(first) = mail.subparts.first() {
1141 any_part_added = self
1142 .parse_mime_recursive(context, first, is_related)
1143 .await?;
1144 }
1145 }
1146 (mime::MULTIPART, "report") => {
1147 if mail.subparts.len() >= 2 {
1149 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1150 Some("disposition-notification") => {
1151 if let Some(report) = self.process_report(context, mail)? {
1152 self.mdn_reports.push(report);
1153 }
1154
1155 let part = Part {
1160 typ: Viewtype::Unknown,
1161 ..Default::default()
1162 };
1163 self.parts.push(part);
1164
1165 any_part_added = true;
1166 }
1167 Some("delivery-status") | None => {
1169 if let Some(report) = self.process_delivery_status(context, mail)? {
1170 self.delivery_report = Some(report);
1171 }
1172
1173 for cur_data in &mail.subparts {
1175 if self
1176 .parse_mime_recursive(context, cur_data, is_related)
1177 .await?
1178 {
1179 any_part_added = true;
1180 }
1181 }
1182 }
1183 Some("multi-device-sync") => {
1184 if let Some(second) = mail.subparts.get(1) {
1185 self.add_single_part_if_known(context, second, is_related)
1186 .await?;
1187 }
1188 }
1189 Some("status-update") => {
1190 if let Some(second) = mail.subparts.get(1) {
1191 self.add_single_part_if_known(context, second, is_related)
1192 .await?;
1193 }
1194 }
1195 Some(_) => {
1196 for cur_data in &mail.subparts {
1197 if self
1198 .parse_mime_recursive(context, cur_data, is_related)
1199 .await?
1200 {
1201 any_part_added = true;
1202 }
1203 }
1204 }
1205 }
1206 }
1207 }
1208 _ => {
1209 for cur_data in &mail.subparts {
1212 if self
1213 .parse_mime_recursive(context, cur_data, is_related)
1214 .await?
1215 {
1216 any_part_added = true;
1217 }
1218 }
1219 }
1220 }
1221
1222 Ok(any_part_added)
1223 }
1224
1225 async fn add_single_part_if_known(
1227 &mut self,
1228 context: &Context,
1229 mail: &mailparse::ParsedMail<'_>,
1230 is_related: bool,
1231 ) -> Result<bool> {
1232 let filename = get_attachment_filename(context, mail)?;
1234 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1235 let raw_mime = mail.ctype.mimetype.to_lowercase();
1236
1237 let old_part_count = self.parts.len();
1238
1239 match filename {
1240 Some(filename) => {
1241 self.do_add_single_file_part(
1242 context,
1243 msg_type,
1244 mime_type,
1245 &raw_mime,
1246 &mail.get_body_raw()?,
1247 &filename,
1248 is_related,
1249 )
1250 .await?;
1251 }
1252 None => {
1253 match mime_type.type_() {
1254 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1255 warn!(context, "Missing attachment");
1256 return Ok(false);
1257 }
1258 mime::TEXT
1259 if mail.get_content_disposition().disposition
1260 == DispositionType::Extension("reaction".to_string()) =>
1261 {
1262 let decoded_data = match mail.get_body() {
1264 Ok(decoded_data) => decoded_data,
1265 Err(err) => {
1266 warn!(context, "Invalid body parsed {:#}", err);
1267 return Ok(false);
1269 }
1270 };
1271
1272 let part = Part {
1273 typ: Viewtype::Text,
1274 mimetype: Some(mime_type),
1275 msg: decoded_data,
1276 is_reaction: true,
1277 ..Default::default()
1278 };
1279 self.do_add_single_part(part);
1280 return Ok(true);
1281 }
1282 mime::TEXT | mime::HTML => {
1283 let decoded_data = match mail.get_body() {
1284 Ok(decoded_data) => decoded_data,
1285 Err(err) => {
1286 warn!(context, "Invalid body parsed {:#}", err);
1287 return Ok(false);
1289 }
1290 };
1291
1292 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1293 let mut dehtml_failed = false;
1294
1295 let SimplifiedText {
1296 text: simplified_txt,
1297 is_forwarded,
1298 is_cut,
1299 top_quote,
1300 footer,
1301 } = if decoded_data.is_empty() {
1302 Default::default()
1303 } else {
1304 let is_html = mime_type == mime::TEXT_HTML;
1305 if is_html {
1306 self.is_mime_modified = true;
1307 if let Some(text) = dehtml(&decoded_data) {
1308 text
1309 } else {
1310 dehtml_failed = true;
1311 SimplifiedText {
1312 text: decoded_data.clone(),
1313 ..Default::default()
1314 }
1315 }
1316 } else {
1317 simplify(decoded_data.clone(), self.has_chat_version())
1318 }
1319 };
1320
1321 self.is_mime_modified = self.is_mime_modified
1322 || ((is_forwarded || is_cut || top_quote.is_some())
1323 && !self.has_chat_version());
1324
1325 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1326 {
1327 format.as_str().eq_ignore_ascii_case("flowed")
1328 } else {
1329 false
1330 };
1331
1332 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1333 && mime_type.subtype() == mime::PLAIN
1334 && is_format_flowed
1335 {
1336 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1337 delsp.as_str().eq_ignore_ascii_case("yes")
1338 } else {
1339 false
1340 };
1341 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1342 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1343 (unflowed_text, unflowed_quote)
1344 } else {
1345 (simplified_txt, top_quote)
1346 };
1347
1348 let (simplified_txt, was_truncated) =
1349 truncate_msg_text(context, simplified_txt).await?;
1350 if was_truncated {
1351 self.is_mime_modified = was_truncated;
1352 }
1353
1354 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1355 let mut part = Part {
1356 dehtml_failed,
1357 typ: Viewtype::Text,
1358 mimetype: Some(mime_type),
1359 msg: simplified_txt,
1360 ..Default::default()
1361 };
1362 if let Some(quote) = simplified_quote {
1363 part.param.set(Param::Quote, quote);
1364 }
1365 part.msg_raw = Some(decoded_data);
1366 self.do_add_single_part(part);
1367 }
1368
1369 if is_forwarded {
1370 self.is_forwarded = true;
1371 }
1372
1373 if self.footer.is_none() && is_plaintext {
1374 self.footer = Some(footer.unwrap_or_default());
1375 }
1376 }
1377 _ => {}
1378 }
1379 }
1380 }
1381
1382 Ok(self.parts.len() > old_part_count)
1384 }
1385
1386 #[expect(clippy::too_many_arguments)]
1387 async fn do_add_single_file_part(
1388 &mut self,
1389 context: &Context,
1390 msg_type: Viewtype,
1391 mime_type: Mime,
1392 raw_mime: &str,
1393 decoded_data: &[u8],
1394 filename: &str,
1395 is_related: bool,
1396 ) -> Result<()> {
1397 if mime_type.type_() == mime::APPLICATION
1399 && mime_type.subtype().as_str() == "pgp-keys"
1400 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1401 {
1402 return Ok(());
1403 }
1404 let mut part = Part::default();
1405 let msg_type = if context
1406 .is_webxdc_file(filename, decoded_data)
1407 .await
1408 .unwrap_or(false)
1409 {
1410 Viewtype::Webxdc
1411 } else if filename.ends_with(".kml") {
1412 if filename.starts_with("location") || filename.starts_with("message") {
1415 let parsed = location::Kml::parse(decoded_data)
1416 .map_err(|err| {
1417 warn!(context, "failed to parse kml part: {:#}", err);
1418 })
1419 .ok();
1420 if filename.starts_with("location") {
1421 self.location_kml = parsed;
1422 } else {
1423 self.message_kml = parsed;
1424 }
1425 return Ok(());
1426 }
1427 msg_type
1428 } else if filename == "multi-device-sync.json" {
1429 if !context.get_config_bool(Config::SyncMsgs).await? {
1430 return Ok(());
1431 }
1432 let serialized = String::from_utf8_lossy(decoded_data)
1433 .parse()
1434 .unwrap_or_default();
1435 self.sync_items = context
1436 .parse_sync_items(serialized)
1437 .map_err(|err| {
1438 warn!(context, "failed to parse sync data: {:#}", err);
1439 })
1440 .ok();
1441 return Ok(());
1442 } else if filename == "status-update.json" {
1443 let serialized = String::from_utf8_lossy(decoded_data)
1444 .parse()
1445 .unwrap_or_default();
1446 self.webxdc_status_update = Some(serialized);
1447 return Ok(());
1448 } else if msg_type == Viewtype::Vcard {
1449 if let Some(summary) = get_vcard_summary(decoded_data) {
1450 part.param.set(Param::Summary1, summary);
1451 msg_type
1452 } else {
1453 Viewtype::File
1454 }
1455 } else if msg_type == Viewtype::Image
1456 || msg_type == Viewtype::Gif
1457 || msg_type == Viewtype::Sticker
1458 {
1459 match get_filemeta(decoded_data) {
1460 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1462 part.param.set_i64(Param::Width, width.into());
1463 part.param.set_i64(Param::Height, height.into());
1464 msg_type
1465 }
1466 _ => Viewtype::File,
1468 }
1469 } else {
1470 msg_type
1471 };
1472
1473 let blob =
1477 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1478 Ok(blob) => blob,
1479 Err(err) => {
1480 error!(
1481 context,
1482 "Could not add blob for mime part {}, error {:#}", filename, err
1483 );
1484 return Ok(());
1485 }
1486 };
1487 info!(context, "added blobfile: {:?}", blob.as_name());
1488
1489 part.typ = msg_type;
1490 part.org_filename = Some(filename.to_string());
1491 part.mimetype = Some(mime_type);
1492 part.bytes = decoded_data.len();
1493 part.param.set(Param::File, blob.as_name());
1494 part.param.set(Param::Filename, filename);
1495 part.param.set(Param::MimeType, raw_mime);
1496 part.is_related = is_related;
1497
1498 self.do_add_single_part(part);
1499 Ok(())
1500 }
1501
1502 async fn try_set_peer_key_from_file_part(
1504 context: &Context,
1505 decoded_data: &[u8],
1506 ) -> Result<bool> {
1507 let key = match str::from_utf8(decoded_data) {
1508 Err(err) => {
1509 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1510 return Ok(false);
1511 }
1512 Ok(key) => key,
1513 };
1514 let key = match SignedPublicKey::from_asc(key) {
1515 Err(err) => {
1516 warn!(
1517 context,
1518 "PGP key attachment is not an ASCII-armored file: {err:#}."
1519 );
1520 return Ok(false);
1521 }
1522 Ok(key) => key,
1523 };
1524 if let Err(err) = key.verify() {
1525 warn!(context, "Attached PGP key verification failed: {err:#}.");
1526 return Ok(false);
1527 }
1528
1529 let fingerprint = key.dc_fingerprint().hex();
1530 context
1531 .sql
1532 .execute(
1533 "INSERT INTO public_keys (fingerprint, public_key)
1534 VALUES (?, ?)
1535 ON CONFLICT (fingerprint)
1536 DO NOTHING",
1537 (&fingerprint, key.to_bytes()),
1538 )
1539 .await?;
1540
1541 info!(context, "Imported PGP key {fingerprint} from attachment.");
1542 Ok(true)
1543 }
1544
1545 fn do_add_single_part(&mut self, mut part: Part) {
1546 if self.was_encrypted() {
1547 part.param.set_int(Param::GuaranteeE2ee, 1);
1548 }
1549 self.parts.push(part);
1550 }
1551
1552 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1553 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1554 return Some(list_id);
1557 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1558 if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
1561 if precedence == "list" || precedence == "bulk" {
1562 return Some(sender);
1566 }
1567 }
1568 }
1569 None
1570 }
1571
1572 pub(crate) fn is_mailinglist_message(&self) -> bool {
1573 self.get_mailinglist_header().is_some()
1574 }
1575
1576 pub(crate) fn is_schleuder_message(&self) -> bool {
1578 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1579 list_help == "<https://schleuder.org/>"
1580 } else {
1581 false
1582 }
1583 }
1584
1585 pub(crate) fn is_call(&self) -> bool {
1587 self.parts
1588 .first()
1589 .is_some_and(|part| part.typ == Viewtype::Call)
1590 }
1591
1592 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1593 self.get_header(HeaderDef::MessageId)
1594 .and_then(|msgid| parse_message_id(msgid).ok())
1595 }
1596
1597 fn remove_secured_headers(
1598 headers: &mut HashMap<String, String>,
1599 removed: &mut HashSet<String>,
1600 ) {
1601 remove_header(headers, "secure-join-fingerprint", removed);
1602 remove_header(headers, "secure-join-auth", removed);
1603 remove_header(headers, "chat-verified", removed);
1604 remove_header(headers, "autocrypt-gossip", removed);
1605
1606 if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
1608 if secure_join == "vc-request" || secure_join == "vg-request" {
1609 headers.insert("secure-join".to_string(), secure_join);
1610 }
1611 }
1612 }
1613
1614 #[allow(clippy::too_many_arguments)]
1615 fn merge_headers(
1616 context: &Context,
1617 headers: &mut HashMap<String, String>,
1618 headers_removed: &mut HashSet<String>,
1619 recipients: &mut Vec<SingleInfo>,
1620 past_members: &mut Vec<SingleInfo>,
1621 from: &mut Option<SingleInfo>,
1622 list_post: &mut Option<String>,
1623 chat_disposition_notification_to: &mut Option<SingleInfo>,
1624 fields: &[mailparse::MailHeader<'_>],
1625 ) {
1626 headers.retain(|k, _| {
1627 !is_protected(k) || {
1628 headers_removed.insert(k.to_string());
1629 false
1630 }
1631 });
1632 for field in fields {
1633 let key = field.get_key().to_lowercase();
1635 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1636 match addrparse_header(field) {
1637 Ok(addrlist) => {
1638 *chat_disposition_notification_to = addrlist.extract_single_info();
1639 }
1640 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1641 }
1642 } else {
1643 let value = field.get_value();
1644 headers.insert(key.to_string(), value);
1645 }
1646 }
1647 let recipients_new = get_recipients(fields);
1648 if !recipients_new.is_empty() {
1649 *recipients = recipients_new;
1650 }
1651 let past_members_addresses =
1652 get_all_addresses_from_header(fields, "chat-group-past-members");
1653 if !past_members_addresses.is_empty() {
1654 *past_members = past_members_addresses;
1655 }
1656 let from_new = get_from(fields);
1657 if from_new.is_some() {
1658 *from = from_new;
1659 }
1660 let list_post_new = get_list_post(fields);
1661 if list_post_new.is_some() {
1662 *list_post = list_post_new;
1663 }
1664 }
1665
1666 fn process_report(
1667 &self,
1668 context: &Context,
1669 report: &mailparse::ParsedMail<'_>,
1670 ) -> Result<Option<Report>> {
1671 let report_body = if let Some(subpart) = report.subparts.get(1) {
1673 subpart.get_body_raw()?
1674 } else {
1675 bail!("Report does not have second MIME part");
1676 };
1677 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1678
1679 if report_fields
1681 .get_header_value(HeaderDef::Disposition)
1682 .is_none()
1683 {
1684 warn!(
1685 context,
1686 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1687 report_fields.get_header_value(HeaderDef::MessageId)
1688 );
1689 return Ok(None);
1690 };
1691
1692 let original_message_id = report_fields
1693 .get_header_value(HeaderDef::OriginalMessageId)
1694 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1697 .and_then(|v| parse_message_id(&v).ok());
1698 let additional_message_ids = report_fields
1699 .get_header_value(HeaderDef::AdditionalMessageIds)
1700 .map_or_else(Vec::new, |v| {
1701 v.split(' ')
1702 .filter_map(|s| parse_message_id(s).ok())
1703 .collect()
1704 });
1705
1706 Ok(Some(Report {
1707 original_message_id,
1708 additional_message_ids,
1709 }))
1710 }
1711
1712 fn process_delivery_status(
1713 &self,
1714 context: &Context,
1715 report: &mailparse::ParsedMail<'_>,
1716 ) -> Result<Option<DeliveryReport>> {
1717 let mut failure = true;
1719
1720 if let Some(status_part) = report.subparts.get(1) {
1721 if status_part.ctype.mimetype != "message/delivery-status"
1724 && status_part.ctype.mimetype != "message/global-delivery-status"
1725 {
1726 warn!(
1727 context,
1728 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1729 );
1730 return Ok(None);
1731 }
1732
1733 let status_body = status_part.get_body_raw()?;
1734
1735 let (_, sz) = mailparse::parse_headers(&status_body)?;
1737
1738 if let Some(status_body) = status_body.get(sz..) {
1740 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1741 if let Some(action) = status_fields.get_first_value("action") {
1742 if action != "failed" {
1743 info!(context, "DSN with {:?} action", action);
1744 failure = false;
1745 }
1746 } else {
1747 warn!(context, "DSN without action");
1748 }
1749 } else {
1750 warn!(context, "DSN without per-recipient fields");
1751 }
1752 } else {
1753 return Ok(None);
1755 }
1756
1757 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1759 p.ctype.mimetype.contains("rfc822")
1760 || p.ctype.mimetype == "message/global"
1761 || p.ctype.mimetype == "message/global-headers"
1762 }) {
1763 let report_body = original_msg.get_body_raw()?;
1764 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1765
1766 if let Some(original_message_id) = report_fields
1767 .get_header_value(HeaderDef::MessageId)
1768 .and_then(|v| parse_message_id(&v).ok())
1769 {
1770 return Ok(Some(DeliveryReport {
1771 rfc724_mid: original_message_id,
1772 failure,
1773 }));
1774 }
1775
1776 warn!(
1777 context,
1778 "ignoring unknown ndn-notification, Message-Id: {:?}",
1779 report_fields.get_header_value(HeaderDef::MessageId)
1780 );
1781 }
1782
1783 Ok(None)
1784 }
1785
1786 fn maybe_remove_bad_parts(&mut self) {
1787 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1788 if good_parts == 0 {
1789 self.parts.truncate(1);
1791 } else if good_parts < self.parts.len() {
1792 self.parts.retain(|p| !p.dehtml_failed);
1793 }
1794
1795 if !self.has_chat_version() && self.is_mime_modified {
1803 fn is_related_image(p: &&Part) -> bool {
1804 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1805 }
1806 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1807 if related_image_cnt > 1 {
1808 let mut is_first_image = true;
1809 self.parts.retain(|p| {
1810 let retain = is_first_image || !is_related_image(&p);
1811 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1812 is_first_image = false;
1813 }
1814 retain
1815 });
1816 }
1817 }
1818 }
1819
1820 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1830 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1831 let text_part_cnt = self
1832 .parts
1833 .iter()
1834 .filter(|p| p.typ == Viewtype::Text)
1835 .count();
1836 if text_part_cnt == 2 {
1837 if let Some(last_part) = self.parts.last() {
1838 if last_part.typ == Viewtype::Text {
1839 self.parts.pop();
1840 }
1841 }
1842 }
1843 }
1844 }
1845
1846 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1850 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1851 let from = from.to_ascii_lowercase();
1852 from.contains("mailer-daemon") || from.contains("mail-daemon")
1853 } else {
1854 false
1855 };
1856 if maybe_ndn && self.delivery_report.is_none() {
1857 for original_message_id in self
1858 .parts
1859 .iter()
1860 .filter_map(|part| part.msg_raw.as_ref())
1861 .flat_map(|part| part.lines())
1862 .filter_map(|line| line.split_once("Message-ID:"))
1863 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1864 {
1865 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1866 {
1867 self.delivery_report = Some(DeliveryReport {
1868 rfc724_mid: original_message_id,
1869 failure: true,
1870 })
1871 }
1872 }
1873 }
1874 }
1875
1876 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1880 for report in &self.mdn_reports {
1881 for original_message_id in report
1882 .original_message_id
1883 .iter()
1884 .chain(&report.additional_message_ids)
1885 {
1886 if let Err(err) =
1887 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1888 {
1889 warn!(context, "Could not handle MDN: {err:#}.");
1890 }
1891 }
1892 }
1893
1894 if let Some(delivery_report) = &self.delivery_report {
1895 if delivery_report.failure {
1896 let error = parts
1897 .iter()
1898 .find(|p| p.typ == Viewtype::Text)
1899 .map(|p| p.msg.clone());
1900 if let Err(err) = handle_ndn(context, delivery_report, error).await {
1901 warn!(context, "Could not handle NDN: {err:#}.");
1902 }
1903 }
1904 }
1905 }
1906
1907 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
1912 let parent_timestamp = if let Some(field) = self
1913 .get_header(HeaderDef::InReplyTo)
1914 .and_then(|msgid| parse_message_id(msgid).ok())
1915 {
1916 context
1917 .sql
1918 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
1919 .await?
1920 } else {
1921 None
1922 };
1923 Ok(parent_timestamp)
1924 }
1925
1926 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
1930 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
1931 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
1932 .map(|h| {
1933 h.split_ascii_whitespace()
1934 .filter_map(|ts| ts.parse::<i64>().ok())
1935 .map(|ts| std::cmp::min(now, ts))
1936 .collect()
1937 })
1938 }
1939
1940 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
1943 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
1944 header
1945 .split_ascii_whitespace()
1946 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
1947 .collect()
1948 } else {
1949 Vec::new()
1950 }
1951 }
1952}
1953
1954fn remove_header(
1955 headers: &mut HashMap<String, String>,
1956 key: &str,
1957 removed: &mut HashSet<String>,
1958) -> Option<String> {
1959 if let Some((k, v)) = headers.remove_entry(key) {
1960 removed.insert(k);
1961 Some(v)
1962 } else {
1963 None
1964 }
1965}
1966
1967async fn parse_gossip_headers(
1973 context: &Context,
1974 from: &str,
1975 recipients: &[SingleInfo],
1976 gossip_headers: Vec<String>,
1977) -> Result<BTreeMap<String, GossipedKey>> {
1978 let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
1980
1981 for value in &gossip_headers {
1982 let header = match value.parse::<Aheader>() {
1983 Ok(header) => header,
1984 Err(err) => {
1985 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
1986 continue;
1987 }
1988 };
1989
1990 if !recipients
1991 .iter()
1992 .any(|info| addr_cmp(&info.addr, &header.addr))
1993 {
1994 warn!(
1995 context,
1996 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
1997 );
1998 continue;
1999 }
2000 if addr_cmp(from, &header.addr) {
2001 warn!(
2003 context,
2004 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
2005 );
2006 continue;
2007 }
2008
2009 let fingerprint = header.public_key.dc_fingerprint().hex();
2010 context
2011 .sql
2012 .execute(
2013 "INSERT INTO public_keys (fingerprint, public_key)
2014 VALUES (?, ?)
2015 ON CONFLICT (fingerprint)
2016 DO NOTHING",
2017 (&fingerprint, header.public_key.to_bytes()),
2018 )
2019 .await?;
2020
2021 let gossiped_key = GossipedKey {
2022 public_key: header.public_key,
2023
2024 verified: header.verified,
2025 };
2026 gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2027 }
2028
2029 Ok(gossiped_keys)
2030}
2031
2032#[derive(Debug)]
2034pub(crate) struct Report {
2035 original_message_id: Option<String>,
2040 additional_message_ids: Vec<String>,
2042}
2043
2044#[derive(Debug)]
2046pub(crate) struct DeliveryReport {
2047 pub rfc724_mid: String,
2048 pub failure: bool,
2049}
2050
2051pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2052 let mut msgids = Vec::new();
2054 for id in ids.split_whitespace() {
2055 let mut id = id.to_string();
2056 if let Some(id_without_prefix) = id.strip_prefix('<') {
2057 id = id_without_prefix.to_string();
2058 };
2059 if let Some(id_without_suffix) = id.strip_suffix('>') {
2060 id = id_without_suffix.to_string();
2061 };
2062 if !id.is_empty() {
2063 msgids.push(id);
2064 }
2065 }
2066 msgids
2067}
2068
2069pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2070 if let Some(id) = parse_message_ids(ids).first() {
2071 Ok(id.to_string())
2072 } else {
2073 bail!("could not parse message_id: {}", ids);
2074 }
2075}
2076
2077fn is_protected(key: &str) -> bool {
2083 key.starts_with("chat-")
2084 || matches!(
2085 key,
2086 "return-path"
2087 | "auto-submitted"
2088 | "autocrypt-setup-message"
2089 | "date"
2090 | "from"
2091 | "sender"
2092 | "reply-to"
2093 | "to"
2094 | "cc"
2095 | "bcc"
2096 | "message-id"
2097 | "in-reply-to"
2098 | "references"
2099 | "secure-join"
2100 )
2101}
2102
2103pub(crate) fn is_hidden(key: &str) -> bool {
2105 matches!(
2106 key,
2107 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2108 )
2109}
2110
2111#[derive(Debug, Default, Clone)]
2113pub struct Part {
2114 pub typ: Viewtype,
2116
2117 pub mimetype: Option<Mime>,
2119
2120 pub msg: String,
2122
2123 pub msg_raw: Option<String>,
2125
2126 pub bytes: usize,
2128
2129 pub param: Params,
2131
2132 pub(crate) org_filename: Option<String>,
2134
2135 pub error: Option<String>,
2137
2138 pub(crate) dehtml_failed: bool,
2140
2141 pub(crate) is_related: bool,
2148
2149 pub(crate) is_reaction: bool,
2151}
2152
2153fn get_mime_type(
2158 mail: &mailparse::ParsedMail<'_>,
2159 filename: &Option<String>,
2160 is_chat_msg: bool,
2161) -> Result<(Mime, Viewtype)> {
2162 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2163
2164 let viewtype = match mimetype.type_() {
2165 mime::TEXT => match mimetype.subtype() {
2166 mime::VCARD => Viewtype::Vcard,
2167 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2168 _ => Viewtype::File,
2169 },
2170 mime::IMAGE => match mimetype.subtype() {
2171 mime::GIF => Viewtype::Gif,
2172 mime::SVG => Viewtype::File,
2173 _ => Viewtype::Image,
2174 },
2175 mime::AUDIO => Viewtype::Audio,
2176 mime::VIDEO => Viewtype::Video,
2177 mime::MULTIPART => Viewtype::Unknown,
2178 mime::MESSAGE => {
2179 if is_attachment_disposition(mail) {
2180 Viewtype::File
2181 } else {
2182 Viewtype::Unknown
2190 }
2191 }
2192 mime::APPLICATION => match mimetype.subtype() {
2193 mime::OCTET_STREAM => match filename {
2194 Some(filename) if !is_chat_msg => {
2195 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2196 Some((viewtype, _)) => viewtype,
2197 None => Viewtype::File,
2198 }
2199 }
2200 _ => Viewtype::File,
2201 },
2202 _ => Viewtype::File,
2203 },
2204 _ => Viewtype::Unknown,
2205 };
2206
2207 Ok((mimetype, viewtype))
2208}
2209
2210fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2211 let ct = mail.get_content_disposition();
2212 ct.disposition == DispositionType::Attachment
2213 && ct
2214 .params
2215 .iter()
2216 .any(|(key, _value)| key.starts_with("filename"))
2217}
2218
2219fn get_attachment_filename(
2226 context: &Context,
2227 mail: &mailparse::ParsedMail,
2228) -> Result<Option<String>> {
2229 let ct = mail.get_content_disposition();
2230
2231 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2234
2235 if desired_filename.is_none() {
2236 if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
2237 warn!(context, "apostrophed encoding invalid: {}", name);
2241 desired_filename = Some(name);
2242 }
2243 }
2244
2245 if desired_filename.is_none() {
2247 desired_filename = ct.params.get("name").map(|s| s.to_string());
2248 }
2249
2250 if desired_filename.is_none() {
2253 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2254 }
2255
2256 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2258 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2259 desired_filename = Some(format!("file.{subtype}",));
2260 } else {
2261 bail!(
2262 "could not determine attachment filename: {:?}",
2263 ct.disposition
2264 );
2265 };
2266 }
2267
2268 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2269
2270 Ok(desired_filename)
2271}
2272
2273pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2275 let to_addresses = get_all_addresses_from_header(headers, "to");
2276 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2277
2278 let mut res = to_addresses;
2279 res.extend(cc_addresses);
2280 res
2281}
2282
2283pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2285 let all = get_all_addresses_from_header(headers, "from");
2286 tools::single_value(all)
2287}
2288
2289pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2291 get_all_addresses_from_header(headers, "list-post")
2292 .into_iter()
2293 .next()
2294 .map(|s| s.addr)
2295}
2296
2297fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2309 let mut result: Vec<SingleInfo> = Default::default();
2310
2311 if let Some(header) = headers
2312 .iter()
2313 .rev()
2314 .find(|h| h.get_key().to_lowercase() == header)
2315 {
2316 if let Ok(addrs) = mailparse::addrparse_header(header) {
2317 for addr in addrs.iter() {
2318 match addr {
2319 mailparse::MailAddr::Single(info) => {
2320 result.push(SingleInfo {
2321 addr: addr_normalize(&info.addr).to_lowercase(),
2322 display_name: info.display_name.clone(),
2323 });
2324 }
2325 mailparse::MailAddr::Group(infos) => {
2326 for info in &infos.addrs {
2327 result.push(SingleInfo {
2328 addr: addr_normalize(&info.addr).to_lowercase(),
2329 display_name: info.display_name.clone(),
2330 });
2331 }
2332 }
2333 }
2334 }
2335 }
2336 }
2337
2338 result
2339}
2340
2341async fn handle_mdn(
2342 context: &Context,
2343 from_id: ContactId,
2344 rfc724_mid: &str,
2345 timestamp_sent: i64,
2346) -> Result<()> {
2347 if from_id == ContactId::SELF {
2348 warn!(
2349 context,
2350 "Ignoring MDN sent to self, this is a bug on the sender device."
2351 );
2352
2353 return Ok(());
2356 }
2357
2358 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2359 .sql
2360 .query_row_optional(
2361 concat!(
2362 "SELECT",
2363 " m.id AS msg_id,",
2364 " c.id AS chat_id,",
2365 " mdns.contact_id AS mdn_contact",
2366 " FROM msgs m ",
2367 " LEFT JOIN chats c ON m.chat_id=c.id",
2368 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
2369 " WHERE rfc724_mid=? AND from_id=1",
2370 " ORDER BY msg_id DESC, mdn_contact=? DESC",
2371 " LIMIT 1",
2372 ),
2373 (&rfc724_mid, from_id),
2374 |row| {
2375 let msg_id: MsgId = row.get("msg_id")?;
2376 let chat_id: ChatId = row.get("chat_id")?;
2377 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2378 Ok((
2379 msg_id,
2380 chat_id,
2381 mdn_contact.is_some(),
2382 mdn_contact == Some(from_id),
2383 ))
2384 },
2385 )
2386 .await?
2387 else {
2388 info!(
2389 context,
2390 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2391 );
2392 return Ok(());
2393 };
2394
2395 if is_dup {
2396 return Ok(());
2397 }
2398 context
2399 .sql
2400 .execute(
2401 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2402 (msg_id, from_id, timestamp_sent),
2403 )
2404 .await?;
2405 if !has_mdns {
2406 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2407 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2409 }
2410 Ok(())
2411}
2412
2413async fn handle_ndn(
2416 context: &Context,
2417 failed: &DeliveryReport,
2418 error: Option<String>,
2419) -> Result<()> {
2420 if failed.rfc724_mid.is_empty() {
2421 return Ok(());
2422 }
2423
2424 let msgs: Vec<_> = context
2427 .sql
2428 .query_map(
2429 "SELECT id FROM msgs
2430 WHERE rfc724_mid=? AND from_id=1",
2431 (&failed.rfc724_mid,),
2432 |row| {
2433 let msg_id: MsgId = row.get(0)?;
2434 Ok(msg_id)
2435 },
2436 |rows| Ok(rows.collect::<Vec<_>>()),
2437 )
2438 .await?;
2439
2440 let error = if let Some(error) = error {
2441 error
2442 } else {
2443 "Delivery to at least one recipient failed.".to_string()
2444 };
2445 let err_msg = &error;
2446
2447 for msg in msgs {
2448 let msg_id = msg?;
2449 let mut message = Message::load_from_db(context, msg_id).await?;
2450 let aggregated_error = message
2451 .error
2452 .as_ref()
2453 .map(|err| format!("{err}\n\n{err_msg}"));
2454 set_msg_failed(
2455 context,
2456 &mut message,
2457 aggregated_error.as_ref().unwrap_or(err_msg),
2458 )
2459 .await?;
2460 }
2461
2462 Ok(())
2463}
2464
2465#[cfg(test)]
2466mod mimeparser_tests;