I have a local mail server with Postfix and Dovecot — I use it as a local SMTP smart host, and “notification center”.

While gathering information for this post; I discovered that my file server have been sending me emails, warning me of S.M.A.R.T. errors on a disk in the ZFS pool. Every day — since September!

So I’ve included a section on how to avoid missing important server emails.

Table of contents

The setup

There are several advantages with having a local smart host; it will handle authentication and encryption for upstream SMTP servers, so local clients don’t have to. This makes it a easy to send emails from simple devices that often have limited configuration options for email.

Switching upstream SMTP server only requires configuration changes on the smart host. I’m currently using my ISP SMTP server. But I’ve also used AWS SES and Mailgun in the past.

By setting up IMAP and a local email account; I’m using the server as a “notification center”. I like getting email about failing cron jobs, and other warnings and notifications, but not in my primary inbox. Instead they get delivered locally, and I use NeoMutt to read them 🙂

Postfix

First things first — installing Postfix:

$ sudo apt-get install postfix

Let’s have a look at the configuration in /etc/postfix/main.cf, here are the important lines:

myhostname = mail.lan.uctrl.net
myorigin = /etc/mailname
mydestination = $myhostname, localhost.lan.uctrl.net, localhost, mail.public-domain.net
relayhost = [smtp.altibox.no]
mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128 10.0.0.0/8 192.168.1.0/24
  • myhostname: Host name of this mail system
  • myorigin: Domain name that locally-posted mail appears to come from
  • mydestination: List of domains that are delivered locally
  • relayhost: The next-hop destination for non-local mail
  • mynetworks: Clients allowed to relay mail
Read more about these, and other settings, in the Postfix manual.

Looking at /etc/mailname, we can see that it also contains our host name:

mail.lan.uctrl.net

So mail sent from this machine will come from user@mail.lan.uctrl.net

Next — let’s look at /etc/aliases:

postmaster: root
root:       hebron
hebron:     thomas
newsletter: :include:/etc/mail/newsletter.list

This lists aliases for local users, but can also forward mail to external addresses, or contain lists. In the file above; mail sent to postmaster, root, or hebron — will be delivered to the local user thomas. Which we will set up later.

The newsletter list, simply contains email addresses:

user1@example.com
user2@example.com

If we do any changes to the alias file, we will need to initialize the database:

$ sudo newaliases

In my configuring — I’m just using the ISP provided SMTP server, which doesn’t require any authentication. More configuration is needed if the SMTP server uses authentication and encryption. I’ve explained how to integrate Amazon SES with Postfix in a previous post.

Right — now that we have configured Postfix, let’s restart it and send a test mail:

$ sudo systemctl reload postfix

$ sudo apt install mailutil
$ echo "Mail body" | mail -s "Mail subject" thomas@mydomain.net

To verify that Postfix is accepting connections on post 25, you can use telnet:

$ telnet mail.lan.uctrl.net 25

Trying 192.168.1.x...
Connected to mail.lan.uctrl.net.
Escape character is '^]'.
220 mail.lan.uctrl.net ESMTP Postfix (Ubuntu)
If you have a publicly available domain in mydestination, port 25 forwarded to the mail server, and an MX record for the public domain — you can receive external emails on your server.

Generic maps

We now have a local SMTP server that will receive local emails, and send non-local emails to the next SMTP server. But there is a problem, local email addresses like user@localdomain isn’t ideal when sending to external addresses. It looks weird and the email might get rejected. We need to rewrite the sender — enter generic maps.

Optional lookup tables that perform address rewriting in the Postfix SMTP client — Postfix manual

In our /etc/postfix/main.cf file, we add the following line:

smtp_generic_maps = regexp:/etc/postfix/generic_maps

My generic map file looks like this:

/^(.*)@(.*)\.lan\.uctrl\.net$/     thomas+${1}.${2}@mail.public-domain.net

For the changes to take effect, we need to reload Postfix:

$ sudo systemctl reload postfix

Using regex; it rewrites the email sender address:

user@host.lan.uctrl.net --> thomas+user.host@mail.public-domain.net

The sender address is now publicly valid, but the local origin is not lost 🙂

You may need to add or update your SPF record, to allow your upstream SMTP server to send emails for your public domain.

Using the Altibox SMTP server, I added the following SPF record to my mail.public-domain.net domain:

