#[macro_use]
extern crate log;
use async_std::fs::File;
use async_std::io::ReadExt;
use async_std::task;
use async_tls::TlsAcceptor;
use rustls::ServerConfig;
use samotop::io::tls::RustlsProvider;
use samotop::mail::spf::Spf;
use samotop::mail::{Builder, DebugService, MailDir, Name};
use samotop::server::TcpServer;
use samotop::smtp::{Esmtp, EsmtpStartTls, Prudence, SmtpParser};
use std::path::{Path, PathBuf};
use std::time::Duration;
use structopt::StructOpt;
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
fn main() -> Result<()> {
env_logger::init();
task::block_on(main_fut())
}
async fn main_fut() -> Result<()> {
let setup = Setup::from_args();
let mut service = Builder
+ Name::new(setup.name())
+ DebugService::default()
+ Esmtp.with(SmtpParser)
+ setup.prudence()
+ Spf
+ MailDir::new(setup.mail_dir())?;
if let Some(cfg) = setup.tls_config().await? {
service += EsmtpStartTls.with(SmtpParser, RustlsProvider::from(TlsAcceptor::from(cfg)));
}
TcpServer::on_all(setup.ports())
.serve(service.build())
.await
}
pub struct Setup {
opt: Opt,
}
impl Setup {
pub fn from_args() -> Setup {
Setup {
opt: Opt::from_args(),
}
}
pub fn prudence(&self) -> Prudence {
let mut prudence = Prudence::default();
if let Some(delay) = self.opt.prudent_banner_delay {
prudence = prudence.with_banner_delay(Duration::from_millis(delay));
}
if let Some(timeout) = self.opt.prudent_command_timeout {
prudence = prudence.with_read_timeout(Duration::from_millis(timeout));
}
prudence
}
pub async fn tls_config(&self) -> Result<Option<ServerConfig>> {
let opt = &self.opt;
if opt.no_tls {
return Ok(None);
}
let key = {
let id_path = self.absolute_path(
&opt.identity_file
.as_ref()
.expect("identity-file must be set unless --no-tls"),
);
let mut idfile = File::open(&id_path)
.await
.map_err(|e| format!("Could not load identity: {:?}", e))?;
let mut idbuf = vec![];
let _ = idfile.read_to_end(&mut idbuf).await?;
let mut idbuf = std::io::BufReader::new(&idbuf[..]);
let keys = rustls::internal::pemfile::pkcs8_private_keys(&mut idbuf)
.map_err(|_| "Could not load identity key".to_string())?;
keys.first()
.ok_or(format!("No private key found in {:?}", id_path))?
.to_owned()
};
let certs = {
let cert_path = self.absolute_path(
&opt.cert_file
.as_ref()
.expect("cert-file must be set unless --no-tls"),
);
let mut certfile = File::open(&cert_path)
.await
.map_err(|e| format!("Could not load certs: {}", e))?;
let mut certbuf = vec![];
let _ = certfile.read_to_end(&mut certbuf).await?;
let mut certbuf = std::io::BufReader::new(&certbuf[..]);
let certs = rustls::internal::pemfile::certs(&mut certbuf)
.map_err(|_| format!("Could not load certs from {:?}", cert_path))?;
certs
.first()
.ok_or(format!("No certs found in {:?}", cert_path))?;
certs
};
let mut config = ServerConfig::new(rustls::NoClientAuth::new());
config.set_single_cert(certs, key)?;
Ok(Some(config))
}
pub fn ports(&self) -> Vec<String> {
if self.opt.ports.is_empty() {
vec!["localhost:25".to_owned()]
} else {
self.opt.ports.to_vec()
}
}
pub fn name(&self) -> String {
match &self.opt.name {
None => match hostname::get() {
Err(e) => {
warn!("Unable to get hostname, using default. {}", e);
"Samotop".into()
}
Ok(name) => match name.into_string() {
Err(e) => {
warn!("Unable to use hostname, using default. {:?}", e);
"Samotop".into()
}
Ok(name) => name,
},
},
Some(name) => name.clone(),
}
}
pub fn mail_dir(&self) -> PathBuf {
self.absolute_path(&self.opt.mail_dir)
}
fn absolute_path(&self, path: impl AsRef<Path>) -> PathBuf {
if path.as_ref().is_absolute() {
path.as_ref().to_owned()
} else {
self.opt.base_dir.join(path)
}
}
}
#[derive(StructOpt, Debug)]
#[structopt(name = "samotop")]
struct Opt {
#[structopt(short = "p", long = "port", name = "port")]
ports: Vec<String>,
#[structopt(long = "no-tls")]
no_tls: bool,
#[structopt(
short = "i",
long = "identity-file",
name = "identity file path",
required_unless = "no-tls"
)]
identity_file: Option<String>,
#[structopt(
short = "c",
long = "cert-file",
name = "cert file path",
required_unless = "no-tls"
)]
cert_file: Option<String>,
#[structopt(short = "n", long = "name", name = "SMTP service name")]
name: Option<String>,
#[structopt(
short = "m",
long = "mail-dir",
name = "mail dir path",
default_value = "inmail"
)]
mail_dir: PathBuf,
#[structopt(
short = "b",
long = "base-dir",
name = "base dir path",
default_value = "."
)]
base_dir: PathBuf,
#[structopt(long = "banner_delay", name = "delay")]
prudent_banner_delay: Option<u64>,
#[structopt(long = "command_timeout", name = "timeout")]
prudent_command_timeout: Option<u64>,
}