Skip to main content

deltachat/
mimefactory.rs

1//! # MIME message production.
2
3use std::collections::{BTreeSet, HashSet};
4use std::io::Cursor;
5
6use anyhow::{Context as _, Result, bail, format_err};
7use base64::Engine as _;
8use data_encoding::BASE32_NOPAD;
9use deltachat_contact_tools::sanitize_bidi_characters;
10use iroh_gossip::proto::TopicId;
11use mail_builder::headers::HeaderType;
12use mail_builder::headers::address::Address;
13use mail_builder::mime::MimePart;
14use tokio::fs;
15
16use crate::aheader::{Aheader, EncryptPreference};
17use crate::blob::BlobObject;
18use crate::chat::{self, Chat, PARAM_BROADCAST_SECRET, load_broadcast_secret};
19use crate::config::Config;
20use crate::constants::{BROADCAST_INCOMPATIBILITY_MSG, Chattype, DC_FROM_HANDSHAKE};
21use crate::contact::{Contact, ContactId, Origin};
22use crate::context::Context;
23use crate::download::PostMsgMetadata;
24use crate::e2ee::EncryptHelper;
25use crate::ensure_and_debug_assert;
26use crate::ephemeral::Timer as EphemeralTimer;
27use crate::headerdef::HeaderDef;
28use crate::key::{DcKey, SignedPublicKey, self_fingerprint};
29use crate::location;
30use crate::log::warn;
31use crate::message::{Message, MsgId, Viewtype};
32use crate::mimeparser::{SystemMessage, is_hidden};
33use crate::param::Param;
34use crate::peer_channels::{create_iroh_header, get_iroh_topic_for_msg};
35use crate::pgp::{SeipdVersion, addresses_from_public_key, pubkey_supports_seipdv2};
36use crate::simplify::escape_message_footer_marks;
37use crate::stock_str;
38use crate::tools::{IsNoneOrEmpty, create_outgoing_rfc724_mid, remove_subject_prefix, time};
39use crate::webxdc::StatusUpdateSerial;
40
41// attachments of 25 mb brutto should work on the majority of providers
42// (brutto examples: web.de=50, 1&1=40, t-online.de=32, gmail=25, posteo=50, yahoo=25, all-inkl=100).
43// to get the netto sizes, we subtract 1 mb header-overhead and the base64-overhead.
44pub const RECOMMENDED_FILE_SIZE: u64 = 24 * 1024 * 1024 / 4 * 3;
45
46#[derive(Debug, Clone)]
47#[expect(clippy::large_enum_variant)]
48pub enum Loaded {
49    Message {
50        chat: Chat,
51        msg: Message,
52    },
53    Mdn {
54        rfc724_mid: String,
55        additional_msg_ids: Vec<String>,
56    },
57}
58
59#[derive(Debug, Clone, PartialEq)]
60pub enum PreMessageMode {
61    /// adds the Chat-Is-Post-Message header in unprotected part
62    Post,
63    /// adds the Chat-Post-Message-ID header to protected part
64    /// also adds metadata and explicitly excludes attachment
65    Pre { post_msg_rfc724_mid: String },
66    /// Atomic ("normal") message.
67    None,
68}
69
70/// Helper to construct mime messages.
71#[derive(Debug, Clone)]
72pub struct MimeFactory {
73    from_addr: String,
74    from_displayname: String,
75
76    /// Goes to the `Sender:`-header, if set.
77    /// For overridden names, `sender_displayname` is set to the
78    /// config-name while `from_displayname` is set to the overridden name.
79    /// From the perspective of the receiver,
80    /// a set `Sender:`-header is used as an indicator that the name is overridden;
81    /// names are alsways read from the `From:`-header.
82    sender_displayname: Option<String>,
83
84    selfstatus: String,
85
86    /// Vector of actual recipient addresses.
87    ///
88    /// This is the list of addresses the message should be sent to.
89    /// It is not the same as the `To` header,
90    /// because in case of "member removed" message
91    /// removed member is in the recipient list,
92    /// but not in the `To` header.
93    /// In case of broadcast channels there are multiple recipients,
94    /// but the `To` header has no members.
95    ///
96    /// If `bcc_self` configuration is enabled,
97    /// this list will be extended with own address later,
98    /// but `MimeFactory` is not responsible for this.
99    recipients: Vec<String>,
100
101    /// Vector of pairs of recipient
102    /// addresses and OpenPGP keys
103    /// to use for encryption.
104    ///
105    /// If `Some`, encrypt to self also.
106    /// `None` if the message is not encrypted.
107    encryption_pubkeys: Option<Vec<(String, SignedPublicKey)>>,
108
109    /// Vector of pairs of recipient name and address that goes into the `To` field.
110    ///
111    /// The list of actual message recipient addresses may be different,
112    /// e.g. if members are hidden for broadcast channels
113    /// or if the keys for some recipients are missing
114    /// and encrypted message cannot be sent to them.
115    to: Vec<(String, String)>,
116
117    /// Vector of pairs of past group member names and addresses.
118    past_members: Vec<(String, String)>,
119
120    /// Fingerprints of the members in the same order as in the `to`
121    /// followed by `past_members`.
122    ///
123    /// If this is not empty, its length
124    /// should be the sum of `to` and `past_members` length.
125    member_fingerprints: Vec<String>,
126
127    /// Timestamps of the members in the same order as in the `to`
128    /// followed by `past_members`.
129    ///
130    /// If this is not empty, its length
131    /// should be the sum of `to` and `past_members` length.
132    member_timestamps: Vec<i64>,
133
134    timestamp: i64,
135    loaded: Loaded,
136    in_reply_to: String,
137
138    /// List of Message-IDs for `References` header.
139    references: Vec<String>,
140
141    /// True if the message requests Message Disposition Notification
142    /// using `Chat-Disposition-Notification-To` header.
143    req_mdn: bool,
144
145    last_added_location_id: Option<u32>,
146
147    /// If the created mime-structure contains sync-items,
148    /// the IDs of these items are listed here.
149    /// The IDs are returned via `RenderedEmail`
150    /// and must be deleted if the message is actually queued for sending.
151    sync_ids_to_delete: Option<String>,
152
153    /// True if the avatar should be attached.
154    pub attach_selfavatar: bool,
155
156    /// This field is used to sustain the topic id of webxdcs needed for peer channels.
157    webxdc_topic: Option<TopicId>,
158
159    /// Pre-message / post-message / atomic message.
160    pre_message_mode: PreMessageMode,
161}
162
163/// Result of rendering a message, ready to be submitted to a send job.
164#[derive(Debug, Clone)]
165pub struct RenderedEmail {
166    pub message: String,
167    // pub envelope: Envelope,
168    pub is_encrypted: bool,
169    pub last_added_location_id: Option<u32>,
170
171    /// A comma-separated string of sync-IDs that are used by the rendered email and must be deleted
172    /// from `multi_device_sync` once the message is actually queued for sending.
173    pub sync_ids_to_delete: Option<String>,
174
175    /// Message ID (Message in the sense of Email)
176    pub rfc724_mid: String,
177
178    /// Message subject.
179    pub subject: String,
180}
181
182fn new_address_with_name(name: &str, address: String) -> Address<'static> {
183    Address::new_address(
184        if name == address || name.is_empty() {
185            None
186        } else {
187            Some(name.to_string())
188        },
189        address,
190    )
191}
192
193impl MimeFactory {
194    /// Returns `MimeFactory` for rendering `msg`.
195    #[expect(clippy::arithmetic_side_effects)]
196    pub async fn from_msg(context: &Context, msg: Message) -> Result<MimeFactory> {
197        let now = time();
198        let chat = Chat::load_from_db(context, msg.chat_id).await?;
199        let attach_profile_data = Self::should_attach_profile_data(&msg);
200        let undisclosed_recipients = should_hide_recipients(&msg, &chat);
201
202        let from_addr = context.get_primary_self_addr().await?;
203        let config_displayname = context
204            .get_config(Config::Displayname)
205            .await?
206            .unwrap_or_default();
207        let (from_displayname, sender_displayname) =
208            if let Some(override_name) = msg.param.get(Param::OverrideSenderDisplayname) {
209                (override_name.to_string(), Some(config_displayname))
210            } else {
211                let name = match attach_profile_data {
212                    true => config_displayname,
213                    false => "".to_string(),
214                };
215                (name, None)
216            };
217
218        let mut recipients = Vec::new();
219        let mut to = Vec::new();
220        let mut past_members = Vec::new();
221        let mut member_fingerprints = Vec::new();
222        let mut member_timestamps = Vec::new();
223        let mut recipient_ids = HashSet::new();
224        let mut req_mdn = false;
225
226        let encryption_pubkeys;
227
228        let self_fingerprint = self_fingerprint(context).await?;
229
230        if chat.is_self_talk() {
231            to.push((from_displayname.to_string(), from_addr.to_string()));
232
233            encryption_pubkeys = Some(Vec::new());
234        } else if chat.is_mailing_list() {
235            let list_post = chat
236                .param
237                .get(Param::ListPost)
238                .context("Can't write to mailinglist without ListPost param")?;
239            to.push(("".to_string(), list_post.to_string()));
240            recipients.push(list_post.to_string());
241
242            // Do not encrypt messages to mailing lists.
243            encryption_pubkeys = None;
244        } else if let Some(fp) = must_have_only_one_recipient(&msg, &chat) {
245            let fp = fp?;
246            // In a broadcast channel, only send member-added/removed messages
247            // to the affected member
248            let (authname, addr) = context
249                .sql
250                .query_row(
251                    "SELECT authname, addr FROM contacts WHERE fingerprint=?",
252                    (fp,),
253                    |row| {
254                        let authname: String = row.get(0)?;
255                        let addr: String = row.get(1)?;
256                        Ok((authname, addr))
257                    },
258                )
259                .await?;
260
261            let public_key_bytes: Vec<_> = context
262                .sql
263                .query_get_value(
264                    "SELECT public_key FROM public_keys WHERE fingerprint=?",
265                    (fp,),
266                )
267                .await?
268                .context("Can't send member addition/removal: missing key")?;
269
270            let public_key = SignedPublicKey::from_slice(&public_key_bytes)?;
271
272            let relays =
273                addresses_from_public_key(&public_key).unwrap_or_else(|| vec![addr.clone()]);
274            recipients.extend(relays);
275            to.push((authname, addr.clone()));
276
277            encryption_pubkeys = Some(vec![(addr, public_key)]);
278        } else {
279            let email_to_remove = if msg.param.get_cmd() == SystemMessage::MemberRemovedFromGroup {
280                msg.param.get(Param::Arg)
281            } else {
282                None
283            };
284
285            let is_encrypted = if msg
286                .param
287                .get_bool(Param::ForcePlaintext)
288                .unwrap_or_default()
289            {
290                false
291            } else {
292                msg.param.get_bool(Param::GuaranteeE2ee).unwrap_or_default()
293                    || chat.is_encrypted(context).await?
294            };
295
296            let mut keys = Vec::new();
297            let mut missing_key_addresses = BTreeSet::new();
298            context
299                .sql
300                // Sort recipients by `add_timestamp DESC` so that if the group is large and there
301                // are multiple SMTP messages, a newly added member receives the member addition
302                // message earlier and has gossiped keys of other members (otherwise the new member
303                // may receive messages from other members earlier and fail to verify them).
304                .query_map(
305                    "SELECT
306                     c.authname,
307                     c.addr,
308                     c.fingerprint,
309                     c.id,
310                     cc.add_timestamp,
311                     cc.remove_timestamp,
312                     k.public_key
313                     FROM chats_contacts cc
314                     LEFT JOIN contacts c ON cc.contact_id=c.id
315                     LEFT JOIN public_keys k ON k.fingerprint=c.fingerprint
316                     WHERE cc.chat_id=?
317                     AND (cc.contact_id>9 OR (cc.contact_id=1 AND ?))
318                     ORDER BY cc.add_timestamp DESC",
319                    (msg.chat_id, chat.typ == Chattype::Group),
320                    |row| {
321                        let authname: String = row.get(0)?;
322                        let addr: String = row.get(1)?;
323                        let fingerprint: String = row.get(2)?;
324                        let id: ContactId = row.get(3)?;
325                        let add_timestamp: i64 = row.get(4)?;
326                        let remove_timestamp: i64 = row.get(5)?;
327                        let public_key_bytes_opt: Option<Vec<u8>> = row.get(6)?;
328                        Ok((authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt))
329                    },
330                    |rows| {
331                        let mut past_member_timestamps = Vec::new();
332                        let mut past_member_fingerprints = Vec::new();
333
334                        for row in rows {
335                            let (authname, addr, fingerprint, id, add_timestamp, remove_timestamp, public_key_bytes_opt) = row?;
336
337                            let public_key_opt = if let Some(public_key_bytes) = &public_key_bytes_opt {
338                                Some(SignedPublicKey::from_slice(public_key_bytes)?)
339                            } else {
340                                None
341                            };
342
343                            let addr = if id == ContactId::SELF {
344                                from_addr.to_string()
345                            } else {
346                                addr
347                            };
348                            let name = match attach_profile_data {
349                                true => authname,
350                                false => "".to_string(),
351                            };
352                            if add_timestamp >= remove_timestamp {
353                                let relays = if let Some(public_key) = public_key_opt {
354                                    let addrs = addresses_from_public_key(&public_key);
355                                    keys.push((addr.clone(), public_key));
356                                    addrs
357                                } else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat) {
358                                    missing_key_addresses.insert(addr.clone());
359                                    if is_encrypted {
360                                        warn!(context, "Missing key for {addr}");
361                                    }
362                                    None
363                                } else {
364                                    None
365                                }.unwrap_or_else(|| vec![addr.clone()]);
366
367                                if !recipients_contain_addr(&to, &addr) {
368                                    if id != ContactId::SELF {
369                                        recipients.extend(relays);
370                                    }
371                                    if !undisclosed_recipients {
372                                        to.push((name, addr.clone()));
373
374                                        if is_encrypted {
375                                            if !fingerprint.is_empty() {
376                                                member_fingerprints.push(fingerprint);
377                                            } else if id == ContactId::SELF {
378                                                member_fingerprints.push(self_fingerprint.to_string());
379                                            } else {
380                                                ensure_and_debug_assert!(member_fingerprints.is_empty(), "If some member is a key-contact, all other members should be key-contacts too");
381                                            }
382                                        }
383                                        member_timestamps.push(add_timestamp);
384                                    }
385                                }
386                                recipient_ids.insert(id);
387                            } else if remove_timestamp.saturating_add(60 * 24 * 3600) > now {
388                                // Row is a tombstone,
389                                // member is not actually part of the group.
390                                if !recipients_contain_addr(&past_members, &addr) {
391                                    if let Some(email_to_remove) = email_to_remove
392                                        && email_to_remove == addr {
393                                            let relays = if let Some(public_key) = public_key_opt {
394                                                let addrs = addresses_from_public_key(&public_key);
395                                                keys.push((addr.clone(), public_key));
396                                                addrs
397                                            } else if id != ContactId::SELF && !should_encrypt_symmetrically(&msg, &chat)  {
398                                                missing_key_addresses.insert(addr.clone());
399                                                if is_encrypted {
400                                                    warn!(context, "Missing key for {addr}");
401                                                }
402                                                None
403                                            } else {
404                                                None
405                                            }.unwrap_or_else(|| vec![addr.clone()]);
406
407                                            // This is a "member removed" message,
408                                            // we need to notify removed member
409                                            // that it was removed.
410                                            if id != ContactId::SELF {
411                                                recipients.extend(relays);
412                                            }
413                                        }
414                                    if !undisclosed_recipients {
415                                        past_members.push((name, addr.clone()));
416                                        past_member_timestamps.push(remove_timestamp);
417
418                                        if is_encrypted {
419                                            if !fingerprint.is_empty() {
420                                                past_member_fingerprints.push(fingerprint);
421                                            } else if id == ContactId::SELF {
422                                                // It's fine to have self in past members
423                                                // if we are leaving the group.
424                                                past_member_fingerprints.push(self_fingerprint.to_string());
425                                            } else {
426                                                ensure_and_debug_assert!(past_member_fingerprints.is_empty(), "If some past member is a key-contact, all other past members should be key-contacts too");
427                                            }
428                                        }
429                                    }
430                                }
431                            }
432                        }
433
434                        ensure_and_debug_assert!(
435                            member_timestamps.len() >= to.len(),
436                            "member_timestamps.len() ({}) < to.len() ({})",
437                            member_timestamps.len(), to.len());
438                        ensure_and_debug_assert!(
439                            member_fingerprints.is_empty() || member_fingerprints.len() >= to.len(),
440                            "member_fingerprints.len() ({}) < to.len() ({})",
441                            member_fingerprints.len(), to.len());
442
443                        if to.len() > 1
444                            && let Some(position) = to.iter().position(|(_, x)| x == &from_addr) {
445                                to.remove(position);
446                                member_timestamps.remove(position);
447                                if is_encrypted {
448                                    member_fingerprints.remove(position);
449                                }
450                            }
451
452                        member_timestamps.extend(past_member_timestamps);
453                        if is_encrypted {
454                            member_fingerprints.extend(past_member_fingerprints);
455                        }
456                        Ok(())
457                    },
458                )
459                .await?;
460            let recipient_ids: Vec<_> = recipient_ids
461                .into_iter()
462                .filter(|id| *id != ContactId::SELF)
463                .collect();
464            if !matches!(
465                msg.param.get_cmd(),
466                SystemMessage::MemberRemovedFromGroup | SystemMessage::SecurejoinMessage
467            ) && !matches!(chat.typ, Chattype::OutBroadcast | Chattype::InBroadcast)
468            {
469                let origin = match recipient_ids.len() {
470                    1 => Origin::OutgoingTo,
471                    // Use the same origin as ChatId::accept_ex() does for groups.
472                    _ => Origin::IncomingTo,
473                };
474                info!(
475                    context,
476                    "Scale up origin of {} recipients to {origin:?}.", chat.id
477                );
478                ContactId::scaleup_origin(context, &recipient_ids, origin).await?;
479            }
480
481            if !msg.is_system_message()
482                && msg.param.get_int(Param::Reaction).unwrap_or_default() == 0
483                && context.should_request_mdns().await?
484            {
485                req_mdn = true;
486            }
487
488            encryption_pubkeys = if !is_encrypted {
489                None
490            } else if should_encrypt_symmetrically(&msg, &chat) {
491                Some(Vec::new())
492            } else {
493                if keys.is_empty() && !recipients.is_empty() {
494                    bail!("No recipient keys are available, cannot encrypt to {recipients:?}.");
495                }
496
497                // Remove recipients for which the key is missing.
498                if !missing_key_addresses.is_empty() {
499                    recipients.retain(|addr| !missing_key_addresses.contains(addr));
500                }
501
502                Some(keys)
503            };
504        }
505
506        let (in_reply_to, references) = context
507            .sql
508            .query_row(
509                "SELECT mime_in_reply_to, IFNULL(mime_references, '')
510                 FROM msgs WHERE id=?",
511                (msg.id,),
512                |row| {
513                    let in_reply_to: String = row.get(0)?;
514                    let references: String = row.get(1)?;
515
516                    Ok((in_reply_to, references))
517                },
518            )
519            .await?;
520        let references: Vec<String> = references
521            .trim()
522            .split_ascii_whitespace()
523            .map(|s| s.trim_start_matches('<').trim_end_matches('>').to_string())
524            .collect();
525        let selfstatus = match attach_profile_data {
526            true => context
527                .get_config(Config::Selfstatus)
528                .await?
529                .unwrap_or_default(),
530            false => "".to_string(),
531        };
532        // We don't display avatars for address-contacts, so sending avatars w/o encryption is not
533        // useful and causes e.g. Outlook to reject a message with a big header, see
534        // https://support.delta.chat/t/invalid-mime-content-single-text-value-size-32822-exceeded-allowed-maximum-32768-for-the-chat-user-avatar-header/4067.
535        let attach_selfavatar =
536            Self::should_attach_selfavatar(context, &msg).await && encryption_pubkeys.is_some();
537
538        ensure_and_debug_assert!(
539            member_timestamps.is_empty()
540                || to.len() + past_members.len() == member_timestamps.len(),
541            "to.len() ({}) + past_members.len() ({}) != member_timestamps.len() ({})",
542            to.len(),
543            past_members.len(),
544            member_timestamps.len(),
545        );
546        let webxdc_topic = get_iroh_topic_for_msg(context, msg.id).await?;
547        let factory = MimeFactory {
548            from_addr,
549            from_displayname,
550            sender_displayname,
551            selfstatus,
552            recipients,
553            encryption_pubkeys,
554            to,
555            past_members,
556            member_fingerprints,
557            member_timestamps,
558            timestamp: msg.timestamp_sort,
559            loaded: Loaded::Message { msg, chat },
560            in_reply_to,
561            references,
562            req_mdn,
563            last_added_location_id: None,
564            sync_ids_to_delete: None,
565            attach_selfavatar,
566            webxdc_topic,
567            pre_message_mode: PreMessageMode::None,
568        };
569        Ok(factory)
570    }
571
572    pub async fn from_mdn(
573        context: &Context,
574        from_id: ContactId,
575        rfc724_mid: String,
576        additional_msg_ids: Vec<String>,
577    ) -> Result<MimeFactory> {
578        let contact = Contact::get_by_id(context, from_id).await?;
579        let from_addr = context.get_primary_self_addr().await?;
580        let timestamp = time();
581
582        let addr = contact.get_addr().to_string();
583        let encryption_pubkeys = if from_id == ContactId::SELF {
584            Some(Vec::new())
585        } else if contact.is_key_contact() {
586            if let Some(key) = contact.public_key(context).await? {
587                Some(vec![(addr.clone(), key)])
588            } else {
589                Some(Vec::new())
590            }
591        } else {
592            None
593        };
594
595        let res = MimeFactory {
596            from_addr,
597            from_displayname: "".to_string(),
598            sender_displayname: None,
599            selfstatus: "".to_string(),
600            recipients: vec![addr],
601            encryption_pubkeys,
602            to: vec![("".to_string(), contact.get_addr().to_string())],
603            past_members: vec![],
604            member_fingerprints: vec![],
605            member_timestamps: vec![],
606            timestamp,
607            loaded: Loaded::Mdn {
608                rfc724_mid,
609                additional_msg_ids,
610            },
611            in_reply_to: String::default(),
612            references: Vec::new(),
613            req_mdn: false,
614            last_added_location_id: None,
615            sync_ids_to_delete: None,
616            attach_selfavatar: false,
617            webxdc_topic: None,
618            pre_message_mode: PreMessageMode::None,
619        };
620
621        Ok(res)
622    }
623
624    fn should_skip_autocrypt(&self) -> bool {
625        match &self.loaded {
626            Loaded::Message { msg, .. } => {
627                msg.param.get_bool(Param::SkipAutocrypt).unwrap_or_default()
628            }
629            Loaded::Mdn { .. } => true,
630        }
631    }
632
633    fn should_attach_profile_data(msg: &Message) -> bool {
634        msg.param.get_cmd() != SystemMessage::SecurejoinMessage || {
635            let step = msg.param.get(Param::Arg).unwrap_or_default();
636            // Don't attach profile data at the earlier SecureJoin steps:
637            // - The corresponding messages, i.e. "v{c,g}-request" and "v{c,g}-auth-required" are
638            //   deleted right after processing, so other devices won't see the avatar etc.
639            // - It's also good for privacy because the contact isn't yet verified and these
640            //   messages are auto-sent unlike usual unencrypted messages.
641            step == "vg-request-with-auth"
642                || step == "vc-request-with-auth"
643                // Note that for "vg-member-added"
644                // get_cmd() returns `MemberAddedToGroup` rather than `SecurejoinMessage`,
645                // so, it wouldn't actually be necessary to have them in the list here.
646                // Still, they are here for completeness.
647                || step == "vg-member-added"
648                || step == "vc-contact-confirm"
649        }
650    }
651
652    async fn should_attach_selfavatar(context: &Context, msg: &Message) -> bool {
653        Self::should_attach_profile_data(msg)
654            && match chat::shall_attach_selfavatar(context, msg.chat_id).await {
655                Ok(should) => should,
656                Err(err) => {
657                    warn!(
658                        context,
659                        "should_attach_selfavatar: cannot get selfavatar state: {err:#}."
660                    );
661                    false
662                }
663            }
664    }
665
666    fn grpimage(&self) -> Option<String> {
667        match &self.loaded {
668            Loaded::Message { chat, msg } => {
669                let cmd = msg.param.get_cmd();
670
671                match cmd {
672                    SystemMessage::MemberAddedToGroup => {
673                        return chat.param.get(Param::ProfileImage).map(Into::into);
674                    }
675                    SystemMessage::GroupImageChanged => {
676                        return msg.param.get(Param::Arg).map(Into::into);
677                    }
678                    _ => {}
679                }
680
681                if msg
682                    .param
683                    .get_bool(Param::AttachChatAvatarAndDescription)
684                    .unwrap_or_default()
685                {
686                    return chat.param.get(Param::ProfileImage).map(Into::into);
687                }
688
689                None
690            }
691            Loaded::Mdn { .. } => None,
692        }
693    }
694
695    async fn subject_str(&self, context: &Context) -> Result<String> {
696        let subject = match &self.loaded {
697            Loaded::Message { chat, msg } => {
698                let quoted_msg_subject = msg.quoted_message(context).await?.map(|m| m.subject);
699
700                if !msg.subject.is_empty() {
701                    return Ok(msg.subject.clone());
702                }
703
704                if (chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast)
705                    && quoted_msg_subject.is_none_or_empty()
706                {
707                    let re = if self.in_reply_to.is_empty() {
708                        ""
709                    } else {
710                        "Re: "
711                    };
712                    return Ok(format!("{}{}", re, chat.name));
713                }
714
715                let parent_subject = if quoted_msg_subject.is_none_or_empty() {
716                    chat.param.get(Param::LastSubject)
717                } else {
718                    quoted_msg_subject.as_deref()
719                };
720                if let Some(last_subject) = parent_subject {
721                    return Ok(format!("Re: {}", remove_subject_prefix(last_subject)));
722                }
723
724                let self_name = match Self::should_attach_profile_data(msg) {
725                    true => context.get_config(Config::Displayname).await?,
726                    false => None,
727                };
728                let self_name = &match self_name {
729                    Some(name) => name,
730                    None => context.get_config(Config::Addr).await?.unwrap_or_default(),
731                };
732                stock_str::subject_for_new_contact(context, self_name)
733            }
734            Loaded::Mdn { .. } => "Receipt Notification".to_string(), // untranslated to no reveal sender's language
735        };
736
737        Ok(subject)
738    }
739
740    pub fn recipients(&self) -> Vec<String> {
741        self.recipients.clone()
742    }
743
744    /// Consumes a `MimeFactory` and renders it into a message which is then stored in
745    /// `smtp`-table to be used by the SMTP loop
746    #[expect(clippy::arithmetic_side_effects)]
747    pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
748        let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new();
749
750        let from = new_address_with_name(&self.from_displayname, self.from_addr.clone());
751
752        let mut to: Vec<Address<'static>> = Vec::new();
753        for (name, addr) in &self.to {
754            to.push(Address::new_address(
755                if name.is_empty() {
756                    None
757                } else {
758                    Some(name.to_string())
759                },
760                addr.clone(),
761            ));
762        }
763
764        let mut past_members: Vec<Address<'static>> = Vec::new(); // Contents of `Chat-Group-Past-Members` header.
765        for (name, addr) in &self.past_members {
766            past_members.push(Address::new_address(
767                if name.is_empty() {
768                    None
769                } else {
770                    Some(name.to_string())
771                },
772                addr.clone(),
773            ));
774        }
775
776        ensure_and_debug_assert!(
777            self.member_timestamps.is_empty()
778                || to.len() + past_members.len() == self.member_timestamps.len(),
779            "to.len() ({}) + past_members.len() ({}) != self.member_timestamps.len() ({})",
780            to.len(),
781            past_members.len(),
782            self.member_timestamps.len(),
783        );
784        if to.is_empty() {
785            to.push(hidden_recipients());
786        }
787
788        // Start with Internet Message Format headers in the order of the standard example
789        // <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.1.1>.
790        headers.push(("From", from.into()));
791
792        if let Some(sender_displayname) = &self.sender_displayname {
793            let sender = new_address_with_name(sender_displayname, self.from_addr.clone());
794            headers.push(("Sender", sender.into()));
795        }
796        headers.push((
797            "To",
798            mail_builder::headers::address::Address::new_list(to.clone()).into(),
799        ));
800        if !past_members.is_empty() {
801            headers.push((
802                "Chat-Group-Past-Members",
803                mail_builder::headers::address::Address::new_list(past_members.clone()).into(),
804            ));
805        }
806
807        if let Loaded::Message { chat, .. } = &self.loaded
808            && chat.typ == Chattype::Group
809        {
810            if !self.member_timestamps.is_empty() && !chat.member_list_is_stale(context).await? {
811                headers.push((
812                    "Chat-Group-Member-Timestamps",
813                    mail_builder::headers::raw::Raw::new(
814                        self.member_timestamps
815                            .iter()
816                            .map(|ts| ts.to_string())
817                            .collect::<Vec<String>>()
818                            .join(" "),
819                    )
820                    .into(),
821                ));
822            }
823
824            if !self.member_fingerprints.is_empty() {
825                headers.push((
826                    "Chat-Group-Member-Fpr",
827                    mail_builder::headers::raw::Raw::new(
828                        self.member_fingerprints
829                            .iter()
830                            .map(|fp| fp.to_string())
831                            .collect::<Vec<String>>()
832                            .join(" "),
833                    )
834                    .into(),
835                ));
836            }
837        }
838
839        let subject_str = self.subject_str(context).await?;
840        headers.push((
841            "Subject",
842            mail_builder::headers::text::Text::new(subject_str.to_string()).into(),
843        ));
844
845        let date = chrono::DateTime::<chrono::Utc>::from_timestamp(self.timestamp, 0)
846            .unwrap()
847            .to_rfc2822();
848        headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
849
850        let rfc724_mid = match &self.loaded {
851            Loaded::Message { msg, .. } => match &self.pre_message_mode {
852                PreMessageMode::Pre { .. } => {
853                    if msg.pre_rfc724_mid.is_empty() {
854                        create_outgoing_rfc724_mid()
855                    } else {
856                        msg.pre_rfc724_mid.clone()
857                    }
858                }
859                _ => msg.rfc724_mid.clone(),
860            },
861            Loaded::Mdn { .. } => create_outgoing_rfc724_mid(),
862        };
863        headers.push((
864            "Message-ID",
865            mail_builder::headers::message_id::MessageId::new(rfc724_mid.clone()).into(),
866        ));
867
868        // Reply headers as in <https://datatracker.ietf.org/doc/html/rfc5322#appendix-A.2>.
869        if !self.in_reply_to.is_empty() {
870            headers.push((
871                "In-Reply-To",
872                mail_builder::headers::message_id::MessageId::new(self.in_reply_to.clone()).into(),
873            ));
874        }
875        if !self.references.is_empty() {
876            headers.push((
877                "References",
878                mail_builder::headers::message_id::MessageId::<'static>::new_list(
879                    self.references.iter().map(|s| s.to_string()),
880                )
881                .into(),
882            ));
883        }
884
885        // Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
886        if let Loaded::Mdn { .. } = self.loaded {
887            headers.push((
888                "Auto-Submitted",
889                mail_builder::headers::raw::Raw::new("auto-replied".to_string()).into(),
890            ));
891        } else if context.get_config_bool(Config::Bot).await? {
892            headers.push((
893                "Auto-Submitted",
894                mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
895            ));
896        }
897
898        if let Loaded::Message { msg, chat } = &self.loaded
899            && (chat.typ == Chattype::OutBroadcast || chat.typ == Chattype::InBroadcast)
900        {
901            headers.push((
902                "Chat-List-ID",
903                mail_builder::headers::text::Text::new(format!("{} <{}>", chat.name, chat.grpid))
904                    .into(),
905            ));
906
907            if msg.param.get_cmd() == SystemMessage::MemberAddedToGroup
908                && let Some(secret) = msg.param.get(PARAM_BROADCAST_SECRET)
909            {
910                headers.push((
911                    "Chat-Broadcast-Secret",
912                    mail_builder::headers::text::Text::new(secret.to_string()).into(),
913                ));
914            }
915        }
916
917        if let Loaded::Message { msg, .. } = &self.loaded {
918            if let Some(original_rfc724_mid) = msg.param.get(Param::TextEditFor) {
919                headers.push((
920                    "Chat-Edit",
921                    mail_builder::headers::message_id::MessageId::new(
922                        original_rfc724_mid.to_string(),
923                    )
924                    .into(),
925                ));
926            } else if let Some(rfc724_mid_list) = msg.param.get(Param::DeleteRequestFor) {
927                headers.push((
928                    "Chat-Delete",
929                    mail_builder::headers::message_id::MessageId::new(rfc724_mid_list.to_string())
930                        .into(),
931                ));
932            }
933        }
934
935        // Non-standard headers.
936        headers.push((
937            "Chat-Version",
938            mail_builder::headers::raw::Raw::new("1.0").into(),
939        ));
940
941        if self.req_mdn {
942            // we use "Chat-Disposition-Notification-To"
943            // because replies to "Disposition-Notification-To" are weird in many cases
944            // eg. are just freetext and/or do not follow any standard.
945            headers.push((
946                "Chat-Disposition-Notification-To",
947                mail_builder::headers::raw::Raw::new(self.from_addr.clone()).into(),
948            ));
949        }
950
951        let grpimage = self.grpimage();
952        let skip_autocrypt = self.should_skip_autocrypt();
953        let encrypt_helper = EncryptHelper::new(context).await?;
954
955        if !skip_autocrypt {
956            // unless determined otherwise we add the Autocrypt header
957            let aheader = encrypt_helper.get_aheader().to_string();
958            headers.push((
959                "Autocrypt",
960                mail_builder::headers::raw::Raw::new(aheader).into(),
961            ));
962        }
963
964        if self.pre_message_mode == PreMessageMode::Post {
965            headers.push((
966                "Chat-Is-Post-Message",
967                mail_builder::headers::raw::Raw::new("1").into(),
968            ));
969        } else if let PreMessageMode::Pre {
970            post_msg_rfc724_mid,
971        } = &self.pre_message_mode
972        {
973            headers.push((
974                "Chat-Post-Message-ID",
975                mail_builder::headers::message_id::MessageId::new(post_msg_rfc724_mid.clone())
976                    .into(),
977            ));
978        }
979
980        let is_encrypted = self.will_be_encrypted();
981
982        // Add ephemeral timer for non-MDN messages.
983        // For MDNs it does not matter because they are not visible
984        // and ignored by the receiver.
985        if let Loaded::Message { msg, .. } = &self.loaded {
986            let ephemeral_timer = msg.chat_id.get_ephemeral_timer(context).await?;
987            if let EphemeralTimer::Enabled { duration } = ephemeral_timer {
988                headers.push((
989                    "Ephemeral-Timer",
990                    mail_builder::headers::raw::Raw::new(duration.to_string()).into(),
991                ));
992            }
993        }
994
995        let is_securejoin_message = if let Loaded::Message { msg, .. } = &self.loaded {
996            msg.param.get_cmd() == SystemMessage::SecurejoinMessage
997        } else {
998            false
999        };
1000
1001        let message: MimePart<'static> = match &self.loaded {
1002            Loaded::Message { msg, .. } => {
1003                let msg = msg.clone();
1004                let (main_part, mut parts) = self
1005                    .render_message(context, &mut headers, &grpimage, is_encrypted)
1006                    .await?;
1007                if parts.is_empty() {
1008                    // Single part, render as regular message.
1009                    main_part
1010                } else {
1011                    parts.insert(0, main_part);
1012
1013                    // Multiple parts, render as multipart.
1014                    if msg.param.get_cmd() == SystemMessage::MultiDeviceSync {
1015                        MimePart::new("multipart/report; report-type=multi-device-sync", parts)
1016                    } else if msg.param.get_cmd() == SystemMessage::WebxdcStatusUpdate {
1017                        MimePart::new("multipart/report; report-type=status-update", parts)
1018                    } else {
1019                        MimePart::new("multipart/mixed", parts)
1020                    }
1021                }
1022            }
1023            Loaded::Mdn { .. } => self.render_mdn()?,
1024        };
1025
1026        let HeadersByConfidentiality {
1027            mut unprotected_headers,
1028            hidden_headers,
1029            protected_headers,
1030        } = group_headers_by_confidentiality(
1031            headers,
1032            &self.from_addr,
1033            self.timestamp,
1034            is_encrypted,
1035            is_securejoin_message,
1036        );
1037
1038        let use_std_header_protection = context
1039            .get_config_bool(Config::StdHeaderProtectionComposing)
1040            .await?;
1041        let outer_message = if let Some(encryption_pubkeys) = self.encryption_pubkeys {
1042            let mut message = add_headers_to_encrypted_part(
1043                message,
1044                &unprotected_headers,
1045                hidden_headers,
1046                protected_headers,
1047                use_std_header_protection,
1048            );
1049
1050            // Add gossip headers in chats with multiple recipients
1051            let multiple_recipients =
1052                encryption_pubkeys.len() > 1 || context.get_config_bool(Config::BccSelf).await?;
1053
1054            let gossip_period = context.get_config_i64(Config::GossipPeriod).await?;
1055            let now = time();
1056
1057            match &self.loaded {
1058                Loaded::Message { chat, msg } => {
1059                    if !should_hide_recipients(msg, chat) {
1060                        for (addr, key) in &encryption_pubkeys {
1061                            let fingerprint = key.dc_fingerprint().hex();
1062                            let cmd = msg.param.get_cmd();
1063                            if self.pre_message_mode == PreMessageMode::Post {
1064                                continue;
1065                            }
1066
1067                            let should_do_gossip = cmd == SystemMessage::MemberAddedToGroup
1068                                || cmd == SystemMessage::SecurejoinMessage
1069                                || multiple_recipients && {
1070                                    let gossiped_timestamp: Option<i64> = context
1071                                        .sql
1072                                        .query_get_value(
1073                                            "SELECT timestamp
1074                                         FROM gossip_timestamp
1075                                         WHERE chat_id=? AND fingerprint=?",
1076                                            (chat.id, &fingerprint),
1077                                        )
1078                                        .await?;
1079
1080                                    // `gossip_period == 0` is a special case for testing,
1081                                    // enabling gossip in every message.
1082                                    //
1083                                    // If current time is in the past compared to
1084                                    // `gossiped_timestamp`, we also gossip because
1085                                    // either the `gossiped_timestamp` or clock is wrong.
1086                                    gossip_period == 0
1087                                        || gossiped_timestamp
1088                                            .is_none_or(|ts| now >= ts + gossip_period || now < ts)
1089                                };
1090
1091                            let verifier_id: Option<u32> = context
1092                                .sql
1093                                .query_get_value(
1094                                    "SELECT verifier FROM contacts WHERE fingerprint=?",
1095                                    (&fingerprint,),
1096                                )
1097                                .await?;
1098
1099                            let is_verified =
1100                                verifier_id.is_some_and(|verifier_id| verifier_id != 0);
1101
1102                            if !should_do_gossip {
1103                                continue;
1104                            }
1105
1106                            let header = Aheader {
1107                                addr: addr.clone(),
1108                                public_key: key.clone(),
1109                                // Autocrypt 1.1.0 specification says that
1110                                // `prefer-encrypt` attribute SHOULD NOT be included.
1111                                prefer_encrypt: EncryptPreference::NoPreference,
1112                                verified: is_verified,
1113                            }
1114                            .to_string();
1115
1116                            message = message.header(
1117                                "Autocrypt-Gossip",
1118                                mail_builder::headers::raw::Raw::new(header),
1119                            );
1120
1121                            context
1122                                .sql
1123                                .execute(
1124                                    "INSERT INTO gossip_timestamp (chat_id, fingerprint, timestamp)
1125                                     VALUES                       (?, ?, ?)
1126                                     ON CONFLICT                  (chat_id, fingerprint)
1127                                     DO UPDATE SET timestamp=excluded.timestamp",
1128                                    (chat.id, &fingerprint, now),
1129                                )
1130                                .await?;
1131                        }
1132                    }
1133                }
1134                Loaded::Mdn { .. } => {
1135                    // Never gossip in MDNs.
1136                }
1137            }
1138
1139            // Disable compression for SecureJoin to ensure
1140            // there are no compression side channels
1141            // leaking information about the tokens.
1142            let compress = match &self.loaded {
1143                Loaded::Message { msg, .. } => {
1144                    msg.param.get_cmd() != SystemMessage::SecurejoinMessage
1145                }
1146                Loaded::Mdn { .. } => true,
1147            };
1148
1149            let shared_secret: Option<String> = match &self.loaded {
1150                Loaded::Message { chat, msg }
1151                    if should_encrypt_with_broadcast_secret(msg, chat) =>
1152                {
1153                    let secret = load_broadcast_secret(context, chat.id).await?;
1154                    if secret.is_none() {
1155                        // If there is no shared secret yet
1156                        // because this is an old broadcast channel,
1157                        // created before we had symmetric encryption,
1158                        // we show an error message.
1159                        let text = BROADCAST_INCOMPATIBILITY_MSG;
1160                        chat::add_info_msg(context, chat.id, text).await?;
1161                        bail!(text);
1162                    }
1163                    secret
1164                }
1165                _ => None,
1166            };
1167
1168            let encrypted = if let Some(shared_secret) = shared_secret {
1169                let sign = true;
1170                encrypt_helper
1171                    .encrypt_symmetrically(context, &shared_secret, message, compress, sign)
1172                    .await?
1173            } else {
1174                // Asymmetric encryption
1175
1176                // Use SEIPDv2 if all recipients support it.
1177                let seipd_version = if encryption_pubkeys
1178                    .iter()
1179                    .all(|(_addr, pubkey)| pubkey_supports_seipdv2(pubkey))
1180                {
1181                    SeipdVersion::V2
1182                } else {
1183                    SeipdVersion::V1
1184                };
1185
1186                // Encrypt to self unconditionally,
1187                // even for a single-device setup.
1188                let mut encryption_keyring = vec![encrypt_helper.public_key.clone()];
1189                encryption_keyring
1190                    .extend(encryption_pubkeys.iter().map(|(_addr, key)| (*key).clone()));
1191
1192                encrypt_helper
1193                    .encrypt(
1194                        context,
1195                        encryption_keyring,
1196                        message,
1197                        compress,
1198                        seipd_version,
1199                    )
1200                    .await?
1201            };
1202
1203            wrap_encrypted_part(encrypted)
1204        } else if matches!(self.loaded, Loaded::Mdn { .. }) {
1205            // Never add outer multipart/mixed wrapper to MDN
1206            // as multipart/report Content-Type is used to recognize MDNs
1207            // by Delta Chat receiver and Chatmail servers
1208            // allowing them to be unencrypted and not contain Autocrypt header
1209            // without resetting Autocrypt encryption or triggering Chatmail filter
1210            // that normally only allows encrypted mails.
1211
1212            // Hidden headers are dropped.
1213            message
1214        } else {
1215            let message = hidden_headers
1216                .into_iter()
1217                .fold(message, |message, (header, value)| {
1218                    message.header(header, value)
1219                });
1220            let message = MimePart::new("multipart/mixed", vec![message]);
1221            let message = protected_headers
1222                .iter()
1223                .fold(message, |message, (header, value)| {
1224                    message.header(*header, value.clone())
1225                });
1226
1227            // Deduplicate unprotected headers that also are in the protected headers:
1228            let protected: HashSet<&str> =
1229                HashSet::from_iter(protected_headers.iter().map(|(header, _value)| *header));
1230            unprotected_headers.retain(|(header, _value)| !protected.contains(header));
1231
1232            message
1233        };
1234
1235        let MimeFactory {
1236            last_added_location_id,
1237            ..
1238        } = self;
1239
1240        let message = render_outer_message(unprotected_headers, outer_message);
1241
1242        Ok(RenderedEmail {
1243            message,
1244            // envelope: Envelope::new,
1245            is_encrypted,
1246            last_added_location_id,
1247            sync_ids_to_delete: self.sync_ids_to_delete,
1248            rfc724_mid,
1249            subject: subject_str,
1250        })
1251    }
1252
1253    /// Returns MIME part with a `message.kml` attachment.
1254    fn get_message_kml_part(&self) -> Option<MimePart<'static>> {
1255        let Loaded::Message { msg, .. } = &self.loaded else {
1256            return None;
1257        };
1258
1259        let latitude = msg.param.get_float(Param::SetLatitude)?;
1260        let longitude = msg.param.get_float(Param::SetLongitude)?;
1261
1262        let kml_file = location::get_message_kml(msg.timestamp_sort, latitude, longitude);
1263        let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_file)
1264            .attachment("message.kml");
1265        Some(part)
1266    }
1267
1268    /// Returns MIME part with a `location.kml` attachment.
1269    async fn get_location_kml_part(
1270        &mut self,
1271        context: &Context,
1272    ) -> Result<Option<MimePart<'static>>> {
1273        let Loaded::Message { msg, .. } = &self.loaded else {
1274            return Ok(None);
1275        };
1276
1277        let Some((kml_content, last_added_location_id)) =
1278            location::get_kml(context, msg.chat_id).await?
1279        else {
1280            return Ok(None);
1281        };
1282
1283        let part = MimePart::new("application/vnd.google-earth.kml+xml", kml_content)
1284            .attachment("location.kml");
1285        if !msg.param.exists(Param::SetLatitude) {
1286            // otherwise, the independent location is already filed
1287            self.last_added_location_id = Some(last_added_location_id);
1288        }
1289        Ok(Some(part))
1290    }
1291
1292    async fn render_message(
1293        &mut self,
1294        context: &Context,
1295        headers: &mut Vec<(&'static str, HeaderType<'static>)>,
1296        grpimage: &Option<String>,
1297        is_encrypted: bool,
1298    ) -> Result<(MimePart<'static>, Vec<MimePart<'static>>)> {
1299        let Loaded::Message { chat, msg } = &self.loaded else {
1300            bail!("Attempt to render MDN as a message");
1301        };
1302        let chat = chat.clone();
1303        let msg = msg.clone();
1304        let command = msg.param.get_cmd();
1305        let mut placeholdertext = None;
1306
1307        let send_verified_headers = match chat.typ {
1308            Chattype::Single => true,
1309            Chattype::Group => true,
1310            // Mailinglists and broadcast channels can actually never be verified:
1311            Chattype::Mailinglist => false,
1312            Chattype::OutBroadcast | Chattype::InBroadcast => false,
1313        };
1314
1315        if send_verified_headers {
1316            let was_protected: bool = context
1317                .sql
1318                .query_get_value("SELECT protected FROM chats WHERE id=?", (chat.id,))
1319                .await?
1320                .unwrap_or_default();
1321
1322            if was_protected {
1323                let unverified_member_exists = context
1324                    .sql
1325                    .exists(
1326                        "SELECT COUNT(*)
1327                        FROM contacts, chats_contacts
1328                        WHERE chats_contacts.contact_id=contacts.id AND chats_contacts.chat_id=?
1329                        AND contacts.id>9
1330                        AND contacts.verifier=0",
1331                        (chat.id,),
1332                    )
1333                    .await?;
1334
1335                if !unverified_member_exists {
1336                    headers.push((
1337                        "Chat-Verified",
1338                        mail_builder::headers::raw::Raw::new("1").into(),
1339                    ));
1340                }
1341            }
1342        }
1343
1344        if chat.typ == Chattype::Group {
1345            // Send group ID unless it is an ad hoc group that has no ID.
1346            if !chat.grpid.is_empty() {
1347                headers.push((
1348                    "Chat-Group-ID",
1349                    mail_builder::headers::raw::Raw::new(chat.grpid.clone()).into(),
1350                ));
1351            }
1352        }
1353
1354        if chat.typ == Chattype::Group || chat.typ == Chattype::OutBroadcast {
1355            headers.push((
1356                "Chat-Group-Name",
1357                mail_builder::headers::text::Text::new(chat.name.to_string()).into(),
1358            ));
1359            if let Some(ts) = chat.param.get_i64(Param::GroupNameTimestamp) {
1360                headers.push((
1361                    "Chat-Group-Name-Timestamp",
1362                    mail_builder::headers::text::Text::new(ts.to_string()).into(),
1363                ));
1364            }
1365        }
1366        if chat.typ == Chattype::Group
1367            || chat.typ == Chattype::OutBroadcast
1368            || chat.typ == Chattype::InBroadcast
1369        {
1370            match command {
1371                SystemMessage::MemberRemovedFromGroup => {
1372                    let email_to_remove = msg.param.get(Param::Arg).unwrap_or_default();
1373                    let fingerprint_to_remove = msg.param.get(Param::Arg4).unwrap_or_default();
1374
1375                    if email_to_remove
1376                        == context
1377                            .get_config(Config::ConfiguredAddr)
1378                            .await?
1379                            .unwrap_or_default()
1380                    {
1381                        placeholdertext = Some(format!("{email_to_remove} left the group."));
1382                    } else {
1383                        placeholdertext = Some(format!("Member {email_to_remove} was removed."));
1384                    };
1385
1386                    if !email_to_remove.is_empty() {
1387                        headers.push((
1388                            "Chat-Group-Member-Removed",
1389                            mail_builder::headers::raw::Raw::new(email_to_remove.to_string())
1390                                .into(),
1391                        ));
1392                    }
1393
1394                    if !fingerprint_to_remove.is_empty() {
1395                        headers.push((
1396                            "Chat-Group-Member-Removed-Fpr",
1397                            mail_builder::headers::raw::Raw::new(fingerprint_to_remove.to_string())
1398                                .into(),
1399                        ));
1400                    }
1401                }
1402                SystemMessage::MemberAddedToGroup => {
1403                    let email_to_add = msg.param.get(Param::Arg).unwrap_or_default();
1404                    let fingerprint_to_add = msg.param.get(Param::Arg4).unwrap_or_default();
1405
1406                    placeholdertext = Some(format!("Member {email_to_add} was added."));
1407
1408                    if !email_to_add.is_empty() {
1409                        headers.push((
1410                            "Chat-Group-Member-Added",
1411                            mail_builder::headers::raw::Raw::new(email_to_add.to_string()).into(),
1412                        ));
1413                    }
1414                    if !fingerprint_to_add.is_empty() {
1415                        headers.push((
1416                            "Chat-Group-Member-Added-Fpr",
1417                            mail_builder::headers::raw::Raw::new(fingerprint_to_add.to_string())
1418                                .into(),
1419                        ));
1420                    }
1421                    if 0 != msg.param.get_int(Param::Arg2).unwrap_or_default() & DC_FROM_HANDSHAKE {
1422                        let step = "vg-member-added";
1423                        info!(context, "Sending secure-join message {:?}.", step);
1424                        headers.push((
1425                            "Secure-Join",
1426                            mail_builder::headers::raw::Raw::new(step.to_string()).into(),
1427                        ));
1428                    }
1429                }
1430                SystemMessage::GroupNameChanged => {
1431                    placeholdertext = Some("Chat name changed.".to_string());
1432                    let old_name = msg.param.get(Param::Arg).unwrap_or_default().to_string();
1433                    headers.push((
1434                        "Chat-Group-Name-Changed",
1435                        mail_builder::headers::text::Text::new(old_name).into(),
1436                    ));
1437                }
1438                SystemMessage::GroupDescriptionChanged => {
1439                    placeholdertext = Some(
1440                        "[Chat description changed. To see this and other new features, please update the app]".to_string(),
1441                    );
1442                    headers.push((
1443                        "Chat-Group-Description-Changed",
1444                        mail_builder::headers::text::Text::new("").into(),
1445                    ));
1446                }
1447                SystemMessage::GroupImageChanged => {
1448                    placeholdertext = Some("Chat image changed.".to_string());
1449                    headers.push((
1450                        "Chat-Content",
1451                        mail_builder::headers::text::Text::new("group-avatar-changed").into(),
1452                    ));
1453                    if grpimage.is_none() && is_encrypted {
1454                        headers.push((
1455                            "Chat-Group-Avatar",
1456                            mail_builder::headers::raw::Raw::new("0").into(),
1457                        ));
1458                    }
1459                }
1460                SystemMessage::Unknown => {}
1461                SystemMessage::AutocryptSetupMessage => {}
1462                SystemMessage::SecurejoinMessage => {}
1463                SystemMessage::LocationStreamingEnabled => {}
1464                SystemMessage::LocationOnly => {}
1465                SystemMessage::EphemeralTimerChanged => {}
1466                SystemMessage::ChatProtectionEnabled => {}
1467                SystemMessage::ChatProtectionDisabled => {}
1468                SystemMessage::InvalidUnencryptedMail => {}
1469                SystemMessage::SecurejoinWait => {}
1470                SystemMessage::SecurejoinWaitTimeout => {}
1471                SystemMessage::MultiDeviceSync => {}
1472                SystemMessage::WebxdcStatusUpdate => {}
1473                SystemMessage::WebxdcInfoMessage => {}
1474                SystemMessage::IrohNodeAddr => {}
1475                SystemMessage::ChatE2ee => {}
1476                SystemMessage::CallAccepted => {}
1477                SystemMessage::CallEnded => {}
1478            }
1479
1480            if command == SystemMessage::GroupDescriptionChanged
1481                || command == SystemMessage::MemberAddedToGroup
1482                || msg
1483                    .param
1484                    .get_bool(Param::AttachChatAvatarAndDescription)
1485                    .unwrap_or_default()
1486            {
1487                let description = chat::get_chat_description(context, chat.id).await?;
1488                headers.push((
1489                    "Chat-Group-Description",
1490                    mail_builder::headers::raw::Raw::new(b_encode(&description)).into(),
1491                ));
1492                if let Some(ts) = chat.param.get_i64(Param::GroupDescriptionTimestamp) {
1493                    headers.push((
1494                        "Chat-Group-Description-Timestamp",
1495                        mail_builder::headers::text::Text::new(ts.to_string()).into(),
1496                    ));
1497                }
1498            }
1499        }
1500
1501        match command {
1502            SystemMessage::LocationStreamingEnabled => {
1503                headers.push((
1504                    "Chat-Content",
1505                    mail_builder::headers::raw::Raw::new("location-streaming-enabled").into(),
1506                ));
1507            }
1508            SystemMessage::EphemeralTimerChanged => {
1509                headers.push((
1510                    "Chat-Content",
1511                    mail_builder::headers::raw::Raw::new("ephemeral-timer-changed").into(),
1512                ));
1513            }
1514            SystemMessage::LocationOnly
1515            | SystemMessage::MultiDeviceSync
1516            | SystemMessage::WebxdcStatusUpdate => {
1517                // This should prevent automatic replies,
1518                // such as non-delivery reports,
1519                // if the message is unencrypted.
1520                //
1521                // See <https://tools.ietf.org/html/rfc3834>
1522                headers.push((
1523                    "Auto-Submitted",
1524                    mail_builder::headers::raw::Raw::new("auto-generated").into(),
1525                ));
1526            }
1527            SystemMessage::SecurejoinMessage => {
1528                let step = msg.param.get(Param::Arg).unwrap_or_default();
1529                if !step.is_empty() {
1530                    info!(context, "Sending secure-join message {step:?}.");
1531                    headers.push((
1532                        "Secure-Join",
1533                        mail_builder::headers::raw::Raw::new(step.to_string()).into(),
1534                    ));
1535
1536                    let param2 = msg.param.get(Param::Arg2).unwrap_or_default();
1537                    if !param2.is_empty() {
1538                        headers.push((
1539                            if step == "vg-request-with-auth" || step == "vc-request-with-auth" {
1540                                "Secure-Join-Auth"
1541                            } else {
1542                                "Secure-Join-Invitenumber"
1543                            },
1544                            mail_builder::headers::text::Text::new(param2.to_string()).into(),
1545                        ));
1546                    }
1547
1548                    let fingerprint = msg.param.get(Param::Arg3).unwrap_or_default();
1549                    if !fingerprint.is_empty() {
1550                        headers.push((
1551                            "Secure-Join-Fingerprint",
1552                            mail_builder::headers::raw::Raw::new(fingerprint.to_string()).into(),
1553                        ));
1554                    }
1555                    if let Some(id) = msg.param.get(Param::Arg4) {
1556                        headers.push((
1557                            "Secure-Join-Group",
1558                            mail_builder::headers::raw::Raw::new(id.to_string()).into(),
1559                        ));
1560                    };
1561                }
1562            }
1563            SystemMessage::ChatProtectionEnabled => {
1564                headers.push((
1565                    "Chat-Content",
1566                    mail_builder::headers::raw::Raw::new("protection-enabled").into(),
1567                ));
1568            }
1569            SystemMessage::ChatProtectionDisabled => {
1570                headers.push((
1571                    "Chat-Content",
1572                    mail_builder::headers::raw::Raw::new("protection-disabled").into(),
1573                ));
1574            }
1575            SystemMessage::IrohNodeAddr => {
1576                let node_addr = context
1577                    .get_or_try_init_peer_channel()
1578                    .await?
1579                    .get_node_addr()
1580                    .await?;
1581
1582                // We should not send `null` as relay URL
1583                // as this is the only way to reach the node.
1584                debug_assert!(node_addr.relay_url().is_some());
1585                headers.push((
1586                    HeaderDef::IrohNodeAddr.into(),
1587                    mail_builder::headers::text::Text::new(serde_json::to_string(&node_addr)?)
1588                        .into(),
1589                ));
1590            }
1591            SystemMessage::CallAccepted => {
1592                headers.push((
1593                    "Chat-Content",
1594                    mail_builder::headers::raw::Raw::new("call-accepted").into(),
1595                ));
1596            }
1597            SystemMessage::CallEnded => {
1598                headers.push((
1599                    "Chat-Content",
1600                    mail_builder::headers::raw::Raw::new("call-ended").into(),
1601                ));
1602            }
1603            _ => {}
1604        }
1605
1606        if let Some(grpimage) = grpimage
1607            && is_encrypted
1608        {
1609            info!(context, "setting group image '{}'", grpimage);
1610            let avatar = build_avatar_file(context, grpimage)
1611                .await
1612                .context("Cannot attach group image")?;
1613            headers.push((
1614                "Chat-Group-Avatar",
1615                mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(),
1616            ));
1617        }
1618
1619        if msg.viewtype == Viewtype::Sticker {
1620            headers.push((
1621                "Chat-Content",
1622                mail_builder::headers::raw::Raw::new("sticker").into(),
1623            ));
1624        } else if msg.viewtype == Viewtype::Call {
1625            headers.push((
1626                "Chat-Content",
1627                mail_builder::headers::raw::Raw::new("call").into(),
1628            ));
1629            placeholdertext = Some(
1630                "[This is a 'Call'. The sender uses an experiment not supported on your version yet]".to_string(),
1631            );
1632        }
1633
1634        if let Some(offer) = msg.param.get(Param::WebrtcRoom) {
1635            headers.push((
1636                "Chat-Webrtc-Room",
1637                mail_builder::headers::raw::Raw::new(b_encode(offer)).into(),
1638            ));
1639        } else if let Some(answer) = msg.param.get(Param::WebrtcAccepted) {
1640            headers.push((
1641                "Chat-Webrtc-Accepted",
1642                mail_builder::headers::raw::Raw::new(b_encode(answer)).into(),
1643            ));
1644        }
1645        if let Some(has_video) = msg.param.get(Param::WebrtcHasVideoInitially) {
1646            headers.push((
1647                "Chat-Webrtc-Has-Video-Initially",
1648                mail_builder::headers::raw::Raw::new(b_encode(has_video)).into(),
1649            ))
1650        }
1651
1652        if msg.viewtype == Viewtype::Voice
1653            || msg.viewtype == Viewtype::Audio
1654            || msg.viewtype == Viewtype::Video
1655        {
1656            if msg.viewtype == Viewtype::Voice {
1657                headers.push((
1658                    "Chat-Voice-Message",
1659                    mail_builder::headers::raw::Raw::new("1").into(),
1660                ));
1661            }
1662            let duration_ms = msg.param.get_int(Param::Duration).unwrap_or_default();
1663            if duration_ms > 0 {
1664                let dur = duration_ms.to_string();
1665                headers.push((
1666                    "Chat-Duration",
1667                    mail_builder::headers::raw::Raw::new(dur).into(),
1668                ));
1669            }
1670        }
1671
1672        // add text part - we even add empty text and force a MIME-multipart-message as:
1673        // - some Apps have problems with Non-text in the main part (eg. "Mail" from stock Android)
1674        // - we can add "forward hints" this way
1675        // - it looks better
1676
1677        let afwd_email = msg.param.exists(Param::Forwarded);
1678        let fwdhint = if afwd_email {
1679            Some(
1680                "---------- Forwarded message ----------\r\n\
1681                 From: Delta Chat\r\n\
1682                 \r\n"
1683                    .to_string(),
1684            )
1685        } else {
1686            None
1687        };
1688
1689        let final_text = placeholdertext.as_deref().unwrap_or(&msg.text);
1690
1691        let mut quoted_text = None;
1692        if let Some(msg_quoted_text) = msg.quoted_text() {
1693            let mut some_quoted_text = String::new();
1694            for quoted_line in msg_quoted_text.split('\n') {
1695                some_quoted_text += "> ";
1696                some_quoted_text += quoted_line;
1697                some_quoted_text += "\r\n";
1698            }
1699            some_quoted_text += "\r\n";
1700            quoted_text = Some(some_quoted_text)
1701        }
1702
1703        if !is_encrypted && msg.param.get_bool(Param::ProtectQuote).unwrap_or_default() {
1704            // Message is not encrypted but quotes encrypted message.
1705            quoted_text = Some("> ...\r\n\r\n".to_string());
1706        }
1707        if quoted_text.is_none() && final_text.starts_with('>') {
1708            // Insert empty line to avoid receiver treating user-sent quote as topquote inserted by
1709            // Delta Chat.
1710            quoted_text = Some("\r\n".to_string());
1711        }
1712
1713        let is_reaction = msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
1714
1715        let footer = if is_reaction { "" } else { &self.selfstatus };
1716
1717        let message_text = if self.pre_message_mode == PreMessageMode::Post {
1718            "".to_string()
1719        } else {
1720            format!(
1721                "{}{}{}{}{}{}",
1722                fwdhint.unwrap_or_default(),
1723                quoted_text.unwrap_or_default(),
1724                escape_message_footer_marks(final_text),
1725                if !final_text.is_empty() && !footer.is_empty() {
1726                    "\r\n\r\n"
1727                } else {
1728                    ""
1729                },
1730                if !footer.is_empty() { "-- \r\n" } else { "" },
1731                footer
1732            )
1733        };
1734
1735        let mut main_part = MimePart::new("text/plain", message_text);
1736        if is_reaction {
1737            main_part = main_part.header(
1738                "Content-Disposition",
1739                mail_builder::headers::raw::Raw::new("reaction"),
1740            );
1741        }
1742
1743        let mut parts = Vec::new();
1744
1745        if msg.has_html() {
1746            let html = if let Some(html) = msg.param.get(Param::SendHtml) {
1747                Some(html.to_string())
1748            } else if let Some(orig_msg_id) = msg.param.get_int(Param::Forwarded)
1749                && orig_msg_id != 0
1750            {
1751                // Legacy forwarded messages may not have `Param::SendHtml` set. Let's hope the
1752                // original message exists.
1753                MsgId::new(orig_msg_id.try_into()?)
1754                    .get_html(context)
1755                    .await?
1756            } else {
1757                None
1758            };
1759            if let Some(html) = html {
1760                main_part = MimePart::new(
1761                    "multipart/alternative",
1762                    vec![main_part, MimePart::new("text/html", html)],
1763                )
1764            }
1765        }
1766
1767        // add attachment part
1768        if msg.viewtype.has_file() {
1769            if let PreMessageMode::Pre { .. } = self.pre_message_mode {
1770                let Some(metadata) = PostMsgMetadata::from_msg(context, &msg).await? else {
1771                    bail!("Failed to generate metadata for pre-message")
1772                };
1773
1774                headers.push((
1775                    HeaderDef::ChatPostMessageMetadata.into(),
1776                    mail_builder::headers::raw::Raw::new(metadata.to_header_value()?).into(),
1777                ));
1778            } else {
1779                let file_part = build_body_file(context, &msg).await?;
1780                parts.push(file_part);
1781            }
1782        }
1783
1784        if let Some(msg_kml_part) = self.get_message_kml_part() {
1785            parts.push(msg_kml_part);
1786        }
1787
1788        if location::is_sending_to_chat(context, msg.chat_id).await?
1789            && let Some(part) = self.get_location_kml_part(context).await?
1790        {
1791            parts.push(part);
1792        }
1793
1794        // we do not piggyback sync-files to other self-sent-messages
1795        // to not risk files becoming too larger and being skipped by download-on-demand.
1796        if command == SystemMessage::MultiDeviceSync {
1797            let json = msg.param.get(Param::Arg).unwrap_or_default();
1798            let ids = msg.param.get(Param::Arg2).unwrap_or_default();
1799            parts.push(context.build_sync_part(json.to_string()));
1800            self.sync_ids_to_delete = Some(ids.to_string());
1801        } else if command == SystemMessage::WebxdcStatusUpdate {
1802            let json = msg.param.get(Param::Arg).unwrap_or_default();
1803            parts.push(context.build_status_update_part(json));
1804        } else if msg.viewtype == Viewtype::Webxdc {
1805            let topic = self
1806                .webxdc_topic
1807                .map(|top| BASE32_NOPAD.encode(top.as_bytes()).to_ascii_lowercase())
1808                .unwrap_or(create_iroh_header(context, msg.id).await?);
1809            headers.push((
1810                HeaderDef::IrohGossipTopic.get_headername(),
1811                mail_builder::headers::raw::Raw::new(topic).into(),
1812            ));
1813            if !matches!(self.pre_message_mode, PreMessageMode::Pre { .. })
1814                && let (Some(json), _) = context
1815                    .render_webxdc_status_update_object(
1816                        msg.id,
1817                        StatusUpdateSerial::MIN,
1818                        StatusUpdateSerial::MAX,
1819                        None,
1820                    )
1821                    .await?
1822            {
1823                parts.push(context.build_status_update_part(&json));
1824            }
1825        }
1826
1827        self.attach_selfavatar =
1828            self.attach_selfavatar && self.pre_message_mode != PreMessageMode::Post;
1829        if self.attach_selfavatar {
1830            match context.get_config(Config::Selfavatar).await? {
1831                Some(path) => match build_avatar_file(context, &path).await {
1832                    Ok(avatar) => headers.push((
1833                        "Chat-User-Avatar",
1834                        mail_builder::headers::raw::Raw::new(format!("base64:{avatar}")).into(),
1835                    )),
1836                    Err(err) => warn!(context, "mimefactory: cannot attach selfavatar: {}", err),
1837                },
1838                None => headers.push((
1839                    "Chat-User-Avatar",
1840                    mail_builder::headers::raw::Raw::new("0").into(),
1841                )),
1842            }
1843        }
1844
1845        Ok((main_part, parts))
1846    }
1847
1848    /// Render an MDN
1849    fn render_mdn(&mut self) -> Result<MimePart<'static>> {
1850        // RFC 6522, this also requires the `report-type` parameter which is equal
1851        // to the MIME subtype of the second body part of the multipart/report
1852        let Loaded::Mdn {
1853            rfc724_mid,
1854            additional_msg_ids,
1855        } = &self.loaded
1856        else {
1857            bail!("Attempt to render a message as MDN");
1858        };
1859
1860        // first body part: always human-readable, always REQUIRED by RFC 6522.
1861        // untranslated to no reveal sender's language.
1862        // moreover, translations in unknown languages are confusing, and clients may not display them at all
1863        let text_part = MimePart::new("text/plain", "This is a receipt notification.");
1864
1865        let mut message = MimePart::new(
1866            "multipart/report; report-type=disposition-notification",
1867            vec![text_part],
1868        );
1869
1870        // second body part: machine-readable, always REQUIRED by RFC 6522
1871        let message_text2 = format!(
1872            "Original-Recipient: rfc822;{}\r\n\
1873             Final-Recipient: rfc822;{}\r\n\
1874             Original-Message-ID: <{}>\r\n\
1875             Disposition: manual-action/MDN-sent-automatically; displayed\r\n",
1876            self.from_addr, self.from_addr, rfc724_mid
1877        );
1878
1879        let extension_fields = if additional_msg_ids.is_empty() {
1880            "".to_string()
1881        } else {
1882            "Additional-Message-IDs: ".to_string()
1883                + &additional_msg_ids
1884                    .iter()
1885                    .map(|mid| render_rfc724_mid(mid))
1886                    .collect::<Vec<String>>()
1887                    .join(" ")
1888                + "\r\n"
1889        };
1890
1891        message.add_part(MimePart::new(
1892            "message/disposition-notification",
1893            message_text2 + &extension_fields,
1894        ));
1895
1896        Ok(message)
1897    }
1898
1899    pub fn will_be_encrypted(&self) -> bool {
1900        self.encryption_pubkeys.is_some()
1901    }
1902
1903    pub fn set_as_post_message(&mut self) {
1904        self.pre_message_mode = PreMessageMode::Post;
1905    }
1906
1907    pub fn set_as_pre_message_for(&mut self, post_message: &RenderedEmail) {
1908        self.pre_message_mode = PreMessageMode::Pre {
1909            post_msg_rfc724_mid: post_message.rfc724_mid.clone(),
1910        };
1911    }
1912}
1913
1914/// Stores the unprotected headers on the outer message, and renders it.
1915pub(crate) fn render_outer_message(
1916    unprotected_headers: Vec<(&'static str, HeaderType<'static>)>,
1917    outer_message: MimePart<'static>,
1918) -> String {
1919    let outer_message = unprotected_headers
1920        .into_iter()
1921        .fold(outer_message, |message, (header, value)| {
1922            message.header(header, value)
1923        });
1924
1925    let mut buffer = Vec::new();
1926    let cursor = Cursor::new(&mut buffer);
1927    outer_message.clone().write_part(cursor).ok();
1928    String::from_utf8_lossy(&buffer).to_string()
1929}
1930
1931/// Takes the encrypted part, wraps it in a MimePart,
1932/// and sets the appropriate Content-Type for the outer message
1933pub(crate) fn wrap_encrypted_part(encrypted: String) -> MimePart<'static> {
1934    MimePart::new(
1935        "multipart/encrypted; protocol=\"application/pgp-encrypted\"",
1936        vec![
1937            // Autocrypt part 1
1938            MimePart::new("application/pgp-encrypted", "Version: 1\r\n"),
1939            // Autocrypt part 2
1940            MimePart::new("application/octet-stream", encrypted),
1941        ],
1942    )
1943}
1944
1945fn add_headers_to_encrypted_part(
1946    message: MimePart<'static>,
1947    unprotected_headers: &[(&'static str, HeaderType<'static>)],
1948    hidden_headers: Vec<(&'static str, HeaderType<'static>)>,
1949    protected_headers: Vec<(&'static str, HeaderType<'static>)>,
1950    use_std_header_protection: bool,
1951) -> MimePart<'static> {
1952    // Store protected headers in the inner message.
1953    let message = protected_headers
1954        .into_iter()
1955        .fold(message, |message, (header, value)| {
1956            message.header(header, value)
1957        });
1958
1959    // Add hidden headers to encrypted payload.
1960    let mut message: MimePart<'static> = hidden_headers
1961        .into_iter()
1962        .fold(message, |message, (header, value)| {
1963            message.header(header, value)
1964        });
1965
1966    if use_std_header_protection {
1967        message = unprotected_headers
1968            .iter()
1969            // Structural headers shouldn't be added as "HP-Outer". They are defined in
1970            // <https://www.rfc-editor.org/rfc/rfc9787.html#structural-header-fields>.
1971            .filter(|(name, _)| {
1972                !(name.eq_ignore_ascii_case("mime-version")
1973                    || name.eq_ignore_ascii_case("content-type")
1974                    || name.eq_ignore_ascii_case("content-transfer-encoding")
1975                    || name.eq_ignore_ascii_case("content-disposition"))
1976            })
1977            .fold(message, |message, (name, value)| {
1978                message.header(format!("HP-Outer: {name}"), value.clone())
1979            });
1980    }
1981
1982    // Set the appropriate Content-Type for the inner message
1983    for (h, v) in &mut message.headers {
1984        if h == "Content-Type"
1985            && let mail_builder::headers::HeaderType::ContentType(ct) = v
1986        {
1987            let mut ct_new = ct.clone();
1988            ct_new = ct_new.attribute("protected-headers", "v1");
1989            if use_std_header_protection {
1990                ct_new = ct_new.attribute("hp", "cipher");
1991            }
1992            *ct = ct_new;
1993            break;
1994        }
1995    }
1996
1997    message
1998}
1999
2000struct HeadersByConfidentiality {
2001    /// Headers that must go into IMF header section.
2002    ///
2003    /// These are standard headers such as Date, In-Reply-To, References, which cannot be placed
2004    /// anywhere else according to the standard. Placing headers here also allows them to be fetched
2005    /// individually over IMAP without downloading the message body. This is why Chat-Version is
2006    /// placed here.
2007    unprotected_headers: Vec<(&'static str, HeaderType<'static>)>,
2008
2009    /// Headers that MUST NOT (only) go into IMF header section:
2010    /// - Large headers which may hit the header section size limit on the server, such as
2011    ///   Chat-User-Avatar with a base64-encoded image inside.
2012    /// - Headers duplicated here that servers mess up with in the IMF header section, like
2013    ///   Message-ID.
2014    /// - Nonstandard headers that should be DKIM-protected because e.g. OpenDKIM only signs
2015    ///   known headers.
2016    ///
2017    /// The header should be hidden from MTA
2018    /// by moving it either into protected part
2019    /// in case of encrypted mails
2020    /// or unprotected MIME preamble in case of unencrypted mails.
2021    hidden_headers: Vec<(&'static str, HeaderType<'static>)>,
2022
2023    /// Opportunistically protected headers.
2024    ///
2025    /// These headers are placed into encrypted part *if* the message is encrypted. Place headers
2026    /// which are not needed before decryption (e.g. Chat-Group-Name) or are not interesting if the
2027    /// message cannot be decrypted (e.g. Chat-Disposition-Notification-To) here.
2028    ///
2029    /// If the message is not encrypted, these headers are placed into IMF header section, so make
2030    /// sure that the message will be encrypted if you place any sensitive information here.
2031    protected_headers: Vec<(&'static str, HeaderType<'static>)>,
2032}
2033
2034/// Split headers based on header confidentiality policy.
2035/// See [`HeadersByConfidentiality`] for more info.
2036fn group_headers_by_confidentiality(
2037    headers: Vec<(&'static str, HeaderType<'static>)>,
2038    from_addr: &str,
2039    timestamp: i64,
2040    is_encrypted: bool,
2041    is_securejoin_message: bool,
2042) -> HeadersByConfidentiality {
2043    let mut unprotected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
2044    let mut hidden_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
2045    let mut protected_headers: Vec<(&'static str, HeaderType<'static>)> = Vec::new();
2046
2047    // MIME header <https://datatracker.ietf.org/doc/html/rfc2045>.
2048    unprotected_headers.push((
2049        "MIME-Version",
2050        mail_builder::headers::raw::Raw::new("1.0").into(),
2051    ));
2052
2053    for header @ (original_header_name, _header_value) in &headers {
2054        let header_name = original_header_name.to_lowercase();
2055        if header_name == "message-id" {
2056            unprotected_headers.push(header.clone());
2057            hidden_headers.push(header.clone());
2058        } else if is_hidden(&header_name) {
2059            hidden_headers.push(header.clone());
2060        } else if header_name == "from" {
2061            // Unencrypted securejoin messages should _not_ include the display name:
2062            if is_encrypted || !is_securejoin_message {
2063                protected_headers.push(header.clone());
2064            }
2065
2066            unprotected_headers.push((
2067                original_header_name,
2068                Address::new_address(None::<&'static str>, from_addr.to_string()).into(),
2069            ));
2070        } else if header_name == "to" {
2071            protected_headers.push(header.clone());
2072            if is_encrypted {
2073                unprotected_headers.push(("To", hidden_recipients().into()));
2074            } else {
2075                unprotected_headers.push(header.clone());
2076            }
2077        } else if header_name == "chat-broadcast-secret" {
2078            if is_encrypted {
2079                protected_headers.push(header.clone());
2080            }
2081        } else if is_encrypted && header_name == "date" {
2082            protected_headers.push(header.clone());
2083
2084            // Randomized date goes to unprotected header.
2085            //
2086            // We cannot just send "Thu, 01 Jan 1970 00:00:00 +0000"
2087            // or omit the header because GMX then fails with
2088            //
2089            // host mx00.emig.gmx.net[212.227.15.9] said:
2090            // 554-Transaction failed
2091            // 554-Reject due to policy restrictions.
2092            // 554 For explanation visit https://postmaster.gmx.net/en/case?...
2093            // (in reply to end of DATA command)
2094            //
2095            // and the explanation page says
2096            // "The time information deviates too much from the actual time".
2097            //
2098            // We also limit the range to 6 days (518400 seconds)
2099            // because with a larger range we got
2100            // error "500 Date header far in the past/future"
2101            // which apparently originates from Symantec Messaging Gateway
2102            // and means the message has a Date that is more
2103            // than 7 days in the past:
2104            // <https://github.com/chatmail/core/issues/7466>
2105            let timestamp_offset = rand::random_range(0..518400);
2106            let protected_timestamp = timestamp.saturating_sub(timestamp_offset);
2107            let unprotected_date =
2108                chrono::DateTime::<chrono::Utc>::from_timestamp(protected_timestamp, 0)
2109                    .unwrap()
2110                    .to_rfc2822();
2111            unprotected_headers.push((
2112                "Date",
2113                mail_builder::headers::raw::Raw::new(unprotected_date).into(),
2114            ));
2115        } else if is_encrypted {
2116            protected_headers.push(header.clone());
2117
2118            match header_name.as_str() {
2119                "subject" => {
2120                    unprotected_headers.push((
2121                        "Subject",
2122                        mail_builder::headers::raw::Raw::new("[...]").into(),
2123                    ));
2124                }
2125                "chat-version" | "autocrypt-setup-message" | "chat-is-post-message" => {
2126                    unprotected_headers.push(header.clone());
2127                }
2128                _ => {
2129                    // Other headers are removed from unprotected part.
2130                }
2131            }
2132        } else {
2133            unprotected_headers.push(header.clone())
2134        }
2135    }
2136    HeadersByConfidentiality {
2137        unprotected_headers,
2138        hidden_headers,
2139        protected_headers,
2140    }
2141}
2142
2143fn hidden_recipients() -> Address<'static> {
2144    Address::new_group(Some("hidden-recipients".to_string()), Vec::new())
2145}
2146
2147fn should_encrypt_with_broadcast_secret(msg: &Message, chat: &Chat) -> bool {
2148    chat.typ == Chattype::OutBroadcast && must_have_only_one_recipient(msg, chat).is_none()
2149}
2150
2151fn should_hide_recipients(msg: &Message, chat: &Chat) -> bool {
2152    should_encrypt_with_broadcast_secret(msg, chat)
2153}
2154
2155fn should_encrypt_symmetrically(msg: &Message, chat: &Chat) -> bool {
2156    should_encrypt_with_broadcast_secret(msg, chat)
2157}
2158
2159/// Some messages sent into outgoing broadcast channels (member-added/member-removed)
2160/// should only go to a single recipient,
2161/// rather than all recipients.
2162/// This function returns the fingerprint of the recipient the message should be sent to.
2163fn must_have_only_one_recipient<'a>(msg: &'a Message, chat: &Chat) -> Option<Result<&'a str>> {
2164    if chat.typ != Chattype::OutBroadcast {
2165        None
2166    } else if let Some(fp) = msg.param.get(Param::Arg4) {
2167        Some(Ok(fp))
2168    } else if matches!(
2169        msg.param.get_cmd(),
2170        SystemMessage::MemberRemovedFromGroup | SystemMessage::MemberAddedToGroup
2171    ) {
2172        Some(Err(format_err!("Missing removed/added member")))
2173    } else {
2174        None
2175    }
2176}
2177
2178async fn build_body_file(context: &Context, msg: &Message) -> Result<MimePart<'static>> {
2179    let file_name = msg.get_filename().context("msg has no file")?;
2180    let blob = msg
2181        .param
2182        .get_file_blob(context)?
2183        .context("msg has no file")?;
2184    let mimetype = msg
2185        .param
2186        .get(Param::MimeType)
2187        .unwrap_or("application/octet-stream")
2188        .to_string();
2189    let body = fs::read(blob.to_abs_path()).await?;
2190
2191    // create mime part, for Content-Disposition, see RFC 2183.
2192    // `Content-Disposition: attachment` seems not to make a difference to `Content-Disposition: inline`
2193    // at least on tested Thunderbird and Gma'l in 2017.
2194    // But I've heard about problems with inline and outl'k, so we just use the attachment-type until we
2195    // run into other problems ...
2196    let mail = MimePart::new(mimetype, body).attachment(sanitize_bidi_characters(&file_name));
2197
2198    Ok(mail)
2199}
2200
2201async fn build_avatar_file(context: &Context, path: &str) -> Result<String> {
2202    let blob = match path.starts_with("$BLOBDIR/") {
2203        true => BlobObject::from_name(context, path)?,
2204        false => BlobObject::from_path(context, path.as_ref())?,
2205    };
2206    let body = fs::read(blob.to_abs_path()).await?;
2207    let encoded_body = base64::engine::general_purpose::STANDARD
2208        .encode(&body)
2209        .chars()
2210        .enumerate()
2211        .fold(String::new(), |mut res, (i, c)| {
2212            if i % 78 == 77 {
2213                res.push(' ')
2214            }
2215            res.push(c);
2216            res
2217        });
2218    Ok(encoded_body)
2219}
2220
2221fn recipients_contain_addr(recipients: &[(String, String)], addr: &str) -> bool {
2222    let addr_lc = addr.to_lowercase();
2223    recipients
2224        .iter()
2225        .any(|(_, cur)| cur.to_lowercase() == addr_lc)
2226}
2227
2228fn render_rfc724_mid(rfc724_mid: &str) -> String {
2229    let rfc724_mid = rfc724_mid.trim().to_string();
2230
2231    if rfc724_mid.chars().next().unwrap_or_default() == '<' {
2232        rfc724_mid
2233    } else {
2234        format!("<{rfc724_mid}>")
2235    }
2236}
2237
2238/// Encodes UTF-8 string as a single B-encoded-word.
2239///
2240/// We manually encode some headers because as of
2241/// version 0.4.4 mail-builder crate does not encode
2242/// newlines correctly if they appear in a text header.
2243fn b_encode(value: &str) -> String {
2244    format!(
2245        "=?utf-8?B?{}?=",
2246        base64::engine::general_purpose::STANDARD.encode(value)
2247    )
2248}
2249
2250pub(crate) async fn render_symm_encrypted_securejoin_message(
2251    context: &Context,
2252    step: &str,
2253    rfc724_mid: &str,
2254    attach_self_pubkey: bool,
2255    auth: &str,
2256    shared_secret: &str,
2257) -> Result<String> {
2258    info!(context, "Sending secure-join message {step:?}.");
2259
2260    let mut headers = Vec::<(&'static str, HeaderType<'static>)>::new();
2261
2262    let from_addr = context.get_primary_self_addr().await?;
2263    let from = new_address_with_name("", from_addr.to_string());
2264    headers.push(("From", from.into()));
2265
2266    let to: Vec<Address<'static>> = vec![hidden_recipients()];
2267    headers.push((
2268        "To",
2269        mail_builder::headers::address::Address::new_list(to.clone()).into(),
2270    ));
2271
2272    headers.push((
2273        "Subject",
2274        mail_builder::headers::text::Text::new("Secure-Join".to_string()).into(),
2275    ));
2276
2277    let timestamp = time();
2278    let date = chrono::DateTime::<chrono::Utc>::from_timestamp(timestamp, 0)
2279        .unwrap()
2280        .to_rfc2822();
2281    headers.push(("Date", mail_builder::headers::raw::Raw::new(date).into()));
2282
2283    headers.push((
2284        "Message-ID",
2285        mail_builder::headers::message_id::MessageId::new(rfc724_mid.to_string()).into(),
2286    ));
2287
2288    // Automatic Response headers <https://www.rfc-editor.org/rfc/rfc3834>
2289    if context.get_config_bool(Config::Bot).await? {
2290        headers.push((
2291            "Auto-Submitted",
2292            mail_builder::headers::raw::Raw::new("auto-generated".to_string()).into(),
2293        ));
2294    }
2295
2296    let encrypt_helper = EncryptHelper::new(context).await?;
2297
2298    if attach_self_pubkey {
2299        let aheader = encrypt_helper.get_aheader().to_string();
2300        headers.push((
2301            "Autocrypt",
2302            mail_builder::headers::raw::Raw::new(aheader).into(),
2303        ));
2304    }
2305
2306    headers.push((
2307        "Secure-Join",
2308        mail_builder::headers::raw::Raw::new(step.to_string()).into(),
2309    ));
2310
2311    headers.push((
2312        "Secure-Join-Auth",
2313        mail_builder::headers::text::Text::new(auth.to_string()).into(),
2314    ));
2315
2316    let message: MimePart<'static> = MimePart::new("text/plain", "Secure-Join");
2317
2318    let is_encrypted = true;
2319    let is_securejoin_message = true;
2320    let HeadersByConfidentiality {
2321        unprotected_headers,
2322        hidden_headers,
2323        protected_headers,
2324    } = group_headers_by_confidentiality(
2325        headers,
2326        &from_addr,
2327        timestamp,
2328        is_encrypted,
2329        is_securejoin_message,
2330    );
2331
2332    let outer_message = {
2333        let use_std_header_protection = true;
2334        let message = add_headers_to_encrypted_part(
2335            message,
2336            &unprotected_headers,
2337            hidden_headers,
2338            protected_headers,
2339            use_std_header_protection,
2340        );
2341
2342        // Disable compression for SecureJoin to ensure
2343        // there are no compression side channels
2344        // leaking information about the tokens.
2345        let compress = false;
2346        // Only sign the message if we attach the pubkey.
2347        let sign = attach_self_pubkey;
2348        let encrypted = encrypt_helper
2349            .encrypt_symmetrically(context, shared_secret, message, compress, sign)
2350            .await?;
2351
2352        wrap_encrypted_part(encrypted)
2353    };
2354
2355    let message = render_outer_message(unprotected_headers, outer_message);
2356
2357    Ok(message)
2358}
2359
2360#[cfg(test)]
2361mod mimefactory_tests;