use std::fs;
use std::io::{self, BufRead, Read, Write};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use std::sync::{Arc, Mutex, Weak};
use lazy_static::lazy_static;
use openssl::{
pkey,
ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode},
x509,
};
use rayon::prelude::*;
use tempfile::TempDir;
use super::server::*;
use crate::account::account::Account;
use crate::account::model::Uid;
use crate::crypt::master_key::MasterKey;
use crate::support::{
append_limit::APPEND_SIZE_LIMIT, error::Error, rcio::RcIo,
system_config::SystemConfig,
};
lazy_static! {
static ref SYSTEM_DIR: Mutex<Weak<TempDir>> = Mutex::new(Weak::new());
}
#[derive(Clone, Debug)]
struct Setup {
system_dir: Arc<TempDir>,
}
fn set_up() -> Setup {
crate::init_test_log();
let mut lock = SYSTEM_DIR.lock().unwrap();
if let Some(system_dir) = lock.upgrade() {
return Setup { system_dir };
}
let setup = set_up_new_root();
*lock = Arc::downgrade(&setup.system_dir);
setup
}
fn set_up_new_root() -> Setup {
let system_dir = Arc::new(TempDir::new().unwrap());
vec!["dib", "gäz", "zim", "gir"]
.into_par_iter()
.for_each(|user_name| {
let user_dir = system_dir.path().join(user_name);
fs::create_dir(&user_dir).unwrap();
let account = Account::new(
"initial-setup".to_owned(),
user_dir,
Some(Arc::new(MasterKey::new())),
);
account.provision(b"hunter2").unwrap();
});
Setup { system_dir }
}
lazy_static! {
static ref CERTIFICATE_PRIVATE_KEY: pkey::PKey<pkey::Private> =
pkey::PKey::from_rsa(openssl::rsa::Rsa::generate(2048).unwrap())
.unwrap();
static ref CERTIFICATE: x509::X509 = {
let mut builder = x509::X509Builder::new().unwrap();
builder.set_pubkey(&CERTIFICATE_PRIVATE_KEY).unwrap();
builder
.sign(
&CERTIFICATE_PRIVATE_KEY,
openssl::hash::MessageDigest::sha256(),
)
.unwrap();
builder.set_version(2).unwrap();
builder
.set_not_before(&openssl::asn1::Asn1Time::from_unix(0).unwrap())
.unwrap();
builder
.set_not_after(&openssl::asn1::Asn1Time::days_from_now(2).unwrap())
.unwrap();
builder.build()
};
}
impl Setup {
fn connect(&self, cxn_name: &'static str) -> impl Read + Write {
let (server_io, client_io) = UnixStream::pair().unwrap();
let data_root: PathBuf = self.system_dir.path().to_owned();
std::thread::spawn(move || {
let server_io = RcIo::wrap(server_io);
let mut ssl_acceptor =
SslAcceptor::mozilla_intermediate_v5(SslMethod::tls_server())
.unwrap();
ssl_acceptor
.set_private_key(&CERTIFICATE_PRIVATE_KEY)
.unwrap();
ssl_acceptor.set_certificate(&CERTIFICATE).unwrap();
let ssl_acceptor = ssl_acceptor.build();
let mut server = Server::new(
Box::new(io::BufReader::new(server_io.clone())),
Box::new(io::BufWriter::new(server_io)),
Arc::new(SystemConfig::default()),
cxn_name.to_owned(),
ssl_acceptor,
data_root,
"localhost".to_owned(),
cxn_name.to_owned(),
);
match server.run() {
Ok(()) => (),
Err(crate::support::error::Error::Io(e))
if io::ErrorKind::UnexpectedEof == e.kind()
|| Some(nix::libc::EPIPE) == e.raw_os_error() =>
{
()
}
Err(e) => panic!("Unexpected server error: {}", e),
}
});
client_io
}
}
fn read_responses(r: &mut impl Read) -> Vec<String> {
let mut ret = Vec::<String>::new();
let mut r = io::BufReader::new(r);
loop {
let mut line = String::new();
r.read_line(&mut line).unwrap();
println!("Read response: {:?}", line);
if line.is_empty() {
panic!("Unexpected EOF");
}
let last = " " == &line[3..4];
ret.push(line);
if last {
break;
}
}
ret
}
fn skip_pleasantries(cxn: &mut (impl Read + Write), name: &str) {
read_responses(cxn);
writeln!(cxn, "LHLO {}\r", name).unwrap();
read_responses(cxn);
}
fn simple_command(cxn: &mut (impl Read + Write), command: &str, prefix: &str) {
writeln!(cxn, "{}\r", command).unwrap();
let responses = read_responses(cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with(prefix));
}
fn received_email(setup: &Setup, account_name: &str, email: &str) -> bool {
let account = Account::new(
"verify".to_owned(),
setup.system_dir.path().join(account_name),
None,
);
let config = account.load_config().unwrap();
let master_key =
MasterKey::from_config(&config.master_key, b"hunter2").unwrap();
let account = Account::new(
"verify".to_owned(),
setup.system_dir.path().join(account_name),
Some(Arc::new(master_key)),
);
let mailbox = account.mailbox("INBOX", true).unwrap();
for uid in 1.. {
let mut r = match mailbox.open_message(Uid::u(uid)) {
Ok((_, r)) => r,
Err(Error::NxMessage)
| Err(Error::UnaddressableMessage)
| Err(Error::ExpungedMessage) => return false,
Err(e) => panic!("Unexpected error: {}", e),
};
let mut data = Vec::new();
r.read_to_end(&mut data).unwrap();
if data.ends_with(email.as_bytes()) {
return true;
}
}
false
}
#[test]
fn first_contact() {
let setup = set_up();
let mut cxn = setup.connect("first_contact");
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(
responses[0].starts_with("220 localhost"),
"Unexpected greeting: {}",
responses[0]
);
writeln!(cxn, "QUIT\r").unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(
responses[0].starts_with("221 2.0.0"),
"Unexpected goodbye: {}",
responses[0]
);
}
#[test]
fn test_lhlo() {
let setup = set_up();
let mut cxn = setup.connect("test_lhlo");
read_responses(&mut cxn);
writeln!(cxn, "LHLO test_lhlo\r").unwrap();
let responses = read_responses(&mut cxn);
assert!(responses[0].starts_with("250-localhost "));
assert!(responses.contains(&"250-STARTTLS\r\n".to_owned()));
assert!(responses.last().unwrap().starts_with("250 "));
}
#[test]
fn misc_commands() {
let setup = set_up();
let mut cxn = setup.connect("misc_commands");
skip_pleasantries(&mut cxn, "misc_commands");
writeln!(cxn, "HELP ME\r").unwrap();
let responses = read_responses(&mut cxn);
assert!(responses.last().unwrap().starts_with("214 2.0.0"));
simple_command(&mut cxn, "VRFY <gäz@localhost>", "252 2.7.0");
simple_command(&mut cxn, "EXPN <list@localhost>", "550 5.3.3");
simple_command(&mut cxn, "NOOP", "250 2.0.0");
}
#[test]
fn data_delivery() {
let setup = set_up();
let mut cxn = setup.connect("data_delivery");
skip_pleasantries(&mut cxn, "data_delivery");
let email_a = "Subject: Email A\r\n\r\nContent A\r\n";
simple_command(&mut cxn, "MAIL FROM:<tallest@irk>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "RCPT TO:<gäz@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", email_a).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(2, responses.len());
assert!(responses[0].starts_with("250-2.0.0"));
assert!(responses[1].starts_with("250 2.0.0"));
let email_b = "Subject: Email B\r\n\r\nContent B\r\n";
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<zim@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", email_b).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
assert!(received_email(&setup, "dib", email_a));
assert!(!received_email(&setup, "dib", email_b));
assert!(received_email(&setup, "gäz", email_a));
assert!(!received_email(&setup, "gäz", email_b));
assert!(!received_email(&setup, "zim", email_a));
assert!(received_email(&setup, "zim", email_b));
}
#[test]
fn bdat_delivery() {
let setup = set_up();
let mut cxn = setup.connect("bdat_delivery");
skip_pleasantries(&mut cxn, "bdat_delivery");
let email_binary = "Subject: Binary email\r\n\r\n\r\r\n\n\r\n.\r\n.a\r\nx";
simple_command(&mut cxn, "MAIL FROM:<> BODY=BINARYMIME", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
let mut count_sent = 0;
for chunk in email_binary.as_bytes().chunks(8) {
count_sent += chunk.len();
writeln!(
cxn,
"BDAT {}{}\r",
chunk.len(),
if count_sent == email_binary.len() {
" LAST"
} else {
""
}
)
.unwrap();
cxn.write_all(chunk).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
}
assert!(received_email(&setup, "dib", email_binary));
let email_followup = "Subject: Followup Email\r\n\r\nbinary followup\r\n";
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<gäz@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", email_followup).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
assert!(!received_email(&setup, "dib", email_followup));
assert!(received_email(&setup, "gäz", email_followup));
}
#[test]
fn large_delivery() {
let setup = set_up();
let mut cxn = setup.connect("large_delivery");
skip_pleasantries(&mut cxn, "large_delivery");
let large_content =
format!("Subject: Large\r\n\r\n{}\r\n", "x".repeat(1024 * 1024));
simple_command(&mut cxn, "MAIL FROM:<tallest@irk>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "RCPT TO:<gäz@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", large_content).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(2, responses.len());
assert!(responses[0].starts_with("250-2.0.0"));
assert!(responses[1].starts_with("250 2.0.0"));
assert!(received_email(&setup, "dib", &large_content));
assert!(received_email(&setup, "gäz", &large_content));
}
#[test]
fn huge_message_rejected() {
let setup = set_up();
let mut cxn = setup.connect("huge_delivery");
skip_pleasantries(&mut cxn, "huge_delivery");
let large_content = format!(
"Subject: Oversize\r\n\r\n{}\r\n",
"x".repeat(APPEND_SIZE_LIMIT as usize)
);
simple_command(&mut cxn, "MAIL FROM:<tallest@irk>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "RCPT TO:<gäz@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", large_content).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(2, responses.len());
assert!(responses[0].starts_with("552-5.2.3"));
assert!(responses[1].starts_with("552 5.2.3"));
assert!(!received_email(&setup, "dib", &large_content));
assert!(!received_email(&setup, "gäz", &large_content));
}
#[test]
fn huge_mail_from_size_rejected() {
let setup = set_up();
let mut cxn = setup.connect("huge_mail_from");
skip_pleasantries(&mut cxn, "huge_mail_from");
simple_command(&mut cxn, "MAIL FROM:<> SIZE=1222333444", "552 5.2.3");
}
#[test]
fn failed_delivery() {
let setup = set_up();
let mut cxn = setup.connect("failed_delivery");
skip_pleasantries(&mut cxn, "failed_delivery");
let failed_email = "Subject: Failed to deliver to gir\r\n\r\ngir\r\n";
simple_command(&mut cxn, "MAIL FROM:<tallest@irk>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<gir@localhost>", "250 2.1.5");
simple_command(&mut cxn, "RCPT TO:<zim@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
fs::remove_dir_all(setup.system_dir.path().join("gir")).unwrap();
writeln!(cxn, "{}.\r", failed_email).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(2, responses.len());
assert!(responses[0].starts_with("450-"));
assert!(responses[1].starts_with("250 2.0.0"));
assert!(received_email(&setup, "zim", failed_email));
}
#[test]
fn failed_rcpt_to() {
let setup = set_up();
let mut cxn = setup.connect("failed_rcpt_to");
skip_pleasantries(&mut cxn, "failed_rcpt_to");
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<nobody@localhost>", "550 5.1.1");
simple_command(&mut cxn, "RCPT TO:<..@localhost>", "550 5.1.1");
}
#[test]
fn out_of_order_commands() {
let setup = set_up();
let mut cxn = setup.connect("out_of_order_commands");
read_responses(&mut cxn);
simple_command(&mut cxn, "MAIL FROM:<>", "503 5.5.1");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "503 5.5.1");
simple_command(&mut cxn, "DATA", "503 5.5.1");
simple_command(&mut cxn, "BDAT 5\r\nfoo", "503 5.5.1");
writeln!(cxn, "LHLO out_of_order_commands\r").unwrap();
let responses = read_responses(&mut cxn);
assert!(responses.last().unwrap().starts_with("250 "));
simple_command(&mut cxn, "LHLO out_of_order_commands", "503 5.5.1");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "503 5.5.1");
simple_command(&mut cxn, "DATA", "503 5.5.1");
simple_command(&mut cxn, "BDAT 5\r\nfoo", "503 5.5.1");
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "MAIL FROM:<>", "503 5.5.1");
simple_command(&mut cxn, "DATA", "503 5.5.1");
simple_command(&mut cxn, "BDAT 5\r\nfoo", "503 5.5.1");
let ooo_email = "Subject: Out of order\r\n\r\n";
let unexpected_email = format!("foo\r\n{}", ooo_email);
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", ooo_email).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
assert!(received_email(&setup, "dib", ooo_email));
assert!(!received_email(&setup, "dib", &unexpected_email));
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "BDAT 5\r\nfoo", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<gäz@localhost>", "503 5.5.1");
simple_command(&mut cxn, "DATA", "503 5.5.1");
simple_command(&mut cxn, "RSET", "250 2.0.0");
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", ooo_email).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
assert!(!received_email(&setup, "dib", &unexpected_email));
}
#[test]
fn start_tls() {
let setup = set_up();
let mut cxn = setup.connect("starttls");
skip_pleasantries(&mut cxn, "starttls");
simple_command(&mut cxn, "STARTTLS", "220 2.0.0");
let mut connector = SslConnector::builder(SslMethod::tls()).unwrap();
connector.set_verify(SslVerifyMode::NONE);
let mut cxn = connector
.build()
.connect("localhost", cxn)
.map_err(|_| "SSL handshake failed")
.unwrap();
skip_pleasantries(&mut cxn, "starttls");
let tls_email = "Subject: TLS\r\n\r\nThis message was sent over TLS.\r\n";
simple_command(&mut cxn, "MAIL FROM:<>", "250 2.0.0");
simple_command(&mut cxn, "RCPT TO:<dib@localhost>", "250 2.1.5");
simple_command(&mut cxn, "DATA", "354 ");
writeln!(cxn, "{}.\r", tls_email).unwrap();
let responses = read_responses(&mut cxn);
assert_eq!(1, responses.len());
assert!(responses[0].starts_with("250 2.0.0"));
assert!(received_email(&setup, "dib", tls_email));
}