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. Open the firewall
7. Set the mail password
8. Set up a mail client
9. Gotchas
10. 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. 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.

📋
pass quick proto tcp from any to $if port { 587, 993 } flags S/SA keep state

7. 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.

8. 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.

9. 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.

10. The script

Steps 1 through 7, automated. 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.

← back home