---
title: Sending / Relaying
lastUpdated: 2025-11-01
slug: ispmail-trixie/relaying
sidebar:
  order: 180
---

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

Your mail server is almost ready for use. But one puzzle piece is missing. Your users can receive emails but they cannot
send them yet. This part of the guide is a bit longer than others because forwarding emails has security implications.

Let's begin with the difference of how **users** send emails versus how **mail servers** send emails.

- A **mail server** fetches the _MX_ record for the destination domain of the recipient’s email address from DNS. That
  tells it which mail server to talk to. Then it opens an SMTP connection (TCP port 25) and sends the email. A mail
  server will not relay an email meant for someone else. It will only accept emails for its own users.
- An **end user** with a mail client such as Thunderbird or Roundcube cannot do that. Mail clients do not include any
  functionality for fetching MX records. In addition, the user is usually on a dynamic IP address that other mail
  servers will not trust and will likely reject. Instead, end users are expected to send their emails through their
  provider’s (=your) mail server: they authenticate with their login credentials and then send the message. This process
  is called relaying, because your mail server acts as an intermediary between the user and other mail servers on the
  internet. For security reasons, authentication is required before the user is allowed to send emails. Users are also
  expected to use the TCP port 587 which is called the _submission_ port. More on that later.

Postfix must only accept an email **from a user**, if…

- The user uses the **submission** port (and not the standard SMTP port)
- The user **authenticates** with the username (in our setup: the email address) and the password
- The connection is **encrypted** so that the password can be safely transmitted
- The **sender address** of the email matches the user's account

## SMTP versus Submission

Let's compare those services and ports side-by-side:

| Service name         | SMTP                        | Submission                   | SubmissionS (S=secure) |
| -------------------- | --------------------------- | ---------------------------- | ---------------------- |
| TCP Port             | 25                          | 587                          | 465                    |
| Meant for            | Other mail servers          | Our users                    | Our users              |
| Used by              | Mail transport agent (MTA)  | Mail user agent (MUA)        | Mail user agent (MUA)  |
| Transport encryption | **Off**. STARTTLS optional. | **Off**. STARTTLS mandatory. | TLS all the time       |
| Authentication       | Optional [^1]               | Mandatory                    | Mandatory              |
| Accepted sender      | Anyone                      | Our own users                | Our own users          |
| Accepted receivers   | Our own users               | Anyone                       | Anyone                 |
| Your home ISP        | may block this port         | will allow this port         | will allow this port   |

[^1]:
    In previous guides I recommended to set `smtpd_sasl_auth_enable=yes` globally in the `main.cf`. That would indeed
    enable authentication on port 25. For a clearer separation I now rather suggest to leave it out (default is `no`)
    and just enable authentication on the `submission` service in the `master.cf` file. A possible scenario that
    required this setting to be `on` would be if your server were a relay (_smarthost_) for other servers using
    authentication..

I hope this helps clarify the distinction. Human users must use the submission service on either port 587 or 465. The
key difference is that connections on port 587 start unencrypted, and the email client must issue the STARTTLS command
to enable encryption. In contrast, port 465 requires the email client to use TLS encryption right from the first byte of
communication.

## Configure the submission[s] services

All Postfix services are declared in the `/etc/postfix/master.cf` file. Most of them are used by Postfix internally,
like `pickup`, `cleanup`, `rewrite` or `bounce`. But `submission` is not configured by default. Let's fix that.

```sh title="Run this on your server" title="Run this on your server"
postconf -M submission/inet="submission inet n - y - - smtpd"
postconf -P "submission/inet/syslog_name=postfix/submission"
postconf -P "submission/inet/smtpd_tls_security_level=encrypt"
postconf -P "submission/inet/smtpd_sasl_auth_enable=yes"
postconf -P "submission/inet/smtpd_sasl_type=dovecot"
postconf -P "submission/inet/smtpd_sasl_path=private/dovecot-auth"
postconf -P "submission/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -P "submission/inet/smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject"

postfix reload
```

These commands added a `submission` service to the `/etc/postfix/master.cf` file. You could have achieved the same thing
by editing that file and adding the following section, but I wanted to make it simpler for you to just copy/paste the
commands into your shell.

