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::contact::ContactId;
22use crate::context::Context;
23use crate::decrypt::{get_encrypted_pgp_message, validate_detached_signature};
24use crate::dehtml::dehtml;
25use crate::download::PostMsgMetadata;
26use crate::events::EventType;
27use crate::headerdef::{HeaderDef, HeaderDefMap};
28use crate::key::{self, DcKey, Fingerprint, SignedPublicKey, load_self_secret_keyring};
29use crate::log::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, tools};
38use crate::{constants, token};
39
40#[derive(Debug)]
43pub struct GossipedKey {
44 pub public_key: SignedPublicKey,
46
47 pub verified: bool,
49}
50
51#[derive(Debug)]
61pub(crate) struct MimeMessage {
62 pub parts: Vec<Part>,
64
65 headers: HashMap<String, String>,
67
68 #[cfg(test)]
69 headers_removed: HashSet<String>,
71
72 pub recipients: Vec<SingleInfo>,
76
77 pub past_members: Vec<SingleInfo>,
79
80 pub from: SingleInfo,
82
83 pub incoming: bool,
85 pub list_post: Option<String>,
88 pub chat_disposition_notification_to: Option<SingleInfo>,
89 pub decrypting_failed: bool,
90
91 pub signature: Option<(Fingerprint, HashSet<Fingerprint>)>,
98
99 pub gossiped_keys: BTreeMap<String, GossipedKey>,
102
103 pub autocrypt_fingerprint: Option<String>,
107
108 pub is_forwarded: bool,
110 pub is_system_message: SystemMessage,
111 pub location_kml: Option<location::Kml>,
112 pub message_kml: Option<location::Kml>,
113 pub(crate) sync_items: Option<SyncItems>,
114 pub(crate) webxdc_status_update: Option<String>,
115 pub(crate) user_avatar: Option<AvatarAction>,
116 pub(crate) group_avatar: Option<AvatarAction>,
117 pub(crate) mdn_reports: Vec<Report>,
118 pub(crate) delivery_report: Option<DeliveryReport>,
119
120 pub(crate) footer: Option<String>,
125
126 pub is_mime_modified: bool,
129
130 pub decoded_data: Vec<u8>,
132
133 pub(crate) hop_info: String,
135
136 pub(crate) is_bot: Option<bool>,
146
147 pub(crate) timestamp_rcvd: i64,
149 pub(crate) timestamp_sent: i64,
152
153 pub(crate) pre_message: PreMessageMode,
154}
155
156#[derive(Debug, Clone, PartialEq)]
157pub(crate) enum PreMessageMode {
158 Post,
162 Pre {
166 post_msg_rfc724_mid: String,
167 metadata: Option<PostMsgMetadata>,
168 },
169 None,
171}
172
173#[derive(Debug, PartialEq)]
174pub(crate) enum AvatarAction {
175 Delete,
176 Change(String),
177}
178
179#[derive(
181 Debug, Default, Display, Clone, Copy, PartialEq, Eq, FromPrimitive, ToPrimitive, ToSql, FromSql,
182)]
183#[repr(u32)]
184pub enum SystemMessage {
185 #[default]
187 Unknown = 0,
188
189 GroupNameChanged = 2,
191
192 GroupImageChanged = 3,
194
195 MemberAddedToGroup = 4,
197
198 MemberRemovedFromGroup = 5,
200
201 AutocryptSetupMessage = 6,
203
204 SecurejoinMessage = 7,
206
207 LocationStreamingEnabled = 8,
209
210 LocationOnly = 9,
212
213 EphemeralTimerChanged = 10,
215
216 ChatProtectionEnabled = 11,
218
219 ChatProtectionDisabled = 12,
221
222 InvalidUnencryptedMail = 13,
225
226 SecurejoinWait = 14,
229
230 SecurejoinWaitTimeout = 15,
233
234 MultiDeviceSync = 20,
237
238 WebxdcStatusUpdate = 30,
242
243 WebxdcInfoMessage = 32,
245
246 IrohNodeAddr = 40,
248
249 ChatE2ee = 50,
251
252 CallAccepted = 66,
254
255 CallEnded = 67,
257
258 GroupDescriptionChanged = 70,
260}
261
262const MIME_AC_SETUP_FILE: &str = "application/autocrypt-setup";
263
264impl MimeMessage {
265 #[expect(clippy::arithmetic_side_effects)]
270 pub(crate) async fn from_bytes(context: &Context, body: &[u8]) -> Result<Self> {
271 let mail = mailparse::parse_mail(body)?;
272
273 let timestamp_rcvd = smeared_time(context);
274 let mut timestamp_sent =
275 Self::get_timestamp_sent(&mail.headers, timestamp_rcvd, timestamp_rcvd);
276 let mut hop_info = parse_receive_headers(&mail.get_headers());
277
278 let mut headers = Default::default();
279 let mut headers_removed = HashSet::<String>::new();
280 let mut recipients = Default::default();
281 let mut past_members = Default::default();
282 let mut from = Default::default();
283 let mut list_post = Default::default();
284 let mut chat_disposition_notification_to = None;
285
286 MimeMessage::merge_headers(
288 context,
289 &mut headers,
290 &mut headers_removed,
291 &mut recipients,
292 &mut past_members,
293 &mut from,
294 &mut list_post,
295 &mut chat_disposition_notification_to,
296 &mail,
297 );
298 headers_removed.extend(
299 headers
300 .extract_if(|k, _v| is_hidden(k))
301 .map(|(k, _v)| k.to_string()),
302 );
303
304 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
306 let (part, mimetype) =
307 if mimetype.type_() == mime::MULTIPART && mimetype.subtype().as_str() == "signed" {
308 if let Some(part) = mail.subparts.first() {
309 timestamp_sent =
313 Self::get_timestamp_sent(&part.headers, timestamp_sent, timestamp_rcvd);
314 MimeMessage::merge_headers(
315 context,
316 &mut headers,
317 &mut headers_removed,
318 &mut recipients,
319 &mut past_members,
320 &mut from,
321 &mut list_post,
322 &mut chat_disposition_notification_to,
323 part,
324 );
325 (part, part.ctype.mimetype.parse::<Mime>()?)
326 } else {
327 (&mail, mimetype)
329 }
330 } else {
331 (&mail, mimetype)
333 };
334 if mimetype.type_() == mime::MULTIPART
335 && mimetype.subtype().as_str() == "mixed"
336 && let Some(part) = part.subparts.first()
337 {
338 for field in &part.headers {
339 let key = field.get_key().to_lowercase();
340 if !headers.contains_key(&key) && is_hidden(&key) || key == "message-id" {
341 headers.insert(key.to_string(), field.get_value());
342 }
343 }
344 }
345
346 if let Some(microsoft_message_id) = remove_header(
350 &mut headers,
351 HeaderDef::XMicrosoftOriginalMessageId.get_headername(),
352 &mut headers_removed,
353 ) {
354 headers.insert(
355 HeaderDef::MessageId.get_headername().to_string(),
356 microsoft_message_id,
357 );
358 }
359
360 let encrypted = false;
363 Self::remove_secured_headers(&mut headers, &mut headers_removed, encrypted);
364
365 let mut from = from.context("No from in message")?;
366 let private_keyring = load_self_secret_keyring(context).await?;
367
368 let dkim_results = handle_authres(context, &mail, &from.addr).await?;
369
370 let mut gossiped_keys = Default::default();
371 hop_info += "\n\n";
372 hop_info += &dkim_results.to_string();
373
374 let incoming = !context.is_self_addr(&from.addr).await?;
375
376 let mut aheader_values = mail.headers.get_all_values(HeaderDef::Autocrypt.into());
377
378 let mut pre_message = if mail
379 .headers
380 .get_header_value(HeaderDef::ChatIsPostMessage)
381 .is_some()
382 {
383 PreMessageMode::Post
384 } else {
385 PreMessageMode::None
386 };
387
388 let encrypted_pgp_message = get_encrypted_pgp_message(&mail)?;
389
390 let secrets: Vec<String>;
391 if let Some(e) = &encrypted_pgp_message
392 && crate::pgp::check_symmetric_encryption(e).is_ok()
393 {
394 secrets = load_shared_secrets(context).await?;
395 } else {
396 secrets = vec![];
397 }
398
399 let mail_raw; let decrypted_msg; let (mail, is_encrypted) = match tokio::task::block_in_place(|| {
403 encrypted_pgp_message.map(|e| crate::pgp::decrypt(e, &private_keyring, &secrets))
404 }) {
405 Some(Ok(mut msg)) => {
406 mail_raw = msg.as_data_vec().unwrap_or_default();
407
408 let decrypted_mail = mailparse::parse_mail(&mail_raw)?;
409 if std::env::var(crate::DCC_MIME_DEBUG).is_ok() {
410 info!(
411 context,
412 "decrypted message mime-body:\n{}",
413 String::from_utf8_lossy(&mail_raw),
414 );
415 }
416
417 decrypted_msg = Some(msg);
418
419 timestamp_sent = Self::get_timestamp_sent(
420 &decrypted_mail.headers,
421 timestamp_sent,
422 timestamp_rcvd,
423 );
424
425 let protected_aheader_values = decrypted_mail
426 .headers
427 .get_all_values(HeaderDef::Autocrypt.into());
428 if !protected_aheader_values.is_empty() {
429 aheader_values = protected_aheader_values;
430 }
431
432 (Ok(decrypted_mail), true)
433 }
434 None => {
435 mail_raw = Vec::new();
436 decrypted_msg = None;
437 (Ok(mail), false)
438 }
439 Some(Err(err)) => {
440 mail_raw = Vec::new();
441 decrypted_msg = None;
442 warn!(context, "decryption failed: {:#}", err);
443 (Err(err), false)
444 }
445 };
446
447 let mut autocrypt_header = None;
448 if incoming {
449 for val in aheader_values.iter().rev() {
451 autocrypt_header = match Aheader::from_str(val) {
452 Ok(header) if addr_cmp(&header.addr, &from.addr) => Some(header),
453 Ok(header) => {
454 warn!(
455 context,
456 "Autocrypt header address {:?} is not {:?}.", header.addr, from.addr
457 );
458 continue;
459 }
460 Err(err) => {
461 warn!(context, "Failed to parse Autocrypt header: {:#}.", err);
462 continue;
463 }
464 };
465 break;
466 }
467 }
468
469 let autocrypt_fingerprint = if let Some(autocrypt_header) = &autocrypt_header {
470 let fingerprint = autocrypt_header.public_key.dc_fingerprint().hex();
471 let inserted = context
472 .sql
473 .execute(
474 "INSERT INTO public_keys (fingerprint, public_key)
475 VALUES (?, ?)
476 ON CONFLICT (fingerprint)
477 DO NOTHING",
478 (&fingerprint, autocrypt_header.public_key.to_bytes()),
479 )
480 .await?;
481 if inserted > 0 {
482 info!(
483 context,
484 "Saved key with fingerprint {fingerprint} from the Autocrypt header"
485 );
486 }
487 Some(fingerprint)
488 } else {
489 None
490 };
491
492 let mut public_keyring = if incoming {
493 if let Some(autocrypt_header) = autocrypt_header {
494 vec![autocrypt_header.public_key]
495 } else {
496 vec![]
497 }
498 } else {
499 key::load_self_public_keyring(context).await?
500 };
501
502 if let Some(signature) = match &decrypted_msg {
503 Some(pgp::composed::Message::Literal { .. }) => None,
504 Some(pgp::composed::Message::Compressed { .. }) => {
505 None
508 }
509 Some(pgp::composed::Message::Signed { reader, .. }) => reader.signature(0),
510 Some(pgp::composed::Message::Encrypted { .. }) => {
511 None
513 }
514 None => None,
515 } {
516 for issuer_fingerprint in signature.issuer_fingerprint() {
517 let issuer_fingerprint =
518 crate::key::Fingerprint::from(issuer_fingerprint.clone()).hex();
519 if let Some(public_key_bytes) = context
520 .sql
521 .query_row_optional(
522 "SELECT public_key
523 FROM public_keys
524 WHERE fingerprint=?",
525 (&issuer_fingerprint,),
526 |row| {
527 let bytes: Vec<u8> = row.get(0)?;
528 Ok(bytes)
529 },
530 )
531 .await?
532 {
533 let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
534 public_keyring.push(public_key)
535 }
536 }
537 }
538
539 let mut signatures = if let Some(ref decrypted_msg) = decrypted_msg {
540 crate::pgp::valid_signature_fingerprints(decrypted_msg, &public_keyring)
541 } else {
542 HashMap::new()
543 };
544
545 let mail = mail.as_ref().map(|mail| {
546 let (content, signatures_detached) = validate_detached_signature(mail, &public_keyring)
547 .unwrap_or((mail, Default::default()));
548 let signatures_detached = signatures_detached
549 .into_iter()
550 .map(|fp| (fp, Vec::new()))
551 .collect::<HashMap<_, _>>();
552 signatures.extend(signatures_detached);
553 content
554 });
555 if let (Ok(mail), true) = (mail, is_encrypted) {
556 if !signatures.is_empty() {
557 remove_header(&mut headers, "subject", &mut headers_removed);
561 remove_header(&mut headers, "list-id", &mut headers_removed);
562 }
563
564 let mut inner_from = None;
570
571 MimeMessage::merge_headers(
572 context,
573 &mut headers,
574 &mut headers_removed,
575 &mut recipients,
576 &mut past_members,
577 &mut inner_from,
578 &mut list_post,
579 &mut chat_disposition_notification_to,
580 mail,
581 );
582
583 if !signatures.is_empty() {
584 let gossip_headers = mail.headers.get_all_values("Autocrypt-Gossip");
589 gossiped_keys =
590 parse_gossip_headers(context, &from.addr, &recipients, gossip_headers).await?;
591 }
592
593 if let Some(inner_from) = inner_from {
594 if !addr_cmp(&inner_from.addr, &from.addr) {
595 warn!(
604 context,
605 "From header in encrypted part doesn't match the outer one",
606 );
607
608 bail!("From header is forged");
613 }
614 from = inner_from;
615 }
616 }
617 if signatures.is_empty() {
618 Self::remove_secured_headers(&mut headers, &mut headers_removed, is_encrypted);
619 }
620 if !is_encrypted {
621 signatures.clear();
622 }
623
624 if let (Ok(mail), true) = (mail, is_encrypted)
625 && let Some(post_msg_rfc724_mid) =
626 mail.headers.get_header_value(HeaderDef::ChatPostMessageId)
627 {
628 let post_msg_rfc724_mid = parse_message_id(&post_msg_rfc724_mid)?;
629 let metadata = if let Some(value) = mail
630 .headers
631 .get_header_value(HeaderDef::ChatPostMessageMetadata)
632 {
633 match PostMsgMetadata::try_from_header_value(&value) {
634 Ok(metadata) => Some(metadata),
635 Err(error) => {
636 error!(
637 context,
638 "Failed to parse metadata header in pre-message for {post_msg_rfc724_mid}: {error:#}."
639 );
640 None
641 }
642 }
643 } else {
644 warn!(
645 context,
646 "Expected pre-message for {post_msg_rfc724_mid} to have metadata header."
647 );
648 None
649 };
650
651 pre_message = PreMessageMode::Pre {
652 post_msg_rfc724_mid,
653 metadata,
654 };
655 }
656
657 let signature = signatures
658 .into_iter()
659 .last()
660 .map(|(fp, recipient_fps)| (fp, recipient_fps.into_iter().collect::<HashSet<_>>()));
661 let mut parser = MimeMessage {
662 parts: Vec::new(),
663 headers,
664 #[cfg(test)]
665 headers_removed,
666
667 recipients,
668 past_members,
669 list_post,
670 from,
671 incoming,
672 chat_disposition_notification_to,
673 decrypting_failed: mail.is_err(),
674
675 signature,
677 autocrypt_fingerprint,
678 gossiped_keys,
679 is_forwarded: false,
680 mdn_reports: Vec::new(),
681 is_system_message: SystemMessage::Unknown,
682 location_kml: None,
683 message_kml: None,
684 sync_items: None,
685 webxdc_status_update: None,
686 user_avatar: None,
687 group_avatar: None,
688 delivery_report: None,
689 footer: None,
690 is_mime_modified: false,
691 decoded_data: Vec::new(),
692 hop_info,
693 is_bot: None,
694 timestamp_rcvd,
695 timestamp_sent,
696 pre_message,
697 };
698
699 match mail {
700 Ok(mail) => {
701 parser.parse_mime_recursive(context, mail, false).await?;
702 }
703 Err(err) => {
704 let txt = "[This message cannot be decrypted.\n\n• It might already help to simply reply to this message and ask the sender to send the message again.\n\n• If you just re-installed Delta Chat then it is best if you re-setup Delta Chat now and choose \"Add as second device\" or import a backup.]";
705
706 let part = Part {
707 typ: Viewtype::Text,
708 msg_raw: Some(txt.to_string()),
709 msg: txt.to_string(),
710 error: Some(format!("Decrypting failed: {err:#}")),
713 ..Default::default()
714 };
715 parser.do_add_single_part(part);
716 }
717 };
718
719 let is_location_only = parser.location_kml.is_some() && parser.parts.is_empty();
720 if parser.mdn_reports.is_empty()
721 && !is_location_only
722 && parser.sync_items.is_none()
723 && parser.webxdc_status_update.is_none()
724 {
725 let is_bot =
726 parser.headers.get("auto-submitted") == Some(&"auto-generated".to_string());
727 parser.is_bot = Some(is_bot);
728 }
729 parser.maybe_remove_bad_parts();
730 parser.maybe_remove_inline_mailinglist_footer();
731 parser.heuristically_parse_ndn(context).await;
732 parser.parse_headers(context).await?;
733 parser.decoded_data = mail_raw;
734
735 Ok(parser)
736 }
737
738 #[expect(clippy::arithmetic_side_effects)]
739 fn get_timestamp_sent(
740 hdrs: &[mailparse::MailHeader<'_>],
741 default: i64,
742 timestamp_rcvd: i64,
743 ) -> i64 {
744 hdrs.get_header_value(HeaderDef::Date)
745 .and_then(|v| mailparse::dateparse(&v).ok())
746 .map_or(default, |value| {
747 min(value, timestamp_rcvd + constants::TIMESTAMP_SENT_TOLERANCE)
748 })
749 }
750
751 fn parse_system_message_headers(&mut self, context: &Context) {
753 if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() && !self.incoming {
754 self.parts.retain(|part| {
755 part.mimetype
756 .as_ref()
757 .is_none_or(|mimetype| mimetype.as_ref() == MIME_AC_SETUP_FILE)
758 });
759
760 if self.parts.len() == 1 {
761 self.is_system_message = SystemMessage::AutocryptSetupMessage;
762 } else {
763 warn!(context, "could not determine ASM mime-part");
764 }
765 } else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
766 if value == "location-streaming-enabled" {
767 self.is_system_message = SystemMessage::LocationStreamingEnabled;
768 } else if value == "ephemeral-timer-changed" {
769 self.is_system_message = SystemMessage::EphemeralTimerChanged;
770 } else if value == "protection-enabled" {
771 self.is_system_message = SystemMessage::ChatProtectionEnabled;
772 } else if value == "protection-disabled" {
773 self.is_system_message = SystemMessage::ChatProtectionDisabled;
774 } else if value == "group-avatar-changed" {
775 self.is_system_message = SystemMessage::GroupImageChanged;
776 } else if value == "call-accepted" {
777 self.is_system_message = SystemMessage::CallAccepted;
778 } else if value == "call-ended" {
779 self.is_system_message = SystemMessage::CallEnded;
780 }
781 } else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
782 self.is_system_message = SystemMessage::MemberRemovedFromGroup;
783 } else if self.get_header(HeaderDef::ChatGroupMemberAdded).is_some() {
784 self.is_system_message = SystemMessage::MemberAddedToGroup;
785 } else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
786 self.is_system_message = SystemMessage::GroupNameChanged;
787 } else if self
788 .get_header(HeaderDef::ChatGroupDescriptionChanged)
789 .is_some()
790 {
791 self.is_system_message = SystemMessage::GroupDescriptionChanged;
792 }
793 }
794
795 fn parse_avatar_headers(&mut self, context: &Context) -> Result<()> {
797 if let Some(header_value) = self.get_header(HeaderDef::ChatGroupAvatar) {
798 self.group_avatar =
799 self.avatar_action_from_header(context, header_value.to_string())?;
800 }
801
802 if let Some(header_value) = self.get_header(HeaderDef::ChatUserAvatar) {
803 self.user_avatar = self.avatar_action_from_header(context, header_value.to_string())?;
804 }
805 Ok(())
806 }
807
808 fn parse_videochat_headers(&mut self) {
809 let content = self
810 .get_header(HeaderDef::ChatContent)
811 .unwrap_or_default()
812 .to_string();
813 let room = self
814 .get_header(HeaderDef::ChatWebrtcRoom)
815 .map(|s| s.to_string());
816 let accepted = self
817 .get_header(HeaderDef::ChatWebrtcAccepted)
818 .map(|s| s.to_string());
819 let has_video = self
820 .get_header(HeaderDef::ChatWebrtcHasVideoInitially)
821 .map(|s| s.to_string());
822 if let Some(part) = self.parts.first_mut() {
823 if let Some(room) = room {
824 if content == "call" {
825 part.typ = Viewtype::Call;
826 part.param.set(Param::WebrtcRoom, room);
827 }
828 } else if let Some(accepted) = accepted {
829 part.param.set(Param::WebrtcAccepted, accepted);
830 }
831 if let Some(has_video) = has_video {
832 part.param.set(Param::WebrtcHasVideoInitially, has_video);
833 }
834 }
835 }
836
837 fn squash_attachment_parts(&mut self) {
843 if self.parts.len() == 2
844 && self.parts.first().map(|textpart| textpart.typ) == Some(Viewtype::Text)
845 && self
846 .parts
847 .get(1)
848 .is_some_and(|filepart| match filepart.typ {
849 Viewtype::Image
850 | Viewtype::Gif
851 | Viewtype::Sticker
852 | Viewtype::Audio
853 | Viewtype::Voice
854 | Viewtype::Video
855 | Viewtype::Vcard
856 | Viewtype::File
857 | Viewtype::Webxdc => true,
858 Viewtype::Unknown | Viewtype::Text | Viewtype::Call => false,
859 })
860 {
861 let mut parts = std::mem::take(&mut self.parts);
862 let Some(mut filepart) = parts.pop() else {
863 return;
865 };
866 let Some(textpart) = parts.pop() else {
867 return;
869 };
870
871 filepart.msg.clone_from(&textpart.msg);
872 if let Some(quote) = textpart.param.get(Param::Quote) {
873 filepart.param.set(Param::Quote, quote);
874 }
875
876 self.parts = vec![filepart];
877 }
878 }
879
880 fn parse_attachments(&mut self) {
882 if self.parts.len() != 1 {
885 return;
886 }
887
888 if let Some(mut part) = self.parts.pop() {
889 if part.typ == Viewtype::Audio && self.get_header(HeaderDef::ChatVoiceMessage).is_some()
890 {
891 part.typ = Viewtype::Voice;
892 }
893 if (part.typ == Viewtype::Image || part.typ == Viewtype::Gif)
894 && let Some(value) = self.get_header(HeaderDef::ChatContent)
895 && value == "sticker"
896 {
897 part.typ = Viewtype::Sticker;
898 }
899 if (part.typ == Viewtype::Audio
900 || part.typ == Viewtype::Voice
901 || part.typ == Viewtype::Video)
902 && let Some(field_0) = self.get_header(HeaderDef::ChatDuration)
903 {
904 let duration_ms = field_0.parse().unwrap_or_default();
905 if duration_ms > 0 && duration_ms < 24 * 60 * 60 * 1000 {
906 part.param.set_int(Param::Duration, duration_ms);
907 }
908 }
909
910 self.parts.push(part);
911 }
912 }
913
914 async fn parse_headers(&mut self, context: &Context) -> Result<()> {
915 self.parse_system_message_headers(context);
916 self.parse_avatar_headers(context)?;
917 self.parse_videochat_headers();
918 if self.delivery_report.is_none() {
919 self.squash_attachment_parts();
920 }
921
922 if !context.get_config_bool(Config::Bot).await?
923 && let Some(ref subject) = self.get_subject()
924 {
925 let mut prepend_subject = true;
926 if !self.decrypting_failed {
927 let colon = subject.find(':');
928 if colon == Some(2)
929 || colon == Some(3)
930 || self.has_chat_version()
931 || subject.contains("Chat:")
932 {
933 prepend_subject = false
934 }
935 }
936
937 if self.is_mailinglist_message() && !self.has_chat_version() {
940 prepend_subject = true;
941 }
942
943 if prepend_subject && !subject.is_empty() {
944 let part_with_text = self
945 .parts
946 .iter_mut()
947 .find(|part| !part.msg.is_empty() && !part.is_reaction);
948 if let Some(part) = part_with_text {
949 part.msg = format!("{} – {}", subject, part.msg);
954 }
955 }
956 }
957
958 if self.is_forwarded {
959 for part in &mut self.parts {
960 part.param.set_int(Param::Forwarded, 1);
961 }
962 }
963
964 self.parse_attachments();
965
966 if !self.decrypting_failed
968 && !self.parts.is_empty()
969 && let Some(ref dn_to) = self.chat_disposition_notification_to
970 {
971 let from = &self.from.addr;
973 if !context.is_self_addr(from).await? {
974 if from.to_lowercase() == dn_to.addr.to_lowercase() {
975 if let Some(part) = self.parts.last_mut() {
976 part.param.set_int(Param::WantsMdn, 1);
977 }
978 } else {
979 warn!(
980 context,
981 "{} requested a read receipt to {}, ignoring", from, dn_to.addr
982 );
983 }
984 }
985 }
986
987 if self.parts.is_empty() && self.mdn_reports.is_empty() {
992 let mut part = Part {
993 typ: Viewtype::Text,
994 ..Default::default()
995 };
996
997 if let Some(ref subject) = self.get_subject()
998 && !self.has_chat_version()
999 && self.webxdc_status_update.is_none()
1000 {
1001 part.msg = subject.to_string();
1002 }
1003
1004 self.do_add_single_part(part);
1005 }
1006
1007 if self.is_bot == Some(true) {
1008 for part in &mut self.parts {
1009 part.param.set(Param::Bot, "1");
1010 }
1011 }
1012
1013 Ok(())
1014 }
1015
1016 #[expect(clippy::arithmetic_side_effects)]
1017 fn avatar_action_from_header(
1018 &mut self,
1019 context: &Context,
1020 header_value: String,
1021 ) -> Result<Option<AvatarAction>> {
1022 let res = if header_value == "0" {
1023 Some(AvatarAction::Delete)
1024 } else if let Some(base64) = header_value
1025 .split_ascii_whitespace()
1026 .collect::<String>()
1027 .strip_prefix("base64:")
1028 {
1029 match BlobObject::store_from_base64(context, base64)? {
1030 Some(path) => Some(AvatarAction::Change(path)),
1031 None => {
1032 warn!(context, "Could not decode avatar base64");
1033 None
1034 }
1035 }
1036 } else {
1037 let mut i = 0;
1040 while let Some(part) = self.parts.get_mut(i) {
1041 if let Some(part_filename) = &part.org_filename
1042 && part_filename == &header_value
1043 {
1044 if let Some(blob) = part.param.get(Param::File) {
1045 let res = Some(AvatarAction::Change(blob.to_string()));
1046 self.parts.remove(i);
1047 return Ok(res);
1048 }
1049 break;
1050 }
1051 i += 1;
1052 }
1053 None
1054 };
1055 Ok(res)
1056 }
1057
1058 pub fn was_encrypted(&self) -> bool {
1064 self.signature.is_some()
1065 }
1066
1067 pub(crate) fn has_chat_version(&self) -> bool {
1070 self.headers.contains_key("chat-version")
1071 }
1072
1073 pub(crate) fn get_subject(&self) -> Option<String> {
1074 self.get_header(HeaderDef::Subject)
1075 .map(|s| s.trim_start())
1076 .filter(|s| !s.is_empty())
1077 .map(|s| s.to_string())
1078 }
1079
1080 pub fn get_header(&self, headerdef: HeaderDef) -> Option<&str> {
1081 self.headers
1082 .get(headerdef.get_headername())
1083 .map(|s| s.as_str())
1084 }
1085
1086 #[cfg(test)]
1087 pub(crate) fn header_exists(&self, headerdef: HeaderDef) -> bool {
1092 let hname = headerdef.get_headername();
1093 self.headers.contains_key(hname) || self.headers_removed.contains(hname)
1094 }
1095
1096 #[cfg(test)]
1097 pub(crate) fn decoded_data_contains(&self, s: &str) -> bool {
1099 assert!(!self.decrypting_failed);
1100 let decoded_str = str::from_utf8(&self.decoded_data).unwrap();
1101 decoded_str.contains(s)
1102 }
1103
1104 pub fn get_chat_group_id(&self) -> Option<&str> {
1106 self.get_header(HeaderDef::ChatGroupId)
1107 .filter(|s| validate_id(s))
1108 }
1109
1110 async fn parse_mime_recursive<'a>(
1111 &'a mut self,
1112 context: &'a Context,
1113 mail: &'a mailparse::ParsedMail<'a>,
1114 is_related: bool,
1115 ) -> Result<bool> {
1116 enum MimeS {
1117 Multiple,
1118 Single,
1119 Message,
1120 }
1121
1122 let mimetype = mail.ctype.mimetype.to_lowercase();
1123
1124 let m = if mimetype.starts_with("multipart") {
1125 if mail.ctype.params.contains_key("boundary") {
1126 MimeS::Multiple
1127 } else {
1128 MimeS::Single
1129 }
1130 } else if mimetype.starts_with("message") {
1131 if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
1132 MimeS::Message
1133 } else {
1134 MimeS::Single
1135 }
1136 } else {
1137 MimeS::Single
1138 };
1139
1140 let is_related = is_related || mimetype == "multipart/related";
1141 match m {
1142 MimeS::Multiple => Box::pin(self.handle_multiple(context, mail, is_related)).await,
1143 MimeS::Message => {
1144 let raw = mail.get_body_raw()?;
1145 if raw.is_empty() {
1146 return Ok(false);
1147 }
1148 let mail = mailparse::parse_mail(&raw).context("failed to parse mail")?;
1149
1150 Box::pin(self.parse_mime_recursive(context, &mail, is_related)).await
1151 }
1152 MimeS::Single => {
1153 self.add_single_part_if_known(context, mail, is_related)
1154 .await
1155 }
1156 }
1157 }
1158
1159 async fn handle_multiple(
1160 &mut self,
1161 context: &Context,
1162 mail: &mailparse::ParsedMail<'_>,
1163 is_related: bool,
1164 ) -> Result<bool> {
1165 let mut any_part_added = false;
1166 let mimetype = get_mime_type(
1167 mail,
1168 &get_attachment_filename(context, mail)?,
1169 self.has_chat_version(),
1170 )?
1171 .0;
1172 match (mimetype.type_(), mimetype.subtype().as_str()) {
1173 (mime::MULTIPART, "alternative") => {
1174 for cur_data in mail.subparts.iter().rev() {
1186 let (mime_type, _viewtype) = get_mime_type(
1187 cur_data,
1188 &get_attachment_filename(context, cur_data)?,
1189 self.has_chat_version(),
1190 )?;
1191
1192 if mime_type == mime::TEXT_PLAIN || mime_type.type_() == mime::MULTIPART {
1193 any_part_added = self
1194 .parse_mime_recursive(context, cur_data, is_related)
1195 .await?;
1196 break;
1197 }
1198 }
1199
1200 for cur_data in mail.subparts.iter().rev() {
1209 let mimetype = cur_data.ctype.mimetype.parse::<Mime>()?;
1210 if mimetype.type_() == mime::TEXT && mimetype.subtype() == "calendar" {
1211 let filename = get_attachment_filename(context, cur_data)?
1212 .unwrap_or_else(|| "calendar.ics".to_string());
1213 self.do_add_single_file_part(
1214 context,
1215 Viewtype::File,
1216 mimetype,
1217 &mail.ctype.mimetype.to_lowercase(),
1218 &mail.get_body_raw()?,
1219 &filename,
1220 is_related,
1221 )
1222 .await?;
1223 }
1224 }
1225
1226 if !any_part_added {
1227 for cur_part in mail.subparts.iter().rev() {
1228 if self
1229 .parse_mime_recursive(context, cur_part, is_related)
1230 .await?
1231 {
1232 any_part_added = true;
1233 break;
1234 }
1235 }
1236 }
1237 if any_part_added && mail.subparts.len() > 1 {
1238 self.is_mime_modified = true;
1242 }
1243 }
1244 (mime::MULTIPART, "signed") => {
1245 if let Some(first) = mail.subparts.first() {
1254 any_part_added = self
1255 .parse_mime_recursive(context, first, is_related)
1256 .await?;
1257 }
1258 }
1259 (mime::MULTIPART, "report") => {
1260 if mail.subparts.len() >= 2 {
1262 match mail.ctype.params.get("report-type").map(|s| s as &str) {
1263 Some("disposition-notification") => {
1264 if let Some(report) = self.process_report(context, mail)? {
1265 self.mdn_reports.push(report);
1266 }
1267
1268 let part = Part {
1273 typ: Viewtype::Unknown,
1274 ..Default::default()
1275 };
1276 self.parts.push(part);
1277
1278 any_part_added = true;
1279 }
1280 Some("delivery-status") | None => {
1282 if let Some(report) = self.process_delivery_status(context, mail)? {
1283 self.delivery_report = Some(report);
1284 }
1285
1286 for cur_data in &mail.subparts {
1288 if self
1289 .parse_mime_recursive(context, cur_data, is_related)
1290 .await?
1291 {
1292 any_part_added = true;
1293 }
1294 }
1295 }
1296 Some("multi-device-sync") => {
1297 if let Some(second) = mail.subparts.get(1) {
1298 self.add_single_part_if_known(context, second, is_related)
1299 .await?;
1300 }
1301 }
1302 Some("status-update") => {
1303 if let Some(second) = mail.subparts.get(1) {
1304 self.add_single_part_if_known(context, second, is_related)
1305 .await?;
1306 }
1307 }
1308 Some(_) => {
1309 for cur_data in &mail.subparts {
1310 if self
1311 .parse_mime_recursive(context, cur_data, is_related)
1312 .await?
1313 {
1314 any_part_added = true;
1315 }
1316 }
1317 }
1318 }
1319 }
1320 }
1321 _ => {
1322 for cur_data in &mail.subparts {
1325 if self
1326 .parse_mime_recursive(context, cur_data, is_related)
1327 .await?
1328 {
1329 any_part_added = true;
1330 }
1331 }
1332 }
1333 }
1334
1335 Ok(any_part_added)
1336 }
1337
1338 async fn add_single_part_if_known(
1340 &mut self,
1341 context: &Context,
1342 mail: &mailparse::ParsedMail<'_>,
1343 is_related: bool,
1344 ) -> Result<bool> {
1345 let filename = get_attachment_filename(context, mail)?;
1347 let (mime_type, msg_type) = get_mime_type(mail, &filename, self.has_chat_version())?;
1348 let raw_mime = mail.ctype.mimetype.to_lowercase();
1349
1350 let old_part_count = self.parts.len();
1351
1352 match filename {
1353 Some(filename) => {
1354 self.do_add_single_file_part(
1355 context,
1356 msg_type,
1357 mime_type,
1358 &raw_mime,
1359 &mail.get_body_raw()?,
1360 &filename,
1361 is_related,
1362 )
1363 .await?;
1364 }
1365 None => {
1366 match mime_type.type_() {
1367 mime::IMAGE | mime::AUDIO | mime::VIDEO | mime::APPLICATION => {
1368 warn!(context, "Missing attachment");
1369 return Ok(false);
1370 }
1371 mime::TEXT
1372 if mail.get_content_disposition().disposition
1373 == DispositionType::Extension("reaction".to_string()) =>
1374 {
1375 let decoded_data = match mail.get_body() {
1377 Ok(decoded_data) => decoded_data,
1378 Err(err) => {
1379 warn!(context, "Invalid body parsed {:#}", err);
1380 return Ok(false);
1382 }
1383 };
1384
1385 let part = Part {
1386 typ: Viewtype::Text,
1387 mimetype: Some(mime_type),
1388 msg: decoded_data,
1389 is_reaction: true,
1390 ..Default::default()
1391 };
1392 self.do_add_single_part(part);
1393 return Ok(true);
1394 }
1395 mime::TEXT | mime::HTML => {
1396 let decoded_data = match mail.get_body() {
1397 Ok(decoded_data) => decoded_data,
1398 Err(err) => {
1399 warn!(context, "Invalid body parsed {:#}", err);
1400 return Ok(false);
1402 }
1403 };
1404
1405 let is_plaintext = mime_type == mime::TEXT_PLAIN;
1406 let mut dehtml_failed = false;
1407
1408 let SimplifiedText {
1409 text: simplified_txt,
1410 is_forwarded,
1411 is_cut,
1412 top_quote,
1413 footer,
1414 } = if decoded_data.is_empty() {
1415 Default::default()
1416 } else {
1417 let is_html = mime_type == mime::TEXT_HTML;
1418 if is_html {
1419 self.is_mime_modified = true;
1420 if let Some(text) = dehtml(&decoded_data) {
1425 text
1426 } else {
1427 dehtml_failed = true;
1428 SimplifiedText {
1429 text: decoded_data.clone(),
1430 ..Default::default()
1431 }
1432 }
1433 } else {
1434 simplify(decoded_data.clone(), self.has_chat_version())
1435 }
1436 };
1437
1438 self.is_mime_modified = self.is_mime_modified
1439 || ((is_forwarded || is_cut || top_quote.is_some())
1440 && !self.has_chat_version());
1441
1442 let is_format_flowed = if let Some(format) = mail.ctype.params.get("format")
1443 {
1444 format.as_str().eq_ignore_ascii_case("flowed")
1445 } else {
1446 false
1447 };
1448
1449 let (simplified_txt, simplified_quote) = if mime_type.type_() == mime::TEXT
1450 && mime_type.subtype() == mime::PLAIN
1451 {
1452 let simplified_txt = match mail
1455 .ctype
1456 .params
1457 .get("hp-legacy-display")
1458 .is_some_and(|v| v == "1")
1459 {
1460 false => simplified_txt,
1461 true => rm_legacy_display_elements(&simplified_txt),
1462 };
1463 if is_format_flowed {
1464 let delsp = if let Some(delsp) = mail.ctype.params.get("delsp") {
1465 delsp.as_str().eq_ignore_ascii_case("yes")
1466 } else {
1467 false
1468 };
1469 let unflowed_text = unformat_flowed(&simplified_txt, delsp);
1470 let unflowed_quote = top_quote.map(|q| unformat_flowed(&q, delsp));
1471 (unflowed_text, unflowed_quote)
1472 } else {
1473 (simplified_txt, top_quote)
1474 }
1475 } else {
1476 (simplified_txt, top_quote)
1477 };
1478
1479 let (simplified_txt, was_truncated) =
1480 truncate_msg_text(context, simplified_txt).await?;
1481 if was_truncated {
1482 self.is_mime_modified = was_truncated;
1483 }
1484
1485 if !simplified_txt.is_empty() || simplified_quote.is_some() {
1486 let mut part = Part {
1487 dehtml_failed,
1488 typ: Viewtype::Text,
1489 mimetype: Some(mime_type),
1490 msg: simplified_txt,
1491 ..Default::default()
1492 };
1493 if let Some(quote) = simplified_quote {
1494 part.param.set(Param::Quote, quote);
1495 }
1496 part.msg_raw = Some(decoded_data);
1497 self.do_add_single_part(part);
1498 }
1499
1500 if is_forwarded {
1501 self.is_forwarded = true;
1502 }
1503
1504 if self.footer.is_none() && is_plaintext {
1505 self.footer = Some(footer.unwrap_or_default());
1506 }
1507 }
1508 _ => {}
1509 }
1510 }
1511 }
1512
1513 Ok(self.parts.len() > old_part_count)
1515 }
1516
1517 #[expect(clippy::too_many_arguments)]
1518 #[expect(clippy::arithmetic_side_effects)]
1519 async fn do_add_single_file_part(
1520 &mut self,
1521 context: &Context,
1522 msg_type: Viewtype,
1523 mime_type: Mime,
1524 raw_mime: &str,
1525 decoded_data: &[u8],
1526 filename: &str,
1527 is_related: bool,
1528 ) -> Result<()> {
1529 if mime_type.type_() == mime::APPLICATION
1531 && mime_type.subtype().as_str() == "pgp-keys"
1532 && Self::try_set_peer_key_from_file_part(context, decoded_data).await?
1533 {
1534 return Ok(());
1535 }
1536 let mut part = Part::default();
1537 let msg_type = if context
1538 .is_webxdc_file(filename, decoded_data)
1539 .await
1540 .unwrap_or(false)
1541 {
1542 Viewtype::Webxdc
1543 } else if filename.ends_with(".kml") {
1544 if filename.starts_with("location") || filename.starts_with("message") {
1547 let parsed = location::Kml::parse(decoded_data)
1548 .map_err(|err| {
1549 warn!(context, "failed to parse kml part: {:#}", err);
1550 })
1551 .ok();
1552 if filename.starts_with("location") {
1553 self.location_kml = parsed;
1554 } else {
1555 self.message_kml = parsed;
1556 }
1557 return Ok(());
1558 }
1559 msg_type
1560 } else if filename == "multi-device-sync.json" {
1561 if !context.get_config_bool(Config::SyncMsgs).await? {
1562 return Ok(());
1563 }
1564 let serialized = String::from_utf8_lossy(decoded_data)
1565 .parse()
1566 .unwrap_or_default();
1567 self.sync_items = context
1568 .parse_sync_items(serialized)
1569 .map_err(|err| {
1570 warn!(context, "failed to parse sync data: {:#}", err);
1571 })
1572 .ok();
1573 return Ok(());
1574 } else if filename == "status-update.json" {
1575 let serialized = String::from_utf8_lossy(decoded_data)
1576 .parse()
1577 .unwrap_or_default();
1578 self.webxdc_status_update = Some(serialized);
1579 return Ok(());
1580 } else if msg_type == Viewtype::Vcard {
1581 if let Some(summary) = get_vcard_summary(decoded_data) {
1582 part.param.set(Param::Summary1, summary);
1583 msg_type
1584 } else {
1585 Viewtype::File
1586 }
1587 } else if msg_type == Viewtype::Image
1588 || msg_type == Viewtype::Gif
1589 || msg_type == Viewtype::Sticker
1590 {
1591 match get_filemeta(decoded_data) {
1592 Ok((width, height)) if width * height <= constants::MAX_RCVD_IMAGE_PIXELS => {
1594 part.param.set_i64(Param::Width, width.into());
1595 part.param.set_i64(Param::Height, height.into());
1596 msg_type
1597 }
1598 _ => Viewtype::File,
1600 }
1601 } else {
1602 msg_type
1603 };
1604
1605 let blob =
1609 match BlobObject::create_and_deduplicate_from_bytes(context, decoded_data, filename) {
1610 Ok(blob) => blob,
1611 Err(err) => {
1612 error!(
1613 context,
1614 "Could not add blob for mime part {}, error {:#}", filename, err
1615 );
1616 return Ok(());
1617 }
1618 };
1619 info!(context, "added blobfile: {:?}", blob.as_name());
1620
1621 part.typ = msg_type;
1622 part.org_filename = Some(filename.to_string());
1623 part.mimetype = Some(mime_type);
1624 part.bytes = decoded_data.len();
1625 part.param.set(Param::File, blob.as_name());
1626 part.param.set(Param::Filename, filename);
1627 part.param.set(Param::MimeType, raw_mime);
1628 part.is_related = is_related;
1629
1630 self.do_add_single_part(part);
1631 Ok(())
1632 }
1633
1634 async fn try_set_peer_key_from_file_part(
1636 context: &Context,
1637 decoded_data: &[u8],
1638 ) -> Result<bool> {
1639 let key = match str::from_utf8(decoded_data) {
1640 Err(err) => {
1641 warn!(context, "PGP key attachment is not a UTF-8 file: {}", err);
1642 return Ok(false);
1643 }
1644 Ok(key) => key,
1645 };
1646 let key = match SignedPublicKey::from_asc(key) {
1647 Err(err) => {
1648 warn!(
1649 context,
1650 "PGP key attachment is not an ASCII-armored file: {err:#}."
1651 );
1652 return Ok(false);
1653 }
1654 Ok(key) => key,
1655 };
1656 if let Err(err) = key.verify_bindings() {
1657 warn!(context, "Attached PGP key verification failed: {err:#}.");
1658 return Ok(false);
1659 }
1660
1661 let fingerprint = key.dc_fingerprint().hex();
1662 context
1663 .sql
1664 .execute(
1665 "INSERT INTO public_keys (fingerprint, public_key)
1666 VALUES (?, ?)
1667 ON CONFLICT (fingerprint)
1668 DO NOTHING",
1669 (&fingerprint, key.to_bytes()),
1670 )
1671 .await?;
1672
1673 info!(context, "Imported PGP key {fingerprint} from attachment.");
1674 Ok(true)
1675 }
1676
1677 pub(crate) fn do_add_single_part(&mut self, mut part: Part) {
1678 if self.was_encrypted() {
1679 part.param.set_int(Param::GuaranteeE2ee, 1);
1680 }
1681 self.parts.push(part);
1682 }
1683
1684 pub(crate) fn get_mailinglist_header(&self) -> Option<&str> {
1685 if let Some(list_id) = self.get_header(HeaderDef::ListId) {
1686 return Some(list_id);
1689 } else if let Some(chat_list_id) = self.get_header(HeaderDef::ChatListId) {
1690 return Some(chat_list_id);
1691 } else if let Some(sender) = self.get_header(HeaderDef::Sender) {
1692 if let Some(precedence) = self.get_header(HeaderDef::Precedence)
1695 && (precedence == "list" || precedence == "bulk")
1696 {
1697 return Some(sender);
1701 }
1702 }
1703 None
1704 }
1705
1706 pub(crate) fn is_mailinglist_message(&self) -> bool {
1707 self.get_mailinglist_header().is_some()
1708 }
1709
1710 pub(crate) fn is_schleuder_message(&self) -> bool {
1712 if let Some(list_help) = self.get_header(HeaderDef::ListHelp) {
1713 list_help == "<https://schleuder.org/>"
1714 } else {
1715 false
1716 }
1717 }
1718
1719 pub(crate) fn is_call(&self) -> bool {
1721 self.parts
1722 .first()
1723 .is_some_and(|part| part.typ == Viewtype::Call)
1724 }
1725
1726 pub(crate) fn get_rfc724_mid(&self) -> Option<String> {
1727 self.get_header(HeaderDef::MessageId)
1728 .and_then(|msgid| parse_message_id(msgid).ok())
1729 }
1730
1731 fn remove_secured_headers(
1737 headers: &mut HashMap<String, String>,
1738 removed: &mut HashSet<String>,
1739 encrypted: bool,
1740 ) {
1741 remove_header(headers, "secure-join-fingerprint", removed);
1742 remove_header(headers, "chat-verified", removed);
1743 remove_header(headers, "autocrypt-gossip", removed);
1744
1745 if headers.get("secure-join") == Some(&"vc-request-pubkey".to_string()) && encrypted {
1746 } else {
1754 remove_header(headers, "secure-join-auth", removed);
1755
1756 if let Some(secure_join) = remove_header(headers, "secure-join", removed)
1758 && (secure_join == "vc-request" || secure_join == "vg-request")
1759 {
1760 headers.insert("secure-join".to_string(), secure_join);
1761 }
1762 }
1763 }
1764
1765 #[allow(clippy::too_many_arguments)]
1771 fn merge_headers(
1772 context: &Context,
1773 headers: &mut HashMap<String, String>,
1774 headers_removed: &mut HashSet<String>,
1775 recipients: &mut Vec<SingleInfo>,
1776 past_members: &mut Vec<SingleInfo>,
1777 from: &mut Option<SingleInfo>,
1778 list_post: &mut Option<String>,
1779 chat_disposition_notification_to: &mut Option<SingleInfo>,
1780 part: &mailparse::ParsedMail,
1781 ) {
1782 let fields = &part.headers;
1783 let has_header_protection = part.ctype.params.contains_key("hp");
1785
1786 headers_removed.extend(
1787 headers
1788 .extract_if(|k, _v| has_header_protection || is_protected(k))
1789 .map(|(k, _v)| k.to_string()),
1790 );
1791 for field in fields {
1792 let key = field.get_key().to_lowercase();
1794 if key == HeaderDef::ChatDispositionNotificationTo.get_headername() {
1795 match addrparse_header(field) {
1796 Ok(addrlist) => {
1797 *chat_disposition_notification_to = addrlist.extract_single_info();
1798 }
1799 Err(e) => warn!(context, "Could not read {} address: {}", key, e),
1800 }
1801 } else {
1802 let value = field.get_value();
1803 headers.insert(key.to_string(), value);
1804 }
1805 }
1806 let recipients_new = get_recipients(fields);
1807 if !recipients_new.is_empty() {
1808 *recipients = recipients_new;
1809 }
1810 let past_members_addresses =
1811 get_all_addresses_from_header(fields, "chat-group-past-members");
1812 if !past_members_addresses.is_empty() {
1813 *past_members = past_members_addresses;
1814 }
1815 let from_new = get_from(fields);
1816 if from_new.is_some() {
1817 *from = from_new;
1818 }
1819 let list_post_new = get_list_post(fields);
1820 if list_post_new.is_some() {
1821 *list_post = list_post_new;
1822 }
1823 }
1824
1825 fn process_report(
1826 &self,
1827 context: &Context,
1828 report: &mailparse::ParsedMail<'_>,
1829 ) -> Result<Option<Report>> {
1830 let report_body = if let Some(subpart) = report.subparts.get(1) {
1832 subpart.get_body_raw()?
1833 } else {
1834 bail!("Report does not have second MIME part");
1835 };
1836 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1837
1838 if report_fields
1840 .get_header_value(HeaderDef::Disposition)
1841 .is_none()
1842 {
1843 warn!(
1844 context,
1845 "Ignoring unknown disposition-notification, Message-Id: {:?}.",
1846 report_fields.get_header_value(HeaderDef::MessageId)
1847 );
1848 return Ok(None);
1849 };
1850
1851 let original_message_id = report_fields
1852 .get_header_value(HeaderDef::OriginalMessageId)
1853 .or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
1856 .and_then(|v| parse_message_id(&v).ok());
1857 let additional_message_ids = report_fields
1858 .get_header_value(HeaderDef::AdditionalMessageIds)
1859 .map_or_else(Vec::new, |v| {
1860 v.split(' ')
1861 .filter_map(|s| parse_message_id(s).ok())
1862 .collect()
1863 });
1864
1865 Ok(Some(Report {
1866 original_message_id,
1867 additional_message_ids,
1868 }))
1869 }
1870
1871 fn process_delivery_status(
1872 &self,
1873 context: &Context,
1874 report: &mailparse::ParsedMail<'_>,
1875 ) -> Result<Option<DeliveryReport>> {
1876 let mut failure = true;
1878
1879 if let Some(status_part) = report.subparts.get(1) {
1880 if status_part.ctype.mimetype != "message/delivery-status"
1883 && status_part.ctype.mimetype != "message/global-delivery-status"
1884 {
1885 warn!(
1886 context,
1887 "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring"
1888 );
1889 return Ok(None);
1890 }
1891
1892 let status_body = status_part.get_body_raw()?;
1893
1894 let (_, sz) = mailparse::parse_headers(&status_body)?;
1896
1897 if let Some(status_body) = status_body.get(sz..) {
1899 let (status_fields, _) = mailparse::parse_headers(status_body)?;
1900 if let Some(action) = status_fields.get_first_value("action") {
1901 if action != "failed" {
1902 info!(context, "DSN with {:?} action", action);
1903 failure = false;
1904 }
1905 } else {
1906 warn!(context, "DSN without action");
1907 }
1908 } else {
1909 warn!(context, "DSN without per-recipient fields");
1910 }
1911 } else {
1912 return Ok(None);
1914 }
1915
1916 if let Some(original_msg) = report.subparts.get(2).filter(|p| {
1918 p.ctype.mimetype.contains("rfc822")
1919 || p.ctype.mimetype == "message/global"
1920 || p.ctype.mimetype == "message/global-headers"
1921 }) {
1922 let report_body = original_msg.get_body_raw()?;
1923 let (report_fields, _) = mailparse::parse_headers(&report_body)?;
1924
1925 if let Some(original_message_id) = report_fields
1926 .get_header_value(HeaderDef::MessageId)
1927 .and_then(|v| parse_message_id(&v).ok())
1928 {
1929 return Ok(Some(DeliveryReport {
1930 rfc724_mid: original_message_id,
1931 failure,
1932 }));
1933 }
1934
1935 warn!(
1936 context,
1937 "ignoring unknown ndn-notification, Message-Id: {:?}",
1938 report_fields.get_header_value(HeaderDef::MessageId)
1939 );
1940 }
1941
1942 Ok(None)
1943 }
1944
1945 fn maybe_remove_bad_parts(&mut self) {
1946 let good_parts = self.parts.iter().filter(|p| !p.dehtml_failed).count();
1947 if good_parts == 0 {
1948 self.parts.truncate(1);
1950 } else if good_parts < self.parts.len() {
1951 self.parts.retain(|p| !p.dehtml_failed);
1952 }
1953
1954 if !self.has_chat_version() && self.is_mime_modified {
1962 fn is_related_image(p: &&Part) -> bool {
1963 (p.typ == Viewtype::Image || p.typ == Viewtype::Gif) && p.is_related
1964 }
1965 let related_image_cnt = self.parts.iter().filter(is_related_image).count();
1966 if related_image_cnt > 1 {
1967 let mut is_first_image = true;
1968 self.parts.retain(|p| {
1969 let retain = is_first_image || !is_related_image(&p);
1970 if p.typ == Viewtype::Image || p.typ == Viewtype::Gif {
1971 is_first_image = false;
1972 }
1973 retain
1974 });
1975 }
1976 }
1977 }
1978
1979 fn maybe_remove_inline_mailinglist_footer(&mut self) {
1989 if self.is_mailinglist_message() && !self.is_schleuder_message() {
1990 let text_part_cnt = self
1991 .parts
1992 .iter()
1993 .filter(|p| p.typ == Viewtype::Text)
1994 .count();
1995 if text_part_cnt == 2
1996 && let Some(last_part) = self.parts.last()
1997 && last_part.typ == Viewtype::Text
1998 {
1999 self.parts.pop();
2000 }
2001 }
2002 }
2003
2004 async fn heuristically_parse_ndn(&mut self, context: &Context) {
2008 let maybe_ndn = if let Some(from) = self.get_header(HeaderDef::From_) {
2009 let from = from.to_ascii_lowercase();
2010 from.contains("mailer-daemon") || from.contains("mail-daemon")
2011 } else {
2012 false
2013 };
2014 if maybe_ndn && self.delivery_report.is_none() {
2015 for original_message_id in self
2016 .parts
2017 .iter()
2018 .filter_map(|part| part.msg_raw.as_ref())
2019 .flat_map(|part| part.lines())
2020 .filter_map(|line| line.split_once("Message-ID:"))
2021 .filter_map(|(_, message_id)| parse_message_id(message_id).ok())
2022 {
2023 if let Ok(Some(_)) = message::rfc724_mid_exists(context, &original_message_id).await
2024 {
2025 self.delivery_report = Some(DeliveryReport {
2026 rfc724_mid: original_message_id,
2027 failure: true,
2028 })
2029 }
2030 }
2031 }
2032 }
2033
2034 pub async fn handle_reports(&self, context: &Context, from_id: ContactId, parts: &[Part]) {
2038 for report in &self.mdn_reports {
2039 for original_message_id in report
2040 .original_message_id
2041 .iter()
2042 .chain(&report.additional_message_ids)
2043 {
2044 if let Err(err) =
2045 handle_mdn(context, from_id, original_message_id, self.timestamp_sent).await
2046 {
2047 warn!(context, "Could not handle MDN: {err:#}.");
2048 }
2049 }
2050 }
2051
2052 if let Some(delivery_report) = &self.delivery_report
2053 && delivery_report.failure
2054 {
2055 let error = parts
2056 .iter()
2057 .find(|p| p.typ == Viewtype::Text)
2058 .map(|p| p.msg.clone());
2059 if let Err(err) = handle_ndn(context, delivery_report, error).await {
2060 warn!(context, "Could not handle NDN: {err:#}.");
2061 }
2062 }
2063 }
2064
2065 pub async fn get_parent_timestamp(&self, context: &Context) -> Result<Option<i64>> {
2070 let parent_timestamp = if let Some(field) = self
2071 .get_header(HeaderDef::InReplyTo)
2072 .and_then(|msgid| parse_message_id(msgid).ok())
2073 {
2074 context
2075 .sql
2076 .query_get_value("SELECT timestamp FROM msgs WHERE rfc724_mid=?", (field,))
2077 .await?
2078 } else {
2079 None
2080 };
2081 Ok(parent_timestamp)
2082 }
2083
2084 #[expect(clippy::arithmetic_side_effects)]
2088 pub fn chat_group_member_timestamps(&self) -> Option<Vec<i64>> {
2089 let now = time() + constants::TIMESTAMP_SENT_TOLERANCE;
2090 self.get_header(HeaderDef::ChatGroupMemberTimestamps)
2091 .map(|h| {
2092 h.split_ascii_whitespace()
2093 .filter_map(|ts| ts.parse::<i64>().ok())
2094 .map(|ts| std::cmp::min(now, ts))
2095 .collect()
2096 })
2097 }
2098
2099 pub fn chat_group_member_fingerprints(&self) -> Vec<Fingerprint> {
2102 if let Some(header) = self.get_header(HeaderDef::ChatGroupMemberFpr) {
2103 header
2104 .split_ascii_whitespace()
2105 .filter_map(|fpr| Fingerprint::from_str(fpr).ok())
2106 .collect()
2107 } else {
2108 Vec::new()
2109 }
2110 }
2111}
2112
2113async fn load_shared_secrets(context: &Context) -> Result<Vec<String>> {
2116 let mut secrets: Vec<String> = context
2120 .sql
2121 .query_map_vec("SELECT invite FROM bobstate", (), |row| {
2122 let invite: crate::securejoin::QrInvite = row.get(0)?;
2123 Ok(invite.authcode().to_string())
2124 })
2125 .await?;
2126 secrets.extend(
2128 context
2129 .sql
2130 .query_map_vec("SELECT secret FROM broadcast_secrets", (), |row| {
2131 let secret: String = row.get(0)?;
2132 Ok(secret)
2133 })
2134 .await?,
2135 );
2136 secrets.extend(token::lookup_all(context, token::Namespace::Auth).await?);
2139 Ok(secrets)
2140}
2141
2142fn rm_legacy_display_elements(text: &str) -> String {
2143 let mut res = None;
2144 for l in text.lines() {
2145 res = res.map(|r: String| match r.is_empty() {
2146 true => l.to_string(),
2147 false => r + "\r\n" + l,
2148 });
2149 if l.is_empty() {
2150 res = Some(String::new());
2151 }
2152 }
2153 res.unwrap_or_default()
2154}
2155
2156fn remove_header(
2157 headers: &mut HashMap<String, String>,
2158 key: &str,
2159 removed: &mut HashSet<String>,
2160) -> Option<String> {
2161 if let Some((k, v)) = headers.remove_entry(key) {
2162 removed.insert(k);
2163 Some(v)
2164 } else {
2165 None
2166 }
2167}
2168
2169async fn parse_gossip_headers(
2175 context: &Context,
2176 from: &str,
2177 recipients: &[SingleInfo],
2178 gossip_headers: Vec<String>,
2179) -> Result<BTreeMap<String, GossipedKey>> {
2180 let mut gossiped_keys: BTreeMap<String, GossipedKey> = Default::default();
2182
2183 for value in &gossip_headers {
2184 let header = match value.parse::<Aheader>() {
2185 Ok(header) => header,
2186 Err(err) => {
2187 warn!(context, "Failed parsing Autocrypt-Gossip header: {}", err);
2188 continue;
2189 }
2190 };
2191
2192 if !recipients
2193 .iter()
2194 .any(|info| addr_cmp(&info.addr, &header.addr))
2195 {
2196 warn!(
2197 context,
2198 "Ignoring gossiped \"{}\" as the address is not in To/Cc list.", &header.addr,
2199 );
2200 continue;
2201 }
2202 if addr_cmp(from, &header.addr) {
2203 warn!(
2205 context,
2206 "Ignoring gossiped \"{}\" as it equals the From address", &header.addr,
2207 );
2208 continue;
2209 }
2210
2211 let fingerprint = header.public_key.dc_fingerprint().hex();
2212 context
2213 .sql
2214 .execute(
2215 "INSERT INTO public_keys (fingerprint, public_key)
2216 VALUES (?, ?)
2217 ON CONFLICT (fingerprint)
2218 DO NOTHING",
2219 (&fingerprint, header.public_key.to_bytes()),
2220 )
2221 .await?;
2222
2223 let gossiped_key = GossipedKey {
2224 public_key: header.public_key,
2225
2226 verified: header.verified,
2227 };
2228 gossiped_keys.insert(header.addr.to_lowercase(), gossiped_key);
2229 }
2230
2231 Ok(gossiped_keys)
2232}
2233
2234#[derive(Debug)]
2236pub(crate) struct Report {
2237 pub original_message_id: Option<String>,
2242 pub additional_message_ids: Vec<String>,
2244}
2245
2246#[derive(Debug)]
2248pub(crate) struct DeliveryReport {
2249 pub rfc724_mid: String,
2250 pub failure: bool,
2251}
2252
2253pub(crate) fn parse_message_ids(ids: &str) -> Vec<String> {
2254 let mut msgids = Vec::new();
2256 for id in ids.split_whitespace() {
2257 let mut id = id.to_string();
2258 if let Some(id_without_prefix) = id.strip_prefix('<') {
2259 id = id_without_prefix.to_string();
2260 };
2261 if let Some(id_without_suffix) = id.strip_suffix('>') {
2262 id = id_without_suffix.to_string();
2263 };
2264 if !id.is_empty() {
2265 msgids.push(id);
2266 }
2267 }
2268 msgids
2269}
2270
2271pub(crate) fn parse_message_id(ids: &str) -> Result<String> {
2272 if let Some(id) = parse_message_ids(ids).first() {
2273 Ok(id.to_string())
2274 } else {
2275 bail!("could not parse message_id: {ids}");
2276 }
2277}
2278
2279fn is_protected(key: &str) -> bool {
2286 key.starts_with("chat-")
2287 || matches!(
2288 key,
2289 "return-path"
2290 | "auto-submitted"
2291 | "autocrypt-setup-message"
2292 | "date"
2293 | "from"
2294 | "sender"
2295 | "reply-to"
2296 | "to"
2297 | "cc"
2298 | "bcc"
2299 | "message-id"
2300 | "in-reply-to"
2301 | "references"
2302 | "secure-join"
2303 )
2304}
2305
2306pub(crate) fn is_hidden(key: &str) -> bool {
2308 matches!(
2309 key,
2310 "chat-user-avatar" | "chat-group-avatar" | "chat-delete" | "chat-edit"
2311 )
2312}
2313
2314#[derive(Debug, Default, Clone)]
2316pub struct Part {
2317 pub typ: Viewtype,
2319
2320 pub mimetype: Option<Mime>,
2322
2323 pub msg: String,
2325
2326 pub msg_raw: Option<String>,
2328
2329 pub bytes: usize,
2331
2332 pub param: Params,
2334
2335 pub(crate) org_filename: Option<String>,
2337
2338 pub error: Option<String>,
2340
2341 pub(crate) dehtml_failed: bool,
2343
2344 pub(crate) is_related: bool,
2351
2352 pub(crate) is_reaction: bool,
2354}
2355
2356fn get_mime_type(
2361 mail: &mailparse::ParsedMail<'_>,
2362 filename: &Option<String>,
2363 is_chat_msg: bool,
2364) -> Result<(Mime, Viewtype)> {
2365 let mimetype = mail.ctype.mimetype.parse::<Mime>()?;
2366
2367 let viewtype = match mimetype.type_() {
2368 mime::TEXT => match mimetype.subtype() {
2369 mime::VCARD => Viewtype::Vcard,
2370 mime::PLAIN | mime::HTML if !is_attachment_disposition(mail) => Viewtype::Text,
2371 _ => Viewtype::File,
2372 },
2373 mime::IMAGE => match mimetype.subtype() {
2374 mime::GIF => Viewtype::Gif,
2375 mime::SVG => Viewtype::File,
2376 _ => Viewtype::Image,
2377 },
2378 mime::AUDIO => Viewtype::Audio,
2379 mime::VIDEO => Viewtype::Video,
2380 mime::MULTIPART => Viewtype::Unknown,
2381 mime::MESSAGE => {
2382 if is_attachment_disposition(mail) {
2383 Viewtype::File
2384 } else {
2385 Viewtype::Unknown
2393 }
2394 }
2395 mime::APPLICATION => match mimetype.subtype() {
2396 mime::OCTET_STREAM => match filename {
2397 Some(filename) if !is_chat_msg => {
2398 match message::guess_msgtype_from_path_suffix(Path::new(&filename)) {
2399 Some((viewtype, _)) => viewtype,
2400 None => Viewtype::File,
2401 }
2402 }
2403 _ => Viewtype::File,
2404 },
2405 _ => Viewtype::File,
2406 },
2407 _ => Viewtype::Unknown,
2408 };
2409
2410 Ok((mimetype, viewtype))
2411}
2412
2413fn is_attachment_disposition(mail: &mailparse::ParsedMail<'_>) -> bool {
2414 let ct = mail.get_content_disposition();
2415 ct.disposition == DispositionType::Attachment
2416 && ct
2417 .params
2418 .iter()
2419 .any(|(key, _value)| key.starts_with("filename"))
2420}
2421
2422fn get_attachment_filename(
2429 context: &Context,
2430 mail: &mailparse::ParsedMail,
2431) -> Result<Option<String>> {
2432 let ct = mail.get_content_disposition();
2433
2434 let mut desired_filename = ct.params.get("filename").map(|s| s.to_string());
2437
2438 if desired_filename.is_none()
2439 && let Some(name) = ct.params.get("filename*").map(|s| s.to_string())
2440 {
2441 warn!(context, "apostrophed encoding invalid: {}", name);
2445 desired_filename = Some(name);
2446 }
2447
2448 if desired_filename.is_none() {
2450 desired_filename = ct.params.get("name").map(|s| s.to_string());
2451 }
2452
2453 if desired_filename.is_none() {
2456 desired_filename = mail.ctype.params.get("name").map(|s| s.to_string());
2457 }
2458
2459 if desired_filename.is_none() && ct.disposition == DispositionType::Attachment {
2461 if let Some(subtype) = mail.ctype.mimetype.split('/').nth(1) {
2462 desired_filename = Some(format!("file.{subtype}",));
2463 } else {
2464 bail!(
2465 "could not determine attachment filename: {:?}",
2466 ct.disposition
2467 );
2468 };
2469 }
2470
2471 let desired_filename = desired_filename.map(|filename| sanitize_bidi_characters(&filename));
2472
2473 Ok(desired_filename)
2474}
2475
2476pub(crate) fn get_recipients(headers: &[MailHeader]) -> Vec<SingleInfo> {
2478 let to_addresses = get_all_addresses_from_header(headers, "to");
2479 let cc_addresses = get_all_addresses_from_header(headers, "cc");
2480
2481 let mut res = to_addresses;
2482 res.extend(cc_addresses);
2483 res
2484}
2485
2486pub(crate) fn get_from(headers: &[MailHeader]) -> Option<SingleInfo> {
2488 let all = get_all_addresses_from_header(headers, "from");
2489 tools::single_value(all)
2490}
2491
2492pub(crate) fn get_list_post(headers: &[MailHeader]) -> Option<String> {
2494 get_all_addresses_from_header(headers, "list-post")
2495 .into_iter()
2496 .next()
2497 .map(|s| s.addr)
2498}
2499
2500fn get_all_addresses_from_header(headers: &[MailHeader], header: &str) -> Vec<SingleInfo> {
2512 let mut result: Vec<SingleInfo> = Default::default();
2513
2514 if let Some(header) = headers
2515 .iter()
2516 .rev()
2517 .find(|h| h.get_key().to_lowercase() == header)
2518 && let Ok(addrs) = mailparse::addrparse_header(header)
2519 {
2520 for addr in addrs.iter() {
2521 match addr {
2522 mailparse::MailAddr::Single(info) => {
2523 result.push(SingleInfo {
2524 addr: addr_normalize(&info.addr).to_lowercase(),
2525 display_name: info.display_name.clone(),
2526 });
2527 }
2528 mailparse::MailAddr::Group(infos) => {
2529 for info in &infos.addrs {
2530 result.push(SingleInfo {
2531 addr: addr_normalize(&info.addr).to_lowercase(),
2532 display_name: info.display_name.clone(),
2533 });
2534 }
2535 }
2536 }
2537 }
2538 }
2539
2540 result
2541}
2542
2543async fn handle_mdn(
2544 context: &Context,
2545 from_id: ContactId,
2546 rfc724_mid: &str,
2547 timestamp_sent: i64,
2548) -> Result<()> {
2549 if from_id == ContactId::SELF {
2550 return Ok(());
2552 }
2553
2554 let Some((msg_id, chat_id, has_mdns, is_dup)) = context
2555 .sql
2556 .query_row_optional(
2557 "SELECT
2558 m.id AS msg_id,
2559 c.id AS chat_id,
2560 mdns.contact_id AS mdn_contact
2561 FROM msgs m
2562 LEFT JOIN chats c ON m.chat_id=c.id
2563 LEFT JOIN msgs_mdns mdns ON mdns.msg_id=m.id
2564 WHERE rfc724_mid=? AND from_id=1
2565 ORDER BY msg_id DESC, mdn_contact=? DESC
2566 LIMIT 1",
2567 (&rfc724_mid, from_id),
2568 |row| {
2569 let msg_id: MsgId = row.get("msg_id")?;
2570 let chat_id: ChatId = row.get("chat_id")?;
2571 let mdn_contact: Option<ContactId> = row.get("mdn_contact")?;
2572 Ok((
2573 msg_id,
2574 chat_id,
2575 mdn_contact.is_some(),
2576 mdn_contact == Some(from_id),
2577 ))
2578 },
2579 )
2580 .await?
2581 else {
2582 info!(
2583 context,
2584 "Ignoring MDN, found no message with Message-ID {rfc724_mid:?} sent by us in the database.",
2585 );
2586 return Ok(());
2587 };
2588
2589 if is_dup {
2590 return Ok(());
2591 }
2592 context
2593 .sql
2594 .execute(
2595 "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?)",
2596 (msg_id, from_id, timestamp_sent),
2597 )
2598 .await?;
2599 if !has_mdns {
2600 context.emit_event(EventType::MsgRead { chat_id, msg_id });
2601 chatlist_events::emit_chatlist_item_changed(context, chat_id);
2603 }
2604 Ok(())
2605}
2606
2607async fn handle_ndn(
2610 context: &Context,
2611 failed: &DeliveryReport,
2612 error: Option<String>,
2613) -> Result<()> {
2614 if failed.rfc724_mid.is_empty() {
2615 return Ok(());
2616 }
2617
2618 let msg_ids = context
2621 .sql
2622 .query_map_vec(
2623 "SELECT id FROM msgs
2624 WHERE rfc724_mid=? AND from_id=1",
2625 (&failed.rfc724_mid,),
2626 |row| {
2627 let msg_id: MsgId = row.get(0)?;
2628 Ok(msg_id)
2629 },
2630 )
2631 .await?;
2632
2633 let error = if let Some(error) = error {
2634 error
2635 } else {
2636 "Delivery to at least one recipient failed.".to_string()
2637 };
2638 let err_msg = &error;
2639
2640 for msg_id in msg_ids {
2641 let mut message = Message::load_from_db(context, msg_id).await?;
2642 let aggregated_error = message
2643 .error
2644 .as_ref()
2645 .map(|err| format!("{err}\n\n{err_msg}"));
2646 set_msg_failed(
2647 context,
2648 &mut message,
2649 aggregated_error.as_ref().unwrap_or(err_msg),
2650 )
2651 .await?;
2652 }
2653
2654 Ok(())
2655}
2656
2657#[cfg(test)]
2658mod mimeparser_tests;