Files
ElyOffice/crates/ely-mail-engine/tests/pop.rs
T
ZacharyZhang-NYandClaude Opus 5.5 9f47b94714 C12.1: ely-mail-engine connects over IMAP, POP3 and SMTP
TLS or STARTTLS with the system trust store; password and XOAUTH2
logins keep the server's words on refusal. IMAP and POP3 are written
in the engine (imap-proto parses IMAP replies): async-imap dropped
NO/BAD on LIST and FETCH and lost BYE text, async-pop kept dot
stuffing and logged passwords. SMTP runs its own transaction with
RFC 5321 mailbox checks, full dot transparency, SMTPUTF8 and RSET.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-19 16:53:53 -04:00

227 lines
5.7 KiB
Rust

mod fake;
use base64::{Engine, engine::general_purpose::STANDARD};
use ely_mail_engine::{Error, Pop, Secret, Security};
use fake::{Step, certs, expect, login, send, serve};
const TOKEN_LINE: &str = "user=ann@example.com\x01auth=Bearer tok-1\x01\x01";
#[tokio::test]
async fn a_password_login_lists_uids_and_fetches() {
let certs = certs();
let (server, script) = serve(
&certs,
Security::Tls,
vec![
send("+OK ready\n"),
expect("USER ann@example.com"),
send("+OK\n"),
expect("PASS pw 1"),
send("+OK in\n"),
expect("UIDL"),
send("+OK\n1 aa1\n2 bb2\n.\n"),
expect("RETR 2"),
send("+OK 27 octets\nSubject: Hi\n\nHello\n..dot\n...\n.\n"),
expect("QUIT"),
send("+OK bye\n"),
],
)
.await;
let mut pop = Pop::connect(
&server,
&login(Secret::Password("pw 1".into())),
&certs.trusted,
)
.await
.unwrap();
assert_eq!(
pop.uids().await.unwrap(),
[(1, "aa1".to_owned()), (2, "bb2".to_owned())]
);
let message = String::from_utf8(pop.fetch(2).await.unwrap()).unwrap();
assert_eq!(message, "Subject: Hi\r\n\r\nHello\r\n.dot\r\n..\r\n");
pop.quit().await.unwrap();
script.await.unwrap().unwrap();
}
#[tokio::test]
async fn stls_then_xoauth2_sends_the_token_after_the_plus() {
let certs = certs();
let (server, script) = serve(
&certs,
Security::StartTls,
vec![
send("+OK ready\n"),
expect("STLS"),
send("+OK begin TLS\n"),
Step::StartTls,
expect("AUTH XOAUTH2"),
send("+ \n"),
expect(&STANDARD.encode(TOKEN_LINE)),
send("+OK in\n"),
expect("QUIT"),
send("+OK bye\n"),
],
)
.await;
let pop = Pop::connect(
&server,
&login(Secret::Token("tok-1".into())),
&certs.trusted,
)
.await
.unwrap();
pop.quit().await.unwrap();
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("AUTH XOAUTH2"),
send("+ \n"),
expect(&STANDARD.encode(TOKEN_LINE)),
send(&format!("+ {}\n", STANDARD.encode(r#"{"status":"401"}"#))),
expect(""),
send("-ERR Authentication failure: unknown user name or bad password.\n"),
],
)
.await;
let err = Pop::connect(
&server,
&login(Secret::Token("tok-1".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(
matches!(&err, Error::Rejected(text) if text.contains("bad password")),
"{err}"
);
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("USER ann@example.com"),
send("+OK\n"),
expect("PASS bad"),
send("-ERR [AUTH] Username and password not accepted.\n"),
],
)
.await;
let err = Pop::connect(
&server,
&login(Secret::Password("bad".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(
matches!(&err, Error::Rejected(text) if text.contains("not accepted")),
"{err}"
);
script.await.unwrap().unwrap();
}
#[tokio::test]
async fn a_greeting_err_keeps_the_server_words() {
let certs = certs();
let (server, script) = serve(&certs, Security::Tls, vec![send("-ERR maildrop busy\n")]).await;
let err = Pop::connect(
&server,
&login(Secret::Password("pw".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(
matches!(&err, Error::Server(text) if text == "maildrop busy"),
"{err}"
);
script.await.unwrap().unwrap();
}
#[tokio::test]
async fn a_connection_lost_before_the_pass_reply_is_an_error() {
let certs = certs();
let (server, script) = serve(
&certs,
Security::Tls,
vec![
send("+OK ready\n"),
expect("USER ann@example.com"),
send("+OK\n"),
expect("PASS pw"),
],
)
.await;
let err = Pop::connect(
&server,
&login(Secret::Password("pw".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(matches!(&err, Error::Connect(_)), "{err}");
script.await.unwrap().unwrap();
}
#[tokio::test]
async fn plain_bytes_after_stls_are_refused() {
let certs = certs();
let (server, _script) = serve(
&certs,
Security::StartTls,
vec![
send("+OK ready\n"),
expect("STLS"),
send("+OK begin TLS\nUIDL\n"),
],
)
.await;
let err = Pop::connect(
&server,
&login(Secret::Password("pw".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(
matches!(&err, Error::Protocol(text) if text.contains("after STLS")),
"{err}"
);
}
#[tokio::test]
async fn a_line_break_in_the_login_never_leaves() {
let certs = certs();
let (server, script) = serve(&certs, Security::Tls, vec![send("+OK ready\n")]).await;
let err = Pop::connect(
&server,
&login(Secret::Password("pw\r\nDELE 1".into())),
&certs.trusted,
)
.await
.err()
.unwrap();
assert!(matches!(&err, Error::Invalid(_)), "{err}");
script.await.unwrap().unwrap();
}