---
title: "Postfix"
lastUpdated: 2025-11-01
slug: ispmail-trixie/postfix
sidebar:
  order: 160
---

import { Aside } from "@astrojs/starlight/components";

import { Steps } from "@astrojs/starlight/components";

import Box from "../../../components/Box.astro";

import StepListReceive from "../../../components/StepListReceive.astro";

<StepListReceive currentStep={3} />

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)_

In SQL language Postfix has to ask MariaDB the 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 title="Run this on your server"
cat > /etc/postfix/mariadb-virtual-mailbox-domains.cf << EOF
user = mailserver
password = MAILSERVER-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.

### 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 more. So the email will go to `john@example.org`.

Run this code to create the appropriate mapping file:

```sh title="Run this on your server"
cat > /etc/postfix/mariadb-virtual-alias-maps.cf << EOF
user = mailserver
password = MAILSERVER-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 title="Run this on your server"
cat > /etc/postfix/mariadb-virtual-mailbox-maps.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT 1 FROM virtual_users WHERE email='%s'
EOF
```

Just the same story as with virtual domains. The database returns `1` if there is a `virtual_users` 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 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

You have created the config files for the three mappings. Now you just need to tell Postfix to use them:

```sh title="Run this on your server"
postconf virtual_mailbox_domains=mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf
postconf virtual_mailbox_maps=mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf
postconf virtual_alias_maps=mysql:/etc/postfix/mariadb-virtual-alias-maps.cf

chown root:postfix /etc/postfix/mariadb-*.cf
chmod o= /etc/postfix/mariadb-*.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.

<Aside icon="approve-check-circle" title="Test it">

Give the mappings a quick test using the `postmap -q` command (_q_ stands for _query_):

```sh title="Run this on your server"
postmap -q example.org mysql:/etc/postfix/mariadb-virtual-mailbox-domains.cf
postmap -q jack@example.org mysql:/etc/postfix/mariadb-virtual-alias-maps.cf
postmap -q john@example.org mysql:/etc/postfix/mariadb-virtual-mailbox-maps.cf
```

These three commands use the three mapping files (`*.cf`) to query your three database tables. The result should show:

```
1
john@example.org
1
```

If you get anything else, please check those three config files again.

</Aside>
