Last modification on
I had been using Fastmail, but 8,400 KRW/month (~6 USD) felt unsustainable for email, so I decided to self-host it on my home server, alpha (AMD Athlon 3000G, 8GB RAM).
My requirements were (a) no exposed ports except WireGuard, (b) full IMAP/SMTP access from anywhere, (c) bypass Gmail's spam filter, and (d) no recurring costs like VPS rentals.
To receive mail directly from other servers, port 25 needs to be open, which violates (a). So I used CF email routing instead. It can be configured to route mail to a CF Worker, which POSTs the mail to a specific endpoint. Using cloudflared, I can expose an HTTP endpoint proxied by Cloudflare without opening any ports.
For this, I built http2lmtp in Rust. It receives mail POSTed to the HTTP endpoint and writes it to an internal Unix socket with LMTP. Since the endpoint is public, it requires a bearer token. You can find CF worker example interacting with it on its README.
In my setup, the Unix socket for LMTP is opened by Dovecot. It stores the mail it receives over LMTP in a local Maildir, and serves that Maildir over IMAP.
To satisfy (c), alpha's public IP needs a reverse DNS record, but that requires permissions from the ISP that owns the IP range — impossible with a residential connection. So instead, I decided to use smtp2go's free tier. It allows up to 1,000 sends per month.
Entering smtp2go's credentials on both my iPhone and laptop is a hassle, though. So instead I run OpenSMTPD on alpha for routing. It only listens on wg0 for submission, so it needs no auth or TLS. /etc/smtpd.conf:
table secrets file:/etc/smtpd/secrets
table vusers file:/etc/smtpd/virtuals
listen on lo
listen on wg0 port 587
# rcpt-to: use To: as dovecot user name ([email protected])
action "local" lmtp "/var/run/dovecot/lmtp" rcpt-to virtual <vusers>
action "out" relay host smtp+tls://[email protected]:2525 \
auth <secrets> helo "dilluti0n.com"
action "out-local" relay host smtp+tls://[email protected]:2525 \
auth <secrets> helo "dilluti0n.com" \
mail-from "[email protected]"
match from any for rcpt-to "[email protected]" action "local"
match from local for any action "out-local"
match from src 10.42.0.0/24 for any action "out"
The benefit from OpenSMTPD is that you can route emails like firewall rules. If an email generated locally is sent to [email protected], it is not sent to [email protected], but directly to the local Dovecot via LMTP.
Also, by using it, no need to make changes on the client side when changing the SMTP routing service or sending it directly via alpha.
On both my laptop and my iPhone, alpha is reachable as 10.42.0.1 through WireGuard. Since both SMTP and IMAP are configured on standard ports, no special settings are required other than the IP address. For IMAP auth, account ID/password set in dovecot is needed.
A warning message appears on the iPhone IMAP setting stating that TLS cannot be used, but it is not necessary since communication is done via WireGuard anyway.
Here is ascii art for the structure:
sender MTA
|
v DNS MX
CF Email Routing
|
v CF Worker: POST
CF tunnel (cloudflared)
|
v HTTP (localhost)
http2lmtp
|
v LMTP
Dovecot <----> Maildir
^ |
| v IMAP (wg0:993)
| LMTP MUA
OpenSMTPD
^ |
| +--> smtp2go --> Internet
|
| SMTP (wg0:587)
MUA
X-Report-Abuse are added by
smtp2go.
Thank you for reading! Please mail me at [email protected] for a comment.
Copyright 2026 Hee-Suk Kim