Simple and minimal SMTP server

1. Introduction

Web applications usually need to send email notifications. For example Moodle needs to notify students and teachers about various events. Without being able to send notifications, Moodle and many other web applications loose half of their usefulness. On many web applications you cannot even finish the registration process and cannot login, unless you verify your email address (the application sends you an email with a link that you have to click).

To send emails, an application needs to contact by SMTP a local or remote mail server. Installing a mail server locally is not so easy because it needs also some DNS and other configurations. Otherwise the mails that are sent will end up being classified as spam and most probably will not reach the recipient.

This article describes how to install your own mail server and how to configure it properly. The aim of this mail server is not to be a full-fledged system, where users can have accounts and use it daily, but just to support web applications (like Moodle) with sending notifications.

For security resons, it allows only some trusted hosts to send emails. This prevents any spammers from abusing it. It also does not have any local accounts and does not accept any emails, making it simpler — no need for antispam and antivirus software, no need for POP and IMAP, etc. It is just a simple and minimal SMTP server, which can be used by (trusted) web applications to send notification emails to their users.

2. Minimal DNS configuration

In order to build a mail server you need to own a domain (say example.org) and be able to customize its DNS records.

For each email domain you need something like this on the DNS configuration:

smtp.example.org.    IN    A           10.11.12.13
example.org.         IN    MX    10    smtp.example.org.
example.org.         IN    MX    20    smtp.example.org.
example.org.         IN    TXT         "v=spf1 mx -all"
Yes, both MX records point to the same server, it is not a typo. It is done because the configuration of the server enables postscreen deep protocol tests in order to block spambots (for more details see this article). This means that the server disconnects new/unknown SMTP clients when they connect for the first time, even if they are legitimate. A legitimate client usually goes on to try the second mailserver in the list, and the second time they try to connect they are accepted.

The last line basically tells to the other SMTP servers that only this server is allowed to send emails on behalf of this domain, and no other servers. This is done to prevent spammers from faking your email addresses. If a spammer tries to send a mail as if it is coming from your domain, the SMTP server that is getting this email will consult this DNS record and will figure out that the server of the spammer is not allowed to send emails on behalf of example.org.

Depending on your DNS server, DNS changes may take from a few minutes to a couple of days to propagate. You can use dig to verify that these DNS records have been activated:

user@host:~$ dig MX example.org +short
10 smtp.example.org.
20 smtp.example.org.

user@host:~$ dig A smtp.example.org +short
10.11.12.13

user@host:~$ dig TXT example.org +short
"v=spf1 mx -all"

3. Build a postfix container

We assume that we have already installed docker-scripts and revproxy. Now let’s install a postfix container:

ds pull postfix
ds init postfix @smtp.example.org
cd /var/ds/smtp.example.org/
vim settings.sh    (1)
ds make
1 Make sure to set proper values to HOSTNAME and VIRTUAL_DOMAINS.

Check the installation:

cd /var/ds/smtp.example.org/

ls sslcert/
ls /etc/cron.d/

ls config/
cat config/trusted_hosts
cat config/virtual_alias_maps
cat config/virtual_alias_maps.regexp
ls config/dkim-keys/

ds shell
ps ax
systemctl status postfix
ls /etc/postfix/
postconf -nf | less
tail /var/log/mail.log
exit

4. Make the mail server trustworthy

To increase the chances that the other mail servers take seriously the emails comming from our mailserver we have to do some extra DNS configurations. Otherwise the mails that are sent may end up being classified as spam and most probably will not reach the recipient.

4.1. Activate a DKIM key

DKIM keys are used by a mail server to sign the emails that it sends, so that the emails cannot be changed in transit, and so that the receiver can verify that this server is authorized to send emails for a domain. It is an important tool against spams and faked emails. If an smtp server signs the messages that it sends, it is less likely that they will be classified as spam.

Installation scripts generate a DKIM key as well, which is on config/dkim-keys/example.org/. To activate it you need to add a record like this on the DNS configuration of the domain:

mail._domainkey.example.org.  IN  TXT  (
    "v=DKIM1; h=sha256; k=rsa; "
    "p=MIIBIjANBgkqhkiG9w0BAQE....kMJdAwIDAQAB"
)

You can find the content of the public key on the file: config/dkim-keys/example.org/mail.txt.

To check whether it has been activated or not, try the command:

dig TXT mail._domainkey.example.org +short

4.2. Create a DMARC record

DMARC is a standard that allows you to set policies on who can send email for your domain based on DKIM and SPF.

