Skip to content

ChaosSMTP Guide

ChaosSMTP is a fake SMTP receiving server that injects configurable faults into outbound email delivery tests. Point an SMTP client at ChaosSMTP instead of a real mail server to verify retries, permanent failure handling, capture behavior, and delivery metrics before production mail leaves your system.

ChaosSMTP never relays mail. It accepts, rejects, drops, or captures messages locally for tests.

Quick Start

# Start with a production-like delivery fault profile
uv run chaossmtp serve --preset=realistic

# Or use the unified CLI
uv run chaosengine smtp serve --preset=realistic

The SMTP listener binds to 127.0.0.1:2525 by default. The HTTP admin sidecar binds to 127.0.0.1:8525.

Send a Message with smtplib

from email.message import EmailMessage
import smtplib

message = EmailMessage()
message["From"] = "sender@example.com"
message["To"] = "recipient@example.com"
message["Subject"] = "ChaosSMTP test"
message.set_content("hello from a resilience test")

with smtplib.SMTP("127.0.0.1", 2525, timeout=5) as client:
    refused = client.send_message(message)

assert refused == {}

Use normal smtplib exceptions to assert failure handling:

import smtplib

try:
    with smtplib.SMTP("127.0.0.1", 2525, timeout=5) as client:
        client.send_message(message)
except smtplib.SMTPRecipientsRefused as exc:
    print(exc.recipients)
except smtplib.SMTPDataError as exc:
    print(exc.smtp_code, exc.smtp_error)

SMTP Listener Defaults

Setting Default Description
SMTP host 127.0.0.1 Loopback-only bind by default.
SMTP port 2525 Non-privileged SMTP test port.
SMTP hostname chaossmtp.local Hostname announced to clients.
DATA size limit 10485760 Maximum DATA payload in bytes.
SMTPUTF8 true Enables SMTPUTF8 support.
STARTTLS required false STARTTLS enforcement is reserved for future TLS context support; true is rejected.
Admin host 127.0.0.1 Loopback-only HTTP admin sidecar.
Admin port 8525 HTTP admin sidecar port.
Metrics database file:chaossmtp-metrics?mode=memory&cache=shared Shared in-memory SQLite metrics store.
Capture mode metadata Store envelope metadata and safe headers.

In YAML config, smtp.port: 0 asks the OS for an ephemeral loopback port. This is useful for fixtures and integration tests. The CLI --port flag accepts explicit ports from 1 to 65535.

Binding the SMTP listener or admin sidecar to all interfaces is blocked by default. Set allow_external_bind: true only for controlled local test environments.

Error Injection

ChaosSMTP injects faults at SMTP stages. Percent fields are floats from 0.0 to 100.0. The current server invokes decisions for MAIL, RCPT, DATA, and ACCEPT handling. CONNECT/banner-stage injection is not exposed until the listener has a real banner hook.

Error Type Stage Config Field Typical Reply or Behavior
Rate limit MAIL/RCPT rate_limit_pct 450 4.7.0 Mailbox temporarily unavailable due to rate limiting
MAIL FROM temporary failure MAIL mail_from_tempfail_pct 451 4.3.0 Temporary sender failure
MAIL FROM permanent rejection MAIL mail_from_reject_pct 550 5.1.0 Sender rejected
RCPT TO temporary failure RCPT rcpt_to_tempfail_pct 451 4.3.0 Temporary recipient failure
RCPT TO permanent rejection RCPT rcpt_to_reject_pct 550 5.1.1 Recipient rejected
DATA temporary failure DATA data_tempfail_pct 451 4.3.0 Temporary message failure
DATA permanent rejection DATA data_reject_pct 554 5.6.0 Message rejected
Accepted then dropped ACCEPT accept_then_drop_pct Returns 250, records accepted_then_dropped, and does not capture the message.
Malformed reply DATA malformed_reply_pct Writes a malformed SMTP reply and closes the transport.
Wrong reply code DATA wrong_reply_code_pct Returns an unexpected 252 reply and records malformed_protocol.
Connection reset MAIL/RCPT/DATA connection_reset_pct Closes the SMTP transport.
Connection stall MAIL/RCPT/DATA connection_stall_pct Sleeps for connection_stall_sec, then closes the SMTP transport.
Slow response MAIL/RCPT/DATA slow_response_pct Adds slow_response_sec delay, then continues the normal SMTP path.

