new page on certbot. re-ordered pages

This commit is contained in:
Christoph Haas 2025-08-24 16:27:27 +02:00
parent a29d2627d6
commit 13b2d7bedd
5 changed files with 379 additions and 228 deletions

View file

@ -3,7 +3,7 @@ title: Installing Debian
lastUpdated: 2025-08-14
slug: ispmail-trixie/install-debian
sidebar:
order: 130
order: 120
---
import { Aside } from "@astrojs/starlight/components";

View file

@ -3,7 +3,7 @@ title: Install software packages
lastUpdated: 2025-08-14
slug: ispmail-trixie/install-the-software-packages
sidebar:
order: 140
order: 130
---
import { Aside } from "@astrojs/starlight/components";
@ -11,14 +11,14 @@ import { Aside } from "@astrojs/starlight/components";
Let's install the necessary Debian packages. Run this command as root on your server as _root_:
```sh
apt update
DEBIAN_FRONTEND=noninteractive \
apt -y install postfix-mysql dovecot-mysql \
dovecot-imapd dovecot-lmtpd dovecot-managesieved \
apache python3-certbot-apache libapache2-mod-php \
php-intl php-mbstring php-xml unzip \
apache2 python3-certbot-apache libapache2-mod-php \
php-intl php-mbstring php-xml unzip certbot \
roundcube-sqlite3 roundcube swaks ufw mutt \
crowdsec crowdsec-firewall-bouncer unattended-upgrades \
mariadb-server
unattended-upgrades mariadb-server
postfix chroot off
```
@ -48,16 +48,16 @@ While the server is downloading and installing the packages, let me give you a q
to Postfix to send emails.
- **swaks** \
The SWiss Army Knife of Smtp. Helpful command-line tool to send test emails to a mail server.
- **certbot** \
Gets a free TLS certificate from [_Let's Encrypt_](https://letsencrypt.org/) for encrypting network communication.
- **ufw** \
Universal FireWall. A simple tool to manage firewall rules to limit access to your server.
Universal FireWall. A simple tool to manage firewall rules to limit access to your server. TODO: omit?
- **rspamd** \
It reliably detects spam. Also handles adding DKIM signature to outgoing email to prevent spoofing your domains.
It reliably detects spam. Also handles adding DKIM signature to outgoing email to prevent spoofing your domains. TODO:
later/optional
- **mutt** \
A console-based program that can speak IMAP and also read Maildirs directly. Very helpful for testing the
functionality of your mail server. Think of it as a text-based Thunderbird.
- **crowdsec** \
Detects attacks to your mail server and blocks the IP address of the attacker. This is a more sophisticated version of
fail2ban.
- **unattended-upgrades** \
Installs security updates automatically.
@ -65,8 +65,6 @@ While the server is downloading and installing the packages, let me give you a q
By default, Debian enables Postfix to run in chroot mode. While this may sound like a security improvement, even
Postfixs author advises against it, arguing that it mostly creates a false sense of security. For this reason, we are
disabling chroot mode—which also simplifies some configuration details for us later on.
disabling chroot mode—which also simplifies some configuration later on.
</Aside>
TODO: move crowdsec into optional chapter

View file

@ -0,0 +1,114 @@
---
title: TLS certificate
lastUpdated: 2025-08-24
slug: ispmail-trixie/tls-certificate
sidebar:
order: 140
---
Your mail server can't work properly without a valid TLS certificate. It will be used in three places:
1. the **webmail** interface (driven by the Apache web server)
2. **Postfix** (to encrypt SMTP communication with other mail servers on the internet where possible)
3. **Dovecot** (to encrypt IMAP/POP3 communication)
Your mail server will be able to host email addresses for many domains. But a TLS certificate is usually issued for a
single [FQDN](https://en.wikipedia.org/wiki/Fully_qualified_domain_name) like “mail.example.org”. That _common name_ is
an important part of the certificate. It must match the name you use to talk to the server.
**Correct**: Your mail server is addressed as `mail.example.org`. The TLS certificate was also issued to
`mail.example.org`.
**Wrong**: Your mail server is addressed as `smtp01.example.com`. But the TLS certificate was issued to
`mail.example.org`.
So choose an FQDN for your mail server. For example I use `webmail.workaround.org` for my personal server.
## Certbot
You already have the `certbot` software and the `apache2` web server installed. Requesting a certificate from Let's
Encrypt is as simple as running the next command. Please replace `mail.example.org` by the FQDN you chose.
```sh
certbot --apache --register-unsafely-without-email --agree-tos -d mail.example.org
```
<details>
<summary>Click here to learn what the options mean…</summary>
- **--apache**: Use the [HTTP challenge](https://letsencrypt.org/docs/challenge-types/) to verify that you are actually
in control of the FQDN. The certbot plugin will add some configuration to intercept HTTP requests to
`/.well-known/acme-challenge/…` on your web server. This is a fully automatic process that does not even have to stop
your web server.
- **--register-unsafely-without-email**: Do not ask for an email address to send to Let's Encrypt. Before June 2025
Let's Encrypt used that address to inform you if your certificate is about to expire and the automatic renewal failed.
Nowadays they do not need an email any more. But for historical reasons they still ask for it by default.
- **--agree-tos**: Confirm that you have read Let's Encrypt's terms and conditions. Let's be honest nobody reads that.
- **-d**: The domain that the certificate will be issued to. This has to be the exact FQDN of your server.
</details>
The output should look like:
```
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for mail.example.org
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mail.example.org/fullchain.pem
Key is saved at: /etc/letsencrypt/live/mail.example.org/privkey.pem
This certificate expires on 2025-11-22.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.
Deploying certificate
Successfully deployed certificate for mail.example.org to /etc/apache2/sites-available/000-default-le-ssl.conf
Congratulations! You have successfully enabled HTTPS on https://mail.example.org
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
```
<details>
<summary>Click here to learn which files have been created…</summary>
In /etc/letsencrypt/live/**mail.example.org** you will find a couple of files now:
- cert.pem: the certificate file
- chain.pem: the _chaining_ or _intermediate_ certificate. This certificate provides information how the LetsEncrypt
certificates are linked to other known certificate authorities. It is generally a good idea to always send this
certificate along with your own for clients who may not know LetsEncrypt properly yet.
- fullchain.pem: this file contains a concatenation of the _cert.pem_ and the _chain.pem_. This is the preferred file to
use when a piece of software asks where to find the _certificate_.
- privkey.pem: the private key file. Keep it secret.
</details>
## Renewal
Your new certificate is only valid for 3 months. Fortunately the renewal is happening automatically. A _systemd timer_
checks your certificate frequently and triggers a renewal one month before it expires.
These commands will show you the definition and status of the timer and the renewal service:
```sh
systemctl status certbot.timer
systemctl cat certbot.timer
systemctl status certbot.service
systemctl cat certbot.service
```
_Systemd timers_ are similar to cron jobs. They require more work to set up but have some nice features and make you
look cooler.
It is important that services that use the certificate load the new certificate after an automatic renewal. This shell
command will create a file `/etc/letsencrypt/cli.ini` that handles it:
```sh
echo > /etc/letsencrypt/cli.ini "post-hook = systemctl reload postfix dovecot apache2"
```
Perfect. You won't have to worry about the certificate again.

View file

@ -15,17 +15,7 @@ import { Steps } from "@astrojs/starlight/components";
import Box from "../../../components/Box.astro";
import StepList from "../../../components/StepList.astro";
<StepList
steps={[
{ title: "Install Astro", description: "Set up your project." },
{ title: "Add Starlight", description: "Install the theme." },
{ title: "Customize", description: "Adjust settings to your needs." },
{ title: "Deploy", description: "Publish to production." },
]}
currentStep={2}
/>
import StepListReceive from "../../../components/StepListReceive.astro";
I have created a slideshow to help you understand the process of receiving an email from a remote mail server. Use the
arrows to navigate between slides:
@ -38,29 +28,17 @@ arrows to navigate between slides:
))}
</Splide>
---
Let's tackle the receiving of emails step by step:
## DNS
<ul class="steps steps-vertical lg:steps-horizontal">
<li class="step step-primary">Register</li>
<li class="step step-primary">Choose plan</li>
<li class="step">Purchase</li>
<li class="step">Receive Product</li>
</ul>
<StepListReceive currentStep={1} />
<Box icon='check-list'>
<Steps>
1. **DNS is used to find your mail server. First the MX record. Then the A and/or AAAA record.**
1. Postfix receives emails using SMTP (the Simple Mail Transport Protocol)
1. MariaDB stores information about your domains and mail users
1. rspamd checks if it is spam (optional)
1. Dovecot saves it to disk
</Steps>
</Box>
Let's begin with setting up the DNS records. Say that you want to receive emails for the `example.com` domain. If some
other mail server on the internet wants to send an email to your server, it looks for two records:
As shown in the slideshow, you need to have proper DNS records set up. Say that you want to receive emails for the
`example.com` domain. If some other mail server on the internet wants to send an email to your server, it looks for two
records:
- **MX record (`example.com`)**
The MX (Mail Exchange) record tells other mail servers _where to deliver emails_ for the domain `example.com`.
@ -101,20 +79,46 @@ If possible, also add a PTR record that makes the IP addresses point back to the
servers may be more likely so flag your sent emails as spam if you do not have a PTR record. Preferable the forward
(A/AAAA) and reverse (PTR) records match.
<div class="scroll-trigger" data-slide="5">
dingdong
</div>
<Aside icon="approve-check-circle" title="Test it">
Use the `host` command to check your domain's MX record.
```sh
$> host -t mx example.com
example.com mail is handled by 10 smtp.example.com.
```
Then check if that result resolves to an IP address:
```sh
$> host smtp.example.com
smtp.example.com has address 49.13.89.249
smtp.example.com has IPv6 address 2a01:4f8:c012:62d1::1
```
Preferably the IP addresses should resolve back to the mail server's name:
```sh
$> host 49.13.89.249
249.89.13.49.in-addr.arpa domain name pointer smtp.example.com.
$> host 2a01:4f8:c012:62d1::1
1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.d.2.6.2.1.0.c.8.f.4.0.1.0.a.2.ip6.arpa domain name pointer smtp.example.com.
```
</Aside>
## Postfix
<StepListReceive currentStep={2} />
Now that other mail servers can locate your mail server, the next step is to make sure it can receive and process
incoming emails. This task is handled by Postfix, which communicates using the SMTP protocol.
Postfix knows if an email address is valid by checking for…
- Virtual Domains (am I responsible for this domain?)
- Virtual Aliases (do I have to redirect this address to another address?)
- Virtual Mailboxes (do I have a valid mailbox to store the email to?)
- **Virtual Domains** (am I responsible for this domain?)
- **Virtual Aliases** (do I have to redirect this address to another address?)
- **Virtual Mailboxes** (do I have a valid mailbox to store the email to?)
### Virtual Domains
@ -128,9 +132,9 @@ _mapping_. Consider it as a _question_ and an _answer_.
If the information is **not** available you just get no answer:
- Question: is `example.net` one of our virtual domains?
- Answer: (no answer)
- Answer: _(no answer)_
So Postfix hast to ask MariaDB that question:
In SQL language Postfix has to ask MariaDB the question:
```sql
SELECT "yes" FROM virtual_domains WHERE name='example.net'
@ -166,9 +170,9 @@ and _answer_ game goes like this:
Postfix would then check if there are further aliases:
- Question: is there an alias for `john@example.org`?
- Answer: (no answer)
- Answer: _(no answer)_
Apparently, there aren't any. So the email will go to `john@example.org`.
Apparently, there aren't any more. So the email will go to `john@example.org`.
Run this code to create the appropriate mapping file:
@ -202,8 +206,9 @@ Just the same story as with virtual domains. The database returns `1` if there i
that specific email address.
Actually the _answer_ is usually not `1` but the path to the mailbox on disk. But that applies only if Postfix would be
saving the email. In our setup the email will be passed on to Dovecot in a later step. Dovecot will then handle the
files on disk. Postfix just has to know is whether a mailbox exists. And that's why any _answer_ is sufficient here.
saving the email to disk on its own. In our setup the email will be passed on to Dovecot in a later step. Dovecot will
then handle the files on disk. Postfix just has to know is whether a mailbox exists. And that's why **any** _answer_ is
sufficient here.
### Tell Postfix
@ -224,7 +229,7 @@ to restart Postfix.
The last two lines set the owner of the config files (_user=root, group=postfix_) and make sure that _others_ have no
access. After all a database password is found in these files.
### Test it
<Aside icon="approve-check-circle" title="Test it">
Give the mappings a quick test using the `postmap -q` command (_q_ stands for _query_):
@ -243,3 +248,5 @@ john@example.org
```
If you get anything else, please check those three config files. Perhaps the database password is wrong?
</Aside>

View file

@ -7,197 +7,229 @@ sidebar:
---
import { Aside } from "@astrojs/starlight/components";
import StepListReceive from "../../../components/StepListReceive.astro";
In the previous chapter you have prepared Postfix to receive emails. As shown in the slideshow, Postfix hands incoming
emails over to Dovecot which in turn saves it to a mailbox on disk.
## About LMTP
So:
<StepListReceive currentStep={3} />
- DNS is used to find your mail server. First the MX record. Then the A and/or AAAA record.
- Postfix receives emails using SMTP (the Simple Mail Transport Protocol)
- MariaDB stores information about your domains and mail users
- rspamd checks if it is spam
- Dovecot saves it to disk
As explained in the previous section, Postfix speaks SMTP and receives the email from the internet. Postfix could even
save the email to a mailbox on disk. But instead we will use Dovecot for the final delivery. Actually Dovecot's main
purpose is to let users fetch their email using the IMAP protocol. But it provides additional features we can use as
well.
## DNS
So we need tell Postfix to hand over the incoming email to Dovecot. The communication between Postfix and Dovecot will
happen using LMTP the [local mail transfer protocol](https://en.wikipedia.org/wiki/Local_Mail_Transfer_Protocol).
LMTP is a variant of SMTP with fewer features. It is meant for email communication between internal services that trust
each other.
Let's begin with setting up the DNS records. Say that you want to receive emails for the `example.com` domain. If some
other mail server on the internet wants to send an email to your server, it looks for two records:
In this chapter we will mainly configure Dovecot so that it knows how to deliver incoming emails.
- **MX record (`example.com`)**
The MX (Mail Exchange) record tells other mail servers _where to deliver emails_ for the domain `example.com`.
Instead of pointing directly to an IP address, it points to a **hostname** (for example, `smtp.example.com`).
MX records also include a **priority value**. If multiple mail servers are listed, the one with the lowest priority
number is tried first. Others are used as fallback if the first is unavailable.
## Setting up the vmail directory
- **A / AAAA record (`smtp.example.com`)**
The hostname specified in the MX record (`smtp.example.com`) must resolve to an IP address so that other mail servers
know how to reach it.
- An **A record** maps the hostname to an IPv4 address.
- An **AAAA record** maps the hostname to an IPv6 address.
On your disk all the mailboxes will live in the directory `/var/vmail`. To separate the mailboxes from the rest of your
system, let's create a new user and group that will own the mailboxes:
Once resolved, the sending mail server connects to that IP address on **TCP port 25**, which is the standard port for
email delivery (SMTP).
```sh
groupadd --system vmail
useradd --system --gid vmail vmail
mkdir -p /var/vmail
chown -R vmail:vmail /var/vmail
```
<Aside>
## Dovecot configuration
If no MX record is found, the remote mail server will instead directly look for an A or AAAA record of `example.com`.
The configuration files for Dovecot are found in `/etc/dovecot/conf.d`. All these files are loaded by Dovecot. This is
done by this magical line at the end of the `/etc/dovecot/dovecot.conf` file:
```
!include conf.d/*.conf
```
It loads all files in `/etc/dovecot/conf.d/` that end in “.conf” in alphanumerical order. Let's edit a couple of files
for our purpose.
### 10-auth.conf
The `/etc/dovecot/conf.d/10-auth.conf` file is dealing with authentication. At the end of this file you will find a list
of authentication backends that Dovecot ships with. By default it will use system users (those from /etc/passwd). But we
want to use the MariaDB database backend so go ahead and change this block to:
```
#!include auth-system.conf.ext
!include auth-sql.conf.ext
#!include auth-ldap.conf.ext
#!include auth-passwdfile.conf.ext
#!include auth-checkpassword.conf.ext
#!include auth-static.conf.ext
```
Every line starting with `#` is disabled. Only lines starting with `!` are considered.
### 10-mail.conf
Edit this file at `/etc/dovecot/conf.d/10-mail.conf`. It contains many settings regarding mailboxes, like where they are
located, who owns them and what is their layout? Set this:
```
mail_driver = maildir
mail_home = /var/vmail/%{user | domain}/%{user | username}
mail_path = ~/Maildir
```
This looks more sophisticated than with versions of Dovecot before 2.4. What it means:
- **mail_driver**: Mailboxes are stored in
[maildir](https://doc.dovecot.org/2.4.1/core/config/mailbox_formats/maildir.html#maildir-mailbox-format) format.
- **mail_home**: Every mail user gets their own _home_ directory in `/var/vmail/DOMAIN/USER`. The `user` variable
contains the email address like `john@example.org`. The expression `user | domain` splits off the _domain_ part from
the email address. Similarly `user | username` takes the part before the `@` sign.
- **mail_path**: Within the _home_ directory the actual mailbox will live in a subdirectory called `Maildir`. The reason
is that we will store other data in the user's _home_ directory as well that should not conflict with the mailbox
directory.
### 10-master.conf
This configuration file at `/etc/dovecot/conf.d/10-master.conf` deals with Dovecot's services.
So most settings are sane here and do not have to be changed. However one change is required in the “service auth”
section because we want Postfix to allow Dovecot as an authentication service. Make it look like this:
```
# Postfix smtp-auth
unix_listener /var/spool/postfix/private/auth {
mode = 0660
user = postfix
group = postfix
}
```
That way Dovecot will put a communication socket into `/var/spool/postfix/private/auth`. It will allow Postfix to
authenticate your users which is relevant later when we set up _relaying_.
### 10-ssl.conf
TODO: verify
Earlier in this guide you created both a key and a certificate file to encrypt the communication with POP3, IMAPs and
HTTPS between the users and your mail server. You need to tell Dovecot where to find these files:
```
ssl\_cert = \</etc/letsencrypt/live/webmail.example.org/fullchain.pem
ssl\_key = \</etc/letsencrypt/live/webmail.example.org/privkey.pem
```
And enforce TLS encryption by setting:
```
ssl = required
```
See the [Dovecot documentation on SSL encryption](https://doc.dovecot.org/admin_manual/ssl/) for more information.
Next lets take a look at how Dovecot knows about users and their passwords:
### auth-sql.conf.ext
Dovecot reads the `auth-sql.conf.ext` which defines how to find user information in your database. Open the file. There
are two sections:
- userdb: where to find a users mailbox in the file system
- passdb: where to find the users hashed password
By default Dovecot will run two queries at your database. One for the _userdb_ that gets information like the user ID,
group ID, home directory and quota. And another for the _passdb_ that gets the hashed password.
The “userdb” section already reads:
```
userdb {
driver = sql
args = /etc/dovecot/dovecot-sql.conf.ext
}
```
As you can see Dovecot uses an SQL database lookup to get that information. And it refers to the dovecot-sql.conf.ext
file for more information. Lets see…
## /etc/dovecot/dovecot-sql.conf.ext
(This configuration file is one level up and not in “conf.d”.)
You will find this file well documented although all configuration directives are commented out. Add these lines at the
bottom of the file:
```
driver = mysql
connect = \
host=127.0.0.1 \
dbname=mailserver \
user=mailserver \
password=x893dNj4stkHy1MKQq0USWBaX4ZZdq
user_query = SELECT email as user, \
concat('*:bytes=', quota) AS quota_rule, \
'/var/vmail/%d/%n' AS home, \
5000 AS uid, 5000 AS gid \
FROM virtual_users WHERE email='%u'
password_query = SELECT password FROM virtual_users WHERE email='%u'
iterate_query = SELECT email AS user FROM virtual_users
```
<Aside type="tip" title="Backslashes">
Ending a line with a backslash (\) means that it is continued on the next line. It keeps the configuration more
readable when it is split over multiple lines.
</Aside>
<Aside type="caution">
What these lines mean:
The MX record must point to a hostname. It **must not** point to an IP address directly.
- driver: the kind of database. MariaDB is the same kind as MySQL.
- connect: where to find the MySQL database and how to access it (username, password)
- user_query: an SQL query that returns the user name (=the email address), the quota, the home directory, user ID and
group ID.
- password_query: this SQL query just gets the password hash from the database
- iterate_query: doveadm uses this query to get a list of all users. That allows you to use the “doveadm user \*'”
command later.
</Aside>
The _user_query_ gets several pieces of information from the database. Lets look at it one by one:
In zone syntax you would create something like:
- email AS user
It gets the the _email_ field from the database which corresponds to the user name. Dovecot expects it in the _user_
field so we set an alias to _“user”._
- userdb_quota_rule
This is the users quota in bytes. Think of it as the maximum possible space on disk that the user can occupy. As
[documented](https://doc.dovecot.org/configuration_manual/quota/#per-user-quota) Dovecot expects the quota in a
special format like “\*:bytes=10000” if the user should not be able to store more than 10,000 bytes. Thats why we
begin the string with \*:bytes=.
- userdb_home
This leads to the directory where all emails and various control files for this user are located. The placeholder
%d replaces the domain and %n the user part. So for John that makes it “/var/vmail/example.org/john”.
- userdb*uid and userdb_gid
Those are the user ID and group ID of \_vmail* user 5000 for both. Dovecot uses it to set the permissions of files
it creates. As all users share the same system user “vmail” this is just a static number.s
## Fix permissions
Make sure that only root can access the SQL configuration file so nobody else is reading your database access passwords:
```
@ IN MX 10 smtp
smtp IN A 100.64.17.3
smtp IN A fd7a:115c:a1e0::17
chown root:root /etc/dovecot/dovecot-sql.conf.ext
chmod go= /etc/dovecot/dovecot-sql.conf.ext
```
If possible, also add a PTR record that makes the IP addresses point back to the name `smtp.example.com`. Other mail
servers may be more likely so flag your sent emails as spam if you do not have a PTR record. Preferable the forward
(A/AAAA) and reverse (PTR) records match.
## Postfix
Now that other mail servers can locate your mail server, the next step is to make sure it can receive and process
incoming emails. This task is handled by Postfix, which communicates using the SMTP protocol.
Postfix knows if an email address is valid by checking for…
- Virtual Domains (am I responsible for this domain?)
- Virtual Aliases (do I have to redirect this address to another address?)
- Virtual Mailboxes (do I have a valid mailbox to store the email to?)
### Virtual Domains
There are different ways for Postfix to get that information. From text files, LDAP, MongoDB or PostgreSQL. Or via
MariaDB which is what we will use. But whatever kind of data source you configure, Postfix needs the information as a
_mapping_. Consider it as a _question_ and an _answer_.
- Question: is `example.org` one of our virtual domains?
- Answer: yes
If the information is **not** available you just get no answer:
- Question: is `example.net` one of our virtual domains?
- Answer: (no answer)
So Postfix hast to ask MariaDB that question:
```sql
SELECT "yes" FROM virtual_domains WHERE name='example.net'
```
Run the following code in your shell to create a configuration file creating that mapping:
```sh
cat > /etc/postfix/virtual-mailbox-domains.cf << EOF
user = mailserver
password = SECOND-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_domains WHERE name='%s'
EOF
```
This has created a configuration file called `/etc/postfix/virtual-mailbox-domains.cf`. The `user`, `password`, `hosts`
and `dbname` definitions tell Postfix how to connect to the database. And the `query` asks the _question_ if a certain
domain is present in the database table `virtual_domains`. Using `SELECT 1` we return just the number 1 if such an entry
was found. In fact it does not matter what we return. Any _answer_ is fine.
Don't forget to replace the `SECOND-PASSWORD-HERE` by your own password for the `mailserver` user.
### Virtual Aliases
Now let's deal with aliases. As shown earlier an alias redirects an email to another email address. So the _question_
and _answer_ game goes like this:
- Question: is there an alias for `jack@example.org`?
- Answer: `john@example.org`
Postfix would then check if there are further aliases:
- Question: is there an alias for `john@example.org`?
- Answer: (no answer)
Apparently, there aren't any. So the email will go to `john@example.org`.
Run this code to create the appropriate mapping file:
```sh
cat > /etc/postfix/virtual-alias-maps.cf << EOF
user = mailserver
password = SECOND-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT destination FROM virtual_aliases WHERE source='%s'
EOF
```
The query now gets all the `destination` email addresses from the database for a certain `source` email address.
### Virtual Mailboxes
The last mapping we need is a query to find valid mailboxes. Without further ado:
```sh
cat > /etc/postfix/virtual-mailbox-maps.cf << EOF
user = mailserver
password = SECOND-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_mailboxes WHERE email='%s'
EOF
```
Just the same story as with virtual domains. The database returns `1` if there is a `virtual_mailboxes` record matching
that specific email address.
Actually the _answer_ is usually not `1` but the path to the mailbox on disk. But that applies only if Postfix would be
saving the email. In our setup the email will be passed on to Dovecot in a later step. Dovecot will then handle the
files on disk. Postfix just has to know is whether a mailbox exists. And that's why any _answer_ is sufficient here.
### Tell Postfix
You have created the config files for the three mappings. Now you just need to tell Postfix to use them:
```sh
postconf virtual_mailbox_domains=mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
postconf virtual_mailbox_maps=mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
postconf virtual_alias_maps=mysql:/etc/postfix/mysql-virtual-alias-maps.cf
chown root:postfix /etc/postfix/*.cf
chmod o= /etc/postfix/*.cf
```
`postconf` is a command that changes configuration in `/etc/postfix/main.cf` and applies them instantly. You don't have
to restart Postfix.
The last two lines set the owner of the config files (_user=root, group=postfix_) and make sure that _others_ have no
access. After all a database password is found in these files.
### Test it
Give the mappings a quick test using the `postmap -q` command (_q_ stands for _query_):
```sh
postmap -q example.org mysql:/etc/postfix/mysql-virtual-mailbox-domains.cf
postmap -q jack@example.org mysql:/etc/postfix/mysql-virtual-alias-maps.cf
postmap -q john@example.org mysql:/etc/postfix/mysql-virtual-mailbox-maps.cf
```
These three commands use the three mapping files (`*.cf`) to query your three database tables. The result should show:
Restart Dovecot from the shell:
```
1
john@example.org
1
systemctl restart dovecot
```
If you get anything else, please check those three config files. Perhaps the database password is wrong?
Look at your /var/log/mail.log logfile. You should see:
```
... Dovecot v2.3.13 (f79e8e7e4) starting up for imap, lmtp, sieve, pop3 (core dumps disabled)
```
If you get any error messages please double-check your configuration files.