Skip to main content

kernoid_client/
e2e.rs

1// SPDX-License-Identifier: MIT
2//! End-to-end encrypted mail (RFC 0006, 4–5) over kernoid's extension `https://kernoid.dev/jmap/openpgp`: the
3//! account's keys with the protected copies of their secret keys (`OpenPgpKey/get`), unlocked here with the recovery
4//! code and kept only in memory; the public keys of others (`OpenPgpKey/lookup`), looked up once per run; the summaries
5//! of encrypted emails for the list; encrypted emails downloaded, decrypted and read here – never through the server's
6//! `Email/parse` –; and messages built, encrypted and signed here, imported into Drafts with their summary and submitted
7//! with an envelope, so that the server sees neither text nor attachments. The cryptography is all `kernoid-e2e`'s.
8
9use std::{
10    collections::BTreeMap,
11    hash::{BuildHasher, Hasher},
12    io::Cursor,
13    path::PathBuf,
14    time::{SystemTime, UNIX_EPOCH},
15};
16
17use base64::{Engine, engine::general_purpose::STANDARD};
18use kernoid_e2e::{
19    Kind, PublicKey, SecretKey,
20    compose::{self, Mailbox as Address},
21    keys::{normalize_recovery_code, recovery_code},
22    mime,
23};
24use serde::Deserialize;
25use serde_json::{Value, json};
26
27mod vault;
28
29use crate::{
30    Client, Error, MAIL, Result, SUBMISSION,
31    mail::{
32        BodyPart, BodyValue, Draft, Email, EmailAddress, EmailSummary, Identity, InlineFile, Inner, Mailbox, Sealed,
33        role,
34    },
35    set_error,
36};
37
38pub const OPENPGP: &str = "https://kernoid.dev/jmap/openpgp";
39
40/// A protected copy of a secret key (RFC 0006, 4.2), base64.
41#[derive(Debug, Clone, Deserialize)]
42pub struct KeyCopy {
43    #[serde(default)]
44    pub protected: String,
45    #[serde(default)]
46    pub salt: Option<String>,
47}
48
49/// One of the account's keys (`OpenPgpKey/get`).
50#[derive(Debug, Clone, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct Key {
53    /// The fingerprint.
54    pub id: String,
55    #[serde(default)]
56    pub kind: String,
57    #[serde(default)]
58    pub public_key: String,
59    #[serde(default)]
60    pub retired: Option<String>,
61    #[serde(default)]
62    pub copies: BTreeMap<String, KeyCopy>,
63}
64
65/// The account's and its tenant's settings (`E2eSettings/get`, RFC 0006, 8).
66#[derive(Debug, Clone, Default, Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct Settings {
69    /// Mail is encrypted on arrival (5.1).
70    #[serde(default)]
71    pub mail: bool,
72    #[serde(default)]
73    pub policy: Policy,
74    #[serde(default)]
75    pub recovery_key: Option<RecoveryKey>,
76}
77
78/// The tenant's policy (RFC 0006, 8.1).
79#[derive(Debug, Clone, Default, Deserialize)]
80pub struct Policy {
81    /// `allowed`, `required` or `forbidden`.
82    #[serde(default)]
83    pub mail: String,
84    /// For vaults: `allowed`, `required` (nothing new outside one) or `forbidden` (6).
85    #[serde(default)]
86    pub vaults: String,
87    /// The tenant can recover the account's keys (8.2).
88    #[serde(default)]
89    pub recovery: bool,
90}
91
92/// The tenant's recovery key: its public key, base64.
93#[derive(Debug, Clone, Deserialize)]
94#[serde(rename_all = "camelCase")]
95pub struct RecoveryKey {
96    #[serde(default)]
97    pub public_key: String,
98}
99
100/// How a message goes out.
101#[derive(Debug, Clone)]
102pub enum Plan {
103    /// End-to-end encrypted to these keys – every recipient's and the sender's own – and signed.
104    Sealed(Vec<PublicKey>),
105    /// As before, with the reason it is not encrypted.
106    Plain(String),
107}
108
109/// A message to store: from whom, in which account, the files to attach (read here, never uploaded as they are).
110#[derive(Debug, Clone, Copy)]
111pub struct Outgoing<'a> {
112    pub account: &'a str,
113    pub identity: &'a Identity,
114    pub draft: &'a Draft,
115    pub files: &'a [PathBuf],
116    pub boxes: &'a [Mailbox],
117}
118
119/// What this run knows of end-to-end encryption: whether the server has it, the account's keys and settings, those
120/// unlocked here, and the public keys of others looked up so far.
121#[derive(Default)]
122pub struct E2e {
123    pub available: bool,
124    pub keys: Vec<Key>,
125    pub settings: Settings,
126    /// The key in use, unlocked.
127    current: Option<SecretKey>,
128    /// Every key unlocked: the one in use and retired ones, for mail encrypted to them before.
129    unlocked: Vec<SecretKey>,
130    /// Public keys by address (lower case); an address without any has none.
131    known: BTreeMap<String, Vec<PublicKey>>,
132    /// The keys of vaults opened so far, by the vault's id (RFC 0006, 6; `vault.rs`).
133    vaults: BTreeMap<String, SecretKey>,
134}
135
136impl std::fmt::Debug for E2e {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        f.debug_struct("E2e")
139            .field("available", &self.available)
140            .field("keys", &self.keys.len())
141            .field("unlocked", &self.current.is_some())
142            .finish_non_exhaustive()
143    }
144}
145
146fn decode(text: &str) -> Result<Vec<u8>> {
147    STANDARD.decode(text).map_err(|_| Error::Protocol("not base64".into()))
148}
149
150fn list<T: serde::de::DeserializeOwned>(result: &Value) -> Result<Vec<T>> {
151    serde_json::from_value(result.get("list").cloned().unwrap_or(Value::Null))
152        .map_err(|error| Error::Protocol(error.to_string()))
153}
154
155impl E2e {
156    /// Reads the account's keys and settings – nothing if the server has no end-to-end extension.
157    pub async fn load(client: &Client) -> Result<E2e> {
158        if !client.session.capabilities.contains_key(OPENPGP) {
159            return Ok(E2e::default());
160        }
161        let account = client.primary(OPENPGP)?;
162        let responses = client
163            .request(
164                &[OPENPGP],
165                vec![
166                    ("OpenPgpKey/get", json!({ "accountId": account }), "k"),
167                    ("E2eSettings/get", json!({ "accountId": account }), "s"),
168                ],
169            )
170            .await?;
171        let keys = list(responses.get("k").unwrap_or(&Value::Null))?;
172        let settings =
173            list::<Settings>(responses.get("s").unwrap_or(&Value::Null))?.into_iter().next().unwrap_or_default();
174        Ok(E2e { available: true, keys, settings, ..E2e::default() })
175    }
176
177    /// Reads keys and settings again, keeping what is unlocked.
178    pub async fn refresh(&mut self, client: &Client) -> Result<()> {
179        let fresh = E2e::load(client).await?;
180        self.available = fresh.available;
181        self.keys = fresh.keys;
182        self.settings = fresh.settings;
183        Ok(())
184    }
185
186    /// Makes the account's key here (RFC 0006, 4.2) for the addresses it sends from, stores it with a copy protected by
187    /// a new recovery code – and the tenant's copy when its policy recovers keys (8.2) – and keeps it unlocked. Returns
188    /// the recovery code, to be shown once.
189    pub async fn setup(&mut self, client: &Client, kind: Kind) -> Result<String> {
190        if !self.available {
191            return Err(Error::E2e("the server has no end-to-end encryption".into()));
192        }
193        if self.has_key() {
194            return Err(Error::E2e("the account has a key already".into()));
195        }
196        let account = client.primary(OPENPGP)?;
197        let mail_account = client.session.primary(MAIL).unwrap_or(&account).to_owned();
198        let mut addresses: Vec<String> =
199            match client.call(&[MAIL, SUBMISSION], "Identity/get", json!({ "accountId": mail_account })).await {
200                Ok(result) => list::<Identity>(&result)?.into_iter().map(|i| i.email.to_lowercase()).collect(),
201                Err(_) => Vec::new(),
202            };
203        addresses.push(client.session.username.to_lowercase());
204        addresses.sort();
205        addresses.dedup();
206        addresses.retain(|a| a.contains('@'));
207        // Making a post-quantum key takes a moment; it does not hold the runtime's other work.
208        let secret = tokio::task::spawn_blocking(move || {
209            let names: Vec<&str> = addresses.iter().map(String::as_str).collect();
210            SecretKey::generate(kind, &names)
211        })
212        .await
213        .map_err(|error| Error::E2e(error.to_string()))??;
214        let code = recovery_code();
215        let protected = STANDARD.encode(secret.protect(&normalize_recovery_code(&code))?);
216        let mut copies = json!({ "recovery": { "protected": protected, "salt": null } });
217        if self.settings.policy.recovery
218            && let Some(recovery) = &self.settings.recovery_key
219        {
220            let tenant = PublicKey::parse(&decode(recovery.public_key.trim())?)?;
221            let armored = secret.export()?;
222            let sealed = kernoid_e2e::message::encrypt(armored.as_bytes(), &[&tenant], None, false)?;
223            copies["tenant"] = json!({ "protected": STANDARD.encode(sealed), "salt": null });
224        }
225        let public = STANDARD.encode(secret.public().to_bytes()?);
226        let create = json!({ "create": { "key": { "publicKey": public, "copies": copies } } });
227        client.set(&[OPENPGP], "OpenPgpKey", &account, create, json!({})).await?;
228        self.refresh(client).await?;
229        self.unlocked = vec![secret.clone()];
230        self.current = Some(secret);
231        Ok(code)
232    }
233
234    /// The key in use (not retired).
235    pub fn current_key(&self) -> Option<&Key> {
236        self.keys.iter().find(|key| key.retired.is_none())
237    }
238
239    pub fn has_key(&self) -> bool {
240        self.current_key().is_some()
241    }
242
243    pub fn is_unlocked(&self) -> bool {
244        self.current.is_some()
245    }
246
247    /// Unlocks the key in use with the recovery code (as typed: separators and case do not matter) – and retired keys
248    /// whose copy the same code opens. They stay in this process's memory only.
249    pub fn unlock(&mut self, code: &str) -> Result<()> {
250        let key = self.current_key().ok_or_else(|| Error::E2e("this account has no key".into()))?;
251        let copy =
252            key.copies.get("recovery").ok_or_else(|| Error::E2e("the key has no copy for a recovery code".into()))?;
253        let code = normalize_recovery_code(code);
254        let current = SecretKey::unlock(&decode(&copy.protected)?, &code)?;
255        if !current.fingerprint().eq_ignore_ascii_case(&key.id) {
256            return Err(Error::E2e("the copy is not of the key in use".into()));
257        }
258        let mut unlocked = vec![current.clone()];
259        for key in self.keys.iter().filter(|key| key.retired.is_some()) {
260            let Some(copy) = key.copies.get("recovery") else { continue };
261            if let Ok(secret) = decode(&copy.protected).and_then(|p| Ok(SecretKey::unlock(&p, &code)?)) {
262                unlocked.push(secret);
263            }
264        }
265        self.unlocked = unlocked;
266        self.current = Some(current);
267        Ok(())
268    }
269
270    /// Forgets the unlocked keys.
271    pub fn lock(&mut self) {
272        self.current = None;
273        self.unlocked.clear();
274        self.vaults.clear();
275    }
276
277    /// The public keys of `addresses` (asked once per run).
278    pub async fn lookup(&mut self, client: &Client, addresses: &[String]) -> Result<BTreeMap<String, Vec<PublicKey>>> {
279        let mut wanted: Vec<String> = addresses.iter().map(|a| a.to_lowercase()).collect();
280        wanted.sort();
281        wanted.dedup();
282        wanted.retain(|address| !self.known.contains_key(address));
283        if !wanted.is_empty() {
284            #[derive(Deserialize)]
285            #[serde(rename_all = "camelCase")]
286            struct Found {
287                public_key: String,
288            }
289            #[derive(Deserialize)]
290            struct Entry {
291                email: String,
292                #[serde(default)]
293                keys: Vec<Found>,
294            }
295            let account = client.primary(OPENPGP)?;
296            let result =
297                client.call(&[OPENPGP], "OpenPgpKey/lookup", json!({ "accountId": account, "emails": wanted })).await?;
298            for entry in list::<Entry>(&result)? {
299                // Keys this library cannot use are left out.
300                let keys =
301                    entry.keys.iter().filter_map(|key| PublicKey::parse(&decode(&key.public_key).ok()?).ok()).collect();
302                self.known.insert(entry.email.to_lowercase(), keys);
303            }
304            for address in wanted {
305                self.known.entry(address).or_default();
306            }
307        }
308        Ok(addresses
309            .iter()
310            .map(|a| (a.to_lowercase(), self.known.get(&a.to_lowercase()).cloned().unwrap_or_default()))
311            .collect())
312    }
313
314    fn secrets(&self) -> Vec<&SecretKey> {
315        self.unlocked.iter().collect()
316    }
317
318    /// Fills in subject, preview and attachments of an encrypted email from its summary, once the key is unlocked.
319    pub fn open_summary(&self, email: &mut EmailSummary) {
320        if !email.encrypted || email.opened || self.unlocked.is_empty() {
321            return;
322        }
323        let Some(sealed) = email.encrypted_summary.as_deref().and_then(|s| STANDARD.decode(s).ok()) else { return };
324        // Encrypted to a key not unlocked here: it stays locked.
325        if let Ok(summary) = mime::decrypt_summary(&sealed, &self.secrets()) {
326            email.subject = summary.subject;
327            email.preview = summary.preview;
328            email.has_attachment = summary.has_attachment;
329            email.opened = true;
330        }
331    }
332
333    /// An email to read: an encrypted one downloaded, decrypted and read into its parts here, its signature checked
334    /// against its sender's keys – locked if the key is not unlocked or does not open it. Other emails as they are.
335    pub async fn open(&mut self, client: &Client, account: &str, mut email: Email) -> Result<Email> {
336        if !email.summary.encrypted {
337            return Ok(email);
338        }
339        self.open_summary(&mut email.summary);
340        let mut locked = email.clone();
341        locked.text_body.clear();
342        locked.html_body.clear();
343        locked.attachments.clear();
344        locked.body_values.clear();
345        locked.sealed = Some(Sealed { locked: true, ..Sealed::default() });
346        let Some(blob) = email.blob_id.clone().filter(|_| self.is_unlocked()) else { return Ok(locked) };
347        let mut raw = Vec::new();
348        client.download(account, &blob, "message.eml", "message/rfc822", &mut raw, &mut |_, _| {}).await?;
349        let senders: Vec<String> = email.summary.from.iter().flatten().map(|a| a.email.clone()).collect();
350        let senders: Vec<PublicKey> = if senders.is_empty() {
351            Vec::new()
352        } else {
353            self.lookup(client, &senders).await.map(|found| found.into_values().flatten().collect()).unwrap_or_default()
354        };
355        let senders: Vec<&PublicKey> = senders.iter().collect();
356        let Ok(opened) = mime::decrypt_message(&raw, &self.secrets(), &senders) else { return Ok(locked) };
357        let Ok(parsed) = compose::parse(&opened.data) else { return Ok(locked) };
358        let addresses = |list: Vec<Address>| -> Vec<EmailAddress> {
359            list.into_iter().map(|a| EmailAddress { name: a.name, email: a.email }).collect()
360        };
361        let part = |id: &str, kind: &str, size: usize| BodyPart {
362            part_id: Some(id.to_owned()),
363            blob_id: None,
364            kind: kind.to_owned(),
365            name: None,
366            size: size as u64,
367            inner: None,
368        };
369        email.summary.subject = parsed.subject;
370        if !parsed.from.is_empty() {
371            email.summary.from = Some(addresses(parsed.from));
372        }
373        if !parsed.to.is_empty() {
374            email.summary.to = Some(addresses(parsed.to));
375        }
376        email.cc = Some(addresses(parsed.cc));
377        email.reply_to = Some(addresses(parsed.reply_to));
378        email.sent_at = parsed.sent_at.or(email.sent_at);
379        email.message_id = parsed.message_id.map(|id| vec![id]).or(email.message_id);
380        if !parsed.references.is_empty() {
381            email.references = Some(parsed.references);
382        }
383        email.text_body.clear();
384        email.html_body.clear();
385        email.body_values.clear();
386        if let Some(text) = parsed.text {
387            email.text_body.push(part("text", "text/plain", text.len()));
388            email.body_values.insert("text".into(), BodyValue { value: text });
389        }
390        if let Some(html) = parsed.html {
391            email.html_body.push(part("html", "text/html", html.len()));
392            email.body_values.insert("html".into(), BodyValue { value: html });
393        } else {
394            email.html_body = email.text_body.clone();
395        }
396        email.attachments = parsed
397            .attachments
398            .into_iter()
399            .map(|p| BodyPart {
400                part_id: None,
401                blob_id: None,
402                kind: p.media_type,
403                name: p.name,
404                size: p.size as u64,
405                inner: Some(p.index),
406            })
407            .collect();
408        email.summary.has_attachment = !email.attachments.is_empty();
409        email.inner = Some(Inner(opened.data));
410        email.sealed = Some(Sealed { locked: false, signed: opened.signed, verified: opened.verified_by.is_some() });
411        Ok(email)
412    }
413
414    /// The key a message from `identity` in `account` is encrypted and signed with: the unlocked key in use, if the
415    /// account is the user's own and the key has the address – else why not.
416    fn own_key(&self, client: &Client, account: &str, identity: &Identity) -> std::result::Result<&SecretKey, String> {
417        if !self.available {
418            return Err("the server has no end-to-end encryption".into());
419        }
420        let Some(current) = &self.current else {
421            return Err(if self.has_key() {
422                "your key is locked (K unlocks it)".into()
423            } else {
424                "your account has no key (made in the web app)".into()
425            });
426        };
427        if client.session.primary(MAIL) != Some(account) {
428            return Err("it is not sent from your own account".into());
429        }
430        if !current.public().addresses().iter().any(|a| a.eq_ignore_ascii_case(&identity.email)) {
431            return Err(format!("your key is not for {}", identity.email));
432        }
433        Ok(current)
434    }
435
436    /// How a message goes out: end-to-end encrypted if the key is unlocked, it is sent from the user's own account
437    /// as an address of the key, and every recipient has a key (RFC 0006, 5.3) – else as before.
438    pub async fn plan(&mut self, client: &Client, account: &str, identity: &Identity, draft: &Draft) -> Plan {
439        let own = match self.own_key(client, account, identity) {
440            Ok(key) => key.public(),
441            Err(reason) => return Plan::Plain(reason),
442        };
443        // An OpenPGP message names the key of each recipient: Bcc recipients would be seen by all.
444        if !draft.bcc.is_empty() {
445            return Plan::Plain("it has Bcc recipients, whom an encrypted message would show".into());
446        }
447        let addresses: Vec<String> = draft.to.iter().chain(&draft.cc).map(|a| a.email.clone()).collect();
448        if addresses.is_empty() {
449            return Plan::Plain("it has no recipients".into());
450        }
451        let found = match self.lookup(client, &addresses).await {
452            Ok(found) => found,
453            Err(error) => return Plan::Plain(format!("the keys could not be looked up: {error}")),
454        };
455        let mut keys = Vec::new();
456        for (address, list) in found {
457            let usable: Vec<PublicKey> = list.into_iter().filter(PublicKey::can_encrypt).collect();
458            if usable.is_empty() {
459                return Plan::Plain(format!("{address} has no key"));
460            }
461            keys.extend(usable);
462        }
463        keys.push(own);
464        Plan::Sealed(keys)
465    }
466
467    /// Whether a draft is kept encrypted to the user's own key – or why not.
468    pub fn draft_plan(&self, client: &Client, account: &str, identity: &Identity, draft: &Draft) -> Plan {
469        match self.own_key(client, account, identity) {
470            Ok(_) if !draft.bcc.is_empty() => Plan::Plain("an encrypted draft cannot keep Bcc".into()),
471            Ok(key) => Plan::Sealed(vec![key.public()]),
472            Err(reason) => Plan::Plain(reason),
473        }
474    }
475
476    /// Sends end-to-end encrypted to `recipients` (from `plan`): the message built, encrypted and signed here,
477    /// uploaded as it is, imported into Drafts with its summary encrypted to the sender, submitted with an envelope
478    /// (To and Cc), and moved to Sent once sent.
479    pub async fn send(&self, client: &Client, outgoing: Outgoing<'_>, recipients: &[PublicKey]) -> Result<()> {
480        self.store(client, outgoing, &[], recipients, true).await
481    }
482
483    /// Keeps a draft encrypted to `keys` (from `draft_plan`).
484    pub async fn keep(&self, client: &Client, outgoing: Outgoing<'_>, keys: &[PublicKey]) -> Result<()> {
485        self.store(client, outgoing, &[], keys, false).await
486    }
487
488    /// [`send`](Self::send), with files held in memory attached as well – the attachments of an encrypted message
489    /// forwarded ([`Email::decrypted_attachments`](crate::mail::Email::decrypted_attachments)).
490    pub async fn send_with(
491        &self,
492        client: &Client,
493        outgoing: Outgoing<'_>,
494        inline: &[InlineFile],
495        recipients: &[PublicKey],
496    ) -> Result<()> {
497        self.store(client, outgoing, inline, recipients, true).await
498    }
499
500    /// [`keep`](Self::keep), with files held in memory attached as well.
501    pub async fn keep_with(
502        &self,
503        client: &Client,
504        outgoing: Outgoing<'_>,
505        inline: &[InlineFile],
506        keys: &[PublicKey],
507    ) -> Result<()> {
508        self.store(client, outgoing, inline, keys, false).await
509    }
510
511    async fn store(
512        &self,
513        client: &Client,
514        outgoing: Outgoing<'_>,
515        inline: &[InlineFile],
516        recipients: &[PublicKey],
517        submit: bool,
518    ) -> Result<()> {
519        let Outgoing { account, identity, draft, files, boxes } = outgoing;
520        let own = self.own_key(client, account, identity).map_err(Error::E2e)?;
521        let drafts = role(boxes, "drafts").ok_or_else(|| Error::Protocol("there is no Drafts mailbox".into()))?;
522        let mut attachments = Vec::new();
523        // Forwarded files lie in the account already; they go inside the encrypted message.
524        for file in &draft.attachments {
525            let mut data = Vec::new();
526            client.download(account, &file.blob_id, &file.name, &file.kind, &mut data, &mut |_, _| {}).await?;
527            attachments.push(compose::Attachment {
528                name: file.name.clone(),
529                media_type: file.kind.clone(),
530                data: STANDARD.encode(data),
531            });
532        }
533        for path in files {
534            let data = std::fs::read(path).map_err(|error| Error::Protocol(format!("{}: {error}", path.display())))?;
535            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("attachment").to_owned();
536            let media_type = crate::files::media_type(&name).to_owned();
537            attachments.push(compose::Attachment { name, media_type, data: STANDARD.encode(data) });
538        }
539        for file in inline {
540            attachments.push(compose::Attachment {
541                name: file.name.clone(),
542                media_type: file.media_type.clone(),
543                data: STANDARD.encode(&file.data),
544            });
545        }
546        let address = |a: &EmailAddress| Address { name: a.name.clone(), email: a.email.clone() };
547        let strip = |ids: &[String]| -> Vec<String> {
548            ids.iter().map(|id| id.trim().trim_start_matches('<').trim_end_matches('>').to_owned()).collect()
549        };
550        let domain = identity.email.rsplit_once('@').map(|(_, d)| d).unwrap_or("localhost");
551        let message = compose::Draft {
552            from: Some(Address {
553                name: (!identity.name.is_empty()).then(|| identity.name.clone()),
554                email: identity.email.clone(),
555            }),
556            to: draft.to.iter().map(address).collect(),
557            cc: draft.cc.iter().map(address).collect(),
558            reply_to: Vec::new(),
559            subject: draft.subject.clone(),
560            message_id: format!("{}@{domain}", random_id()),
561            in_reply_to: strip(&draft.in_reply_to),
562            references: strip(&draft.references),
563            date: chrono::Local::now().to_rfc2822(),
564            text: draft.body.clone(),
565            html: None,
566            attachments,
567        };
568        let raw = compose::build(&message)?;
569        let recipients: Vec<&PublicKey> = recipients.iter().collect();
570        let encrypted = mime::encrypt_message(&raw, &recipients, Some(own))?;
571        let summary = mime::encrypted_summary(&raw, &[&own.public()])?;
572        let size = encrypted.len() as u64;
573        let upload = client.upload(account, "message/rfc822", Cursor::new(encrypted), size, |_, _| {}).await?;
574        let mut calls = vec![(
575            "Email/import",
576            json!({ "accountId": account, "emails": { "draft": {
577                "blobId": upload.blob_id, "mailboxIds": { drafts.id.as_str(): true },
578                "keywords": { "$draft": true, "$seen": true }, "kernoid:encryptedSummary": STANDARD.encode(summary),
579            } } }),
580            "e",
581        )];
582        if submit {
583            let mut update = json!({ "keywords/$draft": null });
584            if let Some(sent) = role(boxes, "sent") {
585                update["mailboxIds"] = json!({ sent.id.as_str(): true });
586            }
587            let rcpt: Vec<Value> =
588                draft.to.iter().chain(&draft.cc).map(|a| json!({ "email": a.email, "parameters": null })).collect();
589            calls.push((
590                "EmailSubmission/set",
591                json!({
592                    "accountId": account,
593                    "create": { "send": {
594                        "identityId": identity.id, "emailId": "#draft",
595                        "envelope": { "mailFrom": { "email": identity.email, "parameters": null }, "rcptTo": rcpt },
596                    } },
597                    "onSuccessUpdateEmail": { "#send": update },
598                }),
599                "s",
600            ));
601        }
602        let responses = client.request(&[MAIL, SUBMISSION, OPENPGP], calls).await?;
603        for (_, result, _) in &responses.0 {
604            set_error(result)?;
605        }
606        Ok(())
607    }
608}
609
610/// 128 bits for a `Message-ID` – unique, not secret: the standard library's per-process random hash keys over the
611/// time.
612fn random_id() -> String {
613    let state = std::collections::hash_map::RandomState::new();
614    let now = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or_default();
615    let half = |n: u8| {
616        let mut hasher = state.build_hasher();
617        hasher.write_u128(now);
618        hasher.write_u32(std::process::id());
619        hasher.write_u8(n);
620        hasher.finish()
621    };
622    format!("{:016x}{:016x}", half(0), half(1))
623}