1e4522933256e03c6e53a399301c667e84766ffb
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(default0.0.0.0:2525). - Supports basic SMTP commands:
HELO/EHLO,MAIL FROM,RCPT TO,DATA,RSET,NOOP, andQUIT. - 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_DIRfor LLM ingestion. - When
SMTP_RECIPIENT_DOMAINSis 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-*, andDKIM*headers exceptX-ReceivedandX-SMTP*, keep plain-text bodies, drop HTML alternatives, and keep attachments with minimal MIME headers.
Configuration
Environment variables:
| 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. |
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.
Languages
Rust
100%