v=spf1 include:_spfsoft.services.altibox.net ~all

Header checks

As I wrote in the introduction — I’m using locally delivered emails for server notifications. But some emails are so important, that I do want them delivered to my main inbox. And for that we can use header checks:

Postfix built-in content inspection — Postfix manual

  • BCC user@domain: Add the specified address as a BCC recipient
  • REDIRECT user@domain: Write a message redirection request to the queue file

In our /etc/postfix/main.cf file, we add the following line:

header_checks = regexp:/etc/postfix/header_checks

My header check files looks like this:

/^Subject: SMART error*/
    BCC thomas@mydomain.net

For the changes to take effect, we need to reload Postfix:

$ sudo systemctl reload postfix

Now all emails where the subject starts with SMART error, will be blind copied to my main inbox 👍

Dovecot

With Postfix all configured — it’s time for the IMAP server; Dovecot:

$ sudo apt install dovecot-imapd

After installing it — we need to enable the IMAP protocol. /usr/share/dovecot/protocols.d/imapd.protocol should look like:

protocols = $protocols imap

Let’s restart Dovecot and test it:

$ sudo systemctl restart dovecot.service

$ telnet localhost imap
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ STARTTLS AUTH=PLAIN] Dovecot (Ubuntu) ready.
To configure Dovecot with SSL — take a look at /etc/dovecot/conf.d/10-ssl.conf, and enable the imaps protocol. I’m only using mine locally, so I haven’t set up encryption.

Lastly we need to create the user that will receive the local emails:

$ sudo useradd --create-home -s /sbin/nologin thomas
$ sudo passwd thomas

Now we can log into IMAP with the user thomas and our chosen password 👍

Note that for the email address thomas@localdomain to work — the local domain must have an MX DNS record pointing to the email server, or an A record pointing to the receiving host.

Ansible

Our local SMTP/IMAP server is ready. But we need to instruct all other clients to use this smart host when sending email — one easy way to do that is with Ansible.

Folder structure:

.
├── postfix.yml
└── roles
    └── postfix
        ├── defaults
        │   └── main.yml
        ├── handlers
        │   └── main.yml
        ├── tasks
        │   └── main.yml
        └── templates
            └── aliases

postfix.yml

We don’t want to run this playbook on the mail server itself, so we exclude it.

- hosts: all !mail.lan.uctrl.net
  vars:
    postfix_conf:
      relayhost: "[mail.lan.uctrl.net]"
      myhostname: "{{ inventory_hostname }}"
      myorigin: "$myhostname"
      mydestination: "$myhostname, localhost"
    ansible_python_interpreter: /usr/bin/python3
  roles:
    - postfix

defaults/main.yml

---
# Postfix configuration dictionary, e.g.:
# postfix_conf:
#   relay_domains: "$mydestination"
#   relay_host: "example.com"
#
postfix_conf: {}

# Whether to run 'postfix check' before it's started
postfix_check: true

handlers/main.yml

---
- name: check postfix
  become: true
  command: postfix check
  when: postfix_check
  listen: check restart postfix

- name: restart postfix
  become: true
  service: name=postfix state=restarted
  listen: check restart postfix

tasks/main.yml

---
- name: Install Postfix
  become: true
  package: name=postfix state=latest

- name: Enable Postfix
  become: true
  service: name=postfix state=started enabled=yes

- name: Install Mailutils
  become: true
  package: name=mailutils state=latest

- name: Copy aliases
  become: true
  template:
    src: "aliases"
    dest: "/etc/aliases"

- name: Generate new alises
  become: true
  command: newaliases

- name: Configure Postfix
  become: true
  command: "postconf -e \"{{ item.key }}={{ item.value }}\""
  ignore_errors: true
  notify: check restart postfix
  with_dict: "{{ postfix_conf }}"

templates/aliases

# See man 5 aliases for format
postmaster:	root
root:		hebron
hebron:		thomas@mail.lan.uctrl.net

Wrapping it up

I always configure email delivery on my servers and containers, and set up forwarding to the local account on the mail server. It’s just easier to have everything in one place. Obviously I need to check this inbox more often, as I had missed important warnings of a failing disk 😛

Playing with emails locally is also a good way to learn about the workings of email 👍

Last commit 2023-12-25, with message: replace emoji slight_smile/slightly_smiling_face