429 lines
12 KiB
Rust
429 lines
12 KiB
Rust
mod fake;
|
|
|
|
use base64::{Engine, engine::general_purpose::STANDARD};
|
|
use ely_mail_engine::{Error, Imap, Role, Secret, Security, system_tls};
|
|
use fake::{certs, expect, login, send, serve};
|
|
use tokio::task::JoinHandle;
|
|
|
|
const TOKEN_LINE: &str = "user=ann@example.com\x01auth=Bearer tok-1\x01\x01";
|
|
|
|
#[tokio::test]
|
|
async fn a_password_login_lists_and_fetches() {
|
|
let certs = certs();
|
|
let lines = "Subject: Hi\n\nHello\n";
|
|
let body = lines.replace('\n', "\r\n");
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"pw 1\""),
|
|
send("{tag} OK done\n"),
|
|
expect("{tag} CAPABILITY"),
|
|
send("* CAPABILITY IMAP4rev1\n{tag} OK caps\n"),
|
|
expect("{tag} LIST \"\" *"),
|
|
send("* LIST (\\HasNoChildren) \"/\" INBOX\n"),
|
|
send("* LIST (\\HasNoChildren \\Sent) \"/\" \"Sent Mail\"\n"),
|
|
send("{tag} OK listed\n"),
|
|
expect("{tag} EXAMINE \"INBOX\""),
|
|
send("* 1 EXISTS\n* OK [UIDVALIDITY 7] v\n{tag} OK [READ-ONLY] in\n"),
|
|
expect("{tag} UID FETCH 42 (UID FLAGS BODY.PEEK[])"),
|
|
send(&format!(
|
|
"* 1 FETCH (UID 42 BODY[] {{{}}}\n{lines})\n{{tag}} OK fetched\n",
|
|
body.len()
|
|
)),
|
|
expect("{tag} LOGOUT"),
|
|
send("* BYE\n{tag} OK bye\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let mut imap = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw 1".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let folders = imap.folders().await.unwrap();
|
|
let names: Vec<_> = folders.iter().map(|f| (f.name.as_str(), f.role)).collect();
|
|
assert_eq!(
|
|
names,
|
|
[
|
|
("INBOX", Some(Role::Inbox)),
|
|
("Sent Mail", Some(Role::Sent))
|
|
]
|
|
);
|
|
let fetched = imap.fetch("INBOX", 42).await.unwrap().unwrap();
|
|
assert_eq!(String::from_utf8(fetched).unwrap(), body);
|
|
imap.logout().await.unwrap();
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn starttls_then_xoauth2() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::StartTls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} STARTTLS"),
|
|
send("{tag} OK go\n"),
|
|
fake::Step::StartTls,
|
|
expect("{tag} AUTHENTICATE XOAUTH2"),
|
|
send("+ \n"),
|
|
expect(&STANDARD.encode(TOKEN_LINE)),
|
|
send("{tag} OK welcome\n"),
|
|
expect("{tag} LOGOUT"),
|
|
send("* BYE\n{tag} OK bye\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let imap = Imap::connect(
|
|
&server,
|
|
&login(Secret::Token("tok-1".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
imap.logout().await.unwrap();
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_refused_password_keeps_the_server_words() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"bad\""),
|
|
send("{tag} NO [AUTHENTICATIONFAILED] Invalid credentials\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("bad".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&err, Error::Rejected(text) if text.contains("Invalid credentials")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_refused_token_answers_the_error_challenge_empty() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} AUTHENTICATE XOAUTH2"),
|
|
send("+ \n"),
|
|
expect(&STANDARD.encode(TOKEN_LINE)),
|
|
send(&format!("+ {}\n", STANDARD.encode(r#"{"status":"401"}"#))),
|
|
expect(""),
|
|
send("{tag} NO [AUTHENTICATIONFAILED] Invalid token\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Token("tok-1".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&err, Error::Rejected(text) if text.contains("Invalid token")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_missing_uid_is_none() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"pw\""),
|
|
send("{tag} OK done\n"),
|
|
expect("{tag} EXAMINE \"INBOX\""),
|
|
send("* 1 EXISTS\n* OK [UIDVALIDITY 7] v\n{tag} OK in\n"),
|
|
expect("{tag} UID FETCH 9 (UID FLAGS BODY.PEEK[])"),
|
|
send("{tag} OK nothing\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let mut imap = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(imap.fetch("INBOX", 9).await.unwrap(), None);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn an_unreadable_reply_is_a_protocol_error() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"pw\""),
|
|
send("{tag} OK done\n"),
|
|
expect("{tag} CAPABILITY"),
|
|
send("* CAPABILITY IMAP4rev1\n{tag} OK caps\n"),
|
|
expect("{tag} LIST \"\" *"),
|
|
send("* LIST (((\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let mut imap = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
let err = imap.folders().await.unwrap_err();
|
|
assert!(matches!(err, Error::Protocol(_)), "{err}");
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn the_system_store_refuses_an_unknown_certificate() {
|
|
let certs = certs();
|
|
let (server, _script) = serve(&certs, Security::Tls, vec![send("* OK ready\n")]).await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&system_tls().unwrap(),
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(matches!(err, Error::Tls(_)), "{err}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_greeting_bye_keeps_the_server_words() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![send("* BYE [UNAVAILABLE] Maintenance in progress\n")],
|
|
)
|
|
.await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&err, Error::Server(text) if text.contains("Maintenance in progress")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
/// Greeting and LOGIN "pw", then `steps`.
|
|
async fn logged_in(steps: Vec<fake::Step>) -> (Imap, JoinHandle<Result<(), String>>) {
|
|
let certs = certs();
|
|
let mut script = vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"pw\""),
|
|
send("{tag} OK done\n"),
|
|
];
|
|
script.extend(steps);
|
|
let (server, script) = serve(&certs, Security::Tls, script).await;
|
|
let imap = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
(imap, script)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn listed_names_lose_quote_escapes_and_open_as_listed() {
|
|
let (mut imap, script) = logged_in(vec![
|
|
expect("{tag} CAPABILITY"),
|
|
send("* CAPABILITY IMAP4rev1\n{tag} OK caps\n"),
|
|
expect("{tag} LIST \"\" *"),
|
|
send("* LIST () \"/\" \"a\\\"b\"\n"),
|
|
send("* LIST () \"/\" \"c\\\\d\"\n"),
|
|
send("* LIST () \"/\" {3}\ne\\\"\n"),
|
|
send("{tag} OK listed\n"),
|
|
expect("{tag} EXAMINE \"a\\\"b\""),
|
|
send("* 1 EXISTS\n* OK [UIDVALIDITY 7] v\n{tag} OK in\n"),
|
|
expect("{tag} UID FETCH 9 (UID FLAGS BODY.PEEK[])"),
|
|
send("{tag} OK nothing\n"),
|
|
])
|
|
.await;
|
|
let names: Vec<_> = imap
|
|
.folders()
|
|
.await
|
|
.unwrap()
|
|
.into_iter()
|
|
.map(|f| f.name)
|
|
.collect();
|
|
assert_eq!(names, ["a\"b", "c\\d", "e\\\""]);
|
|
assert_eq!(imap.fetch(&names[0], 9).await.unwrap(), None);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_refused_list_keeps_the_server_words() {
|
|
let (mut imap, script) = logged_in(vec![
|
|
expect("{tag} CAPABILITY"),
|
|
send("* CAPABILITY IMAP4rev1\n{tag} OK caps\n"),
|
|
expect("{tag} LIST \"\" *"),
|
|
send("* LIST () \"/\" INBOX\n"),
|
|
send("{tag} NO [UNAVAILABLE] Mailbox listing temporarily unavailable\n"),
|
|
])
|
|
.await;
|
|
let err = imap.folders().await.unwrap_err();
|
|
assert!(
|
|
matches!(&err, Error::Server(text) if text.contains("listing temporarily unavailable")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_refused_fetch_is_not_a_missing_uid() {
|
|
let (mut imap, script) = logged_in(vec![
|
|
expect("{tag} EXAMINE \"INBOX\""),
|
|
send("* 1 EXISTS\n* OK [UIDVALIDITY 7] v\n{tag} OK in\n"),
|
|
expect("{tag} UID FETCH 9 (UID FLAGS BODY.PEEK[])"),
|
|
send("{tag} NO [UNAVAILABLE] Message store temporarily unavailable\n"),
|
|
])
|
|
.await;
|
|
let err = imap.fetch("INBOX", 9).await.unwrap_err();
|
|
assert!(
|
|
matches!(&err, Error::Server(text) if text.contains("store temporarily unavailable")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_bye_after_login_keeps_the_server_words() {
|
|
let certs = certs();
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} LOGIN \"ann@example.com\" \"pw\""),
|
|
send("* BYE [UNAVAILABLE] Too many simultaneous connections\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&err, Error::Server(text) if text.contains("Too many simultaneous connections")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
|
|
let (mut imap, script) = logged_in(vec![
|
|
expect("{tag} CAPABILITY"),
|
|
send("* CAPABILITY IMAP4rev1\n{tag} OK caps\n"),
|
|
expect("{tag} LIST \"\" *"),
|
|
send("* BYE Shutting down\n"),
|
|
])
|
|
.await;
|
|
let err = imap.folders().await.unwrap_err();
|
|
assert!(
|
|
matches!(&err, Error::Server(text) if text.contains("Shutting down")),
|
|
"{err}"
|
|
);
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn a_password_beyond_ascii_goes_as_a_literal() {
|
|
let certs = certs();
|
|
let password = "pässwört";
|
|
let (server, script) = serve(
|
|
&certs,
|
|
Security::Tls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect(&format!(
|
|
"{{tag}} LOGIN \"ann@example.com\" {{{}}}",
|
|
password.len()
|
|
)),
|
|
send("+ go on\n"),
|
|
expect(password),
|
|
send("{tag} OK done\n"),
|
|
],
|
|
)
|
|
.await;
|
|
Imap::connect(
|
|
&server,
|
|
&login(Secret::Password(password.into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
script.await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn bytes_before_tls_are_refused() {
|
|
let certs = certs();
|
|
let (server, _script) = serve(
|
|
&certs,
|
|
Security::StartTls,
|
|
vec![
|
|
send("* OK ready\n"),
|
|
expect("{tag} STARTTLS"),
|
|
send("{tag} OK go\n* OK injected\n"),
|
|
],
|
|
)
|
|
.await;
|
|
let err = Imap::connect(
|
|
&server,
|
|
&login(Secret::Password("pw".into())),
|
|
&certs.trusted,
|
|
)
|
|
.await
|
|
.err()
|
|
.unwrap();
|
|
assert!(
|
|
matches!(&err, Error::Protocol(text) if text.contains("before TLS")),
|
|
"{err}"
|
|
);
|
|
}
|