+++
date = '2024-11-24T16:29:01Z'
draft = false
title = 'How to Set up a Mail Server on Alpine Linux with Postfix and Dovecot'
tags = ['mail server', 'alpine linux', 'postfix', 'dovecot']
+++

# About mail servers

Simply put, a mail server sends and receives email. When `ruby@protonmail.com` emails `klaasje@gmail.com`, Protonmail's
mail server's <abbr title="Mail Transfer Agent">MTA</abbr> sends the email to Google's mail server's MTA. Google's
MTA then passes the email on to Google's <abbr title="Mail Delivery Agent">MDA</abbr>, which is responsible for storing
the email. MDAs also run <abbr title="Internet Message Access Protocol">IMAP</abbr> or <abbr
title="Post Office Protocol 3">POP3</abbr> servers so you can read your emails and send emails with an email client.
IMAP and POP3 are protocols for email retrieval over [TCP/IP](https://en.wikipedia.org/wiki/Internet_protocol_suite).
For sending email, you would use [<abbr
title="Simple Mail Transfer Protocol">SMTP</abbr>](https://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol).

Or visualise it like this:

    Ruby's email client via SMTP -> Protonmail's MTA -> Google's MTA -> Google's MDA -> Klaasje's email client via IMAP

A mail server is software which can be run on any computer, including yours. You can host a mail server on a server you
already own that is hosting other services, so long as those other services aren't using any of the [mail
ports](#unblock-your-ports).

## Why run my own mail server?

I'll cut to the chase: the main reason why you'd want to run your own mail server is for related reasons of privacy and
digital sovereignty. For privacy benefits, as much as you have control over your server, you can protect your email
from the eyes of prying server admins (given that you yourself are the admin). Even email providers that market
themselves around privacy (e.g. Protonmail) rely on trust that the provider is not reading your unencrypted incoming
email. This is not an issue exclusive to any particular mail provider; if information arrives unencrypted at a server,
those with access to the server (i.e. administrators) can read that information, simple as. And as nice as it would be
if everyone used GPG end-to-end encryption for email, the vast majority of emails people receive are not end-to-end
encrypted, and entirely legible to the mail servers involved. If you're not exchanging <abbr
title="End-to-End Encrypted">E2EE</abbr> email, you can't mitigate the fact that your exchange is entirely legible to
the mail server of the person you're corresponding with, but you can at least eliminate your anxieties about the mail
server you yourself are using.[^server_trust]

Running your own mail server also allows you to implement things your way, with the features you want. For instance,
[you can run a sieve filter for encrypting all incoming mail with a user's public GPG
key](https://www.grepular.com/Automatically_Encrypting_all_Incoming_Email); for obvious reasons, public (as in, open to
public sign-up) mail servers that implement sieve do not allow users to create their own executables for sieve filters.

Hosting your own mail server is not something I would universally recommend to people. While I'm very much against
"nothing to hide, nothing to fear", a combination of that factor alongside a low state threat model (i.e. there is
little state interest in you, domestic or foreign), a lack of relevant knowledge, and a lack of interest in managing
your own server/learning how to, likely make self-hosting email not a reasonable privacy suggestion.

When you host your own mail server, you are responsible for securing the server. If you entrust Google with your email,
you can at least know that your email is secure, though not private; Google will hire people with the relevant
knowledge and skills to secure a mail server. If you are not confident in your ability to do this and not interested in
learning, you may want to find another solution.

There are also existing solutions that aim to "simplify" the process of managing a mail server; there have always been
plenty of services offering mail server hosting which is managed for you, the customer, and there are also projects
like [Mailcow](https://mailcow.email/), which is an attempt to make a user-friendly distributable mail server stack
based on Docker. It has a web GUI you can use to configure your mail server. This may be an option for people who value
digital sovereignty but don't want to get their hands dirty with config files. I only mention this as something you may
want to look into; I have never used Mailcow and cannot recommend it personally.

# Why this tutorial?

There are many tutorials on the internet about how to set up a mail server. I don't claim that mine is particularly
better than anyone else's; I'm mostly writing this for my own reference so that I can recreate my own setup on new
machines.

There are existing
[mail server tutorials for Alpine Linux](https://wiki.alpinelinux.org/wiki/Hosting_services_on_Alpine#Mail), including
ones for Postfix and Dovecot. This tutorial aims to be "monolithic" (i.e. covers the entire mail server setup with all
its components), making it easier to follow without trying to follow different tutorials that may not be 100%
compatible with each other, and to be adapted to my own specific use-cases, which may not be yours.

I am hugely indebted to [LinuxBabe's mail server tutorial for
Ubuntu](https://www.linuxbabe.com/mail-server/setup-basic-postfix-mail-sever-ubuntu), which is actually what I followed
to set up my own server. Essentially, this tutorial could be thought of as an Alpine Linux adaptation of LinuxBabe's
tutorial. If you want to set up an Ubuntu or Debian mail server, I recommend following LinuxBabe's tutorial, which is
written very clearly and is easy to follow.

The mail server will be composed of the following software:

<!-- Using an html table for colspan & rowspan -->
<table>
    <tr>
        <th>Component</th>
        <th colspan="2">Software</th>
    </tr>
    <tr>
        <td>Mail transfer agent</td>
        <td colspan="2">Postfix</td>
    </tr>
    <tr>
        <td>Mail delivery agent</td>
        <td colspan="2">Dovecot</td>
    </tr>
    <tr>
        <td>SPF authentication</td>
        <td colspan="2">postfix-policyd-spf-perl</td>
    </tr>
    <tr>
        <td>DKIM authentication and signing</td>
        <td colspan="2">OpenDKIM</td>
    </tr>
    <tr>
        <td>DMARC authentication</td>
        <td colspan="2">OpenDMARC</td>
    </tr>
    <tr>
        <td>Spam filter</td>
        <td rowspan="2">Amavis</td>
        <td>SpamAssassin</td>
    </tr>
    <tr>
        <td>Antivirus</td>
        <td>ClamAV</td>
    </tr>
    <tr>
        <td>MDA server-side filtering</td>
        <td colspan="2">Pigeonhole</td>
    </tr>
</table>

Postfix will be our SMTP server, and Dovecot will run an IMAP server for us.

Postfix and Dovecot are required for the minimum of what you'd expect from a working mail server (i.e. a user can log
in through a standard SMTP/IMAP/POP3 email client, read their emails, and send emails). The rest is optional and
modular, i.e. you can opt to have e.g. Pigeonhole but not Amavis.

We will end up with a small-scale mail server running on Alpine Linux with one domain, and we will use Unix user
accounts as mail accounts. We will only set up IMAP, not POP3.

This tutorial was written for Alpine Linux 3.20.3, though will most likely work on other versions too.

This tutorial assumes no prior knowledge about mail servers.

# You will need

## A server

Any computer will do. This tutorial is for Alpine Linux specifically.

The relevant thing to consider for your server is that port 25 (the port for sending email) is not blocked. Most <abbr
title="Virtual Private Server">VPS</abbr> hosts block port 25 because spammers commonly use VPSes to send spam, so you
will need to find one that doesn't block port 25. Some VPS hosts block port 25 by default, but will unblock it upon
request, and re-block it if they find you are spamming.

## A domain name

Self-explanatory. You need a domain name and the ability to set its DNS records.

# Before you start

## Set up your DNS records

Firstly, pick a domain for your mail server. If you're sending emails from `domain.com`, `mail.domain.com` is a common
choice.

### MX record

An MX record denotes that your domain is used to send and receive email, and tells other MTAs the domain name of your
mail server. We will use `mail.domain.com` for your MX record. For instance, my MX record looks like:

```dns
revsuine.xyz.                    14400 IN MX    0 mail.revsuine.xyz
```

### Mail server records (A, AAAA, and/or CNAME)

Now you need to set a record stating the IP address of your mail server (`mail.domain.com` above). Depending on your
setup, you may want to create a CNAME record pointing to `domain.com` if the IP address of `mail.domain.com` is the
same as the IP address of `domain.com`, or an A record if the IP address is not shared with another domain.

I use a CNAME record because the IP addresses of `mail.revsuine.xyz` and `revsuine.xyz` are the same, so my record is:

```dns
mail.revsuine.xyz.               14400 IN CNAME revsuine.xyz
```

If you use an A record, your record may look something like

```dns
mail.domain.com.                    14400 IN A     ip.address.here
```

If you use IPv6, you should also add an AAAA record, e.g.:

```dns
mail.domain.com.                    14400 IN AAAA  ip:address:here::
```

### A note on my DNS records

I use one server with one IP address for hosting several services under one apex domain. For instance, my static
website is hosted at `revsuine.xyz`; my Nextcloud is hosted at `cloud.revsuine.xyz`; and my mail server is hosted at
`mail.revsuine.xyz`. To handle this setup, I've created a subdomain `master.revsuine.xyz` (you can call the subdomain
anything you want) with an *A record* (because your [PTR record](#ptr-record) is expected to be an A record, not a
CNAME record) to my server's IP address.

I set my server's hostname and PTR record to `master.revsuine.xyz`. I will also refer to this `master.revsuine.xyz`
subdomain further down when configuring Postfix.

### PTR record

A PTR record is used for "reverse DNS", or rDNS, lookup: instead of mapping a domain to an IP address, it maps an IP
address to a domain. It is not managed through your DNS manager (e.g. whomever you bought your domain name from), but
through whoever gives you your IP address. If you rent a server, be it a VPS or a dedicated server, your hosting
provider whom you rent from will be able to manage PTR records. You may have an option to add a PTR record in the
dashboard of your hosting provider, or you may have to contact their support to get a PTR record added.

For the above stated reasons, my PTR record for my single server (which hosts all my services) is `master.revsuine.xyz`.

If your hostname is `mail.domain.com`, your PTR record should be `mail.domain.com`.

## Unblock your ports

After unblocking ports from your internet provider (e.g. if your VPS host blocks outgoing port 25), make sure the
following TCP ports are open on your firewall:

| Port | Usage                             |
| ---- | --------------------------------- |
| 25   | SMTP                              |
| 143  | IMAP                              |
| 465  | Email message submission over TLS |
| 587  | Email message submission          |
| 993  | IMAPS (IMAP over TLS)             |
| 4190 | ManageSieve                       |

## Obtain a TLS certificate

To enable TLS encryption, you need a certificate. [Let's Encrypt](https://letsencrypt.org/) provides free TLS
certificates. To get a certificate from them, you can use certbot:

    # apk add certbot

We will need a web server to use certbot. I'm going to use nginx for this guide, because nginx is what I use on my
server, but [the certbot website](https://certbot.eff.org/) has instructions for a variety of setups. If you don't
already have an nginx server, install nginx and set it up now.

Install `certbot-nginx` with:

    # apk add certbot-nginx

Add the following to your nginx config (for instance, inside `http {}` in `/etc/nginx/nginx.conf`, or in a dedicated
virtual host file `/etc/nginx/http.d/mail.domain.com.conf`):

```nginx
server {
    listen 80;
    listen [::]:80;
    server_name mail.domain.com;

    root /usr/share/nginx/html/;

    location ~ /.well-known/acme-challenge {
        allow all;
    }
}
```

Replace `mail.domain.com` with the <abbr title="Fully-Qualified Domain Name">FQDN</abbr> of your mail server.

The `root` can be set to any extant directory on your system that you're happy to publish to the web. You can just make
an empty directory at `/usr/share/nginx/html`, or make this the directory of your website, etc.

Reload or restart nginx for the changes to take effect:

    # rc-service nginx reload

Now run the following command to get your free TLS certificate:

    # certbot certonly -a nginx --staple-ocsp --email your@email.here -d mail.domain.com

If you have several subdomains in your nginx config that you'd like covered by the same certificate, you can omit `-d
mail.domain.com` and get a certificate covering all the domains in your nginx config. On my server, I have one
certificate at `/etc/letsencrypt/live/revsuine.xyz/` covering my apex domain and all subdomains. If you go for a
certificate with only one domain name, e.g. for `mail.domain.com`, it will be at
`/etc/letsencrypt/live/mail.domain.com/`.

# Postfix

Postfix is a [mail transport agent](https://en.wikipedia.org/wiki/Message_transfer_agent) (aka SMTP server). [In its
own words](https://www.postfix.org/):

> Postfix attempts to be fast, easy to administer, and secure. The outside has a definite Sendmail-ish flavor, but the
> inside is completely different.

## Installing Postfix

On your server, install Postfix with:

    # apk add postfix

You likely also want to have Postfix documentation:

    # apk add postfix-doc

Verify that Postfix is installed by checking its version:

    $ postconf mail_version

## Configuring Postfix

Edit `/etc/postfix/main.cf`.

You should set `myhostname` to the hostname of your server; [in my case, this is
`master.revsuine.xyz`](#a-note-on-my-dns-records).

Now set `mydomain` to the domain you intend to send email from. For instance, my email addresses are
`name@revsuine.xyz`, so `mydomain` is set to `revsuine.xyz`.

`myorigin` determines the domain name in the `From:` field of locally sent emails. So you could for instance set this
to `revsuine.xyz`.

Set `mydestination` to the following:

```conf
mydestination = $myhostname, localhost.$mydomain, localhost, $mydomain
```

`mydestination` states the list of domains your machine will consider itself the destination for, e.g. if `mydomain` is
set to `revsuine.xyz` then any emails sent to `username@revsuine.xyz` will be sent to my server according to the above
configuration.

`maillog_file` denotes where Postfix's log file is. By default this is `/var/log/messages`; you may want to configure
Postfix to have a dedicated log file like `/var/log/postfix.log`.

You probably want to have `logrotate` rotate your Postfix log. If there isn't already such a file, you want to create
one at `/etc/logrotate.d/postfix`:

```logrotate
/var/log/postfix*.log
/var/log/mail*.log
{
    daily
    missingok
    notifempty
    rotate 7
}
```

Add the following TLS settings, replacing `your.domain.com` with your mail server's FQDN, [or otherwise where the TLS
certificate we generated would be](#obtain-a-tls-certificate):

```conf
# Enable TLS encryption when Postfix receives incoming emails
smtpd_tls_cert_file = /etc/letsencrypt/live/your.domain.com/fullchain.pem
smtpd_tls_key_file = /etc/letsencrypt/live/your.domain.com/privkey.pem
smtpd_tls_security_level = may
smtpd_tls_loglevel = 1
smtpd_tls_session_cache_database = lmdb:${data_directory}/smtpd_scache

# Enable TLS encryption when Postfix sends outgoing emails
smtp_tls_security_level = may
smtp_tls_loglevel = 1
smtp_tls_session_cache_database = lmdb:${data_directory}/smtp_scache

# Enforce TLSv1.3 or TLSv1.2
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1

# only offer authentication after STARTTLS
smtpd_tls_auth_only = yes

# disable SSL compression
tls_ssl_options = NO_COMPRESSION

# Configure the allowed cipher list
smtpd_tls_mandatory_ciphers = high
smtp_tls_mandatory_ciphers = high
smtpd_tls_ciphers = high
smtpd_tls_mandatory_ciphers = high
tls_high_cipherlist = ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
tls_preempt_cipherlist = yes
```

The allowed cipher list is from [Mailcow](https://docs.mailcow.email/manual-guides/Postfix/u_e-postfix-harden_ciphers/).

If you're using this as a personal mail server, you may not want to have a mailbox size limit, so you can set:

```conf
mailbox_size_limit = 0
```

By default, `mailbox_size_limit` is `51200000`. This number is in bytes. You can similarly set a `message_size_limit`.

Let's hide some sensitive info that tends to get attached to email headers by clients. Edit
`/etc/postfix/header_checks`, or create it if it doesn't already exist, and add the following lines:

```plaintext
/^Received: .*/            IGNORE
/^User-Agent: .*/          IGNORE
/^X-Originating-IP: .*/    IGNORE
/^X-Mailer: .*/            IGNORE
/^Mime-Version: .*/        IGNORE
```

Now add the following to your `/etc/postfix/main.cf`:

```conf
header_checks = regexp:/etc/postfix/header_checks
smtp_header_checks = $header_checks
mime_header_checks = $header_checks
```

Finally, here are some various hardening settings you can add to your `/etc/postfix/main.conf`:

```conf
# connections rate limit: no of connections allowed per unit
# `postconf anvil_rate_time_unit` will give the time unit; by default it's
# 60 seconds, so 600/60=10 connections allowed per second
smtpd_client_connection_rate_limit = 600
# messages rate limit, again over same time limit
smtpd_client_message_rate_limit = 60
# VRFY command used to check if an email address exists
# not needed and can be used to find spam recipients
disable_vrfy_command = yes
# servers that don't use HELO or EHLO are either not properly configured
# or sending spam usually
smtpd_helo_required = yes
smtpd_delay_reject = yes
smtpd_helo_restrictions =
    permit_mynetworks,
    reject_invalid_helo_hostname,
    reject_unknown_helo_hostname,
    permit
```

## Send your first email

Have the `postfix` service auto-start upon boot, and start it during this session:

    # rc-update add postfix default
    # rc-service postfix start

You can now send an email with the following command:

    $ echo "test email" | sendmail user@externaldomain.com

Send this email to your email account with an external server, e.g. a Gmail account. Note that Protonmail has quite
stringent spam filters and this likely would be rejected by Protonmail, i.e. not even reach your spam folder.

## Configure email aliases

You can configure aliases for your mail server. Edit the `/etc/postfix/aliases`[^postfix_aliases_location] file.

You shouldn't receive mail as root, so configure `root` to have an alias to your user, e.g.

```
root: revsuine
```

You also *must* have a `MAILER-DAEMON` and `postmaster` alias present:

```
MAILER-DAEMON: postmaster
postmaster:    root
```

Note how you can have referential aliases; mail to `postmaster` is aliased to `root`, which is aliased to `revsuine`,
so ultimately `revsuine` will get `postmaster`'s mail.

You can continue to populate the aliases file with whatever aliases you want.

## Enable Postfix submission and smtps service

To send emails from email clients, you'll need to enable Postfix's submission service so that Postfix can receive
emails to send via SMTP. Edit `/etc/postfix/master.cf` and ensure that the following lines are present:

```conf
submission inet n       -       n       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtp_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth

smtps     inet  n       -       n       -       -       smtpd
  -o syslog_name=postfix/smtps
  -o smtpd_tls_wrappermode=yes
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_relay_restrictions=permit_sasl_authenticated,reject
  -o smtpd_recipient_restrictions=permit_mynetworks,permit_sasl_authenticated,reject
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/auth
```

You may find them commented out, or partially present without some options, so make sure they are uncommented and
complete as per above.

Restart Postfix.

    # rc-service postfix restart

# Dovecot

[Dovecot](https://www.dovecot.org/) is a popular IMAP and POP3 server which we'll be using for our MDA. Let's install
it:

    # apk add dovecot

Check the Dovecot version with:

    $ dovecot --version

Now let's enable IMAP by editing `/etc/dovecot/dovecot.conf`. Find a `protocols = ` line, or add one, and set it to:

```conf
protocols = imap
```

## Logging

By default, Dovecot logs to the syslog (`/var/log/messages`). It would be easier to monitor Dovecot if it logged to its
own log file, so we'll configure that at `/etc/dovecot/conf.d/10-logging.conf`:

```conf
log_path = /var/log/dovecot.log
```

The file also has various settings on logging verbosity you can configure. I set:

```conf
auth_verbose = yes
auth_debug = yes
mail_debug = yes
```

You should already have an `/etc/logrotate.d/dovecot` file. If not, create it with the following contents:

```logrotate
/var/log/dovecot*.log {
    daily
    missingok
    copytruncate
    rotate 7
    compress
    notifempty
    sharedscripts
    postrotate
        /etc/init.d/dovecot --quiet --ifstarted reopen
    endscript
}
```

## Configure IMAP and email storage

You probably want to use the Maildir format for storing emails, where each user's mail is stored at `~/Maildir` (this
can be set to another location if desired).

In `/etc/dovecot/conf.d/10-mail.conf`, set:

```conf
mail_location = maildir:~/Maildir
mail_privileged_group = mail
```

`mail_privileged_group` tells us which group of Unix users can send mail; in this case, it's anyone in the `mail`
group. You can create the group with:

    # addgroup mail
    # adduser postfix mail
    # adduser dovecot mail

We want to ensure that `postfix` and `dovecot` users have the right to access mail.

To change the Maildir directory, e.g. to set it to `~/mail`, you would set the following:

`/etc/dovecot/conf.d/10-mail.conf`:

```conf
mail_location = maildir:~/mail
```

`/etc/postfix/main.cf`:

```conf
home_mailbox = mail/
```

## Get emails with LMTP

<abbr title="Local Mail Transfer Protocol">LMTP</abbr> is a protocol which can be used for Postfix to pass incoming
emails to Dovecot. To install it for Dovecot:

    # apk add dovecot-lmtpd

Add `lmtp` to the supported protocols in `/etc/dovecot/dovecot.conf`:

```conf
protocols = imap lmtp
```

Now change the LMTP service (or add if it isn't already there) in `/etc/dovecot/conf.d/10-master.conf` to:

```conf
service lmtp {
  unix_listener /var/spool/postfix/private/dovecot-lmtp {
    mode = 0600
    user = postfix
    group = postfix
  }
```

Postfix needs to be configured to use this socket. Edit `/etc/postfix/main.cf` with the following lines:

```conf
mailbox_transport = lmtp:unix:private/dovecot-lmtp
smtputf8_enable = no
```

## Configuring authentication

Edit `/etc/dovecot/conf.d/10-auth.conf` and uncomment the following line:

```conf
disable_plaintext_auth = yes
```

This disables plaintext authentication *unless* SSL/TLS is used.

In the same file, configure `auth_username_format`. As the variable name suggests, this denotes the format the server
expects usernames in for authentication. Setting it to `%n` removes the domain, so to sign in to `user@domain.com`
you'd enter your username as `user`. For this setup, you should set `auth_username_format` to `%n`, because we are
using Unix user accounts for email accounts; Dovecot wouldn't be able to find `user@domain.com` because the mailbox
user is just `user`.

In the same file again, `auth_mechanisms` is a space-separated list of authentication mechanisms your server uses. Set
this to

```conf
auth_mechanisms = plain login
```

`login` is mostly to support older email clients, but is optional.

Edit `/etc/dovecot/conf.d/10-master.conf`, and change `service auth` to the following:

```conf
service auth {
    unix_listener /var/spool/postfix/private/auth {
        mode = 0660
        user = postfix
        group = postfix
    }
}
```

Now we need to set the actual mechanism through which the server knows what password to expect, and what users exist.
We will use a file at `/etc/dovecot/passwd` to manage this. Edit `/etc/dovecot/conf.d/auth-passwdfile.conf.ext` to be
the following:

```conf
passdb {
    driver = passwd-file
    args = scheme=argon2id username_format=%n /etc/dovecot/passwd
}

userdb {
    driver = passwd
}
```

See [this documentation](https://doc.dovecot.org/main/core/config/auth/schemes.html) to decide on a password scheme to
use. I picked `argon2id` as the most secure option, however also the most expensive option, so it may be a poor option
if you have many users. Dovecot recommends that, if using ARGON2ID, you set `vsz_limit = 2G` for the `auth` service. To
do that with our setup, edit `/etc/dovecot/conf.d/10-master.conf` and add the line

```conf
vsz_limit = 2G
```

to the `service auth {}` section.

Now we want to include this file. Edit `/etc/dovecot/10-auth.conf` and ensure it includes this line, and that other
`!include` lines for other `auth-*.conf.ext` files are commented out:

```conf
!include auth-passwdfile.conf.ext
```

## Creating a user

Now let's create a user. We will be using a Unix user account, so create one with `adduser` if you don't already have
one. Their Unix username will be their email username, and what appears before the `@` in their email address. They
should also be in the `mail` group. So for instance:

    # adduser revsuine
    # adduser revsuine mail

We will not, however, be using their Unix password for authentication. Instead, we'll set them a password dedicated for
their email account. Create or edit the `/etc/dovecot/passwd` text file. Have a look at the following example:

```passwd
revsuine:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$H1oyL7UdwUWiBuZGnyXorQ$3aW/cfyNdrjoHw3OK7HlOzwgKqdg61prln8QMtWJijg
muffalo:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$KLnLOiqlhbhOPLhmTUqllA$Raki8Rw/+eOgJzDSEXxtw0mqI+aYLyFf+gpi+MQTdfo
thrumbo:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$8R4rVQ2hlKmiZ0Zmyzwg0g$tkesrePjJfLAEKu1wyJY1tu6V+fR5+C6/etyKq6WJlQ
```

This lists three users, `revsuine`, `muffalo`, and `thrumbo` (those are their usernames). These will all be system
users in the `mail` group. After the colon is the ARGON2ID hash of their password.

If you're storing passwords with ARGON2ID, to get what goes after the colon in your passwd file, run this command:

    # doveadm pw -s argon2id

You will be prompted to enter a password, and then it will output exactly what to put after the colon, such as
`{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$H1oyL7UdwUWiBuZGnyXorQ$3aW/cfyNdrjoHw3OK7HlOzwgKqdg61prln8QMtWJijg`. Each
user is their own line of the file.

Set up your `/etc/dovecot/passwd` file accordingly, making sure each user listed is also a system user, because we are
storing their mail in their home directories.

## Use your TLS certificate

Edit `/etc/dovecot/conf.d/10-ssl.conf` and set the following options (some should already be present, so you should
just change their values):

```conf
ssl = required
ssl_cert = </etc/letsencrypt/live/mail.domain.com/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.domain.com/privkey.pem
ssl_prefer_server_ciphers = yes
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
```

Replace `mail.domain.com` with the domain you obtained your TLS certificate for. You can check

    $ ls /etc/letsencrypt/live

if you're not sure.

The cipher list is from [Mailcow](https://docs.mailcow.email/manual-guides/Dovecot/u_e-dovecot-harden_ciphers/).

OpenSSL 3.x has a FIPS provider which is incompatible with Dovecot, so it should be disabled. Edit
`/etc/ssl/openssl.conf` and make sure the following line is commented out:

```conf
# providers = provider_sect
```

## Configure "special" mailboxes

The file `/etc/dovecot/conf.d/15-mailboxes.conf` denotes "special" mailboxes, such as your spam folder. You can
configure these, including setting these to auto-create, in `/etc/dovecot/conf.d/15-mailboxes.conf`. I have mine set
to:

```conf
# NOTE: Assumes "namespace inbox" has been defined in 10-mail.conf.
namespace inbox {
  # These mailboxes are widely used and could perhaps be created automatically:
  mailbox Drafts {
    special_use = \Drafts
    auto = create
  }
  mailbox Junk {
    special_use = \Junk
  }
  mailbox Spam {
    special_use = \Junk
    auto = create
  }
  mailbox Trash {
    special_use = \Trash
    auto = create
  }

  # For \Sent mailboxes there are two widely used names. We'll mark both of
  # them as \Sent. User typically deletes one of them if duplicates are created.
  mailbox Sent {
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
    auto = create
  }

  mailbox Drafts {
    special_use = \Drafts
    auto = create
  }
}
```

You can set the `auto = create` option for any mailbox you want to be auto-created for users.

Now enable and start the Dovecot service. Keep an eye on terminal output, as if there's a configuration error, you'll
get an error message when restarting.

    # rc-update add dovecot default
    # rc-service dovecot start

## mail\_crypt

Dovecot has a [mail\_crypt plugin](https://doc.dovecot.org/main/core/plugins/mail_crypt.html) which implements
**transparent** encryption at rest for mail. By transparent, I mean "invisible" to email clients; you can use an IMAP
client with your server with no changes, and no difference in user experience. Mail is decrypted on the server and sent
over IMAP.

We will optionally set up global key mail\_crypt encryption. This does not provide protection against an attacker with
root access, or full disk access (which is basically root access), however it can protect against other processes
reading our mail since they can't read the private key.

If you want to implement it, declare usage of the `mail_crypt` plugin in `/etc/dovecot/dovecot.conf`:

```conf
mail_plugins = $mail_plugins mail_crypt
```

Now let's generate some elliptic curve keys for this.

See what curves are available:

    $ openssl ecparam -list_curves

If we pick `prime256v1` as our curve, then run:

    $ openssl ecparam -name prime256v1 -genkey | openssl pkey -out ecprivkey.pem

to generate the private key. To generate the public key:

    $ openssl pkey -in ecprivkey.pem -pubout -out ecpubkey.pem

Now move these keys to `/etc/dovecot/` and make sure they are owned by `dovecot`:

    # mv ecpubkey.pem /etc/dovecot
    # mv ecprivkey.pem /etc/dovecot
    # chown dovecot:dovecot ecpubkey.pem ecprivkey.pem

Give them the correct permissions:

    $ cd /etc/dovecot
    # chmod 644 ecpubkey.pem
    # chmod 600 ecprivkey.pem

Anyway, create and edit `/etc/dovecot/conf.d/90-mail_crypt.conf` and configure the plugin as follows:

```conf
plugin {
    mail_crypt_global_private_key = </etc/dovecot/ecprivkey.pem
    mail_crypt_global_public_key = </etc/dovecot/ecpubkey.pem
    mail_crypt_save_version = 2
}
```

Restart Dovecot for the changes to take effect:

    # rc-service dovecot restart

Note that this will not make any difference to reading unencrypted email that was previously stored in your mailbox.

# Use a local email client

You are now ready to try logging in on a local email client such as Thunderbird, Evolution, Geary, KMail, etc.

To log in:

Enter your display name, email address (this is `[Unix user]@[mydomain variable in Postfix config]`), and the password
you set for yourself in `/etc/dovecot/passwd`. Set the following settings:

<!-- html table so that the headers can be on the left & colspan -->

<table>
    <tr>
        <td></td>
        <th>Incoming</th>
        <th>Outgoing</th>
    </tr>
    <tr>
        <th>Server type</th>
        <td>IMAP</td>
        <td>SMTP</td>
    </tr>
    <tr>
        <th>Server</th>
        <td colspan="2"><code>mail.domain.com</code>, or whatever your MX record is set to</td>
    </tr>
    <tr>
        <th>Port</th>
        <td>993</td>
        <td>465</td>
    </tr>
    <tr>
        <th>Username</th>
        <td colspan="2">Your system username; this should be the contents of your email address before the
                        <code>@</code></td>
    </tr>
    <tr>
        <th>Encryption</th>
        <td colspan="2">SSL/TLS</td>
    </tr>
    <tr>
        <th>Authentication</th>
        <td>Password</td>
        <td>PLAIN (shows up as <code>Normal password</code> in Thunderbird)</td>
    </tr>
</table>

With our setup, we are also able to use STARTTLS on port 143 (incoming) and 587 (outgoing) too.

You should be able to send and receive emails as normal now.

# Set up SPF, DKIM, and DMARC

<abbr title="Sender Policy Framework">SPF</abbr>, <abbr title="DomainKeys Identified Mail">DKIM</abbr>, and <abbr
title="Domain-based Message Authentication, Reporting and Conformance">DMARC</abbr> records are TXT DNS records that
protect against people spoofing your domain. Having correctly configured SPF, DKIM, and DMARC records helps your mail
avoid getting caught by spam filters.

SPF records indicate which hosts or IP addresses are allowed to send email from your domain.

DKIM digitally signs emails with a private key to verify that they came from your server. If someone else without your
private key sends email from your domain, they wouldn't be able to produce a valid DKIM signature. DKIM public keys are
published in TXT DNS records, so that any other mail server can verify a signature of an email from your domain.

DMARC records state that your domain uses SPF and DKIM, and that emails sent from your domain should pass SPF and DKIM
checks.

## Sender Policy Framework

### Set up your DNS record

Add a TXT record for your root domain with the contents `v=spf1 mx ~all`, like:

```dns
revsuine.xyz.                    900   IN TXT   "v=spf1 mx ~all"
```

In your DNS manager, you would enter:

<!-- html table so that header can be column not row -->

<table>
    <tr>
        <th>Name</th>
        <td><code>@</code></td>
    </tr>
    <tr>
        <th>Type</th>
        <td><code>TXT</code></td>
    </tr>
    <tr>
        <th>TTL</th>
        <td>Default (mine is 900)</td>
    </tr>
    <tr>
        <th>TXT data</th>
        <td><code>v=spf1 mx ~all</code></td>
    </tr>
</table>

This is assuming you're sending emails from `domain.com` instead of, e.g. `sub.domain.com`. If your email addresses are
like `user@sub.domain.com`, you would use `sub` as the name in the DNS record instead of `@`, which denotes the apex
domain. The same applies for all the DNS records in this section.

Breaking down the TXT data:

<!-- html table bc the last cell is quite long in contents and markdown requires one cell to all be on one line -->

<table>
    <tr>
        <th>Part</th>
        <th>Explanation</th>
    </tr>
    <tr>
        <td><code>v=spf1</code></td>
        <td>This is an SPF record, and the SPF record version is SPF1.</td>
    </tr>
    <tr>
        <td><code>mx</code></td>
        <td>All hosts listed in MX records are allowed to send emails from this domain.</td>
    </tr>
    <tr>
        <td><code>~all</code></td>
        <td>
            Emails from your domain should only come from hosts specified by your SPF record. Can also be
            <code>+all</code>, <code>-all</code>, <code>?all</code>, but those are rarely used. <code>-all</code> means
            that emails not from SPF-approved hosts should be rejected; however, this option can cause valid emails to
            be rejected if the recipient has multiple SMTP servers, and an email is relayed from one of their SMTP
            servers to another, for instance if one SMTP server goes down and they have a backup server.
        </td>
    </tr>
</table>

### Get Postfix to validate SPF

We're going to use a Postfix SMTPd policy server called postfix-policyd-spf-perl to check SPF of incoming emails.
postfix-policyd-spf-perl is very simple and requires almost no configuration.

Install `postfix-policyd-spf-perl` and create a user, `policyd-spf` for it:

    # apk add postfix-policyd-spf-perl
    # adduser -S -s /sbin/nologin -h /dev/null -H policyd-spf

Explanation of `adduser` flags:

<table>
    <tr>
        <th>Option</th>
        <th>Explanation</th>
    </tr>
    <tr>
        <td><code>-S</code></td>
        <td>Create a system user</td>
    </tr>
    <tr>
        <td><code>-s /sbin/nologin</code></td>
        <td>Set shell to <code>/sbin/nologin</code> so the user doesn't have a shell</td>
    </tr>
    <tr>
        <td><code>-h /dev/null</code></td>
        <td>Set home directory to <code>/dev/null</code></td>
    </tr>
    <tr>
        <td><code>-H</code></td>
        <td>
            Don't create a home directory (if you try to create <code>/dev/null</code> and assign it to
            <code>policyd-spf</code> there will be all sorts of permissions issues)
        </td>
    </tr>
</table>

Now edit `/etc/postfix/master.cf` to tell Postfix to start up the postfix-policyd-spf-perl daemon:

```conf
policyd-spf   unix -     n       n       -       0       spawn
  user=policyd-spf argv=/usr/bin/postfix-policyd-spf-perl
```

Now get Postfix to use postfix-policyd-spf-perl in `/etc/postfix/main.cf` by adding the following lines:

```conf
smtpd_recipient_restrictions =
    permit_mynetworks,
    reject_unauth_destination,
    check_policy_service unix:private/policyd-spf
policyd-spf_time_limit = 3600
```

postfix-policyd-spf-perl is now set up, and you can test it by sending yourself an email from a mainstream email
provider (which ought to have an SPF record) and checking for the presence of this header:

```
Received-SPF: pass (protonmail.com: Sender is authorized to use 'revsuine@protonmail.com' in 'mfrom' identity (mechanism 'include:_spf.protonmail.ch' matched))
```

## DomainKeys Identified Mail

### Configure OpenDKIM

OpenDKIM is an open-source implementation of DKIM signing and authentication. You can install it with:

    # apk add opendkim opendkim-utils

You may also want `opendkim-doc` for documentation.

Enable the service:

    # rc-update add opendkim default
    # rc-service opendkim start

Add the `postfix` user to the `opendkim` group:

    # adduser postfix opendkim

Edit your OpenDKIM config file at `/etc/opendkim/opendkim.conf`. Ensure the following is present (`Canonicalization` is
likely already there, so just change and uncomment things, and add lines where necessary):

```conf
Canonicalization        relaxed/simple
Mode                    sv
SubDomains              no
AutoRestart             yes
AutoRestartRate         10/1M
Background              yes
DNSTimeout              5
SignatureAlgorithm      rsa-sha256
```

Add the following to the config file:

```conf
# OpenDKIM user
# Remember to add user postfix to group opendkim
UserID             opendkim

# Map domains in From addresses to keys used to sign messages
KeyTable           refile:/etc/opendkim/key.table
SigningTable       refile:/etc/opendkim/signing.table

# Hosts to ignore when verifying signatures
ExternalIgnoreList  /etc/opendkim/trusted.hosts

# A set of internal hosts whose mail should be signed
InternalHosts       /etc/opendkim/trusted.hosts
```

Create a directory for OpenDKIM's keys at `/etc/opendkim/keys/`, and ensure the directory is owned by `opendkim` and
that no one else has read or write access to the keys directory:

    # mkdir /etc/opendkim/keys
    # chown -R opendkim:opendkim /etc/opendkim
    # chmod go-rw /etc/opendkim/keys

Create `/etc/opendkim/signing.table` and set its contents to the following:

```
*@domain.com      default._domainkey.domain.com
*@*.domain.com    default._domainkey.domain.com
```

This tells OpenDKIM to use your domain's private key to sign any emails originating from `domain.com` and its
subdomains. Replace `domain.com` with your domain.

Now create `/etc/opendkim/key.table` and set its contents to:

```
default._domainkey.domain.com     domain.com:default:/etc/opendkim/keys/domain.com/default.private
```

Replacing `domain.com` with your domain. This tells OpenDKIM where your private key is stored.

Create `/etc/opendkim/trusted.hosts` and set its contents to

```
127.0.0.1
localhost

.domain.com
```

Similarly, replace `domain.com` with your domain.

### Generate your keys

Create a directory for your domain's keys at `/etc/opendkim/keys/domain.com`. Use `opendkim-genkey` to generate your
keys:

    # opendkim-genkey -b 2048 -d domain.com -D /etc/opendkim/keys/domain.com -s default -v

Replace `domain.com` with your domain.

Set the owner and permissions of the private key so that only `opendkim` has read and write access to it:

    # chown opendkim:opendkim /etc/opendkim/keys/domain.com/default.private
    # chmod 600 /etc/opendkim/keys/domain.com/default.private

### Publish your public key in your DNS records

Look at your public key:

    # cat /etc/opendkim/keys/domain.com/default.txt

You'll see something like this:

```
default._domainkey	IN	TXT	( "v=DKIM1; k=rsa; "
	 "p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7QJxihVsEGhsZBLMxGKCVecIhjINp/zMGH0ExHN5QelxRIKdAHpJ33cZUBEsL0H27/4E8BK9RZ4AOaDMHfIGJ9FybyyD+gHhvKFFHzyts0QpkwLV+3bApsMqSR4ZsSzt3pZvRhQrXaoUds+9CyQBuEGx16PcA2O04/vWlvWhwStJrZWWHDYl2PX3QHfFFSsJaTsiFMwnMECvg"
	 "a6TdBTcsbcHwfedkoeMv5RQFlKisqCEiJkUYbJaczDA88fcZm21eGW8HiCSwq3324ExfWSwPGVsXAn1Blq/oKklpjXQIwwrphglK//G6AF7M3AAfqBdCvUbkAjs28fJwGqiVxaCwIDAQAB" )  ; ----- DKIM key default for revsuine.xyz
```

This is what your DNS record should look like. Copy **everything between the brackets** and add it to your DNS record:

<!-- using html table for above reasons of line breaks... -->

<table>
    <tr>
        <th>Name</th>
        <td><code>default._domainkey</code></td>
    </tr>
    <tr>
        <th>Type</th>
        <td><code>TXT</code></td>
    </tr>
    <tr>
        <th>TTL</th>
        <td>Default value (mine is 900)</td>
    </tr>
    <tr>
        <th>TXT data</th>
        <td>
            What you copied from between the brackets, <em>with all double quotes and extra whitespace (beyond a single
            space character) removed</em>
        </td>
    </tr>
</table>

So with the above public key, my DNS record is

```dns
default._domainkey.revsuine.xyz. 900   IN TXT   (
                                                  "v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQ"
                                                  "EFAAOCAQ8AMIIBCgKCAQEAu7QJxihVsEGhsZBLMx"
                                                  "GKCVecIhjINp/zMGH0ExHN5QelxRIKdAHpJ33cZU"
                                                  "BEsL0H27/4E8BK9RZ4AOaDMHfIGJ9FybyyD+gHhv"
                                                  "KFFHzyts0QpkwLV+3bApsMqSR4ZsSzt3pZvRhQrX"
                                                  "aoUds+9CyQBuEGx16PcA2O04/vWlvWhwStJrZWWH"
                                                  "DYl2PX3QHfFFSsJaTsiFMwnMECvg a6TdBTcsbcH"
                                                  "wfedkoeMv5RQFlKisqCEiJkUYbJaczDA88fcZm21"
                                                  "eGW8HiCSwq3324ExfWSwPGVsXAn1Blq/oKklpjXQ"
                                                  "IwwrphglK//G6AF7M3AAfqBdCvUbkAjs28fJwGqi"
                                                  "VxaCwIDAQAB"
                                                )
```

and what I entered into the data field of my DNS manager when adding the record was

```
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu7QJxihVsEGhsZBLMxGKCVecIhjINp/zMGH0ExHN5QelxRIKdAHpJ33cZUBEsL0H27/4E8BK9RZ4AOaDMHfIGJ9FybyyD+gHhvKFFHzyts0QpkwLV+3bApsMqSR4ZsSzt3pZvRhQrXaoUds+9CyQBuEGx16PcA2O04/vWlvWhwStJrZWWHDYl2PX3QHfFFSsJaTsiFMwnMECvg a6TdBTcsbcHwfedkoeMv5RQFlKisqCEiJkUYbJaczDA88fcZm21eGW8HiCSwq3324ExfWSwPGVsXAn1Blq/oKklpjXQIwwrphglK//G6AF7M3AAfqBdCvUbkAjs28fJwGqiVxaCwIDAQAB
```

You can now test your DKIM key with:

    # opendkim-testkey -d domain.com -s default -vvv

Don't worry about

```
opendkim-testkey: key not secure
```

This just means that DNSSEC isn't enabled for your domain name.

### Use OpenDKIM with Postfix

You can use a Unix socket for this, but I couldn't get the Unix socket to work, so I'll show you how to connect Postfix
to OpenDKIM using a TCP/IP socket on port 8891 instead.

Edit your OpenDKIM config `/etc/opendkim/opendkim.conf`, and ensure that the following line is present:

```conf
Socket                  inet:8891@localhost
```

Also make sure there are no other uncommented `Socket` lines.

Now configure Postfix at `/etc/postfix/main.cf`, and add the following lines:

```conf
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:127.0.0.1:8891
non_smtpd_milters = $smtpd_milters
```

This uses the Milter extension, which is something that can be used to process mail; in this case, to add headers to
emails relating to DKIM.

You can, again, test this on both incoming and outgoing mail. On outgoing mail, there should be a `DKIM-Signature:`
header present. On incoming mail from domains implementing DKIM, there should be a
`Authentication-Results: master.revsuine.xyz;` header (obviously replacing `master.revsuine.xyz` with your hostname)
indicating whether or not the email has passed DKIM authentication.

## Domain-based Message Authentication, Reporting, and Conformance

### Ensure your domains are aligned in email headers

Send a test email from your domain and look at the email headers of the sent email.

```plaintext {linenos=false,hl_lines=[1,11,23]}
Return-Path: <pid1@revsuine.xyz>
Received: from master.revsuine.xyz (master.revsuine.xyz. [93.113.25.226])
        by mx.google.com with ESMTPS id ffacd0b85a97d-3825fb5a132si1538595f8f.66.2024.11.22.08.53.01
        for <revsuine@gmail.com>
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Fri, 22 Nov 2024 08:53:01 -0800 (PST)
Received-SPF: pass (google.com: domain of pid1@revsuine.xyz designates 93.113.25.226 as permitted sender) client-ip=93.113.25.226;
Authentication-Results: mx.google.com;
       dkim=pass header.i=@revsuine.xyz header.s=default header.b=HRewRpbu;
       spf=pass (google.com: domain of pid1@revsuine.xyz designates 93.113.25.226 as permitted sender) smtp.mailfrom=pid1@revsuine.xyz;
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=revsuine.xyz;
        s=default; t=1732294380;
        bh=Q6d5/50T+3BnL4k+i/ARiher4CIHkBrfcsHufAU/i4E=;
        h=Subject:From:To:Date;
        b=HRewRpbuXhaI4vf8ZXc0PzepcNAd5mGDvWh38mAF+JYvefM7pF+q0dNYrA0bCrCys
         a6qev9C++7hJC7oLYKgcXoewHPiGQzfgjj5oIVuIE10CSuSNyk4vd4Nf37P8k1A8lO
         jy+g+kv7VbX5N+ElzQbqXL1PTjgd9seDYoPn5FTTWGfVbLhh6gWfU3xeb88UZQCFJ+
         kgWMFOUEjoTffzqNBe/NICvcoX3ZJvr+vCdZVANHEV1AhVExDB9FM/0TRFnybhTVo0
         eGDCEtvkxgNQYVFVnJUxWZ3MCt+cDVuYVsHw0SXQMToiNSPlsn65iV2+/giw7DmSCo
         fgOvYG+eHu10w==
Message-ID: <47971a581909651b6e089287b5b408fd56352b82.camel@revsuine.xyz>
Subject: Test
From: revsuine <pid1@revsuine.xyz>
```

Ensure that the domains in `Return-Path`, `DKIM-Signature`, and `From:` are the same. If they are, they are strictly
aligned. Or if the `Return-Path` or `DKIM-Signature`'s `d=` parameter use a subdomain instead of the main domain name
in the `From:` field, they have relaxed alignment. Either way, they are aligned.

By default, DMARC uses relaxed alignment. Your domains have to be aligned in order to pass DMARC checks.

### Create a DMARC DNS record

Create a TXT record with `_dmarc` as the record name, and

```
v=DMARC1; p=none; pct=100; fo=1; rua=mailto:dmarc@domain.com
```

as the TXT data, replacing `dmarc@domain.com` with an email address you want mail servers to send their DMARC reports
to.

#### Policies

`p=none` indicates the policy for our DMARC record. The available policies are:

<!-- yk the drill, html table for line breaks -->

<table>
    <tr>
        <th>Policy</th>
        <th>Meaning</th>
    </tr>
    <tr>
        <td><code>none</code></td>
        <td>Don't do anything if the DMARC check fails</td>
    </tr>
    <tr>
        <td><code>quarantine</code></td>
        <td>
            Quarantine emails if DMARC check fails; an admin must approve the email before it reaches the recipient's
            inbox
        </td>
    </tr>
    <tr>
        <td><code>reject</code></td>
        <td>Reject the email if DMARC check fails</td>
    </tr>
</table>

It's recommended to start with `none` or `quarantine` until you're sure that your emails are passing DMARC. `none` is
more appropriate for domains that have already sent some emails; if you've never sent email from your domain before,
try `quarantine` with `pct=30` (see below) as a start.

If your domain is not going to send emails, set `p=reject`.

#### Percentage

`pct=100` means that a mail server should expect 100% of incoming mail from your domain to pass DMARC checks. As you
increase the strictness of your policy, you should also decrease `pct` until you are sure that your email is passing
checks.

[LinuxBabe](https://www.linuxbabe.com/mail-server/create-dmarc-record) recommends the following "progression", moving
to the next row in the table as you've tested the previous row and haven't seen legitimate mail rejected:

| Policy       | Percentage |
| ------------ | ---------- |
| `none`       | 100        |
| `quarantine` | 30         |
| `quarantine` | 70         |
| `quarantine` | 100        |
| `reject`     | 30         |
| `reject`     | 70         |
| `reject`     | 100        |

#### Email reports

You set an email address for DMARC reports to be sent to with `rua=mailto:dmarc@domain.com`. This means that when a
mail server receives email from your domain, if it runs a DMARC check, it will send the results to that email.

The `fo` tag indicates when you would like to receive reports. The options are:

<table>
    <tr>
        <th>Option</th>
        <th>Meaning</th>
    </tr>
    <tr>
        <td><code>0</code></td>
        <td>Generate reports if <strong>all</strong> authentication mechanisms fail</td>
    </tr>
    <tr>
        <td><code>1</code></td>
        <td>Generate reports if <strong>any</strong> authentication mechanisms fail</td>
    </tr>
    <tr>
        <td><code>d</code></td>
        <td>Generate reports if DKIM authentication fails</td>
    </tr>
    <tr>
        <td><code>s</code></td>
        <td>Generate reports if SPF authentication fails</td>
    </tr>
</table>

### OpenDMARC[^spf_processing_redundancy] {#opendmarc}

We can use software called OpenDMARC to enforce DMARC policies for incoming mail. OpenDMARC is another milter. Let's
install it and enable its service:

    # apk add opendmarc
    # rc-update add opendmarc
    # rc-service opendmarc start

Edit the OpenDMARC config at `/etc/opendmarc/opendmarc.conf`.

Change

```conf
AuthservID HOSTNAME
```

to

```conf
AuthservID OpenDMARC
```

This is so that the `Authentication-Results` header from OpenDKIM authentication. This will also make it clear which
program adds which `Authentication-Results` header.

Add the following line, replacing `mail.domain.com` with your *hostname* (so in [my
instance](#a-note-on-my-dns-records), this is `master.revsuine.xyz`).

```conf
TrustedAuthservIDs mail.domain.com
```

This specifies that OpenDMARC should trust authentication results from `mail.domain.com`. Otherwise you would get the
following error message in your syslog:

    ignoring Authentication-Results at 1 from mail.domain.com

Enable `RejectFailures`, which means your server will comply with `p=reject` in DMARC DNS records.

```conf
RejectFailures true
```

You also probably want to enable `RequiredHeaders`, which rejects emails that don't conform to RFC5322 standards, e.g.
are missing a `From:` header.

```conf
RequiredHeaders true
```

In case external SPF validation fails (as in, no SPF results are placed in the message header), you probably want to
add

```conf
SPFSelfValidate true
```

which tells OpenDMARC to perform the SPF check itself if it can't find SPF results in the message header.

Now provide OpenDMARC with a socket to use for communication with sendmail. We will use a TCP socket on port 8893:

```conf
Socket inet:8893@localhost
```

For a Unix socket, you'd use the following format:

```conf
Socket local:/var/run/opendmarc/opendmarc.sock
```

By default, you will have the line

```conf
IgnoreHosts /etc/opendmarc/ignore.hosts
```

in `/etc/opendmarc/opendmarc.conf`. This tells OpenDMARC to not authenticate the list of hosts in
`/etc/opendmarc/ignore.hosts`. An example `ignore.hosts` is

    127.0.0.1
    93.113.25.226

Keep in mind that if you have specified `IgnoreHosts`, this file needs to exist in order for OpenDMARC to run. If you
have the option set, make sure to `touch /etc/opendmarc/ignore.hosts` (or whatever filepath you've specified).
Alternatively, comment out this option in order to use the default, which is to not authenticate mail coming from
127.0.0.1.

Restart OpenDMARC for these changes to take effect:

    # rc-service opendmarc restart

To have Postfix use the OpenDMARC milter, it's simple as adding the socket to the `smptd_milters` and
`non_smtpd_milters` variable in `/etc/postfix/main.cf`:

```conf
milter_default_action = accept
milter_protocol = 6
smtpd_milters = inet:127.0.0.1:8891,inet:127.0.0.1:8893
non_smtpd_milters = $smtpd_milters
```

Restart Postfix for the changes to take effect:

    # rc-service postfix restart

And when you receive emails from a legitimate source that implements DMARC, you should see the following headers in
your emails:

<pre id="double-spf-headers">
<code>Received-SPF: pass (protonmail.com: Sender is authorized to use 'revsuine@protonmail.com' in 'mfrom' identity (mechanism 'include:_spf.protonmail.ch' matched)) receiver=master.revsuine.xyz; identity=mailfrom; envelope-from="revsuine@protonmail.com"; helo=mail-40130.protonmail.ch; client-ip=185.70.40.130
DMARC-Filter: OpenDMARC Filter v1.4.2 master.revsuine.xyz 88CFF1288D1
Authentication-Results: OpenDMARC; dmarc=pass (p=quarantine dis=none) header.from=protonmail.com
Authentication-Results: OpenDMARC; spf=pass smtp.mailfrom=protonmail.com
Authentication-Results: master.revsuine.xyz;
        dkim=pass (2048-bit key; secure) header.d=protonmail.com header.i=@protonmail.com header.a=rsa-sha256 header.s=protonmail3 header.b=nc4YWVM/</code>
</pre>

<!--
    TODO: switch SPF filter to a milter e.g. https://www.acme.com/software/spfmilter/ so that SPF isn't checked twice
-->

### Test SPF, DKIM, and DMARC

You can use [mail-tester.com](https://www.mail-tester.com/) and send an email from your domain to check that SPF, DKIM,
and DMARC are all working for your server.

![Screenshot of mail-tester.com authentication results passing all checks](mail-tester_auth_results.png)

### Failure

There are a few things that can cause SPF, DKIM, and consequently DMARC to fail.

One common scenario is email relaying causing SPF to fail, because the recipient mail server does not receive the email
directly from your mail server.

#### Mailing Lists

"Announcement" type mailing lists can cause DKIM failure, because usually additional headers are added to your emails.
"Discussion" type mailing lists are more problematic; they can also cause SPF failures because emails are coming from
all different mail servers through the mailing list server, and the central mailing list server cannot sign other
domain's DKIM signatures.

For announcements, if you are using an external mailing list provider, just add their server to your SPF record. For
discussion mailing lists, resolving authentication issues is more complex. [GNU Mailman has a wiki article with some
suggestions.](https://wiki.list.org/DEV/DMARC)

# Amavis

Amavis is a high-performance interface between MTAs and content checkers, such as virus scanners and spam filters. We
will use Amavis as a Postfix content filter for virus scanning and spam filtering, with ClamAV and SpamAssassin
respectively.

Install Amavis and enable the service:

    # apk add amavis
    # rc-update add amavisd default
    # rc-service amavisd start

## Amavis and Postfix

Let's set up Amavis as an SMTP proxy. Edit your Postfix config at `/etc/postfix/main.cf`, and add the following to the
end of the file:

```conf
# amavis filtering
# gets overridden by submission & smtps services in master.cf:
content_filter = smtp-amavis:[127.0.0.1]:10024
# delays postfix connection to content filter until entire email message has arrived
smtpd_proxy_options = speed_adjust
```

Amavis listens on port 10024 by default, so this tells Postfix to use Amavis as a content filter.

Now edit `/etc/postfix/master.cf` and add the following lines:

```conf
smtp-amavis   unix -     -       n       -       2       smtp
  -o syslog_name=postfix/amavis
  -o smtp_data_done_timeout=1200
  -o smtp_send_xforward_command=yes
  -o smtp_dns_support_level=disabled
  -o max_use=20
  -o smtp_tls_security_level=none

127.0.0.1:10025   inet   n    -     n     -     -    smtpd
  -o syslog_name=postfix/10025
  -o content_filter=
  -o mynetworks_style=host
  -o mynetworks=127.0.0.0/8
  -o local_recipient_maps=
  -o relay_recipient_maps=
  -o strict_rfc821_envelopes=yes
  -o smtp_tls_security_level=none
  -o smtpd_tls_security_level=none
  -o smtpd_restriction_classes=
  -o smtpd_delay_reject=no
  -o smtpd_client_restrictions=permit_mynetworks,reject
  -o smtpd_helo_restrictions=
  -o smtpd_sender_restrictions=
  -o smtpd_recipient_restrictions=permit_mynetworks,reject
  -o smtpd_end_of_data_restrictions=
  -o smtpd_error_sleep_time=0
  -o smtpd_soft_error_limit=1001
  -o smtpd_hard_error_limit=1000
  -o smtpd_client_connection_count_limit=0
  -o smtpd_client_connection_rate_limit=0
  -o receive_override_options=no_header_body_checks,no_unknown_recipient_checks,no_address_mappings
  # avoid double dkim signing by setting smtpd_milters to empty
  # otherwise will run all milters again after amavis
  -o smtpd_milters=
```

The first block tells Postfix to send emails to Amavis, and the second block tells Postfix to run an extra smtpd daemon
on port 10025 to receive emails back from Amavis. Restart Postfix for the changes to take effect:

    # rc-service postfix restart

It's good practice to use a different port for email submissions from authenticated users. Let's use port 10026 for
this. Edit `/etc/amavisd.conf` and set `$inet_socket_port` to:

```conf
$inet_socket_port = [10024,10026];
```

to listen on multiple TCP ports.

We'll set the policy to `ORIGINATING` for port 10026 in the same Amavis config file:

```conf
$interface_policy{'10026'} = 'ORIGINATING';
```

Then define the `ORIGINATING` policy by adding the following lines:

```conf
$policy_bank{'ORIGINATING'} = {  # mail supposedly originating from our users
  originating => 1,  # declare that mail was submitted by our smtp client
  allow_disclaimers => 1,  # enables disclaimer insertion if available
  # notify administrator of locally originating malware
  virus_admin_maps => ["virusalert\@$mydomain"],
  spam_admin_maps  => ["virusalert\@$mydomain"],
  warnbadhsender   => 1,
  # force MTA conversion to 7-bit (e.g. before DKIM signing)
  smtpd_discard_ehlo_keywords => ['8BITMIME'],
  bypass_banned_checks_maps => [1],  # allow sending any file names and types
  terminate_dsn_on_notify_success => 0,  # don't remove NOTIFY=SUCCESS option
};
```

Note that this is configured to send virus alerts to `virusalert@domain.com`. This should be a real email address, not
an alias, because ClamAV bypasses Postfix and sends emails straight to Dovecot, which doesn't have access to Postfix
aliases.

Restart Amavis for the change to take effect:

    # rc-service amavisd restart

Edit `/etc/postfix/master.cf` now and add the following to the `submission` and `smtps` services:

```conf
  # emails from authenticated SMTP clients will be passed to Amavis listening
  # on port 10026:
  -o content_filter=smtp-amavis:[127.0.0.1]:10026
```

Restart Postfix again for the changes to take effect:

    # rc-service postfix restart

## Amavis and ClamAV

Install ClamAV and enable its daemon:

    # apk add clamav clamav-daemon
    # rc-update add clamd default
    # rc-service clamd start

Enable virus-checking in Amavis by editing `/etc/amavisd.conf` and setting `@bypass_virus_checks_maps` to the
following:

```conf
# enable virus checking
@bypass_virus_checks_maps = (
    \%bypass_virus_checks, \@bypass_virus_checks_acl, \$bypass_virus_checks_re);
```

Add `clamav` to the `amavis` group:

    # adduser clamav amavis

Restart the amavisd and clamd daemons:

    # rc-service amavisd restart
    # rc-service clamd restart

## Amavis and SpamAssassin

Install SpamAssassin:

    # apk add spamassassin

You may also want to install `spamassassin-doc`.

Let's configure SpamAssassin. SpamAssassin is configured at `/etc/mail/spamassassin/local.cf`. You may want to
configure the `required_score` option, which defaults to `5.0`. This is the spam score required for an email to be
marked spam. `5.0` is a sensible default, but you can adjust this if you find that your spam filter needs to be more or
less aggressive.

You can also set options like `rewrite_header` to rewrite headers of a message marked spam, e.g.

```conf
rewrite_header Subject [SPAM]
```

prepends `[SPAM]` to the subject line of a spam message.

Enable the service:

    # rc-update add spamd default
    # rc-service spamd start

Enable spam filtering by setting `@bypass_spam_checks_maps` in your `/etc/amavisd.conf`:

```conf
# enable spam filtering
@bypass_spam_checks_maps = (
    \%bypass_spam_checks, \@bypass_spam_checks_acl, $bypass_spam_checks_re);
```

Restart Amavis:

    # rc-service amavisd restart

Now let's test your spam filter. Send yourself an email containing the following string somewhere in the body:

```
XJS*C4JDBQADN1.NSBN3*2IDNEN*GTUBE-STANDARD-ANTI-UBE-TEST-EMAIL*C.34X
```

You should see the email arrive with the following headers:

```
X-Spam-Flag: YES
X-Spam-Score: 999.802
X-Spam-Level: ****************************************************************
X-Spam-Status: Yes, score=999.802 tagged_above=2 required=6.2
 tests=[DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1,
 DKIM_VALID_EF=-0.1, GTUBE=1000, NO_RECEIVED=-0.001, NO_RELAYS=-0.001,
 TVD_SPACE_RATIO=0.001, URIBL_BLOCKED=0.001, URIBL_DBL_BLOCKED_OPENDNS=0.001,
 URIBL_ZEN_BLOCKED_OPENDNS=0.001] autolearn=no autolearn_force=no
```

# Pigeonhole

Dovecot can do server-side mail filtering with sieve scripts. These are user scripts that can perform actions on mail
based on particular criteria, e.g.

```sieve
require "fileinto";

if address :is "to" "postmaster@revsuine.xyz" {
    fileinto "Postmaster";
}
```

Places mail in the `Postmaster` folder if the `To:` field is `postmaster@revsuine.xyz`. You also can do things
unconditionally, like

```sieve
redirect postmaster@revsuine.xyz;
```

unconditionally redirects all mail to `postmaster@revsuine.xyz`.

Sieve scripts can be both per-user and system-wide.

For more examples, [this page](https://doc.dovecot.org/main/howto/sieve.html) has some good examples.

## Installing and setting up Pigeonhole

To use Sieve, install `dovecot-pigeonhole-plugin`:

    # apk add dovecot-pigeonhole-plugin

Then edit `/etc/dovecot/conf.d/20-lmtp.conf`, and add the `sieve` plugin like so:

```conf
protocol lmtp {
  # Space separated list of plugins to load (default is global mail_plugins).
  mail_plugins = $mail_plugins sieve
}
```

To configure Pigeonhole and sieve, edit `/etc/dovecot/conf.d/90-sieve.conf`. Sieve's options will be configured in the
`plugin {}` block in this file.

We can set the location of user sieve scripts with the `sieve` option.

```conf
sieve = file:~/sieve;active=~/.dovecot.sieve
```

means that `~/sieve` is a directory of sieve scripts, whilst `~/.dovecot.sieve` is a symlink to the "active" one, e.g.

```
sieve
├── script1.sieve
├── script2.sieve
└── script3.sieve
```

could be your `~/sieve/` directory, and to make `script2.sieve` active, you would do

    $ ln -s ~/sieve/script2.sieve ~/.dovecot.sieve

`sieve_before` defines a directory of sieve scripts which will be executed *prior* to any user scripts. e.g.

```conf
sieve_before = /etc/dovecot/sieve
```

means that the sieve scripts in `/etc/dovecot/sieve` will be executed first, then the user's personal scripts at
`~/.dovecot.sieve`.

You can specify multiple directories in order, like so:

```conf
sieve_before = /var/lib/dovecot/sieve.d/
sieve_before2 = ldap:/etc/sieve-ldap.conf;name=ldap-domain
sieve_before3 = /etc/dovecot/sieve
```

etc. The `sieve_after` option also exists, and works the same way.

This is not the same as `sieve_default`, which is *overridden* by user sieve scripts and only executes when a user has
no sieve script.

You can review [this documentation](https://doc.dovecot.org/main/core/plugins/sieve.html) for a full list of Pigeonhole
options.

## ManageSieve

Users can configure their own user sieve scripts using a protocol called ManageSieve. Like how IMAP allows users to
read their emails without having shell access to the mail server, ManageSieve allows users to write sieve scripts
without requiring shell access.

To enable ManageSieve, edit `/etc/dovecot/conf.d/20-managesieve.conf`. Make sure the following line is uncommented:

```conf
protocols = $protocols sieve
```

By default, ManageSieve will listen on port 4190.

## Sieve scripts for spam filtering

Let's use a system-wide sieve script to file SpamAssassin-marked spam into a Spam folder. Create an
`/etc/dovecot/sieve/` directory, and add it to your `sieve_before` settings:

```conf
plugin {
  ...
  sieve_before = /etc/dovecot/sieve/
  ...
}
```

Now create a new sieve script, `/etc/dovecot/sieve/spam_folder.sieve`:

```sieve
require ["fileinto", "mailbox"];
if header :contains "X-Spam-Flag" "YES" {
  fileinto :create "Spam";
}
```

Replace `"Spam"` with the name of your spam folder. If it's a subfolder, e.g.

```
Inbox
└── Spam
```

you would write `"Inbox.Spam"` in that case.

# Miscellaneous suggestions

To use fail2ban for your mail server, create `/etc/fail2ban/jail.d/mail.local`:

```ini
[postfix]
enabled  = true
logpath  = /var/log/postfix.log

[dovecot]
enabled  = true
logpath  = /var/log/dovecot.log

[sieve]
enabled  = true
logpath  = /var/log/dovecot.log
```

These are the log file locations we configured above, but you can set them to the syslog if you didn't give Postfix and
Dovecot their own log files.

You may want to get your domain whitelisted on [dnswl.org](https://www.dnswl.org/), an email whitelist service where
admins can submit their domain and IP address to indicate trustworthiness.

<!-- FOOTNOTES: -->

[^server_trust]: This is only true to the extent that your server is not compromised. You could say there's an order of
    server trust-ability that goes:

    ```
    VPS < rented dedicated server < server you yourself physically own, store, and manage
    ```

    There is little that can be done to secure a VM running on a compromised host. Even with full-disk encryption, the
    host can dump the encryption key from RAM, because the encryption key must be stored in memory whilst a
    full-disk-encrypted system is booted.

    For a dedicated server you rent, there are at least no concerns about a compromised host, but an attacker with
    physical access (in this case, the untrusted people you rent the dedicated server from) can attempt evil maid
    attacks. You are hopefully able to implement mechanisms to detect this, though.

    There are reasons you may want to go with a rented server instead of one you own, though. For instance, if you live
    in a jurisdiction known for terrible privacy laws such as a [5/14 eyes
    country](https://restoreprivacy.com/5-eyes-9-eyes-14-eyes/), or if you are a political dissident with domestic
    state interest in you, you likely want to go offshore for server hosting. Changing the jurisdiction can protect you
    if the jurisdiction you choose won't work with your national intelligence agencies.

[^postfix_aliases_location]: Your aliases file will most likely be in this location by default, but you can run

    ```
    $ postconf alias_maps
    ```

    to find out where this file should be.

[^spf_processing_redundancy]: The OpenDMARC milter will be run before the SPF policy daemon, meaning that OpenDMARC
    must do its own SPF checking. This makes postfix-policyd-spf-perl redundant, and results in multiple SPF checks in
    our email headers (e.g. see email headers [here](#double-spf-headers)).

    To get around this, you'd probably have to use a milter to validate SPF, and just list the milter before OpenDMARC.
    I had a bit of a look around for SPF milters and found [ACME's
    spfmilter](https://www.acme.com/software/spfmilter/), but had issues compiling it on Alpine; it complained about

    ```
    spfmilter.c:1623:2: error: #error "neither libspf nor libspf2 is present - please provide one"
     1623 | #error "neither libspf nor libspf2 is present - please provide one"
          |  ^~~~~
    ```

    despite having `libspf2` installed. Potentially this is a musl issue, but I'm doubtful that a project like this
    would be tied to glibc specifically.

    I tried compiling it on Artix Linux and it compiled just fine with the right dependencies installed, so the program
    does compile.

    If anyone can either [suggest](/contact) a way to get ACME's spfmilter to compile on Alpine, or an alternative
    solution to this problem, that would be helpful.

