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.
This commit is contained in:
Andrey 0xdc09
2026-03-13 20:04:44 +04:00
committed by GitHub
parent b31e13a417
commit 49df79bfa0
7 changed files with 58 additions and 19 deletions
+11
View File
@@ -85,6 +85,17 @@ item may start with `@` to whitelist whole recipient domains.
- `mailboxes_dir` - path to mailboxes directory,
defaults to `/home/vmail/mail/<mail_domain>`.
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:
+13
View File
@@ -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(),
+6 -1
View File
@@ -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<Self, crate::error::Error> {
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);
+14 -5
View File
@@ -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<SocketAddr, error::Error> {
log::debug!("Resolving {host}");
Ok((host, port)
.to_socket_addrs()?
.next()
.ok_or(std::io::Error::other("Cannot resolve host"))?)
}
+9 -4
View File
@@ -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<String>,
}
impl OutgoingBeforeQueueHandler {
pub fn new(config: Config) -> Self {
pub fn new(config: Config) -> Result<Self, crate::error::Error> {
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);
+4 -8
View File
@@ -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:<smtp_port>`.
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();
+1 -1
View File
@@ -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<H>(
addr: &str,
addr: &impl tokio::net::ToSocketAddrs,
handler: Arc<H>,
max_size: usize,
) -> Result<(), Box<dyn std::error::Error>>