You can add a DMARC Record on DNS that will allow you to get weekly reports from major ISPs about the usage of your email domain.

  1. Go to http://dmarc.postmarkapp.com/ and add your email address where you want to receive reports, and email domain name (example.org).

  2. On the DNS configuration of the domain add a TXT record like this:

    _dmarc.example.org.  IN  TXT  (
        "v=DMARC1; p=none; pct=100; "
        "rua=mailto:re+x2i0yw1hoq7@dmarc.postmarkapp.com; "
        "sp=none; aspf=r;"
    )

    The value of this TXT record is the one generated by the website above.

  3. To check that it has been activated, try the command:

    dig TXT _dmarc.example.org. +short

5. Test the SMTP server

For a quick automated test of the basic functionality of the server try: ds test1. The following tests can be done manually.

5.1. Send from inside

Let’s send some test emails from inside the container.

Example 1. Send to an existing account
cd /var/ds/smtp.example.org/
ds shell

swaks --from noreply@example.org --to info@example.org
tail /var/log/mail.log

cat /host/config/virtual_alias_maps
cat /host/config/virtual_alias_maps.regexp

This test succeeds because the recipient (info@example.org) matches the line /^info@/ on the virtual_alias_maps.regexp. The sender can be anything (in this case noreply) and the email will still be sent, since we are sending from localhost, which is a trusted host.

This email is actually forwarded to the address that is listed on virtual_alias_maps.regexp (check also the spam folder if you cannot find it on the inbox).

Example 2. Send to a non-existing account
swaks --from info@example.org --to noreply@example.org
tail /var/log/mail.log

This test fails because the recipient (noreply@example.org) does not match any entry on virtual_alias_maps or virtual_alias_maps.regexp, so the mailserver does not know what to do with this email.

If you edit virtual_alias_maps and add a line like this:

noreply@example.org user@mail.com

then the second test will succeed as well and the email will be forwarded to the given address. But first you will have to update the map with:

postmap /host/config/virtual_alias_maps

5.2. Send to gmail

Let’s send test emails to a gmail account:

Example 3. Send from an existing address
swaks --from info@example.org --to <username>@gmail.com -tlso
tail /var/log/mail.log
Example 4. Send from a non-existing address
swaks --from noreply@example.org --to <username>@gmail.com -tlso
tail /var/log/mail.log

Both of these tests will succeed. The option -tlso enables the TLS encryption of the connection.

On gmail use "Show original" from the menu, to see the source of the received email.

You can also send a reply from <username>@gmail.com to info, but you cannot send to noreply.

5.3. Send from the host

Let’s try to send a test email from the host (outside the container):

cd /var/ds/smtp.example.org/
apt install swaks

swaks --from info@example.org --to admin@example.org -tlso
ds exec tail /var/log/mail.log

It may fail, because the IP of the host might not be on the list of the trusted hosts. Append it to config/trusted_hosts and run ds inject update.sh. Then verify that now it works:

echo '10.11.12.13' >> config/trusted_hosts
ds inject update.sh

swaks --from info@example.org --to admin@example.org -tlso
ds exec tail /var/log/mail.log

5.4. Add a new address

Try to send an email to test1@example.org:

swaks --from info@example.org --to test1@example.org -tlso
...
<** 550 5.1.1 <test1@example.org>: Recipient address rejected:
                          User unknown in virtual alias table
...

It will fail because the recipient does not exist on the alias table. On config/virtual_alias_maps add a line like this:

test1@example.org  <username>@gmail.com

Then update the alias db and verify that now you can send email to this address:

#ds inject update.sh
ds exec postmap /host/config/virtual_alias_maps

swaks --from info@example.org --to test1@example.org -tlso
ds exec tail /var/log/mail.log

5.5. Send to ports 587/465

swaks --server smtp.example.org:587 \
      --from info@example.org --to admin@example.org
swaks --server smtp.example.org --port 465 \
      --from info@example.org --to admin@example.org
On the host these should work. However, make sure that they fail if you try them from another server. If they don’t fail, then everyone can use your SMTP server to send emails, and this is wrong. Something is wrong with the configuration of the server.

To allow a server to send emails from your SMTP, you should append its IP to the list of the trusted hosts:

echo '11.12.13.14' >> config/trusted_hosts
ds inject update.sh

