https://maxadamski.com/blog/2025/10/email.html
Introduction
Self-hosting an email server is useful for automating tasks like
mailing lists, newsletters, or email verification APIs.
The elephant in the room is real-world deliverability. With
self-hosting you risk not receiving mail or someone missing your
mail. I accept this for my personal projects, but you may not. Keep
this in mind.
For me the selling point of self-hosting is that it's practically
free. If you're already self-hosting a website, installing some extra
packages on your server and just a bit of your time is all that's
required. Mail takes very little storage and the software is light,
so you're unlikely to significantly change energy consumption or disk
usage.
For the longest time, I perceived self-hosting email as too
difficult, but after doing it for one of my projects, I can say it's
not much harder or more time-consuming than configuring some email
SaaS.
I changed my goals a bit to make the setup easier though.
Self-hosting a multi-user webmail looks heavy and is more involved
than I was willing to get into, so I just skipped it. That way, I
didn't have to bother with user accounts, databases, or the web at
all, and the task became easy.
With my config, manually sending and receiving email is possible if
you SSH to your mail server and use the minimal sendmail or mailx
commands, or Mutt if you like TUI. I've been semi-comfortably using
mailx for a month already (with its ancient user interface!), so the
setup is enough for me now, but I could expand it in the future, and
multi-user webmail isn't completely off the table. Maybe I'll even
write a simple webmail package myself!
Setting up Postfix
You just need to open port 25, and install and configure Postfix and
OpenDKIM on your machine. Postfix is a complete SMTP server, and is
enough for basic mail alone, but in practice you also need OpenDKIM
to get your mail delivered to popular services like Gmail.
Here's my Postfix config to show how easy it is. I left the master.cf
file as it was, because I'm always submitting email locally.
The default alias and header check config files are practically
self-explanatory (just open them and read the comments!).
/etc/postfix/main.cf
compatibility_level = 3.8
mail_owner = postfix
myhostname = mx.idx.cy
myorigin = idx.cy
mydestination = localhost, idx.cy, maxadamski.com, localchat.cc
inet_interfaces = all
inet_protocols = ipv4
# Addresses
alias_maps = hash:/etc/postfix/aliases
alias_database = $alias_maps
recipient_delimiter = +
# I'm the only user on my machine, so I send from whichever address I want.
#smtpd_sender_login_maps = hash:/etc/postfix/sender_login_maps
#smtpd_sender_restrictions = reject_authenticated_sender_login_mismatch
# spam
#in_flow_delay = 1s
header_checks = regexp:/etc/postfix/header_checks
setgid_group = postdrop
# TLS (strict)
smtpd_tls_cert_file = /etc/ssl/tls/mx.idx.cy.crt
smtpd_tls_key_file = /etc/ssl/tls/mx.idx.cy.key
smtpd_tls_security_level = encrypt
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_security_level = encrypt
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
# DKIM
smtpd_milters = inet:localhost:8891
non_smtpd_milters = inet:localhost:8891
milter_default_action = accept
/etc/postfix/master.cf
# Postfix master process configuration file. For details on the format
# of the file, see the master(5) manual page (command: "man 5 master" or
# on-line: http://www.postfix.org/master.5.html).
#
# Do not forget to execute "postfix reload" after editing this file.
#
# ==========================================================================
# service type private unpriv chroot wakeup maxproc command + args
# (yes) (yes) (no) (never) (100)
# ==========================================================================
smtp inet n - n - - smtpd
pickup unix n - n 60 1 pickup
cleanup unix n - n - 0 cleanup
qmgr unix n - n 300 1 qmgr
#qmgr unix n - n 300 1 oqmgr
tlsmgr unix - - n 1000? 1 tlsmgr
rewrite unix - - n - - trivial-rewrite
bounce unix - - n - 0 bounce
defer unix - - n - 0 bounce
trace unix - - n - 0 bounce
verify unix - - n - 1 verify
flush unix n - n 1000? 0 flush
proxymap unix - - n - - proxymap
proxywrite unix - - n - 1 proxymap
smtp unix - - n - - smtp
relay unix - - n - - smtp
-o syslog_name=postfix/$service_name
# -o smtp_helo_timeout=5 -o smtp_connect_timeout=5
showq unix n - n - - showq
error unix - - n - - error
retry unix - - n - - error
discard unix - - n - - discard
local unix - n n - - local
virtual unix - n n - - virtual
lmtp unix - - n - - lmtp
anvil unix - - n - 1 anvil
scache unix - - n - 1 scache
postlog unix-dgram n - n - 1 postlogd
Notice that there's no mention of POP3 or IMAP! I did waste some time
trying to set them up with Dovecot (because they changed their config
format too much, so guides became outdated, and their web docs were
just hard to read for me). Ultimately I can just SSH to my server and
I feel comfortable with mailx, so I skipped Dovecot. One package less
in my system :)
TLS
You will also need an SSL certificate for encryption in transit. I
hate getting and renewing SSL certificates, because the tools are
bulky and automation is yet another moving part in your system (I
used the lego package, with the manual DNS challenge for simplicity,
but I'm not too happy about it). I won't give you a tutorial on
getting SSL certificates, but note that you don't have to get and
renew a certificate for each of your custom domains!
You just need one SSL certificate for your machine to encrypt data in
transit to other SMTP servers. If you create an A record
mx.example.com pointing to your email machine's IP address, then grab
a free certificate for mx.example.com from Let's Encrypt. Then point
to it in the Postfix configuration, and you've got transport
encryption! In short, only the MX hostname needs a cert for STARTTLS
to be used for encryption.
Why no certificates for your actual email domains like example.com?
Because the email domain has little to do with transport encryption.
TLS only secures the connection between servers. You can still set
whatever you want in the From header.
DKIM, SPF, and DMARC
You should prove that your emails actually come from your domain to
make your mail trustworthy and deliver to Gmail and co. That's what
DKIM is for, and fortunately it's a one-time deal per email domain.
First you generate a key pair for each domain with OpenDKIM, and then
you publish the public key in a TXT record in DNS. The keys don't
expire automatically, but it's best practice to rotate them
periodically. My config uses a naming scheme that allows smooth
rotation, but it doesn't complicate things if you skip it.
There are two more TXT records that you need to publish in DNS: the
SPF and DMARC records. You say which hosts are allowed to send mail
from your email domain, and give instructions to other email servers
about what to do with mail that fails DKIM checks. In my case I told
others to reject mail that can't be verified as coming from my
domains, and send reports to my postmaster address.
Take a look at my OpenDKIM config to understand how things come
together.
/etc/opendkim.conf
UserID opendkim:opendkim
Socket inet:8891@localhost
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
Canonicalization relaxed/relaxed
ReportAddress postmaster@idx.cy
SendReports no
LogWhy yes
Syslog yes
SyslogSuccess no
/etc/opendkim/KeyTable
key1._domainkey.idx.cy idx.cy:key1:/etc/opendkim/keys/idx.cy/key1.private
key1._domainkey.maxadamski.com maxadamski.com:key1:/etc/opendkim/keys/maxadamski.com/key1.private
key1._domainkey.localchat.cc localchat.cc:key1:/etc/opendkim/keys/localchat.cc/key1.private
/etc/opendkim/SigningTable
*@idx.cy key1._domainkey.idx.cy
*@maxadamski.com key1._domainkey.maxadamski.com
*@localchat.cc key1._domainkey.localchat.cc
/etc/opendkim/TrustedHosts
127.0.0.1
localhost
I generate DKIM keys with the following command:
opendkim-genkey -D /etc/opendkim/keys/example.com -d example.com -s key1
And for each email domain I have the following records in DNS:
Type Name Value
MX example.com mx.idx.cy
TXT example.com v=spf1 mx a -all
TXT key1._domainkey v=DKIM1; k=rsa; s=email; p=
TXT _dmarc v=DMARC1; p=reject; rua=mailto:postmaster@idx.cy
Reverse DNS
One more thing about self-hosted email deliverability. I've read that
reverse DNS (PTR record) will boost the reputation of your email
server. The thing is that your ISP has to set it up, and I suspect my
ISP to reply with a polite "no", so I didn't do it yet. As you'll see
in the next section, my email gets delivered to Gmail just fine. GMX
and Outlook also didn't mark my mail as spam. Maybe my IP is lucky :)
But in general, if you want wide deliverability, PTR isn't optional.
Testing with Gmail
To try it out, let's send a test mail to Gmail with the sendmail
command:
sendmail -vt < test.mail
test.mail
Content-Type: text/html
From: max@idx.cy
To: myaddress@gmail.com
Subject: DKIM test
Test message from idx.cy!
I got the mail instantly and Gmail confirmed TLS encryption.
[test-mail-]
Click "Show original" in Gmail to see the raw mail. There's lots of
text in the headers, so let's just focus on passing SPF, DKIM, and
DMARC :)
[test-mail-]
You'll also get a mail with a report because of the -v option. I
receive mail with Heirloom Mail like this:
You have new mail in /var/mail/max
fool ~ | mailx
Heirloom Mail version 12.5 7/5/10. Type ? for help.
"/var/mail/max": 1 message 1 new
>N 1 Mail Delivery System Sat Oct 4 15:40 74/2437 "Mail Delivery Status Report"
I use the p command to print the mail.
& p
Message 1:
From MAILER-DAEMON Sat Oct 4 15:40:50 2025
X-Original-To: max@idx.cy
Delivered-To: max@idx.cy
Date: Sat, 4 Oct 2025 15:40:50 +0200 (CEST)
From: Mail Delivery System
Subject: Mail Delivery Status Report
To: max@idx.cy
Auto-Submitted: auto-replied
Content-Type: multipart/report; report-type=delivery-status;
boundary="3C311BFF8D.1759585250/mx.idx.cy"
Status: R
Part 1:
Content-Description: Notification
Content-Type: text/plain; charset=utf-8
This is the mail system at host mx.idx.cy.
Enclosed is the mail delivery report that you requested.
The mail system
: delivery via
gmail-smtp-in.l.google.com[X.X.X.X]:25: 250 2.0.0 OK 1759585250
4fb4d7f45d1cf-6393b6ba951si3187039a12.40 - gsmtp
Great, everything is working!
If something isn't working for you, please double-check your DNS
records, and triple-check that TLS certificates are readable by the
Postfix user, and that DKIM keys are readable by the OpenDKIM user.
Postfix and OpenDKIM logs will also be useful. The OpenDKIM config
file is especially unforgiving of typos, so watch out for small
mistakes!
Next steps
In my next post on email, I'll show you how to use Python to build
useful email applications. Thanks for reading!
Btw, if you notice anything about my config (or want to share some
thoughts) just email me at max@idx.cy :)