feat(transport): Remote delivery over SMTP (#104)

Implements a new mode "transport",
that allows filtermail to be used for
remote delivery.

In transport mode, filtermail listens for LMTP
connections, splits messages by domain
and performs delivery to remote MTAs over SMTP.

SMTP client tries to open socket on all resolved
addresses in parallel and uses one that succeeds
the first, fixing the issue described in:
chatmail/relay#900

Extends the built-in SMTP client
with STARTTLS support.

Extends the built-in SMTP server
with LMTP greeting support.

Groundwork required for mxdeliv endpoint
(HTTP channel for MTA-to-MTA communication):
chatmail/relay#900

Signed-off-by: Jagoda Ślązak <jslazak@jslazak.com>
This commit is contained in:
Jagoda Estera Ślązak
2026-04-15 22:12:09 +02:00
committed by GitHub
parent ec0413bd5f
commit cf64778f71
15 changed files with 815 additions and 120 deletions
+72 -43
View File
@@ -33,40 +33,45 @@ pub(crate) mod openpgp;
pub(crate) mod outbound;
pub(crate) mod smtp_client;
pub(crate) mod smtp_server;
mod tls;
mod transport;
pub(crate) mod utils;
use crate::transport::TransportHandler;
use config::Config;
use env_logger::Env;
use inbound::IncomingBeforeQueueHandler;
use outbound::OutgoingBeforeQueueHandler;
use smtp_server::run_smtp_server;
use std::env;
use std::net::{SocketAddr, ToSocketAddrs};
use std::process;
use std::str::FromStr;
use std::sync::Arc;
const ENCRYPTION_NEEDED_523: &str = "523 Encryption Needed: Invalid Unencrypted Mail";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Mode {
Outgoing,
Incoming,
Transport,
}
impl std::str::FromStr for Mode {
impl FromStr for Mode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
fn from_str(mode: &str) -> Result<Self, Self::Err> {
match mode {
"outgoing" => Ok(Mode::Outgoing),
"incoming" => Ok(Mode::Incoming),
_ => Err("Error: mode must be 'incoming' or 'outgoing'"),
"transport" => Ok(Mode::Transport),
_ => Err("Error: mode must be 'incoming', 'outgoing' or 'transport'"),
}
}
}
#[tokio::main]
async fn main() {
async fn main() -> Result<(), error::Error> {
// default to info level
let env = Env::new().filter_or("RUST_LOG", "info");
env_logger::Builder::from_env(env)
@@ -74,13 +79,17 @@ async fn main() {
.format_timestamp(None)
.init();
tokio_rustls::rustls::crypto::ring::default_provider()
.install_default()
.expect("Failed to set up rustls crypto provider.");
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!(
"Usage: {} <config_file> <mode>",
args.first().unwrap_or(&"filtermail".to_string())
);
eprintln!(" mode: incoming or outgoing");
eprintln!(" mode: incoming, outgoing or transport");
process::exit(1);
}
@@ -91,7 +100,7 @@ async fn main() {
unreachable!("args length checked above")
};
let mode = match mode.parse::<Mode>() {
let mode = match Mode::from_str(mode) {
Ok(mode) => mode,
Err(e) => {
eprintln!("{e}");
@@ -107,45 +116,65 @@ async fn main() {
}
};
if mode == Mode::Outgoing {
let addr = (config.filtermail_host, config.filtermail_smtp_port);
let handler = Arc::new(OutgoingBeforeQueueHandler::new(config.clone()).unwrap());
let max_size = config.max_message_size;
log::debug!("Outgoing SMTP server listening on {}:{}", addr.0, addr.1);
match mode {
Mode::Outgoing => {
let addr = (config.filtermail_host, config.filtermail_smtp_port);
let handler = Arc::new(OutgoingBeforeQueueHandler::new(config.clone())?);
let max_size = config.max_message_size;
log::debug!("Outgoing SMTP server listening on {}:{}", addr.0, addr.1);
if let Err(e) = run_smtp_server(&addr, handler, max_size).await {
eprintln!("Server error: {}", e);
process::exit(1);
if let Err(e) = run_smtp_server(&addr, handler, max_size).await {
eprintln!("Server error: {}", e);
process::exit(1);
}
}
} else {
// Skip DKIM verification (used for tests).
let skip_dkim = env::var("FILTERMAIL_SKIP_DKIM")
.map(|val| val == "1" || val.eq_ignore_ascii_case("true"))
.unwrap_or(false);
Mode::Incoming => {
// Skip DKIM verification (used for tests).
let skip_dkim = env::var("FILTERMAIL_SKIP_DKIM")
.map(|val| val == "1" || val.eq_ignore_ascii_case("true"))
.unwrap_or(false);
if skip_dkim {
log::warn!("DKIM verification DISABLED! This should not be used in production.");
if skip_dkim {
log::warn!("DKIM verification DISABLED! This should not be used in production.");
}
let handler = Arc::new(IncomingBeforeQueueHandler::new(config.clone(), skip_dkim)?);
let max_size = config.max_message_size;
let mut server_set = tokio::task::JoinSet::new();
let addr_smtp = (config.filtermail_host, config.filtermail_smtp_port_incoming);
let handler_smtp = handler.clone();
server_set
.spawn(async move { run_smtp_server(&addr_smtp, handler_smtp, max_size).await });
log::debug!(
"Incoming SMTP server listening on {}:{}",
addr_smtp.0,
addr_smtp.1
);
while let Some(result) = server_set.join_next().await {
if let Err(e) = result {
eprintln!("Server error: {}", e);
process::exit(1);
}
}
}
Mode::Transport => {
let addr = (
config.filtermail_host,
config.filtermail_lmtp_port_transport,
);
let handler = Arc::new(TransportHandler::new(config.clone())?);
let max_size = config.max_message_size;
log::debug!("Transport SMTP server listening on {}:{}", addr.0, addr.1);
let addr = (config.filtermail_host, config.filtermail_smtp_port_incoming);
let handler = Arc::new(
// We want to panic here if the handler cannot be created.
IncomingBeforeQueueHandler::new(config.clone(), skip_dkim).unwrap(),
);
let max_size = config.max_message_size;
log::debug!("Incoming SMTP server listening on {}:{}", addr.0, addr.1);
if let Err(e) = run_smtp_server(&addr, handler, max_size).await {
eprintln!("Server error: {}", e);
process::exit(1);
if let Err(e) = run_smtp_server(&addr, handler, max_size).await {
eprintln!("Server error: {}", e);
process::exit(1);
}
}
}
}
};
fn resolve_addr(host: &str, port: u16) -> Result<SocketAddr, error::Error> {
log::debug!("Resolving {host}");
Ok((host, port)
.to_socket_addrs()?
.next()
.ok_or(std::io::Error::other("Cannot resolve host"))?)
Ok(())
}