343 lines
9.8 KiB
Rust
343 lines
9.8 KiB
Rust
/*
|
|
* This file is part of mailpot
|
|
*
|
|
* Copyright 2020 - Manos Pitsidianakis
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#![allow(clippy::new_without_default)]
|
|
|
|
use std::net::IpAddr; //, Ipv4Addr, Ipv6Addr};
|
|
use std::{
|
|
borrow::Cow,
|
|
net::ToSocketAddrs,
|
|
sync::{Arc, Mutex, Once},
|
|
thread,
|
|
};
|
|
|
|
pub use assert_cmd;
|
|
pub use log::{trace, warn};
|
|
use mailin_embedded::{
|
|
response::{INTERNAL_ERROR, OK},
|
|
Handler, Response, Server,
|
|
};
|
|
pub use predicates;
|
|
pub use tempfile::{self, TempDir};
|
|
|
|
static INIT_STDERR_LOGGING: Once = Once::new();
|
|
|
|
pub fn init_stderr_logging() {
|
|
INIT_STDERR_LOGGING.call_once(|| {
|
|
stderrlog::new()
|
|
.quiet(false)
|
|
.verbosity(log::LevelFilter::Trace)
|
|
.show_module_names(true)
|
|
.timestamp(stderrlog::Timestamp::Millisecond)
|
|
.init()
|
|
.unwrap();
|
|
});
|
|
}
|
|
pub const ADDRESS: &str = "127.0.0.1:8825";
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum Message {
|
|
Helo,
|
|
Mail {
|
|
from: String,
|
|
},
|
|
Rcpt {
|
|
from: String,
|
|
to: Vec<String>,
|
|
},
|
|
DataStart {
|
|
from: String,
|
|
to: Vec<String>,
|
|
},
|
|
Data {
|
|
#[allow(dead_code)]
|
|
from: String,
|
|
to: Vec<String>,
|
|
buf: Vec<u8>,
|
|
},
|
|
}
|
|
|
|
#[allow(clippy::type_complexity)]
|
|
#[derive(Debug, Clone)]
|
|
pub struct TestSmtpHandler {
|
|
address: Cow<'static, str>,
|
|
ssl: SslConfig,
|
|
envelope_from: Cow<'static, str>,
|
|
auth: mailpot::melib::smtp::SmtpAuth,
|
|
pub messages: Arc<Mutex<Vec<((IpAddr, String), Message)>>>,
|
|
pub stored: Arc<Mutex<Vec<(String, mailpot::melib::Envelope)>>>,
|
|
}
|
|
|
|
impl Handler for TestSmtpHandler {
|
|
fn helo(&mut self, ip: IpAddr, domain: &str) -> Response {
|
|
//eprintln!("helo ip {:?} domain {:?}", ip, domain);
|
|
self.messages
|
|
.lock()
|
|
.unwrap()
|
|
.push(((ip, domain.to_string()), Message::Helo));
|
|
OK
|
|
}
|
|
|
|
fn mail(&mut self, ip: IpAddr, domain: &str, from: &str) -> Response {
|
|
//eprintln!("mail() ip {:?} domain {:?} from {:?}", ip, domain, from);
|
|
if let Some((_, message)) = self
|
|
.messages
|
|
.lock()
|
|
.unwrap()
|
|
.iter_mut()
|
|
.rev()
|
|
.find(|((i, d), _)| (i, d.as_str()) == (&ip, domain))
|
|
{
|
|
if let Message::Helo = &message {
|
|
*message = Message::Mail {
|
|
from: from.to_string(),
|
|
};
|
|
return OK;
|
|
}
|
|
}
|
|
INTERNAL_ERROR
|
|
}
|
|
|
|
fn rcpt(&mut self, _to: &str) -> Response {
|
|
//eprintln!("rcpt() to {:?}", _to);
|
|
if let Some((_, message)) = self.messages.lock().unwrap().last_mut() {
|
|
if let Message::Mail { from } = message {
|
|
*message = Message::Rcpt {
|
|
from: from.clone(),
|
|
to: vec![_to.to_string()],
|
|
};
|
|
return OK;
|
|
} else if let Message::Rcpt { to, .. } = message {
|
|
to.push(_to.to_string());
|
|
return OK;
|
|
}
|
|
}
|
|
INTERNAL_ERROR
|
|
}
|
|
|
|
fn data_start(
|
|
&mut self,
|
|
_domain: &str,
|
|
_from: &str,
|
|
_is8bit: bool,
|
|
_to: &[String],
|
|
) -> Response {
|
|
// eprintln!( "data_start() domain {:?} from {:?} is8bit {:?} to {:?}", _domain,
|
|
// _from, _is8bit, _to);
|
|
if let Some(((_, d), ref mut message)) = self.messages.lock().unwrap().last_mut() {
|
|
if d != _domain {
|
|
return INTERNAL_ERROR;
|
|
}
|
|
if let Message::Rcpt { from, to } = message {
|
|
*message = Message::DataStart {
|
|
from: from.to_string(),
|
|
to: to.to_vec(),
|
|
};
|
|
return OK;
|
|
}
|
|
}
|
|
INTERNAL_ERROR
|
|
}
|
|
|
|
fn data(&mut self, _buf: &[u8]) -> Result<(), std::io::Error> {
|
|
if let Some(((_, _), ref mut message)) = self.messages.lock().unwrap().last_mut() {
|
|
if let Message::DataStart { from, to } = message {
|
|
*message = Message::Data {
|
|
from: from.to_string(),
|
|
to: to.clone(),
|
|
buf: _buf.to_vec(),
|
|
};
|
|
return Ok(());
|
|
} else if let Message::Data { buf, .. } = message {
|
|
buf.extend(_buf.iter());
|
|
return Ok(());
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn data_end(&mut self) -> Response {
|
|
let last = self.messages.lock().unwrap().pop();
|
|
if let Some(((ip, domain), Message::Data { from: _, to, buf })) = last {
|
|
for to in to {
|
|
match mailpot::melib::Envelope::from_bytes(&buf, None) {
|
|
Ok(env) => {
|
|
self.stored.lock().unwrap().push((to.clone(), env));
|
|
}
|
|
Err(err) => {
|
|
panic!("envelope parse error {}", err);
|
|
}
|
|
}
|
|
}
|
|
self.messages
|
|
.lock()
|
|
.unwrap()
|
|
.push(((ip, domain), Message::Helo));
|
|
return OK;
|
|
}
|
|
panic!("last self.messages item was not Message::Data: {last:?}"); //INTERNAL_ERROR
|
|
}
|
|
}
|
|
|
|
impl TestSmtpHandler {
|
|
#[inline]
|
|
pub fn smtp_conf(&self) -> mailpot::melib::smtp::SmtpServerConf {
|
|
use mailpot::melib::smtp::*;
|
|
let sockaddr = self
|
|
.address
|
|
.as_ref()
|
|
.to_socket_addrs()
|
|
.unwrap()
|
|
.next()
|
|
.unwrap();
|
|
let ip = sockaddr.ip();
|
|
let port = sockaddr.port();
|
|
|
|
SmtpServerConf {
|
|
hostname: ip.to_string(),
|
|
port,
|
|
envelope_from: self.envelope_from.to_string(),
|
|
auth: self.auth.clone(),
|
|
security: SmtpSecurity::None,
|
|
extensions: Default::default(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl TestSmtpHandler {
|
|
pub fn builder() -> TestSmtpHandlerBuilder {
|
|
TestSmtpHandlerBuilder::new()
|
|
}
|
|
}
|
|
|
|
pub struct TestSmtpHandlerBuilder {
|
|
address: Cow<'static, str>,
|
|
ssl: SslConfig,
|
|
auth: mailpot::melib::smtp::SmtpAuth,
|
|
envelope_from: Cow<'static, str>,
|
|
}
|
|
|
|
impl TestSmtpHandlerBuilder {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
address: ADDRESS.into(),
|
|
ssl: SslConfig::None,
|
|
auth: mailpot::melib::smtp::SmtpAuth::None,
|
|
envelope_from: "foo-chat@example.com".into(),
|
|
}
|
|
}
|
|
|
|
pub fn address(self, address: impl Into<Cow<'static, str>>) -> Self {
|
|
Self {
|
|
address: address.into(),
|
|
..self
|
|
}
|
|
}
|
|
|
|
pub fn ssl(self, ssl: SslConfig) -> Self {
|
|
Self { ssl, ..self }
|
|
}
|
|
|
|
pub fn build(self) -> TestSmtpHandler {
|
|
let Self {
|
|
address,
|
|
ssl,
|
|
auth,
|
|
envelope_from,
|
|
} = self;
|
|
let handler = TestSmtpHandler {
|
|
address,
|
|
ssl,
|
|
auth,
|
|
envelope_from,
|
|
messages: Arc::new(Mutex::new(vec![])),
|
|
stored: Arc::new(Mutex::new(vec![])),
|
|
};
|
|
crate::init_stderr_logging();
|
|
let handler2 = handler.clone();
|
|
let _smtp_handle = thread::spawn(move || {
|
|
let address = handler2.address.clone();
|
|
let ssl = handler2.ssl.clone();
|
|
|
|
let mut server = Server::new(handler2.clone());
|
|
let sockaddr = address.as_ref().to_socket_addrs().unwrap().next().unwrap();
|
|
let ip = sockaddr.ip();
|
|
let port = sockaddr.port();
|
|
let addr = std::net::SocketAddr::new(ip, port);
|
|
eprintln!("Running smtp server at {}", addr);
|
|
server
|
|
.with_name("example.com")
|
|
.with_ssl((&ssl).into())
|
|
.unwrap()
|
|
.with_addr(addr)
|
|
.unwrap();
|
|
server.serve().expect("Could not run server");
|
|
});
|
|
handler
|
|
}
|
|
}
|
|
|
|
/// Mirror struct for [`mailin_embedded::SslConfig`] because it does not
|
|
/// implement Debug or Clone.
|
|
#[derive(Clone, Debug)]
|
|
pub enum SslConfig {
|
|
/// Do not support STARTTLS
|
|
None,
|
|
/// Use a self-signed certificate for STARTTLS
|
|
SelfSigned {
|
|
/// Certificate path
|
|
cert_path: String,
|
|
/// Path to key file
|
|
key_path: String,
|
|
},
|
|
/// Use a certificate from an authority
|
|
Trusted {
|
|
/// Certificate path
|
|
cert_path: String,
|
|
/// Key file path
|
|
key_path: String,
|
|
/// Path to CA bundle
|
|
chain_path: String,
|
|
},
|
|
}
|
|
|
|
impl From<&SslConfig> for mailin_embedded::SslConfig {
|
|
fn from(val: &SslConfig) -> Self {
|
|
match val {
|
|
SslConfig::None => Self::None,
|
|
SslConfig::SelfSigned {
|
|
ref cert_path,
|
|
ref key_path,
|
|
} => Self::SelfSigned {
|
|
cert_path: cert_path.to_string(),
|
|
key_path: key_path.to_string(),
|
|
},
|
|
SslConfig::Trusted {
|
|
ref cert_path,
|
|
ref key_path,
|
|
ref chain_path,
|
|
} => Self::Trusted {
|
|
cert_path: cert_path.to_string(),
|
|
key_path: key_path.to_string(),
|
|
chain_path: chain_path.to_string(),
|
|
},
|
|
}
|
|
}
|
|
}
|