SMTP Honeypot slop
Find a file
2026-11-10 18:28:39 +01:00
.gitignore initial commit 2026-11-10 18:28:39 +01:00
config.example.toml initial commit 2026-11-10 18:28:39 +01:00
config.go initial commit 2026-11-10 18:28:39 +01:00
config_test.go initial commit 2026-11-10 18:28:39 +01:00
e2e_test.go initial commit 2026-11-10 18:28:39 +01:00
go.mod initial commit 2026-11-10 18:28:39 +01:00
go.sum initial commit 2026-11-10 18:28:39 +01:00
honeypot.go initial commit 2026-11-10 18:28:39 +01:00
honeypot_test.go initial commit 2026-11-10 18:28:39 +01:00
logging.go initial commit 2026-11-10 18:28:39 +01:00
main.go initial commit 2026-11-10 18:28:39 +01:00
README.md initial commit 2026-11-10 18:28:39 +01:00
server.go initial commit 2026-11-10 18:28:39 +01:00

smtppot

An SMTP honeypot: it talks real SMTP (including STARTTLS), records who spammers try to send to and what the message subject is - and throws the message body away.

  • TOML configuration - everything lives in one config file.
  • STARTTLS - advertise and accept opportunistic TLS upgrades.
  • Envelope logging only - sender, recipients, subject and size are logged; the message body is never stored, and even the other headers are discarded.
  • Configurable rejection - rejection reply code and message text are configurable, including {sender} / {rcpt} / {user} / {domain} / {helo} placeholders and an optional tarpit delay.
  • Structured logging (text or json, log/slog), connection and message size limits, graceful shutdown on SIGINT/SIGTERM.

Building

go build -o smtppot .

Running

./smtppot -config config.toml

Without -config, the config.toml in the working directory is used; if that does not exist either, built-in defaults are used (:25, logging to stderr, accept everything).

Port 25 requires privileges; use e.g. setcap cap_net_bind_service=ep ./smtppot, run it in a network namespace/container, or pick a high port such as :2525 for testing.

A ready-made starting point is config.example.toml; the Configuration section below explains each key.

STARTTLS certificate

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
    -keyout key.pem -out cert.pem -days 365 -subj "/CN=mx.example.org"

Reference the files in the config:

[tls]
enabled = true
cert_file = "/etc/smtppot/cert.pem"
key_file = "/etc/smtppot/key.pem"

What gets logged

Every session produces structured log records:

msg fields
helo remote, helo, tls
mail_from remote, sender (plus accepted, reason if refused)
rcpt_to remote, recipient, accepted
message remote, sender, recipients, subject, bytes, action

The message.subject is the value of the Subject: header, RFC 2047 decoded, with control characters stripped and optionally truncated (logging.max_subject_len). Nothing else of the message content is kept: the body is streamed past, only its total byte count is recorded. message.truncated=true means the client aborted the transfer mid-flight.

Addresses can be suppressed: with logging.log_addresses = false the HELO name, sender and recipients are logged as the literal redacted, and with logging.log_subject = false the subject field is omitted entirely and message headers are never parsed at all.

At logging.level = "debug" the raw protocol transcript of every session is logged as well, which is handy to study odd clients (suppressed when log_addresses is off, since the transcript contains the addresses).

Operating modes (honeypot.mode)

mode behaviour
accept Accept everything (default). Addresses and subjects are captured.
reject_rcpt Log the envelope, but reject every RCPT TO. Subjects are never captured because no message data is ever accepted.
reject_data Receive the full message and log sender/recipients/subject, then answer with the rejection - the spammer's send is counted as failure after wasting their resources.

Rejections (in both reject modes, and require_starttls refusals) use the configured reply:

[honeypot]
mode = "reject_data"
rejection_code = 550
rejection_message = "<{rcpt}>: Recipient address rejected: User unknown in local recipient table"
tarpit_delay = "5s"

rejection_message supports the placeholders {sender}, {rcpt}, {user}, {domain} and {helo} (see Configuration for the mailbox-name/domain split and a redirect-style example); addresses are sanitised so a client cannot forge SMTP reply lines. tarpit_delay inserts a pause before each rejection reply to slow down automated senders.

Note that reject_data rejections arrive after the message was received - that is exactly what makes it interesting for a honeypot; the sender's message is collected for intelligence and then refused.

Configuration

Every key shown can be omitted; the default is used then.

[server]

key default meaning
listen :25 TCP listen address
hostname machine name advertised in greeting/EHLO
max_recipients 100 RCPT TO limit per message (0 = unlimited)
max_message_bytes 10485760 message size limit in bytes (0 = unlimited)
max_line_length 2000 longest SMTP command line (0 = unlimited)
max_connections 256 concurrent connections (0 = unlimited)
read_timeout "5m" per-connection read deadline
write_timeout "5m" per-connection write deadline

[tls]

key default meaning
enabled false advertise and support STARTTLS (needs cert + key)
cert_file - PEM certificate chain
key_file - PEM private key
require_starttls false refuse MAIL FROM on non-TLS connections (530)
min_version "1.2" "1.0", "1.1", "1.2" or "1.3"

[honeypot]

key default meaning
mode "accept" "accept", "reject_rcpt" or "reject_data"
rejection_code 550 SMTP reply code used for rejections
rejection_message <{rcpt}>: ... rejection text; placeholders {sender}, {rcpt}, {user}, {domain}, {helo}
rejection_enhanced_code "" enhanced status code ("5.1.1", "none", or "" for a generic one)
tarpit_delay "0s" pause before each rejection reply

{user} and {domain} split the rejected recipient at the last @, which makes redirect-style rejections possible:

[honeypot]
mode = "reject_rcpt"
rejection_code = 550
rejection_enhanced_code = "5.1.1"
rejection_message = "<{rcpt}>: User unknown. Please send to {user}@catchall.example.org instead"

Note that these placeholders are protocol output, not logging: the sending client always already knows the addresses it transmitted, so they are never redacted in replies.

[logging]

key default meaning
file stderr stderr, stdout or file path (append mode)
format "text" "text" or "json"
level "info" debug (incl. protocol transcript), info, warn, error
log_addresses true log HELO name, sender and recipients; false logs redacted instead
log_subject true log the message subject; false skips subject recording (and header parsing) entirely
max_subject_len 512 truncate subjects (0 = no limit)

Setting log_addresses = false also suppresses the raw protocol transcript of level = "debug", since that unavoidably contains the envelope addresses.

systemd unit

/etc/systemd/system/smtppot.service:

[Unit]
Description=SMTP honeypot
After=network.target

[Service]
Type=simple
User=smtppot
ExecStart=/usr/local/bin/smtppot -config /etc/smtppot/config.toml
Restart=on-failure
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target

Testing

go test ./...

Privacy note

Logged senders, recipients and subjects can contain personal data of third parties. Run this only on infrastructure you are allowed to monitor, keep the log file protected and rotate/purge it as appropriate.

Acknowledgements

Protocol handling by emersion/go-smtp, configuration parsing by BurntSushi/toml.