6. Check the health of the mail server

  • Send an email to check-auth@verifier.port25.com:

    swaks --server smtp.example.org -tlso \
          --from info@example.org \
          --to check-auth@verifier.port25.com

    The automatic reply will give you important information about the status and health of your email server (for example whether the mails sent from it pass the SPF and DKIM checks, whether they are considered spam or not, etc.)

  • Go to https://www.mail-tester.com/ and send a message to the email address displayed there, like this:

    swaks --server smtp.example.org -tlso \
          --from info@example.org \
          --to test-1p4f6@mail-tester.com

    Then click the button for checking the score.

  • There are lots of other tools and websites that help to check the configuration of a mail server (DNS settings, configuration, security features, etc.) For example:

7. Add another email domain

This smtp server can support more than one mail domains. If we want to add another mail domain, for example example.com, we have to do these:

  1. Set DNS configurations like this:

    ; mail for example.com
    smtp.example.com.   IN   CNAME    smtp.example.org.
    example.com.    IN    MX    10    smtp.example.com.
    example.com.    IN    MX    20    smtp.example.com.
    example.com.    IN    TXT         "v=spf1 mx -all"

    Note that the first record (CNAME) redirects the new smtp domain to the first one.

    You can check these DNS configurations like this:

    dig +short smtp.example.com. A
    dig +short example.com. MX
    dig +short example.com. TXT
  2. Edit settings.sh and add the new domain at VIRTUAL_DOMAINS:

    VIRTUAL_DOMAINS='
        example.org
        example.com
    '
  3. Run ds make to rebuild the container. The rebuild is done because we need to request a new SSL cert from letsencrypt, which covers all the domains (including the new one).

  4. Go to http://dmarc.postmarkapp.com/ and generate a DMARC record for the new domain.

  5. Update the DNS configuration with records like these:

    mail._domainkey.example.com.  IN  TXT  (
        "v=DKIM1; h=sha256; k=rsa; "
        "p=MIIBIjANBgkqhkiG9w0BAQE....kMJdAwIDAQAB"
    )
    
    _dmarc.example.com.           IN  TXT  (
        "v=DMARC1; p=none; pct=100; "
        "rua=mailto:re+x2i0yw1hoq7@dmarc.postmarkapp.com; sp=none; aspf=r;"
    )

    Note that:

    • The value of the key for the DKIM record can be found on the file: config/dkim-keys/example.com/mail.txt

    • The value of the DMARC record is the one obtained on the previous step.

    You can check them like this:

    dig +short mail._domainkey.example.com. TXT
    dig +short _dmarc.example.com. TXT

8. Relay domains

If another mail server is installed on the same machine where this SMTP server is installed, there is a problem, because they both need to use the port 25 for receiving mails. SMTP ports cannot be shared by more than one application, and it is not possible to proxy them (like HTTP or HTTPS ports, for example).

A workaround in this case would be to use this simple SMTP server as a relay for the mail domains that are served by the other mail server. It works like this:

  1. When a connection comes on the port 25 (someone is trying to send an email), it is handled by the simple SMTP server, since this port is forwarded to this server.

  2. If the domain of the receipient’s address is one of the domains that is managed by the simple SMTP server, the mail is handled by this server itself.

  3. Otherwise, if the domain is one of those that are managed by the other mail server, it is relayed instead to that mail server.

In other words, the simple SMTP server behaves like a kind of router for the emails that are received, based on the email domain of the receipent’s address.

Let’s see how to implement this SMTP relay. We will assume that the domains that are managed by the other mail server are example1.org, example2.org and example3.org. We can create the custom command ds relay-setup like this:

cd /var/ds/smtp.example.org/
mkdir -p cmd

cat <<'__EOF__' > cmd/relay-setup.sh
cmd_relay-setup() {
    # create a config file for relay_domains
    cat <<EOF > config/relay_domains
example1.org
example2.org
example3.org
EOF

    # create a config file for transport_maps
    local smtp_relay='smtp:10.214.77.201:25'    (1)
    cat <<EOF > config/transport_maps
example1.org    $smtp_relay
example2.org    $smtp_relay
example3.org    $smtp_relay
EOF

    # setup transport_maps
    ds exec postconf -e 'transport_maps=hash:/host/config/transport_maps'
    ds exec postmap /host/config/transport_maps

    # setup relay_domains
    ds exec postconf -e relay_domains=/host/config/relay_domains

    # reload postfix configuration
    ds exec postfix reload
}
__EOF__

cat cmd/relay-setup.sh
1 We assume that 10.214.77.201 is the internal (fixed) IP of the other mailserver.

In order to call this command automatically whenever we rebuild the container, we can override the command ds config like this:

cat <<'EOF' > cmd/config.sh
rename_function cmd_config standard_config
cmd_config() {
    standard_config
    ds relay-setup
}
EOF

cat cmd/config.sh