Files
ElyOffice/crates/ely-mail-engine/tests/accounts.rs
T
雷电芽衣 1c225f6812 C12.5: mail accounts, add and remove
ElyMail's window lists accounts and adds Gmail, Microsoft, iCloud
(app-specific password), IMAP and POP3 ones; a check logs in to both
servers and reports each. Accounts live in the store as JSON with a
case-insensitive unique address; secrets go to the keychain. Removal
names the cache it deletes, takes the secret first and puts it back
when local removal fails. Originals are set aside until commit and
restored after a crash. Requests are numbered so late results stay
with their own dialog. The account list is keyboard-navigable.
2026-10-19 20:17:19 -04:00

201 lines
6.0 KiB
Rust

mod fake;
use base64::{Engine, engine::general_purpose::STANDARD};
use ely_mail_engine::{
Account, Auth, Cached, Error, Fetched, Key, Protocol, Secret, Security, Server, Store,
};
use fake::{Certs, account, certs, expect, send, serve};
fn pop_oauth(address: &str) -> Account {
Account {
address: address.into(),
user: "bo".into(),
auth: Auth::Microsoft,
protocol: Protocol::Pop,
incoming: Server {
host: "pop.example.com".into(),
port: 995,
security: Security::Tls,
},
outgoing: Server {
host: "smtp.example.com".into(),
port: 587,
security: Security::StartTls,
},
}
}
#[test]
fn accounts_come_back_as_added() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
let ann = account("ann@example.com");
let bo = pop_oauth("bo@example.com");
let first = store.add_account(&ann).unwrap();
let second = store.add_account(&bo).unwrap();
drop(store);
let store = Store::open(dir.path()).unwrap();
assert_eq!(store.accounts().unwrap(), [(first, ann), (second, bo)]);
}
#[test]
fn an_address_is_added_once() {
let dir = tempfile::tempdir().unwrap();
let store = Store::open(dir.path()).unwrap();
store.add_account(&account("ann@example.com")).unwrap();
let err = store
.add_account(&pop_oauth("ANN@Example.com"))
.unwrap_err();
assert!(
matches!(&err, Error::Taken(address) if address == "ANN@Example.com"),
"{err}"
);
assert_eq!(store.accounts().unwrap().len(), 1);
}
#[test]
fn removing_an_account_keeps_what_another_shares() {
let dir = tempfile::tempdir().unwrap();
let mut store = Store::open(dir.path()).unwrap();
let ann = store.add_account(&account("ann@example.com")).unwrap();
let bo = store.add_account(&account("bo@example.com")).unwrap();
let (ann_inbox, bo_inbox) = (
store.mailbox(ann, "INBOX").unwrap(),
store.mailbox(bo, "INBOX").unwrap(),
);
let shared = b"Subject: Both\r\n\r\nshared\r\n";
let own = b"Subject: Ann only\r\n\r\nmine and mine alone\r\n";
let key = |uid| Key::Imap {
uidvalidity: 1,
uid,
};
store
.put(ann_inbox, &key(1), &[], Fetched::Whole(shared))
.unwrap();
store
.put(ann_inbox, &key(2), &[], Fetched::Whole(own))
.unwrap();
let kept = store
.put(bo_inbox, &key(1), &[], Fetched::Whole(shared))
.unwrap();
assert_eq!(
store.cached(ann).unwrap(),
Cached {
messages: 2,
bytes: own.len() as u64
}
);
store.remove_account(ann).unwrap();
assert_eq!(store.accounts().unwrap().len(), 1);
assert_eq!(store.original(kept).unwrap().unwrap(), shared);
assert_eq!(store.sizes().unwrap().originals, shared.len() as u64);
let err = store.remove_account(ann).unwrap_err();
assert!(matches!(err, Error::Store(_)), "{err}");
}
/// IMAP and SMTP scripts that log in and leave.
async fn servers(
certs: &Certs,
imap_answer: &str,
) -> (Account, Vec<tokio::task::JoinHandle<Result<(), String>>>) {
let mut imap = vec![
send("* OK ready\n"),
expect("{tag} LOGIN \"ann@example.com\" \"pw\""),
send(imap_answer),
];
if imap_answer.contains(" OK ") {
imap.extend([expect("{tag} LOGOUT"), send("* BYE\n{tag} OK bye\n")]);
}
let (incoming, imap) = serve(certs, Security::Tls, imap).await;
let smtp = vec![
send("220 mx ready\n"),
expect("EHLO [127.0.0.1]"),
send("250-mx\n250 AUTH PLAIN\n"),
expect(&format!(
"AUTH PLAIN {}",
STANDARD.encode("\0ann@example.com\0pw")
)),
send("235 ok\n"),
expect("QUIT"),
send("221 bye\n"),
];
let (outgoing, smtp) = serve(certs, Security::Tls, smtp).await;
let account = Account {
incoming,
outgoing,
..account("ann@example.com")
};
(account, vec![imap, smtp])
}
#[tokio::test]
async fn a_check_logs_in_to_both_servers() {
let certs = certs();
let (account, scripts) = servers(&certs, "{tag} OK in\n").await;
let checked = account
.check(Secret::Password("pw".into()), &certs.trusted)
.await;
assert!(checked.passed());
for script in scripts {
script.await.unwrap().unwrap();
}
}
#[tokio::test]
async fn each_server_answers_for_itself() {
let certs = certs();
let (account, scripts) =
servers(&certs, "{tag} NO [AUTHENTICATIONFAILED] bad password\n").await;
let checked = account
.check(Secret::Password("pw".into()), &certs.trusted)
.await;
assert!(!checked.passed());
match checked.incoming {
Err(Error::Rejected(text)) => assert!(text.contains("bad password"), "{text}"),
other => panic!("incoming: {:?}", other.err()),
}
checked.outgoing.unwrap();
for script in scripts {
script.await.unwrap().unwrap();
}
}
#[test]
fn what_no_server_could_carry_is_refused() {
account("ann@example.com").valid().unwrap();
let broken = [
Account {
address: "ann".into(),
..account("ann@example.com")
},
Account {
user: String::new(),
..account("ann@example.com")
},
Account {
user: "ann\r\nRSET".into(),
..account("ann@example.com")
},
Account {
incoming: Server {
host: "imap example.com".into(),
port: 993,
security: Security::Tls,
},
..account("ann@example.com")
},
Account {
outgoing: Server {
host: "smtp.example.com".into(),
port: 0,
security: Security::Tls,
},
..account("ann@example.com")
},
];
for account in broken {
let err = account.valid().unwrap_err();
assert!(matches!(err, Error::Invalid(_)), "{err}");
}
}