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.
201 lines
6.0 KiB
Rust
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}");
|
|
}
|
|
}
|