rmail

Production-grade mail server stack in pure Python 3 asyncio — SMTP, IMAP4rev1, and Exchange REST API.

Sending Mail via SMTP

rmail accepts authenticated SMTP on port 587 (submission) and unauthenticated inbound delivery on port 25. Implicit TLS (SMTPS) is available on port 465 when TLS is configured.

When TLS is configured, AUTH is not advertised on cleartext connections. Clients must negotiate STARTTLS before credentials are accepted. The Python example below reflects this correctly.

Python (with STARTTLS)

import smtplib, ssl

server = smtplib.SMTP("localhost", 587)
server.ehlo("client.example.com")
server.starttls(context=ssl.create_default_context())
server.ehlo("client.example.com")
server.login("alice@example.com", "password")
server.sendmail(
    "alice@example.com",
    ["bob@example.com"],
    "From: alice@example.com\r\nTo: bob@example.com\r\nSubject: Hello\r\n\r\nBody\r\n"
)
server.quit()

Python (implicit TLS on port 465)

import smtplib, ssl

ctx = ssl.create_default_context()
with smtplib.SMTP_SSL("localhost", 465, context=ctx) as server:
    server.login("alice@example.com", "password")
    server.sendmail(
        "alice@example.com",
        ["bob@example.com"],
        "From: alice@example.com\r\nTo: bob@example.com\r\nSubject: Hello\r\n\r\nBody\r\n"
    )

SMTP EHLO Feature Behaviour

When TLS is configured, the EHLO response on cleartext connections advertises STARTTLS but not AUTH. After STARTTLS negotiation, AUTH PLAIN LOGIN becomes available. On connections without TLS configuration, AUTH is always advertised.

Reading Mail via IMAP

IMAP4rev1 is available on port 143 (plain/STARTTLS) and port 993 (implicit TLS).

Python

import imaplib

imap = imaplib.IMAP4("localhost", 143)
imap.login("alice@example.com", "password")
imap.select("INBOX")
status, data = imap.search(None, "ALL")
for num in data[0].split():
    status, msg = imap.fetch(num, "(BODY.PEEK[])")
    print(msg)
imap.logout()

Supported IMAP Commands

CAPABILITY, LOGIN, AUTHENTICATE (PLAIN, LOGIN), STARTTLS, SELECT, EXAMINE, LIST, LSUB, SUBSCRIBE, UNSUBSCRIBE, STATUS, FETCH, STORE, APPEND, EXPUNGE, COPY, MOVE, UID, SEARCH, IDLE, ID, ENABLE, CHECK, UNSELECT, RENAME, CREATE, DELETE, NAMESPACE, NOOP, LOGOUT, CLOSE.

Supported SEARCH Criteria

ALL, UNSEEN, SEEN, FLAGGED, UNFLAGGED, ANSWERED, UNANSWERED, DELETED, UNDELETED, DRAFT, UNDRAFT, NEW, OLD, RECENT, UID, FROM, TO, SUBJECT, BODY, TEXT, HEADER, BEFORE, SINCE, ON, LARGER, SMALLER, NOT, OR.

IMAP Capabilities Advertised

Always: IMAP4rev1, IDLE, SPECIAL-USE, NAMESPACE, UIDPLUS, MOVE, ID, ENABLE, UNSELECT, LITERAL+. Conditionally: AUTH=PLAIN, AUTH=LOGIN (when TLS active or no TLS configured), STARTTLS (when TLS configured on cleartext connection).

Exchange REST API

All endpoints require a Bearer JWT token obtained via POST /auth/login. UIDs are scoped per folder. Tokens expire after 3600 seconds.

Authenticate

TOKEN=$(curl -s -X POST http://localhost:9002/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","password":"password","domain":"example.com"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

List Folders

curl -s http://localhost:9002/folders \
  -H "Authorization: Bearer $TOKEN"

List Messages (with pagination)

curl -s "http://localhost:9002/folders/INBOX/messages?offset=0&limit=25" \
  -H "Authorization: Bearer $TOKEN"

Get a Message

curl -s http://localhost:9002/folders/INBOX/messages/1 \
  -H "Authorization: Bearer $TOKEN"

Update Flags

curl -s -X PATCH http://localhost:9002/folders/INBOX/messages/1/flags \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"flags":["\\Seen","\\Flagged"]}'

Send a Message

curl -s -X POST http://localhost:9002/messages/send \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"to":["bob@example.com"],"subject":"Hello","body":"Test"}'

Delete a Message

curl -s -X DELETE http://localhost:9002/folders/INBOX/messages/1 \
  -H "Authorization: Bearer $TOKEN"

Manage Aliases (admin only)

# Add an alias (admin token required)
curl -s -X POST http://localhost:9002/aliases \
  -H "Authorization: Bearer $ADMIN_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"pattern":"sales-.*","target":"alice"}'

# Remove an alias by path (admin token required)
curl -s -X DELETE "http://localhost:9002/aliases/sales-.*" \
  -H "Authorization: Bearer $ADMIN_TOKEN"

API Endpoint Reference

Method Path Auth Description
POST/auth/loginNoneAuthenticate, returns JWT token (3600s expiry).
GET/foldersUserList folders with message counts.
GET/folders/{folder}/messagesUserList messages; supports offset and limit query params.
POST/folders/{folder}/messagesUserAppend a raw RFC822 message.
GET/folders/{folder}/messages/{uid}UserRetrieve a message by UID.
DELETE/folders/{folder}/messages/{uid}UserDelete a message.
PATCH/folders/{folder}/messages/{uid}/flagsUserReplace the flag set on a message.
POST/messages/sendUserCompose and deliver a message.
GET/aliasesUserList aliases for the authenticated domain.
POST/aliasesAdminAdd an alias rule.
DELETE/aliasesAdminRemove an alias rule (pattern in request body).
DELETE/aliases/{pattern}AdminRemove an alias rule (pattern in URL path).
OPTIONS*NoneCORS preflight; returns 204 with CORS headers.
GET/.well-known/autoconfig/mail/config-v1.1.xmlNoneThunderbird autoconfig XML.
POST/autodiscover/autodiscover.xmlNoneOutlook autodiscover XML.

Alias Management

Aliases are per-domain regex rules that route addresses to existing user accounts. Exact user lookup takes priority; pattern rules are evaluated in declaration order.

python run.py alias list example.com
python run.py alias add example.com ".*" alice          # catch-all
python run.py alias add example.com "sales-.*" alice    # prefix match
python run.py alias remove example.com "sales-.*"

Alias rules can also be managed via Makefile targets:

make alias
make delalias
make listaliases
Alias creation and deletion via the Exchange API require an admin-level JWT token. Any authenticated user may list aliases.

Terminal Client

rmail includes a curses-based IMAP/SMTP client for browsing, reading, composing, and replying to mail:

python -m rmail.client

Or using the Makefile shortcut:

make client

Running Tests and Diagnostics

python3 run.py test

Runs the end-to-end test suite against a live server instance.

Diagnostics

sudo python3 run.py diagnose

Executes an 8-phase diagnostic sequence: configuration, user management, backend storage, SMTP protocol, delivery verification, IMAP protocol, Exchange API, and cleanup. A healthy server produces 0 failures and 0 warnings.