1use crate::SyncError;
17use crate::fetch::block_on;
18use crate::imap::{self, COMMAND, Server};
19use crate::mailbox;
20use crate::sasl::{self, OAuth};
21use mail_send::SmtpClientBuilder;
22use mail_send::smtp::message::Message;
23use sioul_core::compose::{self, Draft, DraftKind, Outgoing};
24use sioul_core::config::{self, Account, Security};
25use sioul_core::folders::Role;
26use sioul_core::i18n::Translator;
27use sioul_core::pgp::{self, Place};
28use sioul_core::securitykey::{self, CardAccess, CardError, CardSigner, Purpose};
29use std::path::Path;
30use std::time::{Duration, SystemTime, UNIX_EPOCH};
31
32const SMTP: Duration = Duration::from_secs(60);
33
34pub fn send_draft(config_path: &Path, account: &Account, password: &str, draft: &Draft, tr: &Translator) -> Result<(), SyncError> {
39 let account = with_smtp(config_path, account)?;
40 let outgoing = build_draft(&account, draft, tr, None).map_err(|e| e.into_sync(tr))?;
41 send(&account, password, &outgoing)
42}
43
44pub fn send_built(config_path: &Path, account: &Account, password: &str, outgoing: &Outgoing) -> Result<(), SyncError> {
47 let account = with_smtp(config_path, account)?;
48 send(&account, password, outgoing)
49}
50
51#[derive(Debug)]
53pub enum BuildError {
54 CardNeeded(String),
57 OpenNeeded(String),
61 Card(CardError),
63 Other(SyncError),
65}
66
67impl BuildError {
68 pub fn into_sync(self, tr: &Translator) -> SyncError {
70 match self {
71 BuildError::CardNeeded(_) => SyncError::Message(tr.text("seckey-sign-in-window", None)),
72 BuildError::OpenNeeded(_) => SyncError::Message(tr.text("seckey-open-in-window", None)),
73 BuildError::Card(e) => SyncError::Message(e.sentence(tr, Purpose::Sign)),
74 BuildError::Other(e) => e,
75 }
76 }
77}
78
79pub fn build_draft(account: &Account, draft: &Draft, tr: &Translator, card: Option<&CardAccess>) -> Result<Outgoing, BuildError> {
84 build_draft_with(&pgp::Keys::load(), account, draft, tr, card)
85}
86
87fn build_draft_with(keys: &pgp::Keys, account: &Account, draft: &Draft, tr: &Translator, card: Option<&CardAccess>) -> Result<Outgoing, BuildError> {
88 let name = account.name.clone().unwrap_or_default();
89 let address = account.address.clone().unwrap_or_default();
90 let date = SystemTime::now().duration_since(UNIX_EPOCH).map_or(0, |d| i64::try_from(d.as_secs()).unwrap_or(0));
91 let own = keys.own_for(&address);
95 let yours = keys.self_cert_for(&address);
96 let autocrypt = yours.and_then(|cert| pgp::autocrypt_header(cert, &address));
97 let say = |id: &str, pairs: &[(&str, String)]| {
98 let mut args = sioul_core::i18n::args();
99 for (name, value) in pairs {
100 args.set(*name, value.clone());
101 }
102 BuildError::Other(SyncError::Message(tr.text(id, Some(&args))))
103 };
104 let original = match draft.original_here() {
114 None => None,
115 Some(path) => {
116 let raw = sioul_core::filelock::read_whole(&path).map_err(|e| BuildError::Other(SyncError::Disk(format!("{}: {e}", path.display()))))?;
118 if !pgp::is_encrypted(&raw) {
119 Some(raw)
120 } else if !draft.encrypt {
121 if draft.kind == DraftKind::Forward {
122 return Err(say("pgp-forward-needs-encryption", &[]));
123 }
124 None
125 } else {
126 let passphrase = |fingerprint: &pgp::Fingerprint| crate::secret::pgp_passphrase(&fingerprint.to_hex());
127 let unlock = pgp::Unlock::new(&passphrase, pgp::SessionKeys::global());
128 let unlock = match card {
129 Some(card) => unlock.with_card(card),
130 None => unlock,
131 };
132 match pgp::open(&raw, keys, &unlock) {
133 Some((Some(opened), _)) => Some(opened),
134 _ => {
135 match (card.and_then(CardAccess::failure), unlock.wanted(), card) {
136 (Some(failure), _, _) => return Err(BuildError::Card(failure)),
137 (None, Some(_), None) if !crate::securitykey::usable_here() && draft.kind != DraftKind::Forward => None,
139 (None, Some(ident), None) => return Err(BuildError::OpenNeeded(ident)),
140 _ if draft.kind == DraftKind::Forward => return Err(say("pgp-forward-locked", &[])),
141 (None, Some(_), Some(_)) => return Err(say("pgp-quote-locked", &[])),
143 (None, None, _) => None,
144 }
145 }
146 }
147 }
148 }
149 };
150 let mut here = None;
152 let mut on_card = None;
153 if draft.sign {
154 match &own {
155 Some(pgp::Own { cert, place: Place::Here }) => {
156 let passphrase = crate::secret::pgp_passphrase(&cert.fingerprint().to_hex()).unwrap_or_default();
157 here = Some(pgp::here_signer(cert, &passphrase).map_err(|e| BuildError::Other(SyncError::Message(e)))?);
158 }
159 Some(pgp::Own { cert, place: Place::Card { ident } }) => {
160 let access = card.ok_or_else(|| BuildError::CardNeeded(ident.clone()))?;
161 let fingerprint = keys.card_for(&address).map(|(known, _)| known.sign.clone()).unwrap_or_default();
162 let public = pgp::key_of(cert, &fingerprint).ok_or(BuildError::Card(CardError::OtherKeys))?;
163 on_card = Some(CardSigner::new(access, ident, public));
164 }
165 None => {
167 return Err(match keys.card_for(&address) {
168 Some((known, cert)) => {
169 let now = date;
170 let expires = securitykey::expiry_of(cert, &[&known.sign, &known.decrypt]);
171 let line = securitykey::expiry_line(tr, expires, &cert.fingerprint().to_hex(), now).map(|(line, _)| line);
172 BuildError::Other(SyncError::Message(line.unwrap_or_else(|| tr.text("seckey-cannot-sign", None))))
173 }
174 None => say("pgp-no-own-key", &[("address", address.clone())]),
175 });
176 }
177 }
178 }
179 let signer: Option<&mut pgp::Signing<'_>> = match (&mut here, &mut on_card) {
180 (Some(pair), _) => Some(pair),
181 (_, Some(signer)) => Some(signer),
182 _ => None,
183 };
184 let mut protection = if draft.sign || draft.encrypt {
185 let mut encrypt_to = Vec::new();
186 if draft.encrypt {
187 for recipient in draft.recipients() {
188 encrypt_to.push(keys.for_address(&recipient).ok_or_else(|| say("pgp-missing-key", &[("address", recipient.clone())]))?);
189 }
190 if let Some(cert) = yours
192 && !encrypt_to.iter().any(|c| c.fingerprint() == cert.fingerprint())
193 {
194 encrypt_to.push(cert);
195 }
196 }
197 Some(compose::Protection { sign: signer, encrypt_to })
198 } else {
199 None
200 };
201 compose::build_with(draft, (&name, &address), tr, date, protection.as_mut(), autocrypt.as_deref(), original.as_deref()).map_err(|e| match card.and_then(|c| c.failure()) {
202 Some(failure) => BuildError::Card(failure),
203 None => BuildError::Other(SyncError::Message(e)),
204 })
205}
206
207fn with_smtp(config_path: &Path, account: &Account) -> Result<Account, SyncError> {
208 if account.smtp_host.is_some() {
209 return Ok(account.clone());
210 }
211 let smtp = crate::discover_smtp(account.address.as_deref().unwrap_or(""), account.host.as_deref())?;
212 config::set_smtp(config_path, &account.id, &smtp.host, smtp.port, smtp.security).map_err(SyncError::Disk)?;
213 let mut account = account.clone();
214 account.smtp_host = Some(smtp.host);
215 account.smtp_port = Some(smtp.port);
216 account.smtp_security = smtp.security;
217 Ok(account)
218}
219
220fn smtp_builder<'a>(account: &Account, host: &'a str, credentials: Option<(&'a str, &'a str)>) -> Result<SmtpClientBuilder<&'a str>, SyncError> {
223 let builder = SmtpClientBuilder::new(host, account.smtp_port_or_default())
224 .map_err(SyncError::Network)?
225 .implicit_tls(account.smtp_security == Security::Tls)
226 .helo_host("[127.0.0.1]")
230 .timeout(SMTP);
231 let builder = match credentials {
232 Some(credentials) => builder.credentials(credentials),
233 None => builder,
234 };
235 #[cfg(target_os = "android")]
239 let builder = {
240 let mut builder = builder;
241 builder.tls_connector = tokio_rustls::TlsConnector::from(crate::imap::tls_config());
242 builder
243 };
244 #[cfg(feature = "insecure-test-tls")]
245 let builder = if std::env::var_os("SIOUL_TEST_INSECURE_TLS").is_some() { builder.allow_invalid_certs() } else { builder };
246 Ok(builder)
247}
248
249pub fn send(account: &Account, password: &str, outgoing: &Outgoing) -> Result<(), SyncError> {
254 let host = account.smtp_host.clone().ok_or(SyncError::NoServer)?;
255 let login = account.login().ok_or(SyncError::NoServer)?.to_string();
256 let oauth = OAuth::of(account);
257 block_on(async {
258 let mut client = match oauth {
259 None => smtp_builder(account, &host, Some((login.as_str(), password)))?.connect().await.map_err(smtp_error)?,
260 Some(oauth) => {
261 oauth.may_reach(&host, true)?;
263 let connect = || async { smtp_builder(account, &host, None)?.connect().await.map_err(smtp_error) };
264 smtp_sign_in_with_tokens(&login, connect, |fresh| oauth.token_async(&login, fresh), oauth).await?
265 }
266 };
267 let message = Message::new(outgoing.from.as_str(), outgoing.recipients.iter().map(String::as_str), outgoing.raw.as_slice());
268 client.send(message).await.map_err(smtp_error)?;
269 let _ = client.quit().await;
270 Ok(())
271 })?;
272 if files_its_own_sent(account) {
273 return Ok(());
274 }
275 file_in_sent(account, password, &outgoing.raw).map_err(|e| SyncError::NotFiled(e.detail().to_string()))
277}
278
279async fn smtp_sign_in_with_tokens<T, C, CF, K, KF>(login: &str, connect: C, token: K, oauth: OAuth) -> Result<mail_send::SmtpClient<T>, SyncError>
284where
285 T: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
286 C: Fn() -> CF,
287 CF: Future<Output = Result<mail_send::SmtpClient<T>, SyncError>>,
288 K: Fn(bool) -> KF,
289 KF: Future<Output = Result<String, SyncError>>,
290{
291 let mut refusal = String::new();
292 for fresh in [false, true] {
293 let token = token(fresh).await?;
294 let mut client = connect().await?;
295 match sasl::smtp_xoauth2(&mut client, login, &token).await.map_err(smtp_error)? {
296 Ok(()) => return Ok(client),
297 Err(said) => {
298 let _ = client.quit().await;
299 if let Some(lasting) = oauth.smtp_refused(&said) {
300 return Err(lasting);
301 }
302 refusal = said;
303 }
304 }
305 }
306 Err(oauth.refused(&refusal))
307}
308
309fn files_its_own_sent(account: &Account) -> bool {
313 account.host.as_deref().is_some_and(|h| h.eq_ignore_ascii_case(crate::discover::GOOGLE_IMAP) || crate::microsoft::is_imap_host(h))
314}
315
316fn file_in_sent(account: &Account, password: &str, raw: &[u8]) -> Result<(), SyncError> {
318 let server = Server::of(account)?;
319 block_on(async {
320 let mut session = imap::open(&server, password).await?;
321 let sent = mailbox::role_folder(&mut session, account, Role::Sent, "Sent").await?;
322 let result = imap::within(COMMAND, session.append(&sent, Some("(\\Seen)"), None, raw)).await?.map_err(imap::server);
323 let _ = session.logout().await;
324 result
325 })
326}
327
328fn smtp_error(e: mail_send::Error) -> SyncError {
329 match e {
330 mail_send::Error::AuthenticationFailed(r) => SyncError::Login(r.message().to_string()),
331 mail_send::Error::MissingCredentials | mail_send::Error::UnsupportedAuthMechanism => SyncError::Login(e.to_string()),
332 mail_send::Error::Tls(t) => SyncError::Tls(t.to_string()),
333 mail_send::Error::InvalidTLSName => SyncError::Tls(e.to_string()),
334 mail_send::Error::Io(_) | mail_send::Error::Timeout => SyncError::Network(e.to_string()),
335 mail_send::Error::UnexpectedReply(r) => SyncError::Refused(r.message().to_string()),
336 other => SyncError::Refused(other.to_string()),
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use sioul_core::pgp::{Keys, SessionKeys, Unlock};
344 use sioul_core::securitykey::{KnownCard, SoftCard, SoftReaders};
345
346 #[test]
350 fn microsoft_smtp_refusals() {
351 use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
352 let runtime = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
353 runtime.block_on(async {
354 for (first, second, connections) in [("535 5.7.3 Authentication unsuccessful", Some("235 2.7.0 Authentication successful"), 2), ("535 5.7.139 Authentication unsuccessful, SmtpClientAuthentication is disabled for the Tenant. Visit https://aka.ms/smtp_auth_disabled for more information.", None, 1)] {
355 let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
356 let port = listener.local_addr().unwrap().port();
357 let server = tokio::spawn(async move {
358 let mut heard = Vec::new();
359 for answer in [Some(first), second].into_iter().flatten().take(connections) {
360 let (stream, _) = listener.accept().await.unwrap();
361 let (read, mut write) = stream.into_split();
362 let mut lines = BufReader::new(read).lines();
363 write.write_all(b"220 stand-in Microsoft ESMTP\r\n").await.unwrap();
364 let _ehlo = lines.next_line().await.unwrap().unwrap();
365 write.write_all(b"250-stand-in\r\n250-AUTH LOGIN XOAUTH2\r\n250 8BITMIME\r\n").await.unwrap();
366 heard.push(lines.next_line().await.unwrap().unwrap());
367 write.write_all(format!("{answer}\r\n").as_bytes()).await.unwrap();
368 if let Ok(Some(_)) = lines.next_line().await {
370 let _ = write.write_all(b"221 bye\r\n").await;
371 }
372 }
373 heard
374 });
375 let connect = || async move { mail_send::SmtpClientBuilder::new("127.0.0.1", port).unwrap().helo_host("[127.0.0.1]").connect_plain().await.map_err(smtp_error) };
376 let asked = std::sync::Mutex::new(Vec::new());
377 let token = |fresh: bool| {
378 asked.lock().unwrap().push(fresh);
379 async move { Ok::<_, SyncError>(if fresh { "fresh".to_string() } else { "kept".to_string() }) }
380 };
381 let outcome = smtp_sign_in_with_tokens("you@example.org", connect, token, OAuth::Microsoft { personal: false }).await.map(drop);
383 let heard = server.await.unwrap();
384 match second {
385 Some(_) => {
386 assert!(outcome.is_ok(), "{:?}", outcome.err());
387 assert_eq!(*asked.lock().unwrap(), vec![false, true], "the kept token, then a fresh one, once");
388 assert_eq!(heard, vec![format!("AUTH XOAUTH2 {}", sasl::xoauth2_base64("you@example.org", "kept")), format!("AUTH XOAUTH2 {}", sasl::xoauth2_base64("you@example.org", "fresh"))]);
389 }
390 None => {
391 assert_eq!(outcome.err(), Some(SyncError::Said(crate::microsoft::SMTP_OFF, first.to_string())));
392 assert_eq!(*asked.lock().unwrap(), vec![false], "no second token: none would change it");
393 }
394 }
395 }
396 });
397 }
398
399 #[test]
401 fn microsoft_files_its_own_sent_copy() {
402 let mut account = Account::imap("work", "you@contoso.example", "outlook.office365.com", 993, Security::Tls, None);
403 account.auth = Some("microsoft".into());
404 assert!(files_its_own_sent(&account));
405 assert!(files_its_own_sent(&Account::imap("g", "you@gmail.com", "imap.gmail.com", 993, Security::Tls, None)));
406 assert!(!files_its_own_sent(&Account::imap("o", "you@example.org", "mail.example.org", 993, Security::Tls, None)));
407 }
408
409 #[test]
410 fn a_draft_your_security_key_signs_is_built_with_the_key() {
411 let (card, public) = SoftCard::generate("0006:12345678", &["Card Holder <me@example.org>"], "123456").unwrap();
412 let known = KnownCard {
413 ident: "0006:12345678".into(),
414 sign: card.info.sign.as_ref().unwrap().fingerprint.clone(),
415 decrypt: card.info.decrypt.as_ref().unwrap().fingerprint.clone(),
416 cert: pgp::certificates_in(&public)[0].fingerprint().to_hex(),
417 addresses: vec!["me@example.org".into()],
418 ..KnownCard::default()
419 };
420 let keys = Keys { own: pgp::certificates_in(&public), cards: vec![known], ..Keys::default() };
421 let account: Account = toml::from_str("id = \"me\"\naddress = \"me@example.org\"\nname = \"Card Holder\"\n").unwrap();
422 let mut draft = Draft::new("me");
423 draft.to = vec!["Jane <jane@example.org>".into()];
424 draft.subject = "Signed at Send".into();
425 draft.body = "Hello.".into();
426 draft.sign = true;
427 let tr = Translator::new("en");
428 assert!(matches!(build_draft_with(&keys, &account, &draft, &tr, None), Err(BuildError::CardNeeded(ident)) if ident == "0006:12345678"));
430 let readers = SoftReaders::with(vec![card]);
431 let wrong = sioul_core::securitykey::Password::from("000000");
432 let access = CardAccess::new(&readers, &wrong, &|| {});
433 assert!(matches!(build_draft_with(&keys, &account, &draft, &tr, Some(&access)), Err(BuildError::Card(CardError::WrongPin { left: 2 }))));
434 let pin = sioul_core::securitykey::Password::from("123456");
435 let access = CardAccess::new(&readers, &pin, &|| {});
436 let outgoing = build_draft_with(&keys, &account, &draft, &tr, Some(&access)).unwrap();
437 let text = String::from_utf8_lossy(&outgoing.raw).to_string();
438 assert!(text.contains("multipart/signed") && text.contains("Autocrypt: addr=me@example.org"), "{text}");
439 let reader = Keys { others: pgp::certificates_in(&public), ..Keys::default() };
441 let sessions = SessionKeys::new();
442 let (_, view) = pgp::open(&outgoing.raw, &reader, &Unlock::new(&|_| None, &sessions)).unwrap();
443 assert!(view.signatures.len() == 1 && view.signatures[0].good, "{view:?}");
444 assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(1));
445 assert!(BuildError::CardNeeded("x".into()).into_sync(&tr).detail().contains("window"));
447 }
448
449 #[test]
455 fn the_phone_encrypts_to_its_recipients_and_to_you() {
456 let (card, public) = SoftCard::generate("0006:12345678", &["Card Holder <me@example.org>"], "123456").unwrap();
457 let mine = pgp::certificates_in(&public);
458 let known = KnownCard {
459 ident: "0006:12345678".into(),
460 sign: card.info.sign.as_ref().unwrap().fingerprint.clone(),
461 decrypt: card.info.decrypt.as_ref().unwrap().fingerprint.clone(),
462 cert: mine[0].fingerprint().to_hex(),
463 addresses: vec!["me@example.org".into()],
464 ..KnownCard::default()
465 };
466 let (jane_card, jane_public) = SoftCard::generate("0006:00000002", &["Jane <jane@example.org>"], "654321").unwrap();
468 let jane_cert = pgp::certificates_in(&jane_public);
469 let jane_known = KnownCard {
470 ident: "0006:00000002".into(),
471 sign: jane_card.info.sign.as_ref().unwrap().fingerprint.clone(),
472 decrypt: jane_card.info.decrypt.as_ref().unwrap().fingerprint.clone(),
473 cert: jane_cert[0].fingerprint().to_hex(),
474 addresses: vec!["jane@example.org".into()],
475 ..KnownCard::default()
476 };
477 let keys = Keys { own: mine.clone(), others: jane_cert.clone(), cards: vec![known], ..Keys::default() };
478 let account: Account = toml::from_str("id = \"me\"\naddress = \"me@example.org\"\nname = \"Card Holder\"\n").unwrap();
479 let mut draft = Draft::new("me");
480 draft.to = vec!["Jane <jane@example.org>".into()];
481 draft.subject = "Encrypted from the phone".into();
482 draft.body = "Hello Jane.".into();
483 draft.encrypt = true;
484 let tr = Translator::new("en");
485 let outgoing = build_draft_with(&keys, &account, &draft, &tr, None).unwrap();
486 let text = String::from_utf8_lossy(&outgoing.raw).to_string();
487 assert!(text.contains("multipart/encrypted") && text.contains("Autocrypt: addr=me@example.org"), "{text}");
488 let recipients = pgp::recipients_of(&outgoing.raw);
491 let decrypts = [jane_known.decrypt.clone(), keys.cards[0].decrypt.clone()];
492 assert_eq!(recipients.len(), 2, "{recipients:?}");
493 for recipient in &recipients {
494 assert!(decrypts.iter().any(|d| !recipient.is_empty() && d.to_ascii_uppercase().ends_with(&recipient.to_ascii_uppercase())), "{recipient} among {decrypts:?}");
495 }
496 assert!(decrypts.iter().all(|d| recipients.iter().any(|r| d.to_ascii_uppercase().ends_with(&r.to_ascii_uppercase()))), "Jane and you");
497 let _ = (jane_card, card);
498 let sessions = SessionKeys::new();
500 let (opened, view) = pgp::open(&outgoing.raw, &keys, &Unlock::new(&|_| None, &sessions)).unwrap();
501 assert!(opened.is_none() && view.security_key.as_deref() == Some("0006:12345678"), "{view:?}");
502 draft.to.push("Bob <bob@example.org>".into());
504 assert!(build_draft_with(&keys, &account, &draft, &tr, None).is_err());
505 }
506
507 const LEASE: &str = "From: Jane <jane@example.org>\r\nTo: Card Holder <me@example.org>\r\nSubject: The lease\r\nDate: Thu, 01 Oct 2026 10:00:00 +0200\r\nMessage-ID: <lease-send@example.org>\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThe secret clause is on page four.\r\n";
508
509 fn answer_to(copy: &Path) -> Draft {
510 let mut draft = Draft::new("me");
511 draft.kind = DraftKind::Reply;
512 draft.original = Some(copy.to_path_buf());
513 draft.in_reply_to = Some("lease-send@example.org".into());
514 draft.to = vec!["Jane <jane@example.org>".into()];
515 draft.subject = "Re: The lease".into();
516 draft.body = "Fine by me.".into();
517 draft.encrypt = true;
518 draft
519 }
520
521 fn as_jane_reads(raw: &[u8], jane: &Keys) -> String {
523 let sessions = SessionKeys::new();
524 let (opened, _) = pgp::open(raw, jane, &Unlock::new(&|_| None, &sessions)).expect("protected");
525 String::from_utf8_lossy(&opened.expect("Jane opens it")).to_string()
526 }
527
528 #[test]
535 fn an_encrypted_message_answered_opens_at_send_with_the_key_that_signs() {
536 let (card, public) = SoftCard::generate_rsa("0006:12345678", &["Card Holder <me@example.org>"], "123456").unwrap();
537 let mine = pgp::certificates_in(&public);
538 let known = KnownCard {
539 ident: "0006:12345678".into(),
540 sign: card.info.sign.as_ref().unwrap().fingerprint.clone(),
541 decrypt: card.info.decrypt.as_ref().unwrap().fingerprint.clone(),
542 cert: mine[0].fingerprint().to_hex(),
543 addresses: vec!["me@example.org".into()],
544 ..KnownCard::default()
545 };
546 let jane = pgp::certificates_in(&pgp::stand_in_key("Jane <jane@example.org>").unwrap());
547 let keys = Keys { own: mine.clone(), others: jane.clone(), cards: vec![known], ..Keys::default() };
548 let jane_keys = Keys { own: jane.clone(), others: mine.clone(), ..Keys::default() };
549 let dir = std::env::temp_dir().join(format!("sioul-send-quote-{}", std::process::id()));
550 std::fs::create_dir_all(&dir).unwrap();
551 let copy = dir.join("q1.eml");
552 let sealed = pgp::seal_message(LEASE.as_bytes(), &[&mine[0]]).unwrap();
553 std::fs::write(©, &sealed).unwrap();
554 let account: Account = toml::from_str("id = \"me\"\naddress = \"me@example.org\"\nname = \"Card Holder\"\n").unwrap();
555 let mut draft = answer_to(©);
556 draft.sign = true;
557 let tr = Translator::new("en");
558 assert!(matches!(build_draft_with(&keys, &account, &draft, &tr, None), Err(BuildError::OpenNeeded(ident)) if ident == "0006:12345678"));
560 assert!(BuildError::OpenNeeded("x".into()).into_sync(&tr).detail().contains("window"));
561 let readers = SoftReaders::with(vec![card]);
562 let pin = sioul_core::securitykey::Password::from("123456");
563 let access = CardAccess::new(&readers, &pin, &|| {});
564 let outgoing = build_draft_with(&keys, &account, &draft, &tr, Some(&access)).unwrap();
565 assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(2), "opened, then signed");
566 let text = String::from_utf8_lossy(&outgoing.raw).to_string();
567 assert!(text.contains("multipart/encrypted") && !text.contains("secret clause"), "{text}");
568 let read = as_jane_reads(&outgoing.raw, &jane_keys);
569 assert!(read.contains("Fine by me.") && read.contains("The secret clause is on page four."), "{read}");
570 let recipients = pgp::recipients_of(&outgoing.raw);
572 assert!(recipients.iter().any(|r| keys.cards[0].decrypt.to_ascii_uppercase().ends_with(&r.to_ascii_uppercase())), "{recipients:?}");
573 assert_eq!(std::fs::read(©).unwrap(), sealed);
575 draft.sign = false;
577 let again = build_draft_with(&keys, &account, &draft, &tr, None).unwrap();
578 assert!(as_jane_reads(&again.raw, &jane_keys).contains("The secret clause"));
579 assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(2), "the key not asked again");
580 draft.encrypt = false;
582 let plain = build_draft_with(&keys, &account, &draft, &tr, None).unwrap();
583 let plain = String::from_utf8_lossy(&plain.raw).to_string();
584 assert!(plain.contains("Fine by me.") && !plain.contains("secret clause") && !plain.contains("multipart/encrypted"), "{plain}");
585 draft.kind = DraftKind::Forward;
586 assert!(matches!(build_draft_with(&keys, &account, &draft, &tr, None), Err(BuildError::Other(SyncError::Message(m))) if m == tr.text("pgp-forward-needs-encryption", None)));
587 let _ = std::fs::remove_dir_all(&dir);
588 }
589
590 #[test]
594 fn a_key_here_opens_the_copy_and_a_stranger_s_does_not() {
595 let me = pgp::certificates_in(&pgp::stand_in_key("Me <me@example.org>").unwrap());
596 let jane = pgp::certificates_in(&pgp::stand_in_key("Jane <jane@example.org>").unwrap());
597 let bob = pgp::certificates_in(&pgp::stand_in_key("Bob <bob@example.org>").unwrap());
598 let keys = Keys { own: me.clone(), others: jane.clone(), ..Keys::default() };
599 let jane_keys = Keys { own: jane.clone(), others: me.clone(), ..Keys::default() };
600 let dir = std::env::temp_dir().join(format!("sioul-send-here-{}", std::process::id()));
601 std::fs::create_dir_all(&dir).unwrap();
602 let (mine, bobs) = (dir.join("q2.eml"), dir.join("q3.eml"));
603 std::fs::write(&mine, pgp::seal_message(LEASE.as_bytes(), &[&me[0]]).unwrap()).unwrap();
604 std::fs::write(&bobs, pgp::seal_message(LEASE.as_bytes(), &[&bob[0]]).unwrap()).unwrap();
605 let account: Account = toml::from_str("id = \"me\"\naddress = \"me@example.org\"\nname = \"Me\"\n").unwrap();
606 let tr = Translator::new("en");
607 let outgoing = build_draft_with(&keys, &account, &answer_to(&mine), &tr, None).unwrap();
608 assert!(as_jane_reads(&outgoing.raw, &jane_keys).contains("The secret clause is on page four."));
609 assert!(!String::from_utf8_lossy(&outgoing.raw).contains("secret clause"));
610 let unquoted = build_draft_with(&keys, &account, &answer_to(&bobs), &tr, None).unwrap();
611 let read = as_jane_reads(&unquoted.raw, &jane_keys);
612 assert!(read.contains("Fine by me.") && !read.contains("secret clause"), "{read}");
613 let mut forward = answer_to(&bobs);
614 forward.kind = DraftKind::Forward;
615 assert!(matches!(build_draft_with(&keys, &account, &forward, &tr, None), Err(BuildError::Other(SyncError::Message(m))) if m == tr.text("pgp-forward-locked", None)));
616 let _ = std::fs::remove_dir_all(&dir);
617 }
618}