SMTP — Sending Mail
Authenticated submission (port 587)
import smtplib, ssl
s = smtplib.SMTP("mail.example.com", 587, timeout=10)
s.ehlo("client.example.com")
s.starttls(context=ssl.create_default_context())
s.ehlo("client.example.com")
s.login("alice@example.com", "password")
s.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"
)
s.quit()
Outbound relay to external domains
Authenticated users may send to any external address. rmail resolves MX records for the recipient domain and delivers directly, with opportunistic STARTTLS. Unauthenticated senders to external domains are always rejected.
s.login("alice@example.com", "password")
s.sendmail(
"alice@example.com",
["recipient@gmail.com"],
"From: alice@example.com\r\nTo: recipient@gmail.com\r\nSubject: External\r\n\r\nBody\r\n"
)
If
relay.enabled is false, sending to external domains returns 550 Relay not permitted even for authenticated users.
Telnet session (port 25)
$ telnet 127.0.0.1 25
220 mail.example.com ESMTP rmail
EHLO client.example.com
250-mail.example.com
250-SIZE 52428800
250-8BITMIME
250-STARTTLS
250 ENHANCEDSTATUSCODES
MAIL FROM:<sender@external.com>
250 2.1.0 OK
RCPT TO:<alice@example.com>
250 2.1.5 OK
DATA
354 End data with <CR><LF>.<CR><LF>
From: sender@external.com
To: alice@example.com
Subject: Inbound test
Body text.
.
250 2.0.0 OK: message accepted for delivery
QUIT
221 2.0.0 mail.example.com closing connection
IMAP — Reading Mail
Python imaplib
import imaplib
imap = imaplib.IMAP4("mail.example.com", 143)
imap.login("alice@example.com", "password")
imap.select("INBOX")
# Search all messages
status, data = imap.search(None, "ALL")
uids = data[0].split()
# Fetch latest message
if uids:
status, data = imap.fetch(uids[-1], "(BODY.PEEK[])")
print(data[0][1].decode())
# Mark as seen
imap.store(uids[-1], "+FLAGS", "\\Seen")
imap.logout()
UID operations
# Search by subject
status, data = imap.uid("SEARCH", "SUBJECT", '"Hello"')
# Fetch by UID
imap.uid("FETCH", data[0].decode(), "(FLAGS RFC822.SIZE)")
# Move to Trash
imap.uid("MOVE", uid, "Trash")
IDLE (push notifications)
imap.select("INBOX")
imap.send(b"a001 IDLE\r\n")
# Server sends: + idling
# Wait for EXISTS untagged response when new mail arrives
# Send DONE to terminate IDLE
imap.send(b"DONE\r\n")
Exchange HTTP API
Authenticate
curl -s -X POST http://mail.example.com:9002/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"alice","password":"password","domain":"example.com"}'
# Response: {"token": "..."}
List folders
curl -s http://mail.example.com:9002/folders \
-H "Authorization: Bearer $TOKEN"
List messages with pagination
curl -s "http://mail.example.com:9002/folders/INBOX/messages?offset=0&limit=25" \
-H "Authorization: Bearer $TOKEN"
Get a message (raw RFC822)
curl -s http://mail.example.com:9002/folders/INBOX/messages/42 \
-H "Authorization: Bearer $TOKEN"
Update flags
curl -s -X PATCH http://mail.example.com:9002/folders/INBOX/messages/42/flags \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"flags":["\\Seen","\\Flagged"]}'
Send a message
curl -s -X POST http://mail.example.com:9002/messages/send \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"to":["bob@example.com"],"subject":"Hello","body":"Message body."}'
Delete a message
curl -s -X DELETE http://mail.example.com:9002/folders/INBOX/messages/42 \
-H "Authorization: Bearer $TOKEN"
API endpoints reference
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /auth/login | None | Authenticate, returns JWT |
| GET | /folders | User | List folders with counts |
| GET | /folders/{folder}/messages | User | List messages (offset, limit) |
| POST | /folders/{folder}/messages | User | Append raw RFC822 message |
| GET | /folders/{folder}/messages/{uid} | User | Fetch message body |
| DELETE | /folders/{folder}/messages/{uid} | User | Delete message |
| PATCH | /folders/{folder}/messages/{uid}/flags | User | Replace flags |
| POST | /messages/send | User | Compose and send |
| GET | /aliases | User | List domain aliases |
| POST | /aliases | Admin | Add alias rule |
| DELETE | /aliases/{pattern} | Admin | Remove alias by pattern |
| GET | /.well-known/autoconfig/mail/config-v1.1.xml | None | Thunderbird autoconfig |
| POST | /autodiscover/autodiscover.xml | None | Exchange autodiscover |
| OPTIONS | * | None | CORS preflight (204) |
CLI Commands
| Command | Description |
|---|---|
python run.py | Start the mail server (uses config.json) |
python run.py /path/to/config.json | Start with a specific config file |
python run.py init | Generate default config.json |
python run.py install | Install as systemd service |
python run.py uninstall | Remove systemd service |
python run.py migrate | Migrate from index.json storage to shard storage |
python run.py diagnose | Run 8-phase diagnostic suite |
python run.py test | Run integration test suite |
python run.py alias list example.com | List aliases for domain |
python run.py alias add example.com ".*" alice | Add catch-all alias |
python run.py alias remove example.com "sales-.*" | Remove alias |
Terminal client
python -m rmail.client
Curses-based TUI for browsing mailboxes, reading and composing messages. Connects over IMAP and SMTP.
Aliases and Catch-All
Aliases route addresses matching a regex pattern to an existing local mailbox. Resolution follows exact user lookup first, then pattern rules in declaration order — consistent with the Postfix model.
# Route all unmatched addresses to alice
python run.py alias add example.com ".*" alice
# Route sales-* to the sales mailbox
python run.py alias add example.com "sales-.*" sales
# Remove a rule
python run.py alias remove example.com "sales-.*"
Alias rules are per-domain and stored in
{mail_root}/domains/{domain}/aliases.json.
Signal Handling
SIGINT/SIGTERM— graceful shutdown, drains delivery and relay queuesSIGHUP— reload configuration without restart