The selection_mode field controls how active errors are chosen:

  • priority (default): each stage evaluates its configured errors in a fixed order.
  • weighted: active percentages are treated as weights for a single selection roll.

Burst mode temporarily raises temporary failure and rate-limit percentages:

error_injection:
  burst:
    enabled: true
    interval_sec: 60
    duration_sec: 8
    rcpt_to_tempfail_pct: 50.0
    rate_limit_pct: 40.0

Note: The burst.tempfail_pct field was renamed to burst.rcpt_to_tempfail_pct — it only overrides the RCPT TO tempfail weight, and the old name was misleading. Update any YAML or CLI flags (--burst-rcpt-to-tempfail-pct) accordingly.

Capture Modes

ChaosSMTP records accepted messages according to capture.mode:

Mode What Gets Stored Use When
discard Metrics only; no captured message list. You only need delivery outcomes and latency.
metadata Envelope sender, recipients, size, subject, and safe headers. Default for most tests.
full Metadata plus base64-encoded message bytes up to max_message_bytes. You need to assert body content or MIME structure.

metadata and full captures are returned under messages in /admin/export and via the fixture's export_metrics() helper. capture.max_messages bounds the in-memory capture list and drops the oldest records first.

Admin Sidecar

ChaosSMTP uses a real SMTP listener plus a separate HTTP admin sidecar for health, metrics, export, reset, and runtime configuration updates.

Endpoint Method Auth Description
/health GET None Health check with smtp_running, run_id, started_utc, and in_burst.
/admin/config GET Bearer token View runtime-updatable config (error_injection, capture, latency).
/admin/config POST Bearer token Update runtime config with a partial JSON object.
/admin/stats GET Bearer token Metrics summary for the current run.
/admin/export GET Bearer token Raw metrics, captured messages, and run config.
/admin/reset POST Bearer token Clear metrics and captured messages, then start a new run.

Admin endpoints require Authorization: Bearer <admin_token>. The token is auto-generated unless you set admin.admin_token in config or --admin-token on the CLI. chaossmtp show-config redacts this token, but the running admin endpoints still require it.

curl -H "Authorization: Bearer $ADMIN_TOKEN" \
  http://127.0.0.1:8525/admin/stats

curl -X POST http://127.0.0.1:8525/admin/config \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"error_injection": {"rcpt_to_tempfail_pct": 25.0}}'

Repository Pytest Fixture

The errorworks repository test suite includes a chaossmtp_server fixture under tests/fixtures. It is a contributor/maintainer helper for source-checkout tests, not an installed package import. The fixture starts ChaosSMTP on an ephemeral loopback TCP port because standard SMTP clients require a real socket.

from email.message import EmailMessage

def test_mail_delivery(chaossmtp_server):
    message = EmailMessage()
    message["From"] = "sender@example.com"
    message["To"] = "recipient@example.com"
    message["Subject"] = "Fixture test"
    message.set_content("hello")

    assert chaossmtp_server.send_message(message) == {}
    assert chaossmtp_server.wait_for_messages(1)
    assert chaossmtp_server.get_stats()["total_requests"] == 1

Use marker kwargs to set presets and error percentages:

import pytest
import smtplib
from email.message import EmailMessage

@pytest.mark.chaossmtp(rcpt_to_reject_pct=100.0)
def test_recipient_rejection(chaossmtp_server):
    message = EmailMessage()
    message["From"] = "sender@example.com"
    message["To"] = "recipient@example.com"
    message["Subject"] = "Fixture test"
    message.set_content("hello")

    with pytest.raises(smtplib.SMTPRecipientsRefused):
        chaossmtp_server.send_message(message)
  • Presets -- Full preset comparison and customization
  • Configuration -- YAML config file structure and precedence rules
  • Metrics -- SMTP metrics fields and captured messages
  • Testing Fixtures -- Repository fixture setup and marker options