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 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 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
441 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)?
442 } else {
443 HashSet::new()
444 };
445
446 let mail = mail.as_ref().map(|mail| {
447 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
448 .unwrap_or((mail, Default::default()));
449 signatures.extend(signatures_detached);
450 content
451 });
452 if let (Ok(mail), true) = (mail, is_encrypted) {
453 if !signatures.is_empty() {
454 remove_header(&mut headers, "subject", &mut headers_removed);
458 remove_header(&mut headers, "list-id", &mut headers_removed);
459 }
460
461 let mut inner_from = None;
467
468 MimeMessage::merge_headers(
469 context,
470 &mut headers,
471 &mut headers_removed,
472 &mut recipients,
473 &mut past_members,
474 &mut inner_from,
475 &mut list_post,
476 &mut chat_disposition_notification_to,
477 &mail.headers,
478 );
479
480 if !signatures.is_empty() {
481 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
486 gossiped_keys =
487 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
488 }
489
490 if let Some(inner_from) = inner_from {
491 if !addr_cmp(&inner_from.addr, &from.addr) {
492 warn!(
501 context,
502 "From header in encrypted part doesn't match the outer one",
503 );
504
505 bail!("From header is forged");
510 }
511 from = inner_from;
512 }
513 }
514 if signatures.is_empty() {
515 Self::remove_secured_headers(&mut headers, &mut headers_removed);
516 }
517 if !is_encrypted {
518 signatures.clear();
519 }
520
521 let mut parser = MimeMessage {
522 parts: Vec::new(),
523 headers,
524 #[cfg(test)]
525 headers_removed,
526
527 recipients,
528 past_members,
529 list_post,
530 from,
531 incoming,
532 chat_disposition_notification_to,
533 decrypting_failed: mail.is_err(),
534
535 signatures,
537 autocrypt_fingerprint,
538 gossiped_keys,
539 is_forwarded: false,
540 mdn_reports: Vec::new(),
541 is_system_message: SystemMessage::Unknown,
542 location_kml: None,
543 message_kml: None,
544 sync_items: None,
545 webxdc_status_update: None,
546 user_avatar: None,
547 group_avatar: None,
548 delivery_report: None,
549 footer: None,
550 is_mime_modified: false,
551 decoded_data: Vec::new(),
552 hop_info,
553 is_bot: None,
554 timestamp_rcvd,
555 timestamp_sent,
556 };
557
558 match partial {
559 Some(org_bytes) => {
560 parser
561 .create_stub_from_partial_download(context, org_bytes)
562 .await?;
563 }
564 None => match mail {
565 Ok(mail) => {
566 parser.parse_mime_recursive(context, mail, false).await?;
567 }
568 Err(err) => {
569 let msg_body = stock_str::cant_decrypt_msg_body(context).await;
570 let txt = format!("[{msg_body}]");
571
572 let part = Part {
573 typ: Viewtype::Text,
574 msg_raw: Some(txt.clone()),
575 msg: txt,
576 error: Some(format!("Decrypting failed: {err:#}")),
579 ..Default::default()
580 };
581 parser.parts.push(part);
582 }
583 },
584 };
585
586 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
587 if parser.mdn_reports.is_empty()
588 && !is_location_only
589 && parser.sync_items.is_none()
590 && parser.webxdc_status_update.is_none()
591 {
592 let is_bot =
593 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
594 parser.is_bot = Some(is_bot);
595 }
596 parser.maybe_remove_bad_parts();
597 parser.maybe_remove_inline_mailinglist_footer();
598 parser.heuristically_parse_ndn(context).await;
599 parser.parse_headers(context).await?;
600 parser.decoded_data = mail_raw;
601
602 Ok(parser)
603 }
604
605 fn get_timestamp_sent(
606 hdrs: &[mailparse::MailHeader<'_>],
607 default: i64,
608 timestamp_rcvd: i64,
609 ) -> i64 {
610 hdrs.get_header_value(HeaderDef::Date)
611 .and_then(|v| mailparse::dateparse(&v).ok())
612 .map_or(default, |value| {
613 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
614 })
615 }
616
617 fn parse_system_message_headers(&mut self, context: &Context) {
619 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
620 self.parts.retain(|part| {
621 part.mimetype.is_none()
622 || part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
623 });
624
625 if self.parts.len() == 1 {
626 self.is_system_message = SystemMessage::AutocryptSetupMessage;
627 } else {
628 warn!(context, "could not determine ASM mime-part");
629 }
630 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
631 if value == "location-streaming-enabled" {
632 self.is_system_message = SystemMessage::LocationStreamingEnabled;
633 } else if value == "ephemeral-timer-changed" {
634 self.is_system_message = SystemMessage::EphemeralTimerChanged;
635 } else if value == "protection-enabled" {
636 self.is_system_message = SystemMessage::ChatProtectionEnabled;
637 } else if value == "protection-disabled" {
638 self.is_system_message = SystemMessage::ChatProtectionDisabled;
639 } else if value == "group-avatar-changed" {
640 self.is_system_message = SystemMessage::GroupImageChanged;
641 }
642 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
643 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
644 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
645 self.is_system_message = SystemMessage::MemberAddedToGroup;
646 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
647 self.is_system_message = SystemMessage::GroupNameChanged;
648 }
649 }
650
651 fn parse_avatar_headers(&mut self, context: &Context) {
653 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
654 self.group_avatar = self.avatar_action_from_header(context, header_value.to_string());
655 }
656
657 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
658 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string());
659 }
660 }
661
662 fn parse_videochat_headers(&mut self) {
663 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
664 if value == "videochat-invitation" {
665 let instance = self
666 .get_header(HeaderDef::ChatWebrtcRoom)
667 .map(|s| s.to_string());
668 if let Some(part) = self.parts.first_mut() {
669 part.typ = Viewtype::VideochatInvitation;
670 part.param
671 .set(Param::WebrtcRoom, instance.unwrap_or_default());
672 }
673 }
674 }
675 }
676
677 fn squash_attachment_parts(&mut self) {
683 if self.parts.len() == 2
684 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
685 && self
686 .parts
687 .get(1)
688 .is_some_and(|filepart| match filepart.typ {
689 Viewtype::Image
690 | Viewtype::Gif
691 | Viewtype::Sticker
692 | Viewtype::Audio
693 | Viewtype::Voice
694 | Viewtype::Video
695 | Viewtype::Vcard
696 | Viewtype::File
697 | Viewtype::Webxdc => true,
698 Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false,
699 })
700 {
701 let mut parts = std::mem::take(&mut self.parts);
702 let Some(mut filepart) = parts.pop() else {
703 return;
705 };
706 let Some(textpart) = parts.pop() else {
707 return;
709 };
710
711 filepart.msg.clone_from(&textpart.msg);
712 if let Some(quote) = textpart.param.get(Param::Quote) {
713 filepart.param.set(Param::Quote, quote);
714 }
715
716 self.parts = vec![filepart];
717 }
718 }
719
720 fn parse_attachments(&mut self) {
722 if self.parts.len() != 1 {
725 return;
726 }
727
728 if let Some(mut part) = self.parts.pop() {
729 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
730 {
731 part.typ = Viewtype::Voice;
732 }
733 if part.typ == Viewtype::Image || part.typ == Viewtype::Gif {
734 if let Some(value) = self.get_header(HeaderDef::ChatContent) {
735 if value == "sticker" {
736 part.typ = Viewtype::Sticker;
737 }
738 }
739 }
740 if part.typ == Viewtype::Audio
741 || part.typ == Viewtype::Voice
742 || part.typ == Viewtype::Video
743 {
744 if let Some(field_0) = self.get_header(HeaderDef::ChatDuration) {
745 let duration_ms = field_0.parse().unwrap_or_default();
746 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
747 part.param.set_int(Param::Duration, duration_ms);
748 }
749 }
750 }
751
752 self.parts.push(part);
753 }
754 }
755
756 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
757 self.parse_system_message_headers(context);
758 self.parse_avatar_headers(context);
759 self.parse_videochat_headers();
760 if self.delivery_report.is_none() {
761 self.squash_attachment_parts();
762 }
763
764 if !context.get_config_bool(Config::Bot).await? {
765 if let Some(ref subject) = self.get_subject() {
766 let mut prepend_subject = true;
767 if !self.decrypting_failed {
768 let colon = subject.find(':');
769 if colon == Some(2)
770 || colon == Some(3)
771 || self.has_chat_version()
772 || subject.contains("Chat:")
773 {
774 prepend_subject = false
775 }
776 }
777
778 if self.is_mailinglist_message() && !self.has_chat_version() {
781 prepend_subject = true;
782 }
783
784 if prepend_subject && !subject.is_empty() {
785 let part_with_text = self
786 .parts
787 .iter_mut()
788 .find(|part| !part.msg.is_empty() && !part.is_reaction);
789 if let Some(part) = part_with_text {
790 part.msg = format!("{} – {}", subject, part.msg);
791 }
792 }
793 }
794 }
795
796 if self.is_forwarded {
797 for part in &mut self.parts {
798 part.param.set_int(Param::Forwarded, 1);
799 }
800 }
801
802 self.parse_attachments();
803
804 if !self.decrypting_failed && !self.parts.is_empty() {
806 if let Some(ref dn_to) = self.chat_disposition_notification_to {
807 let from = &self.from.addr;
809 if !context.is_self_addr(from).await? {
810 if from.to_lowercase() == dn_to.addr.to_lowercase() {
811 if let Some(part) = self.parts.last_mut() {
812 part.param.set_int(Param::WantsMdn, 1);
813 }
814 } else {
815 warn!(
816 context,
817 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
818 );
819 }
820 }
821 }
822 }
823
824 if self.parts.is_empty() && self.mdn_reports.is_empty() {
829 let mut part = Part {
830 typ: Viewtype::Text,
831 ..Default::default()
832 };
833
834 if let Some(ref subject) = self.get_subject() {
835 if !self.has_chat_version() && self.webxdc_status_update.is_none() {
836 part.msg = subject.to_string();
837 }
838 }
839
840 self.do_add_single_part(part);
841 }
842
843 if self.is_bot == Some(true) {
844 for part in &mut self.parts {
845 part.param.set(Param::Bot, "1");
846 }
847 }
848
849 Ok(())
850 }
851
852 fn avatar_action_from_header(
853 &mut self,
854 context: &Context,
855 header_value: String,
856 ) -> Option<AvatarAction> {
857 if header_value == "0" {
858 Some(AvatarAction::Delete)
859 } else if let Some(base64) = header_value
860 .split_ascii_whitespace()
861 .collect::<String>()
862 .strip_prefix("base64:")
863 {
864 match BlobObject::store_from_base64(context, base64) {
865 Ok(path) => Some(AvatarAction::Change(path)),
866 Err(err) => {
867 warn!(
868 context,
869 "Could not decode and save avatar to blob file: {:#}", err,
870 );
871 None
872 }
873 }
874 } else {
875 let mut i = 0;
878 while let Some(part) = self.parts.get_mut(i) {
879 if let Some(part_filename) = &part.org_filename {
880 if part_filename == &header_value {
881 if let Some(blob) = part.param.get(Param::File) {
882 let res = Some(AvatarAction::Change(blob.to_string()));
883 self.parts.remove(i);
884 return res;
885 }
886 break;
887 }
888 }
889 i += 1;
890 }
891 None
892 }
893 }
894
895 pub fn was_encrypted(&self) -> bool {
901 !self.signatures.is_empty()
902 }
903
904 pub(crate) fn has_chat_version(&self) -> bool {
907 self.headers.contains_key("chat-version")
908 }
909
910 pub(crate) fn get_subject(&self) -> Option<String> {
911 self.get_header(HeaderDef::Subject)
912 .map(|s| s.trim_start())
913 .filter(|s| !s.is_empty())
914 .map(|s| s.to_string())
915 }
916
917 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
918 self.headers
919 .get(headerdef.get_headername())
920 .map(|s| s.as_str())
921 }
922
923 #[cfg(test)]
924 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
929 let hname = headerdef.get_headername();
930 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
931 }
932
933 pub fn get_chat_group_id(&self) -> Option<&str> {
935 self.get_header(HeaderDef::ChatGroupId)
936 .filter(|s| validate_id(s))
937 }
938
939 async fn parse_mime_recursive<'a>(
940 &'a mut self,
941 context: &'a Context,
942 mail: &'a mailparse::ParsedMail<'a>,
943 is_related: bool,
944 ) -> Result<bool> {
945 enum MimeS {
946 Multiple,
947 Single,
948 Message,
949 }
950
951 let mimetype = mail.ctype.mimetype.to_lowercase();
952
953 let m = if mimetype.starts_with("multipart") {
954 if mail.ctype.params.contains_key("boundary") {
955 MimeS::Multiple
956 } else {
957 MimeS::Single
958 }
959 } else if mimetype.starts_with("message") {
960 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
961 MimeS::Message
962 } else {
963 MimeS::Single
964 }
965 } else {
966 MimeS::Single
967 };
968
969 let is_related = is_related || mimetype == "multipart/related";
970 match m {
971 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
972 MimeS::Message => {
973 let raw = mail.get_body_raw()?;
974 if raw.is_empty() {
975 return Ok(false);
976 }
977 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
978
979 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
980 }
981 MimeS::Single => {
982 self.add_single_part_if_known(context, mail, is_related)
983 .await
984 }
985 }
986 }
987
988 async fn handle_multiple(
989 &mut self,
990 context: &Context,
991 mail: &mailparse::ParsedMail<'_>,
992 is_related: bool,
993 ) -> Result<bool> {
994 let mut any_part_added = false;
995 let mimetype = get_mime_type(
996 mail,
997 &get_attachment_filename(context, mail)?,
998 self.has_chat_version(),
999 )?
1000 .0;
1001 match (mimetype.type_(), mimetype.subtype().as_str()) {
1002 (mime::MULTIPART, "alternative") => {
1007 for cur_data in &mail.subparts {
1008 let mime_type = get_mime_type(
1009 cur_data,
1010 &get_attachment_filename(context, cur_data)?,
1011 self.has_chat_version(),
1012 )?
1013 .0;
1014 if mime_type == "multipart/mixed" || mime_type == "multipart/related" {
1015 any_part_added = self
1016 .parse_mime_recursive(context, cur_data, is_related)
1017 .await?;
1018 break;
1019 }
1020 }
1021 if !any_part_added {
1022 for cur_data in &mail.subparts {
1024 if get_mime_type(
1025 cur_data,
1026 &get_attachment_filename(context, cur_data)?,
1027 self.has_chat_version(),
1028 )?
1029 .0
1030 .type_()
1031 == mime::TEXT
1032 {
1033 any_part_added = self
1034 .parse_mime_recursive(context, cur_data, is_related)
1035 .await?;
1036 break;
1037 }
1038 }
1039 }
1040 if !any_part_added {
1041 for cur_part in &mail.subparts {
1043 if self
1044 .parse_mime_recursive(context, cur_part, is_related)
1045 .await?
1046 {
1047 any_part_added = true;
1048 break;
1049 }
1050 }
1051 }
1052 if any_part_added && mail.subparts.len() > 1 {
1053 self.is_mime_modified = true;
1057 }
1058 }
1059 (mime::MULTIPART, "signed") => {
1060 if let Some(first) = mail.subparts.first() {
1069 any_part_added = self
1070 .parse_mime_recursive(context, first, is_related)
1071 .await?;
1072 }
1073 }
1074 (mime::MULTIPART, "report") => {
1075 if mail.subparts.len() >= 2 {
1077 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1078 Some("disposition-notification") => {
1079 if let Some(report) = self.process_report(context, mail)? {
1080 self.mdn_reports.push(report);
1081 }
1082
1083 let part = Part {
1088 typ: Viewtype::Unknown,
1089 ..Default::default()
1090 };
1091 self.parts.push(part);
1092
1093 any_part_added = true;
1094 }
1095 Some("delivery-status") | None => {
1097 if let Some(report) = self.process_delivery_status(context, mail)? {
1098 self.delivery_report = Some(report);
1099 }
1100
1101 for cur_data in &mail.subparts {
1103 if self
1104 .parse_mime_recursive(context, cur_data, is_related)
1105 .await?
1106 {
1107 any_part_added = true;
1108 }
1109 }
1110 }
1111 Some("multi-device-sync") => {
1112 if let Some(second) = mail.subparts.get(1) {
1113 self.add_single_part_if_known(context, second, is_related)
1114 .await?;
1115 }
1116 }
1117 Some("status-update") => {
1118 if let Some(second) = mail.subparts.get(1) {
1119 self.add_single_part_if_known(context, second, is_related)
1120 .await?;
1121 }
1122 }
1123 Some(_) => {
1124 for cur_data in &mail.subparts {
1125 if self
1126 .parse_mime_recursive(context, cur_data, is_related)
1127 .await?
1128 {
1129 any_part_added = true;
1130 }
1131 }
1132 }
1133 }
1134 }
1135 }
1136 _ => {
1137 for cur_data in &mail.subparts {
1140 if self
1141 .parse_mime_recursive(context, cur_data, is_related)
1142 .await?
1143 {
1144 any_part_added = true;
1145 }
1146 }
1147 }
1148 }
1149
1150 Ok(any_part_added)
1151 }
1152
1153 async fn add_single_part_if_known(
1155 &mut self,
1156 context: &Context,
1157 mail: &mailparse::ParsedMail<'_>,
1158 is_related: bool,
1159 ) -> Result<bool> {
1160 let filename = get_attachment_filename(context, mail)?;
1162 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1163 let raw_mime = mail.ctype.mimetype.to_lowercase();
1164
1165 let old_part_count = self.parts.len();
1166
1167 match filename {
1168 Some(filename) => {
1169 self.do_add_single_file_part(
1170 context,
1171 msg_type,
1172 mime_type,
1173 &raw_mime,
1174 &mail.get_body_raw()?,
1175 &filename,
1176 is_related,
1177 )
1178 .await?;
1179 }
1180 None => {
1181 match mime_type.type_() {
1182 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1183 warn!(context, "Missing attachment");
1184 return Ok(false);
1185 }
1186 mime::TEXT
1187 if mail.get_content_disposition().disposition
1188 == DispositionType::Extension("reaction".to_string()) =>
1189 {
1190 let decoded_data = match mail.get_body() {
1192 Ok(decoded_data) => decoded_data,
1193 Err(err) => {
1194 warn!(context, "Invalid body parsed {:#}", err);
1195 return Ok(false);
1197 }
1198 };
1199
1200 let part = Part {
1201 typ: Viewtype::Text,
1202 mimetype: Some(mime_type),
1203 msg: decoded_data,
1204 is_reaction: true,
1205 ..Default::default()
1206 };
1207 self.do_add_single_part(part);
1208 return Ok(true);
1209 }
1210 mime::TEXT | mime::HTML => {
1211 let decoded_data = match mail.get_body() {
1212 Ok(decoded_data) => decoded_data,
1213 Err(err) => {
1214 warn!(context, "Invalid body parsed {:#}", err);
1215 return Ok(false);
1217 }
1218 };
1219
1220 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1221 let mut dehtml_failed = false;
1222
1223 let SimplifiedText {
1224 text: simplified_txt,
1225 is_forwarded,
1226 is_cut,
1227 top_quote,
1228 footer,
1229 } = if decoded_data.is_empty() {
1230 Default::default()
1231 } else {
1232 let is_html = mime_type == mime::TEXT_HTML;
1233 if is_html {
1234 self.is_mime_modified = true;
1235 if let Some(text) = dehtml(&decoded_data) {
1236 text
1237 } else {
1238 dehtml_failed = true;
1239 SimplifiedText {
1240 text: decoded_data.clone(),
1241 ..Default::default()
1242 }
1243 }
1244 } else {
1245 simplify(decoded_data.clone(), self.has_chat_version())
1246 }
1247 };
1248
1249 self.is_mime_modified = self.is_mime_modified
1250 || ((is_forwarded || is_cut || top_quote.is_some())
1251 && !self.has_chat_version());
1252
1253 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1254 {
1255 format.as_str().eq_ignore_ascii_case("flowed")
1256 } else {
1257 false
1258 };
1259
1260 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1261 && mime_type.subtype() == mime::PLAIN
1262 && is_format_flowed
1263 {
1264 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1265 delsp.as_str().eq_ignore_ascii_case("yes")
1266 } else {
1267 false
1268 };
1269 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1270 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1271 (unflowed_text, unflowed_quote)
1272 } else {
1273 (simplified_txt, top_quote)
1274 };
1275
1276 let (simplified_txt, was_truncated) =
1277 truncate_msg_text(context, simplified_txt).await?;
1278 if was_truncated {
1279 self.is_mime_modified = was_truncated;
1280 }
1281
1282 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1283 let mut part = Part {
1284 dehtml_failed,
1285 typ: Viewtype::Text,
1286 mimetype: Some(mime_type),
1287 msg: simplified_txt,
1288 ..Default::default()
1289 };
1290 if let Some(quote) = simplified_quote {
1291 part.param.set(Param::Quote, quote);
1292 }
1293 part.msg_raw = Some(decoded_data);
1294 self.do_add_single_part(part);
1295 }
1296
1297 if is_forwarded {
1298 self.is_forwarded = true;
1299 }
1300
1301 if self.footer.is_none() && is_plaintext {
1302 self.footer = Some(footer.unwrap_or_default());
1303 }
1304 }
1305 _ => {}
1306 }
1307 }
1308 }
1309
1310 Ok(self.parts.len() > old_part_count)
1312 }
1313
1314 #[expect(clippy::too_many_arguments)]
1315 async fn do_add_single_file_part(
1316 &mut self,
1317 context: &Context,
1318 msg_type: Viewtype,
1319 mime_type: Mime,
1320 raw_mime: &str,
1321 decoded_data: &[u8],
1322 filename: &str,
1323 is_related: bool,
1324 ) -> Result<()> {
1325 if decoded_data.is_empty() {
1326 return Ok(());
1327 }
1328
1329 if mime_type.type_() == mime::APPLICATION
1331 && mime_type.subtype().as_str() == "pgp-keys"
1332 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1333 {
1334 return Ok(());
1335 }
1336 let mut part = Part::default();
1337 let msg_type = if context
1338 .is_webxdc_file(filename, decoded_data)
1339 .await
1340 .unwrap_or(false)
1341 {
1342 Viewtype::Webxdc
1343 } else if filename.ends_with(".kml") {
1344 if filename.starts_with("location") || filename.starts_with("message") {
1347 let parsed = location::Kml::parse(decoded_data)
1348 .map_err(|err| {
1349 warn!(context, "failed to parse kml part: {:#}", err);
1350 })
1351 .ok();
1352 if filename.starts_with("location") {
1353 self.location_kml = parsed;
1354 } else {
1355 self.message_kml = parsed;
1356 }
1357 return Ok(());
1358 }
1359 msg_type
1360 } else if filename == "multi-device-sync.json" {
1361 if !context.get_config_bool(Config::SyncMsgs).await? {
1362 return Ok(());
1363 }
1364 let serialized = String::from_utf8_lossy(decoded_data)
1365 .parse()
1366 .unwrap_or_default();
1367 self.sync_items = context
1368 .parse_sync_items(serialized)
1369 .map_err(|err| {
1370 warn!(context, "failed to parse sync data: {:#}", err);
1371 })
1372 .ok();
1373 return Ok(());
1374 } else if filename == "status-update.json" {
1375 let serialized = String::from_utf8_lossy(decoded_data)
1376 .parse()
1377 .unwrap_or_default();
1378 self.webxdc_status_update = Some(serialized);
1379 return Ok(());
1380 } else if msg_type == Viewtype::Vcard {
1381 if let Some(summary) = get_vcard_summary(decoded_data) {
1382 part.param.set(Param::Summary1, summary);
1383 msg_type
1384 } else {
1385 Viewtype::File
1386 }
1387 } else if msg_type == Viewtype::Image
1388 || msg_type == Viewtype::Gif
1389 || msg_type == Viewtype::Sticker
1390 {
1391 match get_filemeta(decoded_data) {
1392 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1394 part.param.set_i64(Param::Width, width.into());
1395 part.param.set_i64(Param::Height, height.into());
1396 msg_type
1397 }
1398 _ => Viewtype::File,
1400 }
1401 } else {
1402 msg_type
1403 };
1404
1405 let blob =
1409 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1410 Ok(blob) => blob,
1411 Err(err) => {
1412 error!(
1413 context,
1414 "Could not add blob for mime part {}, error {:#}", filename, err
1415 );
1416 return Ok(());
1417 }
1418 };
1419 info!(context, "added blobfile: {:?}", blob.as_name());
1420
1421 part.typ = msg_type;
1422 part.org_filename = Some(filename.to_string());
1423 part.mimetype = Some(mime_type);
1424 part.bytes = decoded_data.len();
1425 part.param.set(Param::File, blob.as_name());
1426 part.param.set(Param::Filename, filename);
1427 part.param.set(Param::MimeType, raw_mime);
1428 part.is_related = is_related;
1429
1430 self.do_add_single_part(part);
1431 Ok(())
1432 }
1433
1434 async fn try_set_peer_key_from_file_part(
1436 context: &Context,
1437 decoded_data: &[u8],
1438 ) -> Result<bool> {
1439 let key = match str::from_utf8(decoded_data) {
1440 Err(err) => {
1441 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1442 return Ok(false);
1443 }
1444 Ok(key) => key,
1445 };
1446 let key = match SignedPublicKey::from_asc(key) {
1447 Err(err) => {
1448 warn!(
1449 context,
1450 "PGP key attachment is not an ASCII-armored file: {err:#}."
1451 );
1452 return Ok(false);
1453 }
1454 Ok((key, _)) => key,
1455 };
1456 if let Err(err) = key.verify() {
1457 warn!(context, "Attached PGP key verification failed: {err:#}.");
1458 return Ok(false);
1459 }
1460
1461 let fingerprint = key.dc_fingerprint().hex();
1462 context
1463 .sql
1464 .execute(
1465 "INSERT INTO public_keys (fingerprint, public_key)
1466 VALUES (?, ?)
1467 ON CONFLICT (fingerprint)
1468 DO NOTHING",
1469 (&fingerprint, key.to_bytes()),
1470 )
1471 .await?;
1472
1473 info!(context, "Imported PGP key {fingerprint} from attachment.");
1474 Ok(true)
1475 }
1476
1477 fn do_add_single_part(&mut self, mut part: Part) {
1478 if self.was_encrypted() {
1479 part.param.set_int(Param::GuaranteeE2ee, 1);
1480 }
1481 self.parts.push(part);
1482 }
1483
1484 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1485 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1486 return Some(list_id);
1489 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1490 if let Some(precedence) = self.get_header(HeaderDef::Precedence) {
1493 if precedence == "list" || precedence == "bulk" {
1494 return Some(sender);
1498 }
1499 }
1500 }
1501 None
1502 }
1503
1504 pub(crate) fn is_mailinglist_message(&self) -> bool {
1505 self.get_mailinglist_header().is_some()
1506 }
1507
1508 pub(crate) fn is_schleuder_message(&self) -> bool {
1510 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1511 list_help == "<https://schleuder.org/>"
1512 } else {
1513 false
1514 }
1515 }
1516
1517 pub fn replace_msg_by_error(&mut self, error_msg: &str) {
1518 self.is_system_message = SystemMessage::Unknown;
1519 if let Some(part) = self.parts.first_mut() {
1520 part.typ = Viewtype::Text;
1521 part.msg = format!("[{error_msg}]");
1522 self.parts.truncate(1);
1523 }
1524 }
1525
1526 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1527 self.get_header(HeaderDef::MessageId)
1528 .and_then(|msgid| parse_message_id(msgid).ok())
1529 }
1530
1531 fn remove_secured_headers(
1532 headers: &mut HashMap<String, String>,
1533 removed: &mut HashSet<String>,
1534 ) {
1535 remove_header(headers, "secure-join-fingerprint", removed);
1536 remove_header(headers, "secure-join-auth", removed);
1537 remove_header(headers, "chat-verified", removed);
1538 remove_header(headers, "autocrypt-gossip", removed);
1539
1540 if let Some(secure_join) = remove_header(headers, "secure-join", removed) {
1542 if secure_join == "vc-request" || secure_join == "vg-request" {
1543 headers.insert("secure-join".to_string(), secure_join);
1544 }
1545 }
1546 }
1547
1548 #[allow(clippy::too_many_arguments)]
1549 fn merge_headers(
1550 context: &Context,
1551 headers: &mut HashMap<String, String>,
1552 headers_removed: &mut HashSet<String>,
1553 recipients: &mut Vec<SingleInfo>,
1554 past_members: &mut Vec<SingleInfo>,
1555 from: &mut Option<SingleInfo>,
1556 list_post: &mut Option<String>,
1557 chat_disposition_notification_to: &mut Option<SingleInfo>,
1558 fields: &[mailparse::MailHeader<'_>],
1559 ) {
1560 headers.retain(|k, _| {
1561 !is_protected(k) || {
1562 headers_removed.insert(k.to_string());
1563 false
1564 }
1565 });
1566 for field in fields {
1567 let key = field.get_key().to_lowercase();
1569 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1570 match addrparse_header(field) {
1571 Ok(addrlist) => {
1572 *chat_disposition_notification_to = addrlist.extract_single_info();
1573 }
1574 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1575 }
1576 } else {
1577 let value = field.get_value();
1578 headers.insert(key.to_string(), value);
1579 }
1580 }
1581 let recipients_new = get_recipients(fields);
1582 if !recipients_new.is_empty() {
1583 *recipients = recipients_new;
1584 }
1585 let past_members_addresses =
1586 get_all_addresses_from_header(fields, "chat-group-past-members");
1587 if !past_members_addresses.is_empty() {
1588 *past_members = past_members_addresses;
1589 }
1590 let from_new = get_from(fields);
1591 if from_new.is_some() {
1592 *from = from_new;
1593 }
1594 let list_post_new = get_list_post(fields);
1595 if list_post_new.is_some() {
1596 *list_post = list_post_new;
1597 }
1598 }
1599
1600 fn process_report(
1601 &self,
1602 context: &Context,
1603 report: &mailparse::ParsedMail<'_>,
1604 ) -> Result<Option<Report>> {
1605 let report_body = if let Some(subpart) = report.subparts.get(1) {
1607 subpart.get_body_raw()?
1608 } else {
1609 bail!("Report does not have second MIME part");
1610 };
1611 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1612
1613 if report_fields
1615 .get_header_value(HeaderDef::Disposition)
1616 .is_none()
1617 {
1618 warn!(
1619 context,
1620 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1621 report_fields.get_header_value(HeaderDef::MessageId)
1622 );
1623 return Ok(None);
1624 };
1625
1626 let original_message_id = report_fields
1627 .get_header_value(HeaderDef::OriginalMessageId)
1628 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1631 .and_then(|v| parse_message_id(&v).ok());
1632 let additional_message_ids = report_fields
1633 .get_header_value(HeaderDef::AdditionalMessageIds)
1634 .map_or_else(Vec::new, |v| {
1635 v.split(' ')
1636 .filter_map(|s| parse_message_id(s).ok())
1637 .collect()
1638 });
1639
1640 Ok(Some(Report {
1641 original_message_id,
1642 additional_message_ids,
1643 }))
1644 }
1645
1646 fn process_delivery_status(
1647 &self,
1648 context: &Context,
1649 report: &mailparse::ParsedMail<'_>,
1650 ) -> Result<Option<DeliveryReport>> {
1651 let mut failure = true;
1653
1654 if let Some(status_part) = report.subparts.get(1) {
1655 if status_part.ctype.mimetype != "message/delivery-status"
1658 && status_part.ctype.mimetype != "message/global-delivery-status"
1659 {
1660 warn!(
1661 context,
1662 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1663 );
1664 return Ok(None);
1665 }
1666
1667 let status_body = status_part.get_body_raw()?;
1668
1669 let (_, sz) = mailparse::parse_headers(&status_body)?;
1671
1672 if let Some(status_body) = status_body.get(sz..) {
1674 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1675 if let Some(action) = status_fields.get_first_value("action") {
1676 if action != "failed" {
1677 info!(context, "DSN with {:?} action", action);
1678 failure = false;
1679 }
1680 } else {
1681 warn!(context, "DSN without action");
1682 }
1683 } else {
1684 warn!(context, "DSN without per-recipient fields");
1685 }
1686 } else {
1687 return Ok(None);
1689 }
1690
1691 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1693 p.ctype.mimetype.contains("rfc822")
1694 || p.ctype.mimetype == "message/global"
1695 || p.ctype.mimetype == "message/global-headers"
1696 }) {
1697 let report_body = original_msg.get_body_raw()?;
1698 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1699
1700 if let Some(original_message_id) = report_fields
1701 .get_header_value(HeaderDef::MessageId)
1702 .and_then(|v| parse_message_id(&v).ok())
1703 {
1704 return Ok(Some(DeliveryReport {
1705 rfc724_mid: original_message_id,
1706 failure,
1707 }));
1708 }
1709
1710 warn!(
1711 context,
1712 "ignoring unknown ndn-notification, Message-Id: {:?}",
1713 report_fields.get_header_value(HeaderDef::MessageId)
1714 );
1715 }
1716
1717 Ok(None)
1718 }
1719
1720 fn maybe_remove_bad_parts(&mut self) {
1721 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1722 if good_parts == 0 {
1723 self.parts.truncate(1);
1725 } else if good_parts < self.parts.len() {
1726 self.parts.retain(|p| !p.dehtml_failed);
1727 }
1728
1729 if !self.has_chat_version() && self.is_mime_modified {
1737 fn is_related_image(p: &&Part) -> bool {
1738 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1739 }
1740 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1741 if related_image_cnt > 1 {
1742 let mut is_first_image = true;
1743 self.parts.retain(|p| {
1744 let retain = is_first_image || !is_related_image(&p);
1745 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1746 is_first_image = false;
1747 }
1748 retain
1749 });
1750 }
1751 }
1752 }
1753
1754 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1764 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1765 let text_part_cnt = self
1766 .parts
1767 .iter()
1768 .filter(|p| p.typ == Viewtype::Text)
1769 .count();
1770 if text_part_cnt == 2 {
1771 if let Some(last_part) = self.parts.last() {
1772 if last_part.typ == Viewtype::Text {
1773 self.parts.pop();
1774 }
1775 }
1776 }
1777 }
1778 }
1779
1780 async fn heuristically_parse_ndn(&mut self, context: &Context) {
1784 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
1785 let from = from.to_ascii_lowercase();
1786 from.contains("mailer-daemon") || from.contains("mail-daemon")
1787 } else {
1788 false
1789 };
1790 if maybe_ndn && self.delivery_report.is_none() {
1791 for original_message_id in self
1792 .parts
1793 .iter()
1794 .filter_map(|part| part.msg_raw.as_ref())
1795 .flat_map(|part| part.lines())
1796 .filter_map(|line| line.split_once("Message-ID:"))
1797 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
1798 {
1799 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
1800 {
1801 self.delivery_report = Some(DeliveryReport {
1802 rfc724_mid: original_message_id,
1803 failure: true,
1804 })
1805 }
1806 }
1807 }
1808 }
1809
1810 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
1814 for report in &self.mdn_reports {
1815 for original_message_id in report
1816 .original_message_id
1817 .iter()
1818 .chain(&report.additional_message_ids)
1819 {
1820 if let Err(err) =
1821 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
1822 {
1823 warn!(context, "Could not handle MDN: {err:#}.");
1824 }
1825 }
1826 }
1827
1828 if let Some(delivery_report) = &self.delivery_report {
1829 if delivery_report.failure {
1830 let error = parts
1831 .iter()
1832 .find(|p| p.typ == Viewtype::Text)
1833 .map(|p| p.msg.clone());
1834 if let Err(err) = handle_ndn(context, delivery_report, error).await {
1835 warn!(context, "Could not handle NDN: {err:#}.");
1836 }
1837 }
1838 }
1839 }
1840
1841 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
1846 let parent_timestamp = if let Some(field) = self
1847 .get_header(HeaderDef::InReplyTo)
1848 .and_then(|msgid| parse_message_id(msgid).ok())
1849 {
1850 context
1851 .sql
1852 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
1853 .await?
1854 } else {
1855 None
1856 };
1857 Ok(parent_timestamp)
1858 }
1859
1860 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
1864 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
1865 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
1866 .map(|h| {
1867 h.split_ascii_whitespace()
1868 .filter_map(|ts| ts.parse::<i64>().ok())
1869 .map(|ts| std::cmp::min(now, ts))
1870 .collect()
1871 })
1872 }
1873
1874 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
1877 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
1878 header
1879 .split_ascii_whitespace()
1880 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
1881 .collect()
1882 } else {
1883 Vec::new()
1884 }
1885 }
1886}
1887
1888fn remove_header(
1889 headers: &mut HashMap<String, String>,
1890 key: &str,
1891 removed: &mut HashSet<String>,
1892) -> Option<String> {
1893 if let Some((k, v)) = headers.remove_entry(key) {
1894 removed.insert(k);
1895 Some(v)
1896 } else {
1897 None
1898 }
1899}
1900
1901async fn parse_gossip_headers(
1907 context: &Context,
1908 from: &str,
1909 recipients: &[SingleInfo],
1910 gossip_headers: Vec<String>,
1911) -> Result<HashMap<String, SignedPublicKey>> {
1912 let mut gossiped_keys: HashMap<String, SignedPublicKey> = Default::default();
1914
1915 for value in &gossip_headers {
1916 let header = match value.parse::<Aheader>() {
1917 Ok(header) => header,
1918 Err(err) => {
1919 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
1920 continue;
1921 }
1922 };
1923
1924 if !recipients
1925 .iter()
1926 .any(|info| addr_cmp(&info.addr, &header.addr))
1927 {
1928 warn!(
1929 context,
1930 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
1931 );
1932 continue;
1933 }
1934 if addr_cmp(from, &header.addr) {
1935 warn!(
1937 context,
1938 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
1939 );
1940 continue;
1941 }
1942
1943 let fingerprint = header.public_key.dc_fingerprint().hex();
1944 context
1945 .sql
1946 .execute(
1947 "INSERT INTO public_keys (fingerprint, public_key)
1948 VALUES (?, ?)
1949 ON CONFLICT (fingerprint)
1950 DO NOTHING",
1951 (&fingerprint, header.public_key.to_bytes()),
1952 )
1953 .await?;
1954
1955 gossiped_keys.insert(header.addr.to_lowercase(), header.public_key);
1956 }
1957
1958 Ok(gossiped_keys)
1959}
1960
1961#[derive(Debug)]
1963pub(crate) struct Report {
1964 original_message_id: Option<String>,
1969 additional_message_ids: Vec<String>,
1971}
1972
1973#[derive(Debug)]
1975pub(crate) struct DeliveryReport {
1976 pub rfc724_mid: String,
1977 pub failure: bool,
1978}
1979
1980pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
1981 let mut msgids = Vec::new();
1983 for id in ids.split_whitespace() {
1984 let mut id = id.to_string();
1985 if let Some(id_without_prefix) = id.strip_prefix('<') {
1986 id = id_without_prefix.to_string();
1987 };
1988 if let Some(id_without_suffix) = id.strip_suffix('>') {
1989 id = id_without_suffix.to_string();
1990 };
1991 if !id.is_empty() {
1992 msgids.push(id);
1993 }
1994 }
1995 msgids
1996}
1997
1998pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
1999 if let Some(id) = parse_message_ids(ids).first() {
2000 Ok(id.to_string())
2001 } else {
2002 bail!("could not parse message_id: {}", ids);
2003 }
2004}
2005
2006fn is_protected(key: &str) -> bool {
2012 key.starts_with("chat-")
2013 || matches!(
2014 key,
2015 "return-path"
2016 | "auto-submitted"
2017 | "autocrypt-setup-message"
2018 | "date"
2019 | "from"
2020 | "sender"
2021 | "reply-to"
2022 | "to"
2023 | "cc"
2024 | "bcc"
2025 | "message-id"
2026 | "in-reply-to"
2027 | "references"
2028 | "secure-join"
2029 )
2030}
2031
2032pub(crate) fn is_hidden(key: &str) -> bool {
2034 matches!(
2035 key,
2036 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2037 )
2038}
2039
2040#[derive(Debug, Default, Clone)]
2042pub struct Part {
2043 pub typ: Viewtype,
2045
2046 pub mimetype: Option<Mime>,
2048
2049 pub msg: String,
2051
2052 pub msg_raw: Option<String>,
2054
2055 pub bytes: usize,
2057
2058 pub param: Params,
2060
2061 pub(crate) org_filename: Option<String>,
2063
2064 pub error: Option<String>,
2066
2067 pub(crate) dehtml_failed: bool,
2069
2070 pub(crate) is_related: bool,
2077
2078 pub(crate) is_reaction: bool,
2080}
2081
2082fn get_mime_type(
2087 mail: &mailparse::ParsedMail<'_>,
2088 filename: &Option<String>,
2089 is_chat_msg: bool,
2090) -> Result<(Mime, Viewtype)> {
2091 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2092
2093 let viewtype = match mimetype.type_() {
2094 mime::TEXT => match mimetype.subtype() {
2095 mime::VCARD => Viewtype::Vcard,
2096 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2097 _ => Viewtype::File,
2098 },
2099 mime::IMAGE => match mimetype.subtype() {
2100 mime::GIF => Viewtype::Gif,
2101 mime::SVG => Viewtype::File,
2102 _ => Viewtype::Image,
2103 },
2104 mime::AUDIO => Viewtype::Audio,
2105 mime::VIDEO => Viewtype::Video,
2106 mime::MULTIPART => Viewtype::Unknown,
2107 mime::MESSAGE => {
2108 if is_attachment_disposition(mail) {
2109 Viewtype::File
2110 } else {
2111 Viewtype::Unknown
2119 }
2120 }
2121 mime::APPLICATION => match mimetype.subtype() {
2122 mime::OCTET_STREAM => match filename {
2123 Some(filename) if !is_chat_msg => {
2124 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2125 Some((viewtype, _)) => viewtype,
2126 None => Viewtype::File,
2127 }
2128 }
2129 _ => Viewtype::File,
2130 },
2131 _ => Viewtype::File,
2132 },
2133 _ => Viewtype::Unknown,
2134 };
2135
2136 Ok((mimetype, viewtype))
2137}
2138
2139fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2140 let ct = mail.get_content_disposition();
2141 ct.disposition == DispositionType::Attachment
2142 && ct
2143 .params
2144 .iter()
2145 .any(|(key, _value)| key.starts_with("filename"))
2146}
2147
2148fn get_attachment_filename(
2155 context: &Context,
2156 mail: &mailparse::ParsedMail,
2157) -> Result<Option<String>> {
2158 let ct = mail.get_content_disposition();
2159
2160 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2163
2164 if desired_filename.is_none() {
2165 if let Some(name) = ct.params.get("filename*").map(|s| s.to_string()) {
2166 warn!(context, "apostrophed encoding invalid: {}", name);
2170 desired_filename = Some(name);
2171 }
2172 }
2173
2174 if desired_filename.is_none() {
2176 desired_filename = ct.params.get("name").map(|s| s.to_string());
2177 }
2178
2179 if desired_filename.is_none() {
2182 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2183 }
2184
2185 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2187 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2188 desired_filename = Some(format!("file.{subtype}",));
2189 } else {
2190 bail!(
2191 "could not determine attachment filename: {:?}",
2192 ct.disposition
2193 );
2194 };
2195 }
2196
2197 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2198
2199 Ok(desired_filename)
2200}
2201
2202pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2204 let to_addresses = get_all_addresses_from_header(headers, "to");
2205 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2206
2207 let mut res = to_addresses;
2208 res.extend(cc_addresses);
2209 res
2210}
2211
2212pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2214 let all = get_all_addresses_from_header(headers, "from");
2215 tools::single_value(all)
2216}
2217
2218pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2220 get_all_addresses_from_header(headers, "list-post")
2221 .into_iter()
2222 .next()
2223 .map(|s| s.addr)
2224}
2225
2226fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2238 let mut result: Vec<SingleInfo> = Default::default();
2239
2240 if let Some(header) = headers
2241 .iter()
2242 .rev()
2243 .find(|h| h.get_key().to_lowercase() == header)
2244 {
2245 if let Ok(addrs) = mailparse::addrparse_header(header) {
2246 for addr in addrs.iter() {
2247 match addr {
2248 mailparse::MailAddr::Single(info) => {
2249 result.push(SingleInfo {
2250 addr: addr_normalize(&info.addr).to_lowercase(),
2251 display_name: info.display_name.clone(),
2252 });
2253 }
2254 mailparse::MailAddr::Group(infos) => {
2255 for info in &infos.addrs {
2256 result.push(SingleInfo {
2257 addr: addr_normalize(&info.addr).to_lowercase(),
2258 display_name: info.display_name.clone(),
2259 });
2260 }
2261 }
2262 }
2263 }
2264 }
2265 }
2266
2267 result
2268}
2269
2270async fn handle_mdn(
2271 context: &Context,
2272 from_id: ContactId,
2273 rfc724_mid: &str,
2274 timestamp_sent: i64,
2275) -> Result<()> {
2276 if from_id == ContactId::SELF {
2277 warn!(
2278 context,
2279 "Ignoring MDN sent to self, this is a bug on the sender device."
2280 );
2281
2282 return Ok(());
2285 }
2286
2287 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2288 .sql
2289 .query_row_optional(
2290 concat!(
2291 "SELECT",
2292 " m.id AS msg_id,",
2293 " c.id AS chat_id,",
2294 " mdns.contact_id AS mdn_contact",
2295 " FROM msgs m ",
2296 " LEFT JOIN chats c ON m.chat_id=c.id",
2297 " LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id",
2298 " WHERE rfc724_mid=? AND from_id=1",
2299 " ORDER BY msg_id DESC, mdn_contact=? DESC",
2300 " LIMIT 1",
2301 ),
2302 (&rfc724_mid, from_id),
2303 |row| {
2304 let msg_id: MsgId = row.get("msg_id")?;
2305 let chat_id: ChatId = row.get("chat_id")?;
2306 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2307 Ok((
2308 msg_id,
2309 chat_id,
2310 mdn_contact.is_some(),
2311 mdn_contact == Some(from_id),
2312 ))
2313 },
2314 )
2315 .await?
2316 else {
2317 info!(
2318 context,
2319 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2320 );
2321 return Ok(());
2322 };
2323
2324 if is_dup {
2325 return Ok(());
2326 }
2327 context
2328 .sql
2329 .execute(
2330 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2331 (msg_id, from_id, timestamp_sent),
2332 )
2333 .await?;
2334 if !has_mdns {
2335 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2336 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2338 }
2339 Ok(())
2340}
2341
2342async fn handle_ndn(
2345 context: &Context,
2346 failed: &DeliveryReport,
2347 error: Option<String>,
2348) -> Result<()> {
2349 if failed.rfc724_mid.is_empty() {
2350 return Ok(());
2351 }
2352
2353 let msgs: Vec<_> = context
2356 .sql
2357 .query_map(
2358 "SELECT id FROM msgs
2359 WHERE rfc724_mid=? AND from_id=1",
2360 (&failed.rfc724_mid,),
2361 |row| {
2362 let msg_id: MsgId = row.get(0)?;
2363 Ok(msg_id)
2364 },
2365 |rows| Ok(rows.collect::<Vec<_>>()),
2366 )
2367 .await?;
2368
2369 let error = if let Some(error) = error {
2370 error
2371 } else {
2372 "Delivery to at least one recipient failed.".to_string()
2373 };
2374 let err_msg = &error;
2375
2376 for msg in msgs {
2377 let msg_id = msg?;
2378 let mut message = Message::load_from_db(context, msg_id).await?;
2379 let aggregated_error = message
2380 .error
2381 .as_ref()
2382 .map(|err| format!("{err}\n\n{err_msg}"));
2383 set_msg_failed(
2384 context,
2385 &mut message,
2386 aggregated_error.as_ref().unwrap_or(err_msg),
2387 )
2388 .await?;
2389 }
2390
2391 Ok(())
2392}
2393
2394#[cfg(test)]
2395mod mimeparser_tests;