```text title="What it did…"
submission inet  n       -       y       -       -       smtpd
  -o syslog_name=postfix/submission
  -o smtpd_tls_security_level=encrypt
  -o smtpd_sasl_auth_enable=yes
  -o smtpd_sasl_type=dovecot
  -o smtpd_sasl_path=private/dovecot-auth
  -o smtpd_recipient_restrictions=permit_sasl_authenticated,reject
```

Some notes on that:

- Lines with **options** (`-o`) need to be indented so that Postfix understands that they belong to the same service.
- The word **submission** refers to the definition of the service in the `/etc/services` file. So it translates to TCP
  port 587.
- The **smtpd** in the last column of the first row tells Postfix that connections should be handled as incoming SMTP
  connections.
- **syslog_name** is adding `postfix/submission` to log files so that you can distinguish it from other connections.
- **smtpd_tls_security_level** enabled and enforces the use of STARTTLS. The connection starts without encryption but
  the remote party is required to send the `STARTTLS` command to switch to an encrypted connection.
- **smtpd_sasl_auth_enable** enables authentication on this port. SASL is short for _Simple Authentication and Security
  Layer_. That way only our own users can use this service.
- **smtpd_sasl_type** tells Postfix to use Dovecot's service to authentication a uaser.
- **smtpd_sasl_path** tells Postfix that it should talk to Dovecot through the socket file
  `/var/spool/postfix/private/dovecot-auth`. Earlier in this guide you told Dovecot to create that socket at that
  location.
- **smtpd_recipient_restrictions** define that only authenticated connections are permitted. Without a username and a
  password your connection will be rejected. The **recipient** keyword tells us that this restricts who the recipient
  can be.
