Howdy!

This is just your run-of-the-mill SMTP, IMAP, and CalDAV server. If you'd like to setup something similar, check out this article from poolp.org for a detailed overview, or keep reading for a quick run-down :)

What?

Why?

Because I can. Being able to run a mail server is a handy skill for any sysadmin, and it ain't like it's terribly expensive to do so in this day and age of virtual servers and cloud computing. Also, the more people and organizations running their own mail servers, the less of a stranglehold centralized providers like Google and Microsoft get to have on email.

How?

I mostly followed the setup steps from Gilles Chehade's excellent article on the subject when setting up this server. Considering he's one of the main OpenSMTPD developers, I'd say his site's an excellent resource in general on this sort of thing. I did deviate from his guide in a couple places, so the rest of this page documents how I've got everything setup, and how you might go about setting up something equivalent.

Some conventions (if you see these below, replace 'em with your own values):

Server

Hosted as a VPS from 1984, which is based out of Iceland. I started using 'em years ago specifically because they support OpenBSD as an install option, and I've been absolutely happy with that decision.

Really, any hosting provider (be it VPS or dedicated) will do, provided it runs OpenBSD (if you're following these instructions), you've got an IP address that doesn't suck (i.e. not blacklisted by the rest of the email-sending world, whether individually or as part of a blacklisted subnet), and you can set rDNS for that IP address. 1984 passes all these criteria.

This server has 4 vCPUs, 8GB of RAM, 160GB of SSD space, and 5TB of monthly data transfer; all of these things are almost certainly overkill for a personal mailserver, but I also use it for other domains, and it's obviously hosting these instructions, so better safe than sorry. 1984's cheap enough that it ain't all that big of a deal.

OS

A bog-standard OpenBSD 6.8 6.9 7.4 install. Some notes:

DKIM and anti-spam

Run pkg_add redis rspamd opensmtpd-filter-rspamd to get everything installed. We'll then need an /etc/rspamd/local.d/dkim_signing.conf

allow_username_mismatch = true;

domain {
    [[DOMAIN]] {
        path = "/etc/mail/dkim/mail.[[DOMAIN]].key";
        selector = "[[DKIM_SELECTOR]]";
    }
}
    

Next up, for greylisting to work we need /etc/rspamd/local.d/redis.conf...

servers = "127.0.0.1";
    

...and /etc/rspamd/local.d/greylist.conf:

servers = "127.0.0.1:6379";
    

Last but not least, we need to tell Rspamd about different settings to use for mail we send. Once upon a time (i.e. at some point between when I originally wrote this document and in 2024 when I'm now updating it) this didn't seem to be necessary, but nowadays you need to tell Rspamd to not run your outbound mails through the full anti-spam pipeline. I failed to do this, and ended up wondering why Rspamd was greylisting everything I tried to send (and also was failing to add DKIM signatures despite the above), but adding a custom /etc/rspamd/local.d/settings.conf (and telling OpenSMTPD to use it, which you'll see later) fixed it:

outbound {
    id = "outbound";
    apply {
        groups_enabled = ["dkim"];
        actions {
            reject = 100.0;
            greylist = 100.0;
            "add header" = 100.0;
        }
    }
}
    

Now, let's generate our DKIM key, get the permissions dialed in, and fire up Rspamd:

openssl genrsa \
  -out /etc/mail/dkim/mail.[[DOMAIN]].key 1024
openssl rsa \
  -in /etc/mail/dkim/mail.[[DOMAIN]].key \
  -pubout -out /etc/mail/dkim/mail.[[DOMAIN]].pub

chown -R :_rspamd /etc/mail/dkim
chmod 750 /etc/mail/dkim
chmod 640 /etc/mail/dkim/*

rcctl enable redis
rcctl enable rspamd
rcctl start redis
rcctl start rspamd
    

Take a note of the contents of the mail.[[DOMAIN]].pub we created above; the contents of that file (without the -----BEGIN PUBLIC KEY----- / -----END PUBLIC KEY----- and any newlines) is what you'll need to use for [[DKIM_PUBKEY]] below.

DNS

My domain's hosted through Free DNS, and has been for years. Pretty much any other DNS host will work fine for this, too. You'll need a couple DNS records to reliably serve up mail:

HTTP

We'll need an /etc/httpd.conf:

server "mail.[[DOMAIN]]" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location * {
    block return 302 "https://$HTTP_HOST$REQUEST_URI"
  }
}

# The following is optional, and might actually fail to work if you don't
# already have SSL certs present; I haven't tested that.  If you include this,
# you'll probably want to "mkdir -p /var/www/htdocs/mail.[[DOMAIN]]" and include
# at least an index.html in that folder.

server "mail.[[DOMAIN]]" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/mail.[[DOMAIN]]/fullchain.pem"
    key "/etc/ssl/private/mail.[[DOMAIN]]/privkey.pem"
  }
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location * {
    root "/htdocs/mail.[[DOMAIN]]"
    directory auto index
  }
}
    

Then run rcctl enable httpd && rcctl start httpd.

TLS Certificate

We're using Let's Encrypt, via OpenBSD's built-in acme-client. We'll need an /etc/acme-client.conf:

authority letsencrypt {
  api url "https://acme-v02.api.letsencrypt.org/directory"
  account key "/etc/acme/letsencrypt-privkey.pem"
}

domain mail.[[DOMAIN]] {
  domain key "/etc/ssl/private/mail.[[DOMAIN]]/privkey.pem"
  domain full chain certificate "/etc/ssl/mail.[[DOMAIN]]/fullchain.pem"
  sign with letsencrypt
}
    

And now it's just a matter of running acme-client -v mail.[[DOMAIN]] && rcctl reload httpd to get it going. To automatically renew, we can add it to root's crontab, but it's even easier to just make it part of the built-in weekly maintenance by creating a /etc/weekly.local like so:

# certs
next_part "Renewing TLS certificate:"
acme-client mail.[[DOMAIN]]
case $? in
    0)
        echo "TLS certificate renwed; reloading relevant daemons..."
        rcctl reload httpd >/dev/null 2>&1
        case $? in
            0)
                echo "httpd reloaded"
                ;;
            *)
                echo "httpd failed to reload"
                ;;
        esac
        rcctl restart smtpd >/dev/null 2>&1
        case $? in
            0)
                echo "smtpd restarted (reload not supported)"
                ;;
            *)
                echo "smtpd failed to reload"
                ;;
        esac
        rcctl reload dovecot >/dev/null 2>&1
        case $? in
            0)
                echo "dovecot reloaded"
                ;;
            *)
                echo "dovecot failed to reload"
                ;;
        esac
        ;;
    2)
        echo "TLS certificate ain't expiring anytime soon; skipping"
        ;;
    *)
        echo "TLS certificate failed to renew"
        ;;
esac
    

SMTP

Let's start with our /etc/mail/smtpd.conf (you might want to move the existing version out of the way first):

pki mail.[[DOMAIN]] cert \
  "/etc/ssl/mail.[[DOMAIN]]/fullchain.pem"
pki mail.[[DOMAIN]] key \
  "/etc/ssl/private/mail.[[DOMAIN]]/privkey.pem"

table aliases file:/etc/mail/aliases
table vusers file:/etc/mail/vusers
table vdomains file:/etc/mail/vdomains

filter check_dyndns phase connect \
  match rdns regex { '.*\.dyn\..*', '.*\.dsl\..*' } \
  junk
filter check_rdns phase connect \
  match !rdns \
  junk
filter check_fcrdns phase connect \
  match !fcrdns \
  junk
filter senderscore \
  proc-exec "filter-senderscore -junkBelow 70 -slowFactor 5000"
filter rspamd \
  proc-exec "filter-rspamd"
filter rspamd-outbound \
  proc-exec "filter-rspamd -settings-id outbound"
filter inbound-filters \
  chain { check_dyndns, check_rdns, check_fcrdns, senderscore, rspamd }
filter outbound-filters \
  chain { rspamd-outbound }

listen on all tls pki mail.[[DOMAIN]] filter inbound-filters
listen on all port submission tls-require pki mail.[[DOMAIN]] \
  auth filter outbound-filters

action "inbound" maildir junk virtual 
action "outbound" relay

match for local action "inbound"
match from any for domain  action "inbound"
match from any auth for any action "outbound"
match for any action "outbound"
    

We'll then need to setup our /etc/mail/vdomains with at least two entries — one for the domain and one for the server itself (if this server is handling mail for multiple domains, you'd include those here, too):

[[DOMAIN]]
mail.[[DOMAIN]]
    

Next, our /etc/mail/vusers (if this server is handling mail for multiple domains, you'll probably want to duplicate "Standard destinations" for each one):

# Yourself and other local users
[[USER]]@[[DOMAIN]] [[USER]]

# System stuff
root@[[DOMAIN]] [[USER]]
root@mail.[[DOMAIN]] [[USER]]

# Standard destinations
daemon@[[DOMAIN]] [[USER]]
ftp-bugs@[[DOMAIN]] [[USER]]
operator@[[DOMAIN]] [[USER]]
www@[[DOMAIN]] [[USER]]
postmaster@[[DOMAIN]] [[USER]]
MAILER-DAEMON@[[DOMAIN]] [[USER]]
webmaster@[[DOMAIN]] [[USER]]
abuse@[[DOMAIN]] [[USER]]
    

Then just run rcctl restart smtpd.

IMAP

Run pkg_add dovecot, and then add a dovecot class in /etc/login.conf to give it a needed resource bump:

dovecot:\
    :openfiles-cur=1024:\
    :openfiles-max=2048:\
    :tc=daemon:
    

We'll then need to tweak the following in /etc/dovecot/conf.d/10-ssl.conf:

ssl_cert = </etc/ssl/mail.[[DOMAIN]]/fullchain.pem
ssl_key = </etc/ssl/private/mail.[[DOMAIN]]/privkey.pem
    

And then we'll need to tweak the following in /etc/dovecot/conf.d/10-mail.conf:

mail_location = maildir:~/Maildir
    

And finally, a quick rcctl enable dovecot && rcctl start dovecot to get IMAP up and running.

Bonus: CalDAV

Ain't strictly necessary, but it'd sure be nice to have calendar syncing, right? First, as root (where [[USER]] is some username):

pkg_add kcaldav
rcctl enable slowcgi
rcctl start slowcgi
kcaldav.passwd -Cu [[USER]]  # repeat for each user
chown www /var/www/caldav/kcaldav.db
    

Next, we need to update our httpd.conf:

server "mail.[[DOMAIN]]" {
  listen on * port 80
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location * {
    block return 302 "https://$HTTP_HOST$REQUEST_URI"
  }
}

server "mail.[[DOMAIN]]" {
  listen on * tls port 443
  tls {
    certificate "/etc/ssl/mail.[[DOMAIN]]/fullchain.pem"
    key "/etc/ssl/private/mail.[[DOMAIN]]/privkey.pem"
  }
  location "/.well-known/acme-challenge/*" {
    root "/acme"
    request strip 2
  }
  location "/cgi-bin/*" {
    root "/"
    fastcgi
  }
  location "/.well-known/caldav" {
    block return 301 "https://mail.[[DOMAIN]]/cgi-bin/kcaldav"
  }
  location "/kcaldav/*" {
    root "/htdocs/kcaldav/"
    request strip 1
  }
  location * {
    root "/htdocs/mail.[[DOMAIN]]"
    directory auto index
  }
}
    

And finally, a quick rcctl reload httpd as root. Your mail server should now work as a calendar host, too (and should even provide a slick web interface via /kcaldav/home.html to let you manage your calendars).

Who?

I'm Ryan S. Northrup, a.k.a. "RyNo". I'm a programmer based out of Reno, NV, USA.