From 49df79bfa009f4eadb04cb353b757f3696047b4b Mon Sep 17 00:00:00 2001 From: Andrey 0xdc09 <50486086+DarkCat09@users.noreply.github.com> Date: Fri, 13 Mar 2026 20:04:44 +0400 Subject: [PATCH] feat: configurable hosts for listen and reinject (#84) Adds configurable listen IP and postfix host via `filtermail_host` and `postfix_host` chatmail.ini config fields. --- filtermail/README.md | 11 +++++++++++ filtermail/src/config.rs | 13 +++++++++++++ filtermail/src/inbound.rs | 7 ++++++- filtermail/src/main.rs | 19 ++++++++++++++----- filtermail/src/outbound.rs | 13 +++++++++---- filtermail/src/smtp_client.rs | 12 ++++-------- filtermail/src/smtp_server.rs | 2 +- 7 files changed, 58 insertions(+), 19 deletions(-) diff --git a/filtermail/README.md b/filtermail/README.md index bf29732a..d28d9d21 100644 --- a/filtermail/README.md +++ b/filtermail/README.md @@ -85,6 +85,17 @@ item may start with `@` to whitelist whole recipient domains. - `mailboxes_dir` - path to mailboxes directory, defaults to `/home/vmail/mail/`. +The following options are Filtermail-specific, +they are not read by other chatmail relay components +and usually do not need to be set at all: + +- `filtermail_host` - IP address to listen on, +defaults to `127.0.0.1`. +- `postfix_host` - hostname or IP address where postfix is set up, +a host is resolved only on Filtermail startup, +useful in case MTA runs somewhere outside of localhost, +defaults to `127.0.0.1`. + ### Environment variables Additional options that can be set using environment variables: diff --git a/filtermail/src/config.rs b/filtermail/src/config.rs index f399f9ec..46c3c9f1 100644 --- a/filtermail/src/config.rs +++ b/filtermail/src/config.rs @@ -1,16 +1,21 @@ //! Configuration file handling for filtermail. use serde::{Deserialize, Deserializer}; +use std::net::IpAddr; use std::num::NonZeroU32; use std::path::{Path, PathBuf}; /// Chatmail configuration subset used by filtermail. #[derive(Debug, Clone, Deserialize)] pub struct Config { + #[serde(default = "Config::default_filtermail_host")] + pub filtermail_host: IpAddr, #[serde(default = "Config::default_filtermail_smtp_port")] pub filtermail_smtp_port: u16, #[serde(default = "Config::default_filtermail_smtp_port_incoming")] pub filtermail_smtp_port_incoming: u16, + #[serde(default = "Config::default_postfix_host")] + pub postfix_host: String, #[serde(default = "Config::default_postfix_reinject_port")] pub postfix_reinject_port: u16, #[serde(default = "Config::default_postfix_reinject_port_incoming")] @@ -82,12 +87,18 @@ impl Config { // Following are needed since serde does not support default literals. + const fn default_filtermail_host() -> IpAddr { + IpAddr::V4(std::net::Ipv4Addr::LOCALHOST) + } const fn default_filtermail_smtp_port() -> u16 { 10080 } const fn default_filtermail_smtp_port_incoming() -> u16 { 10081 } + fn default_postfix_host() -> String { + "127.0.0.1".to_owned() + } const fn default_postfix_reinject_port() -> u16 { 10025 } @@ -112,8 +123,10 @@ impl Default for Config { /// Used for tests. fn default() -> Self { Self { + filtermail_host: Self::default_filtermail_host(), filtermail_smtp_port: Self::default_filtermail_smtp_port(), filtermail_smtp_port_incoming: Self::default_filtermail_smtp_port_incoming(), + postfix_host: Self::default_postfix_host(), postfix_reinject_port: Self::default_postfix_reinject_port(), postfix_reinject_port_incoming: Self::default_postfix_reinject_port_incoming(), max_message_size: Self::default_max_message_size(), diff --git a/filtermail/src/inbound.rs b/filtermail/src/inbound.rs index dc981855..747157a4 100644 --- a/filtermail/src/inbound.rs +++ b/filtermail/src/inbound.rs @@ -9,6 +9,7 @@ use crate::smtp_server::SmtpHandler; use crate::utils::{AddressDomain, extract_address, log_eml}; use async_trait::async_trait; use mailparse::{MailHeaderMap, parse_mail}; +use std::net::SocketAddr; use std::str::FromStr; /// Handler for incoming SMTP messages. @@ -16,14 +17,18 @@ pub struct IncomingBeforeQueueHandler { config: Config, dkim_verifier: DkimVerifier, skip_dkim: bool, + reinject_addr: SocketAddr, } impl IncomingBeforeQueueHandler { pub fn new(config: Config, skip_dkim: bool) -> Result { + let reinject_addr = + crate::resolve_addr(&config.postfix_host, config.postfix_reinject_port_incoming)?; Ok(Self { config, dkim_verifier: DkimVerifier::new()?, skip_dkim, + reinject_addr, }) } @@ -127,7 +132,7 @@ impl SmtpHandler for IncomingBeforeQueueHandler { async fn reinject_mail(&self, envelope: &Envelope) -> Result<(), String> { log::debug!("Re-injecting the mail that passed checks"); - crate::smtp_client::send(self.config.postfix_reinject_port_incoming, envelope) + crate::smtp_client::send(self.reinject_addr, envelope) .await .map_err(|e| { log::warn!("Failed to re-inject mail: {}", e); diff --git a/filtermail/src/main.rs b/filtermail/src/main.rs index 1992f23c..d923b599 100644 --- a/filtermail/src/main.rs +++ b/filtermail/src/main.rs @@ -41,6 +41,7 @@ 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::sync::Arc; @@ -107,10 +108,10 @@ async fn main() { }; if mode == Mode::Outgoing { - let handler = Arc::new(OutgoingBeforeQueueHandler::new(config.clone())); - let addr = format!("127.0.0.1:{}", config.filtermail_smtp_port); + 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}"); + 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); @@ -126,13 +127,13 @@ async fn main() { log::warn!("DKIM verification DISABLED! This should not be used in production."); } + 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 addr = format!("127.0.0.1:{}", config.filtermail_smtp_port_incoming); let max_size = config.max_message_size; - log::debug!("Incoming SMTP server listening on {addr}"); + 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); @@ -140,3 +141,11 @@ async fn main() { } } } + +fn resolve_addr(host: &str, port: u16) -> Result { + log::debug!("Resolving {host}"); + Ok((host, port) + .to_socket_addrs()? + .next() + .ok_or(std::io::Error::other("Cannot resolve host"))?) +} diff --git a/filtermail/src/outbound.rs b/filtermail/src/outbound.rs index 6614da66..6b2ab0f7 100644 --- a/filtermail/src/outbound.rs +++ b/filtermail/src/outbound.rs @@ -9,21 +9,26 @@ use crate::utils::extract_address; use async_trait::async_trait; use governor::{DefaultKeyedRateLimiter, Quota, RateLimiter}; use mailparse::{MailHeaderMap, parse_mail}; +use std::net::SocketAddr; /// Handler for outgoing SMTP messages. pub struct OutgoingBeforeQueueHandler { config: Config, + reinject_addr: SocketAddr, send_rate_limiter: DefaultKeyedRateLimiter, } impl OutgoingBeforeQueueHandler { - pub fn new(config: Config) -> Self { + pub fn new(config: Config) -> Result { + let reinject_addr = + crate::resolve_addr(&config.postfix_host, config.postfix_reinject_port)?; let quota = Quota::per_minute(config.max_user_send_per_minute) .allow_burst(config.max_user_send_burst_size); - Self { + Ok(Self { config, + reinject_addr, send_rate_limiter: RateLimiter::keyed(quota), - } + }) } } @@ -125,7 +130,7 @@ impl SmtpHandler for OutgoingBeforeQueueHandler { async fn reinject_mail(&self, envelope: &Envelope) -> Result<(), String> { log::debug!("Re-injecting the mail that passed checks"); - crate::smtp_client::send(self.config.postfix_reinject_port, envelope) + crate::smtp_client::send(self.reinject_addr, envelope) .await .map_err(|e| { log::warn!("Failed to re-inject mail: {}", e); diff --git a/filtermail/src/smtp_client.rs b/filtermail/src/smtp_client.rs index a2dee266..64e9ad95 100644 --- a/filtermail/src/smtp_client.rs +++ b/filtermail/src/smtp_client.rs @@ -1,20 +1,16 @@ use crate::smtp_server::Envelope; -use std::net::{IpAddr, SocketAddr}; +use std::net::SocketAddr; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufStream}; use tokio::net::TcpSocket; -const LOCALHOST: IpAddr = IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)); - -/// Sends an email using an SMTP server at `localhost:`. -pub async fn send(smtp_port: u16, envelope: &Envelope) -> Result<(), crate::error::Error> { +/// Sends an email using an SMTP server at `smtp_addr`. +pub async fn send(smtp_addr: SocketAddr, envelope: &Envelope) -> Result<(), crate::error::Error> { let socket = TcpSocket::new_v4()?; // Disable Nagle's algorithm. socket.set_nodelay(true)?; - let stream = socket - .connect(SocketAddr::new(LOCALHOST, smtp_port)) - .await?; + let stream = socket.connect(smtp_addr).await?; let mut buf_stream = BufStream::new(stream); let mut response = String::new(); diff --git a/filtermail/src/smtp_server.rs b/filtermail/src/smtp_server.rs index e3411590..4114d034 100644 --- a/filtermail/src/smtp_server.rs +++ b/filtermail/src/smtp_server.rs @@ -43,7 +43,7 @@ pub trait SmtpHandler: Send + Sync { /// Runs the SMTP server on the specified address with the given handler and maximum message size. pub async fn run_smtp_server( - addr: &str, + addr: &impl tokio::net::ToSocketAddrs, handler: Arc, max_size: usize, ) -> Result<(), Box>