Self-hosting email (IMAP + relay)
This assumes mail for your domain is already arriving on your server through opensmtpd (MX records pointed at it, inbound delivery working), and that you have a working web server and TLS certificate for the domain already. What's missing is a way to actually read that mail from a phone or desktop client, and a reliable way to send mail that doesn't get silently dropped by spam filters.
Reasoning: a fresh server IP has no sending reputation. Mail sent directly from it gets filtered or bounced by most major inboxes, regardless of how correctly SPF/DKIM/DMARC are configured. Relaying outbound mail through a provider with established reputation (Postmark, here) fixes that without giving up control of inbound delivery or storage. Dovecot then exposes that mail over IMAP so any mail client can read it.
A script at the end does all of this. Read it before running it.
Contents:
1. Install dovecot
2. Configure dovecot
3. Add Sent, Drafts, Trash, and Junk
4. Add an authenticated submission listener
5. Relay outbound mail through Postmark
6. SPF, DKIM, and DMARC
7. Open the firewall
8. Set the mail password
9. Set up a mail client
10. Gotchas
11. The script
1. Install dovecot
๐
pkg_add dovecot
Dovecot's package ships its configuration as examples only โ nothing is active until you copy it into place, the same disabled-by-default pattern as OpenBSD's PHP extensions:
๐
cp -r /etc/dovecot/example-config/* /etc/dovecot/
2. Configure dovecot
Reuse the same certificate your web server already has for the domain โ no need for a separate one. Write /etc/dovecot/local.conf:
๐
protocols = imap
listen = *, ::
ssl = required
ssl_cert = </etc/ssl/yourdomain.com:443.crt
ssl_key = </etc/ssl/private/yourdomain.com:443.key
disable_plaintext_auth = yes
mail_location = maildir:~/Maildir
passdb {
driver = bsdauth
}
userdb {
driver = passwd
}
Then make sure it's actually included:
๐
echo "!include local.conf" >> /etc/dovecot/dovecot.conf
rcctl enable dovecot
rcctl restart dovecot
rcctl check dovecot
bsdauth/passwd means dovecot checks logins against the server's own system password database โ the same one passwd writes to, and a completely different secret from any SSH key. See the gotchas section below; this trips people up.
3. Add Sent, Drafts, Trash, and Junk
A bare dovecot install only gives you INBOX. Without a properly-tagged Sent folder, mail clients either don't know where to file sent messages or show an unlabeled folder you have to map by hand. Append this to local.conf:
๐
namespace inbox {
inbox = yes
mailbox Sent {
special_use = \Sent
auto = subscribe
}
mailbox Drafts {
special_use = \Drafts
auto = subscribe
}
mailbox Trash {
special_use = \Trash
auto = subscribe
}
mailbox Junk {
special_use = \Junk
auto = subscribe
}
}
auto = subscribe creates and subscribes the folder the next time dovecot touches that user's mailbox โ which in practice means the first IMAP login after a restart. To have it exist immediately instead of waiting on a client:
๐
rcctl restart dovecot
doveadm mailbox create -u yourusername Sent
doveadm mailbox create -u yourusername Drafts
doveadm mailbox create -u yourusername Trash
doveadm mailbox create -u yourusername Junk
The special_use line is what actually matters here โ it's the IMAP attribute Thunderbird, K-9, and every other client use to auto-map "this folder is where sent mail goes," rather than a name they guess at.
4. Add an authenticated submission listener
Your existing /etc/mail/smtpd.conf probably already has a pki block and a plain listen on all tls line for receiving mail. Add a second listener on the submission port (587) that requires authentication, so mail clients can send through it:
๐
listen on all port submission tls-require pki "yourdomain.com" auth
The trailing auth is what actually turns on the AUTH capability โ without it, a client's AUTH LOGIN gets rejected outright with something like 503 Command not supported, before your password is ever checked.
5. Relay outbound mail through Postmark
Sign up for Postmark (or another transactional relay), verify your domain, and grab a server API token. Postmark's SMTP auth is unusual: the token is used as both the username and the password.
๐
echo "postmark YOUR_TOKEN YOUR_TOKEN" > /etc/mail/secrets
chmod 640 /etc/mail/secrets
Then in smtpd.conf:
๐
table secrets file:/etc/mail/secrets
action outbound relay host smtp+tls://postmark@smtp.postmarkapp.com:587 auth <secrets>
match from any auth for any action outbound
match from local for any action outbound
Check and reload:
๐
smtpd -n
rcctl reload smtpd
6. SPF, DKIM, and DMARC
Relaying through Postmark fixes sending reputation, but it doesn't stop someone else from spoofing your address in a phishing email unless your DNS actually says who's allowed to send as you. Postmark's dashboard (Sending Domains โ your domain) gives you exact, account-specific SPF and DKIM values โ don't hardcode ones from a blog post, they're unique per account. It'll show something like:
๐
SPF (TXT, at the root of your domain):
v=spf1 a mx include:spf.mtasv.net ~all
๐
DKIM (TXT, at the selector host Postmark shows you):
k=rsa; p=<a long key Postmark generates for you>
๐
Return-Path / bounce domain (CNAME):
pm-bounces.yourdomain.com โ pm.mtasv.net
DMARC isn't Postmark-specific โ publish it yourself once SPF/DKIM are in place, starting permissive so you can see reports before anything gets rejected:
๐
_dmarc.yourdomain.com TXT:
v=DMARC1; p=quarantine; rua=mailto:postmaster@yourdomain.com
Tighten p=quarantine to p=reject after watching reports for a few weeks with no surprises.
7. Open the firewall
Ports 993 (IMAPS) and 587 (submission) need to be reachable from wherever you'll actually check mail โ unlike the RSS reader and ntfy, this one is meant to be reachable from the whole internet, not just your tailnet, since phones without Tailscale running still need to fetch mail.
That also means it's the one surface here actually worth rate-limiting โ anyone can hammer these ports with login attempts. A pf table plus a source-tracking option on the rule handles it without a separate daemon:
๐
table <bruteforce> persist
block quick from <bruteforce>
pass quick proto tcp from any to $if port { 587, 993 } flags S/SA keep state \
(max-src-conn 20, max-src-conn-rate 8/30, overload <bruteforce> flush global)
More than 8 new connections in 30 seconds from one IP gets it added to <bruteforce> and its existing connections dropped โ loose enough that a phone reconnecting after a network switch won't trip it, tight enough to stop the constant background scanning any public IMAPS port attracts. Already have a <bruteforce> table from hardening SSH first? Reuse it โ this doesn't need its own.
8. Set the mail password
๐
passwd yourusername
This is the password IMAP and SMTP auth will check. If you've only ever logged into this server over SSH with a key, don't assume one already exists โ see the gotchas section.
9. Set up a mail client
Any IMAP-capable client works. Settings are the same everywhere:
Incoming (IMAP): your domain, port 993, SSL/TLS, normal password
Outgoing (SMTP): your domain, port 587, STARTTLS, normal password
Username: your system username (not the full email address, since this isn't a virtual-domain setup)
Proton Mail's app is a notable exception โ it only handles @proton.me/@pm.me addresses and can't add an external IMAP account at all (Proton Mail Bridge does this, but it's desktop-only and needs a paid plan). Thunderbird, K-9 Mail, FairEmail, and Gmail's "Other (IMAP)" option all work fine.
10. Gotchas
SSH keys and the system password are two different secrets. If PasswordAuthentication no is set (it should be), you may have never set an actual password for your account โ SSH never needed one. Dovecot's bsdauth/passwd backend checks that system password specifically, not your SSH key. If IMAP rejects a password you're sure is right, it's likely because no system password was ever set: run passwd yourusername.
Dovecot ships inert. Same as the PHP extension issue elsewhere on this site: nothing in /etc/dovecot/ is active until you copy the example config in. A fresh pkg_add dovecot with no further steps does nothing.
Missing auth on the submission listener fails loudly but unhelpfully. Without it, opensmtpd answers AUTH LOGIN with a flat "not supported" โ no hint that the fix is one keyword away.
Thunderbird's "Re-test" button is not a real test. It re-runs Thunderbird's own autoconfiguration probe (ISPDB lookup, guessed hostnames, SRV records) โ for a self-hosted domain with none of that published, it fails every time regardless of whether the settings you manually typed are correct. It can also trip opensmtpd's strict protocol parser (logged as something like Pipelining not supported) or connect and disconnect from dovecot without ever attempting a login, because the probe doesn't behave like Thunderbird's real mail-sending code. If your server logs show a connection that hangs up without ever sending a password, the wizard never actually tried logging in. Fill in the manual fields, click Done, and test with a real message instead.
A wildcard CNAME can silently swallow your DMARC record. If you already have a * โ yourdomain.com CNAME for other subdomains, a lookup at _dmarc.yourdomain.com will resolve through it instead of erroring โ easy to mistake for "no DMARC record" when it's actually "matched the wildcard, found nothing there either." Harmless either way: an explicit _dmarc TXT record always takes precedence over a wildcard, so just add it โ no need to touch the wildcard first.
11. The script
Automates everything except step 6 (the DNS records are specific to your provider and Postmark account, so those stay manual). Run as root on a server that already receives mail for the domain:
๐
ftp https://nevrast.xyz/mail.sh
sh mail.sh
It pauses where a step needs you: the domain, the mailbox username, your Postmark API token, and setting the password.
Source is also on GitHub, alongside the RSS reader and ntfy scripts.
Email me with questions or fixes.