generic-smtp-listener

A small persistent SMTP listener that accepts incoming mail and writes each complete message as a file in an inbox directory.

This project is intentionally permissive: it accepts any sender and recipient and does not perform spam filtering. It is intended for private capture/ingestion workflows, not as a full MTA replacement.

Behavior

  • Listens on SMTP_LISTEN_ADDR (default 0.0.0.0:2525).
  • Supports basic SMTP commands: HELO/EHLO, MAIL FROM, RCPT TO, DATA, RSET, NOOP, and QUIT.
  • Applies coarse global and per-envelope-sender rate limits.
  • Applies a concurrent connection limit and an idle command/DATA timeout.
  • Writes message data to a temporary directory first, then atomically renames the complete file into the inbox.
  • Also writes a cleaned copy into SMTP_CLEANED_INBOX_DIR for LLM ingestion.
  • When SMTP_RECIPIENT_DOMAINS is set, only messages addressed to those domains are stored, with one copy per matching local-part username folder.
  • Keeps temporary files outside both inboxes by default, so rsync jobs can read inboxes without seeing partial messages.
  • Prepends envelope metadata headers before the received DATA payload.
  • Message filenames use [YYYY]-[MM]-[DD]-[base64_url encoded sha1 content hash].eml, based on the full stored message content.
  • Cleaned copies strip X-*, ARC-*, and DKIM* headers except X-Received and X-SMTP*, keep plain-text bodies, drop HTML alternatives, and keep attachments with minimal MIME headers.

Configuration

Configuration is read from /etc/smtp-receiver/config when that file exists, or from the file named by SMTP_CONFIG_FILE. The file uses simple key=value lines; blank lines and # comments are ignored. Keys may be written either with or without the SMTP_ prefix, for example listen_addr=0.0.0.0:25 or SMTP_LISTEN_ADDR=0.0.0.0:25.

Environment variables with the same names are still supported and override file values, mainly for local development and container orchestration.

Key / Variable Default Description
SMTP_LISTEN_ADDR 0.0.0.0:2525 TCP listen address. Use 0.0.0.0:25 when deployed with permission to bind port 25.
SMTP_INBOX_DIR inbox Directory where complete .eml files are delivered.
SMTP_CLEANED_INBOX_DIR sibling inbox-cleaned Directory where stripped LLM-ingestion .eml files are delivered.
SMTP_TEMP_DIR sibling .smtp-receiver-tmp Temporary write directory used before atomic rename into the inboxes.
SMTP_MAX_MESSAGE_BYTES 26214400 Maximum SMTP DATA size.
SMTP_GLOBAL_RATE_PER_MINUTE 600 Global accepted MAIL FROM commands per rolling minute. Set 0 to disable.
SMTP_SENDER_RATE_PER_MINUTE 60 Per-sender accepted MAIL FROM commands per rolling minute. Set 0 to disable.
SMTP_MAX_CONNECTIONS 100 Maximum concurrent SMTP sessions. Set 0 to use Tokio's maximum semaphore permit count.
SMTP_COMMAND_TIMEOUT_SECONDS 300 Idle timeout for SMTP commands and DATA reads.
SMTP_RECIPIENT_DOMAINS unset Comma-separated recipient domains to route into username folders, for example kautschuk.com,undenheim.kautschuk.com. Matching uses envelope recipients plus To, Cc, and Bcc headers. If set and no recipient matches, the message is accepted but not stored.
SMTP_AUTH_RESULTS_MODE off Offline Authentication-Results gate. Use require to accept but not store messages unless a trusted Authentication-Results header satisfies the configured result requirements. This makes no DNS or network requests and is only as trustworthy as the configured upstream path.
SMTP_AUTH_RESULTS_TRUSTED_SERVERS unset Comma-separated authserv-id values whose Authentication-Results headers may be trusted, for example mx.google.com. Required when SMTP_AUTH_RESULTS_MODE=require.
SMTP_AUTH_RESULTS_REQUIRED unset Comma-separated lowercase result fragments to require inside a trusted header, for example dkim=pass,dmarc=pass. Required when SMTP_AUTH_RESULTS_MODE=require.
SMTP_AUTH_RESULTS_MATCH any Whether any or all configured required result fragments must appear in the trusted Authentication-Results header.

Example config file:

listen_addr=0.0.0.0:25
inbox_dir=/var/lib/smtp-receiver/dropoff
cleaned_inbox_dir=/var/lib/smtp-receiver/dropoff-cleaned
temp_dir=/var/lib/smtp-receiver/tmp
max_message_bytes=26214400
global_rate_per_minute=600
sender_rate_per_minute=60
max_connections=100
command_timeout_seconds=300
recipient_domains=kautschuk.com,undenheim.kautschuk.com
auth_results_mode=require
auth_results_trusted_servers=mx.google.com
auth_results_required=dkim=pass,dmarc=pass
auth_results_match=any

Local development

cargo fmt
cargo test
cargo clippy --all-targets -- -D warnings
SMTP_LISTEN_ADDR=127.0.0.1:2525 SMTP_INBOX_DIR=tmp/inbox cargo run

Example manual smoke test:

nc 127.0.0.1 2525

Then type:

EHLO local
MAIL FROM:<sender@example.test>
RCPT TO:<inbox@example.test>
DATA
Subject: hello

body
.
QUIT

Security notes

  • This listener is deliberately accepting. Put it behind appropriate network controls for your use case.
  • The cleaned inbox is for token reduction, not for security sanitization. Treat email contents and attachments as untrusted input.
  • The server does not implement authentication, TLS, spam filtering, DKIM/SPF/DMARC validation, mailbox routing, or outbound mail delivery.
S
Description
No description provided
Readme
362 KiB
0 Stars 1 Watchers 0 Forks
Languages
Rust 100%