- **smtpd_sender_restrictions** restricts who the **sender** may be. I will explain that
  [later](#protecting-against-forged-sender-addresses).

While you are at it, create the `submissions` (with an "s" at the end) service as well:

```sh title="Run this on your server"
postconf -M submissions/inet="submissions inet n - y - - smtpd"
postconf -P "submissions/inet/syslog_name=postfix/submissions"
postconf -P "submissions/inet/smtpd_tls_wrappermode=yes"
postconf -P "submissions/inet/smtpd_sasl_auth_enable=yes"
postconf -P "submissions/inet/smtpd_sasl_type=dovecot"
postconf -P "submissions/inet/smtpd_sasl_path=private/dovecot-auth"
postconf -P "submissions/inet/smtpd_recipient_restrictions=permit_sasl_authenticated,reject"
postconf -P "submissions/inet/smtpd_sender_restrictions=reject_sender_login_mismatch,permit_sasl_authenticated,reject"
postfix reload
```

The only difference to the `submission` (without the "s" at the end) is the `smtpd_tls_wrappermode` that switches on TLS
encryption right from the start of a connection.

## Set up encryption

Whenever possible we should encrypt our traffic. For our users' passwords we make it mandatory. But even for
server-to-server connections we should encourage it. Do not confuse it with end-to-end encryption of emails. That
required using PGP or S/MIME. What we are dealing here is _transport encryption_. It encrypts the communication between
a peer server and your mail server. What the peer server does with our email is beyond our control.

<Aside type="danger" title="Important">

Please replace `mail.example.org` by the FQDN you chose.

</Aside>

```sh title="Run this on your server"
postconf smtp_tls_security_level=encrypt
postconf smtpd_tls_security_level=encrypt
postconf smtp_tls_mandatory_protocols=">=TLSv1.2"
postconf smtpd_tls_mandatory_protocols=">=TLSv1.2"
postconf smtp_tls_mandatory_ciphers=high
postconf smtpd_tls_mandatory_ciphers=high
postconf smtpd_tls_cert_file=/etc/letsencrypt/live/mail.example.org/fullchain.pem
postconf smtpd_tls_key_file=/etc/letsencrypt/live/mail.example.org/privkey.pem
```

What it means:

- **smtp_tls_security_level**: \
  Enforce encrypted _outgoing_ SMTP connections.
- **smtpd_tls_security_level**: \
  Enforce encrypted _incoming_ SMTP connections.
- **smtp_tls_mandatory_protocols** / **smtpd_tls_mandatory_protocols**: \
  Only allow recommended versions of [TLS](https://en.wikipedia.org/wiki/Transport_Layer_Security). Version 1.2 has been
  introduced 2008. Version 1.3 is from 2018.
- **smtp_tls_mandatory_ciphers** / **smtpd_tls_mandatory_ciphers**: \
  The list of encryption algorithms that you allow for communnication. Default is _medium_.
- **smtpd_tls_cert_file** and **smtpd_tls_key_file**: \
  Where to find the private key and certificate for encryption.

<Aside type="tip" title="Losen the encryption?">
  In rare cases you may fail to communicate with other computers because they only support older TLS versions (up to
  1.1) or ciphers that are not considered secure nowadays. In that case you can remove the `smtpd_tls_security_level`,
  `smtpd_tls_mandatory_protocols` and `smtpd_tls_mandatory_ciphers` settings from your `main.cf` file and fall back to
  the relaxed security requirements.
</Aside>

Please note that configuration in `main.cf` applies to all Postfix services. So the above configuration enabled
encryption for both server-to-server and human-to-server (`submission`) services. The `smtpd_sasl_auth_enable` will only
be set for the `submission` service to enable authentication there.[^1]

<Aside type="tip" title="smtp or smtpd?">
  Look closely. Some settings start with `smtp_` and others start with with `smtpd_`. That is not a typo. `smtp_` refers
  to the SMTP _client_. That is the component that sends out emails **from** Postfix **to** other servers. Whereas
  `smtpd_` means the SMTP _server._ That in turn is the component that **receives** emails **from** other systems –
  either from a remote mail server or one of your users.
</Aside>

## Protecting against forged sender addresses

Before we can actually test the setup, there is another security issue we need to address. While Postfix is configured
to relay emails only after a successful login with a valid username and password, it does not prevent an authenticated
user from impersonating another sender. For example, someone could log in as `john@example.org` but send an email
appearing to come from `brunhilde@example.net`.

We should fix that. For that purpose Postfix provides a setting called
[smtpd_sender_login_maps](http://www.postfix.org/postconf.5.html#smtpd_sender_login_maps). From the "maps" part you can
deduce that it expects a _mapping_ again:

- _Question_: who you **claim** to be (email’s sender address)
- _Answer_: who you **logged in as** (user name after authentiation)

If you think of it, the mapping is simple. The _question_ must match the _answer_. If `john@example.org` wants to be the
sender, he must also authenticate as `john@example.org`.

As we use the email address also as a user name we simply need a mapping where the email address is listed on both
sides. Let's create it:

```sh title="Run this on your server"
# Create a mapping configuration from the user to themself
cat > /etc/postfix/mariadb-email2email.cf << EOF
user = mailserver
password = MAILSERVER-PASSWORD-HERE
hosts = 127.0.0.1
dbname = mailserver
query = SELECT email FROM virtual_users WHERE email='%s'
EOF

# Fix permissions
chown root:postfix /etc/postfix/mariadb-email2email.cf
chmod u=rw,g=r,o= /etc/postfix/mariadb-email2email.cf

# Tell Postfix to use this mapping
postconf smtpd_sender_login_maps=mysql:/etc/postfix/mariadb-email2email.cf
```

What you accomplished:

1. Create the _mapping_ file
1. Fix the permissions of it so that only Postfix can read it (there is a password in it)
1. Make Postfix use that mapping for its `smtpd_sender_login_maps` setting
1. Add `smtpd_sender_restrictions=reject_sender_login_mismatch` to the `submission` and `submissions` services

You can now try to send email as a different user than you are logged in. Let’s us swaks again to send a spoofed email:

```sh title="Run this on your server"
swaks --server localhost:587 \
     --from brunhilde@example.org \
     --to list@example.com \
     --tls \
     --auth-user john@example.org \
     --auth-password summersun
```

The server should rightfully reject that wrong sender:

```
~> MAIL FROM:john@example.com
<~ 250 2.1.0 Ok
~> RCPT TO:john@example.org
<~ * 553 5.7.1 john@example.com: Sender address rejected: not owned by user john@example.org
```

Now let's send a proper email from John:

```sh title="Run this on your server"
swaks --server localhost:587 \
     --from john@example.org \
     --to list@example.com \
     --tls \
     --auth-user john@example.org \
     --auth-password summersun
```

That will work:

```
<~  250 2.0.0 Ok: queued as AFFF12292A
```

<Aside type="tip" title="Allow aliases?">

The setup so far works for personal mails from John. But if John would be a member of a distribution list
`pizza@example.org` then would not be able to use `pizza@example.org` as a sender address.

If you want to allow that, create a new \*.cf file with a mapping query like this:
`SELECT email FROM virtual_users WHERE email='%s' UNION SELECT destination FROM virtual_aliases WHERE source='%s'`.

</Aside>

<Aside type="danger" title="Check your $mydestination">

Please check the `mydestination` setting in your `main.cf` file. Make sure that it does not contain any domain name that
you also use as one of your _virtual domains_. If in doubt just set it to `localhost`.

</Aside>

## Test it

### No encryption, no authentication

So far you have set up the `submission` service(s), enabled encryption and connected Postfix to Dovecot for
authentication. Let's try it. Our first test email will be sent without encryption and authentication:

```sh title="Run this on your server"
swaks --server localhost:587 \
      --from john@example.org \
      --to lisa@example.com
```

Postfix should tell you that it will not accept an email without encryption:

```
<** 530 5.7.0 Must issue a STARTTLS command first
```

### With encryption, no authentication

Our second attempt will **enable** (STARTTLS) encryption – but still no authentication:

```sh title="Run this on your server"
swaks --server localhost:587 \
      --from john@example.org \
      --to lisa@example.com \
      -tls
```

Postfix will still reject the email because we refused to authenticate:

```
 ~> MAIL FROM:<john@example.org>
<~  250 2.1.0 Ok
 ~> RCPT TO:<lisa@example.com>
<~* 553 5.7.1 <john@example.org>: Sender address rejected: not logged in
```

### With encryption and authentication

```sh title="Run this on your server"
swaks --server localhost:587 \
    --from john@example.org \
    --to lisa@example.com \
    -tls \
    --auth-user john@example.org \
    --auth-password summersun
```

Also give the `submissions` service a try:

```sh title="Run this on your server"
swaks --server localhost:465 \
    --from john@example.org \
    --to lisa@example.com \
    --tls-on-connect \
    --auth-user john@example.org \
    --auth-password summersun
```

Did you notice the difference? The second time we used port **465** and instead of the `-tls` option (which uses
STARTTLS to switch to encryption) we use `--tls-on-connect` that uses TLS right from from the start.

Put simply, a mail server will accept an email if…

- it is responsible for handling mail for the recipient’s domain (the domain is listed as a virtual domain), or
- the sender has authenticated successfully with a valid username and password.

I have created two illustrations to explain it.

## Gory details of authentication

<Aside type="tip" title="For science">

This section is just here to show you the communication that happens on the network when relaying an email. It is no
mandatory reading matter. But relaying is a complex matter where things can go wrong. So taking a peek under hood may
improve your understanding.

</Aside>

```
telnet localhost submission
```

The server will let you in:

```
Trying 127.0.0.1…
Connected to localhost.
Escape character is '^]'.
220 webmail ESMTP Postfix (Debian)
```

Say hello:

```
ehlo example.com
```

Postfix will present a list of features that are available for you:

```
250-mailtest
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250-SMTPUTF8
250 CHUNKING
```

Let me briefly explain what these lines mean:

- PIPELINING  
   This is a feature to speed up SMTP communication. Usually the remote system has to wait for a response to every
  command it sends. Pipelining allows the remote server to send several commands in a batch without waiting for a
  response. Postfix will just store these commands and execute them one by one. If you told Postfix to forbid pipelining
  it would disconnect the remote server when it tries to send bulks of commands without waiting for the proper reply. It
  is mainly a feature against rogue senders.
- SIZE 10240000  
   The remote server is allowed to send emails up to 10 MB large. This has long been a common maximum size for emails.
  However nowadays 40 MB or even more are more common sizes because emails have grown larger. Some mail servers even
  allow 100 MB.
- VRFY  
   Allows remote servers to verify a given name or email address. For example the remote server could send "VRFY john"
  and your server might respond `250 Jo You can skip it for now.hn Doe <john@example.org>`. It can be used to verify
  that a certain recipient email address is deliverable
- ETRN  
   A command that a remote system can send to flush the Postfix queue of mails for a certain domain. It can be used if
  the remote system had technical problems and failed to receive email for a while. Then it could send an ETRN command
  to make your server start sending outstanding emails for that domain. It is rarely used.
- STARTTLS  
   This tells the remote system that it might start switching from this unencrypted to an encrypted connection by
  sending the "STARTTLS" command. It will then start negotiating a TLS-encrypted connection. You could compare it to an
  HTTP connection that suddenly switches over to an encrypted HTTPS connection. The advantage is that you can start
  talking SMTP on TCP port 25 and don’t have to open up a second TCP port like 465 which is the "SSMTP" (secure SMTP)
  port and only accepts encrypted connections.
- ENHANCEDSTATUSCODES  
   This enables more three-digit return codes for various conditions. See the
  [RFC2034](http://tools.ietf.org/html/rfc2034) if you are curious.
- 8BITMIME  
   In ancient times SMTP only processed 7-bit characters. You couldn’t transfer special characters like "Ä" or "ß"
  without special encoding. 8BITMIME allows a transmission of emails using 8-bit characters. Still many emails are
  specially encoded using ISO8859-1 or UTF-8.
- DSN  
   It enables DSNs (delivery status notofications) that allows the sender to control the messages that Postfix creates
  when an email could not be delivered as intended.
- SMTPUTF8  
   In addition to 8BITMIME you can use UTF8 encoded characters in header fields.
- CHUNKING  
   This feature (described in [RFC 3030](https://tools.ietf.org/html/rfc3030)) makes sending of large emails more
  efficient.

However one important line is missing here that would allow us to send our username and password:

```
250-AUTH PLAIN LOGIN
```

We told Postfix to only allow authentication when the connection is encrypted. So we are not offered authentication over
this plain text connection. That’s what we want.

Are you still connected? Okay, good. So we need an encrypted connection using TLS. Using the STARTTLS feature we can
switch over from unencrypted to encrypted without having to reconnect. Enter…

```
STARTTLS
```

And the server replies:

```
220 2.0.0 Ready to start TLS
```

However now it’s getting weird because you would have to speak TLS encryption which is not a language that humans speak.
So let’s quit this using the "QUIT" command and do that differently:

```
QUIT
```

We can use OpenSSL to help us with the decryption. Run:

```
openssl s_client -connect localhost:submission -starttls smtp
```

You will see a lot of output. OpenSSL has connected to TCP port 25 and issued a STARTTLS command to switch to an
encrypted connection. So whatever you type now will get encrypted. Enter:

```
EHLO example.com
```

And Postfix will send a list of capabilities that will look like this:

```
250-PIPELINING
250-SIZE 10240000
250-VRFY
250-ETRN
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 DSN
```

And now that we are using an encrypted connection Postfix offers us to authenticate. So let us send the authentication
string with a Base64-encoded password:

```
AUTH PLAIN AGpvaG5AZXhhbXBsZS5vcmcAc3VtbWVyc3Vu
```

Unless you have changed John’s password to something else than "summersun" the server should accept that authentication:

```
235 2.7.0 Authentication successful
```

Excellent. You are logged in through SMTP. You could now send an email to be forwarded to another mail server. I just
wanted to show that authentication works if you use an encrypted connection.

Disconnect from Postfix:

```
QUIT
```

Authentication works. Well done.

Wait a minute. What was that crazy cryptic string? There was no username and password in it. Was it encrypted?

No, that was no encryption. It was merely an _encoded_ version of the username and password. Why that? Well usually in
SMTP you can only transfer ASCII characters. But the password may contain special characters which are not covered by
ASCII. So in the _PLAIN_ method that information is [Base64](https://en.wikipedia.org/wiki/Base64) encoded.

What is actually converted to Base64…

```
NULL-BYTE + USERNAME + NULL-BYTE + PASSWORD
```

So for John’s case you can easily create the Base64 string using:

```
printf '\0john@example.org\0summersun' | base64
```

As a result you will get the exact same string you used above with "AUTH PLAIN".
