Debian 10: System monitoring using e-mail (Exim as a smarthost)

Recently, IT infrastructure monitoring tools have been springing up like mushrooms after a rain. But let’s take a step back and look at a traditional and very basic way to monitor a system – using e-mail. Yes, you heard right; that internet app invented in the 1960s!

For some events and incidents on a Debian system, the superuser of the system is still, by default, informed via e-mail. For example, mdadm shoots an e-mail to root when there is a software RAID array degradation. But by default, these e-mails are just dumped to a mail spool file, never inspected. This is really bad when your hard disk just died, and when you were never informed that the other one in the RAID array died too just a couple of months ago!

It has been 8 years since I wrote last about how to configure my favorite Mail Transport Agent, Exim. Back then, as an e-mail service provider, I wanted to run Exim as a production MTA; receiving, sending, and relaying e-mail in the internet. It was a daunting task, to say the least.

This time, however, I just want to run Exim locally, behind a firewall. It should receive locally submitted e-mail, and rather than dump the e-mails to a local mail spool file, forward it to a remote e-mail address, where I can read it on my phone. This mode of an MTA is called smarthost.

A smarthost is a local e-mail MTA/server which receives SMTP messages to any e-mail address locally, and forwards them as an authenticated client to the next MTA (that is why it must be “smart” – it must have the right credentials). The submission must be authenticated (submitting username and password), because it is unlikely that your local IP address from your ISP is not in any e-mail blacklist. And internet-facing MTAs will refuse to accept e-mails from such IP addresses.

It was not so straightforward to accomplish this, as always. But it is totally doable, and in a short time, too. This blog post documents the steps for Debian 10. They should continue to work in later Debian versions.

You will need:

  • A working e-mail account at an e-mail provider of your choice (SMTP server FQDN, port number, username and password). It is advisable that you generate a dedicated username and password. Do not use your primary username and password! If the credentials leak or are stolen (they will be stored on the local hard drive, readable only by root), then you can simply disable the affected e-mail account without further impact. Furthermore, for security, we are going to require that the SMTP server offers a port featuring TLS-on-connect, as suggested by RFC-8314 (“cleartext considered obsolete”).
  • Debian 10 (newer should work too, because the fundamentals rarely change)
  • Internet connection
  • 30 minutes of time
  • root access. Do all the following steps as root.

First, make sure that the FQDN of the SMTP server of your e-mail provider is not a DNS alias, otherwise domain matching inside of Exim will not work (Exim often uses reverse-DNS lookups to find actual FQDNs). Check this by entering:


If this prints "" is an alias for "", then resolve all aliases and choose the final FQDN. Another way to get the final FQDN is by doing a reverse DNS lookup on the server’s IP address, e.g. by running:

dig -x <ip address>

Use the printed FQDN to the right of “PTR”. In our example, this is

Now, let’s actually start by installing Exim (exim4-daemon-light is enough):

apt install exim4

Next, configure exim4:

dpkg-reconfigure exim4-config

At the prompts, do the following:

  1. Select the option “mail sent by smarthost; no local mail”
  2. For “System mail name” leave the pre-filled hostname or PQDN
  3. “IP-addresses to listen on”: leave just “”. We’re not going to use IPv6 right now.
  4. “Other destinations for which mail is accepted”: leave the pre-filled hostname or PQDN.
  5. “Visible domain name for local users”: leave the pre-filled hostname or PQDN.
  6. For “IP address or host name of the outgoing smarthost” enter the final FQDN which you have found previously, plus a double colon ::, plus the port number. For example:
  7. “Keep number of DNS-queries minimal”: leave the default “No”.
  8. “Split configuration into small files”: leave the default “No”.

Next, generate certificates for Exim by running the command below. These will be used for the TLS connections. To test, you can leave defaults for all the questions which the following command asks you. Later, you could upgrade to more professional certificates:


Next, define the recipient e-mail address as an alias of the system user root. Add the following line to /etc/aliases:


The remote SMTP server may reject mail without a proper “From:” address. Usually, the server expects the address in the “From:” field to have the same domain name as the MTA itself. For this reason, add the following line to /etc/email-addresses.


Next, add credentials for the SMTP submission to the file /etc/exim4/passwd.client in the format <FQDN>:<username>:<password> For example:

The default configuration of Exim shipped in the Debian package is excellent and very flexible. But TLS is not enabled by default, and so we still need to make a few adaptations to the default config file.

Add the following lines to /etc/exim4/exim4.conf.localmacros. Create the file if it doesn’t exist:

# Enable TLS

# Require TLS for all remote hosts (STARTTLS or TLS-on-connect)

# Require TLS-on-connect for RFC-8314

Next, make the following adaption to the file /etc/exim4/exim4.conf.template. After .ifdef REMOTE_SMTP_SMARTHOST_HOSTS_REQUIRE_TLS.endif add the following:


Run update-exim4.conf and systemctl restart exim4. The final configuration file is written to /var/lib/exim4/config.autogenerated.

Testing with an Exim test instance

To test your setup, you can start a test instance of Exim in the local root console, listening on port 26. It will use the same configuration file, but runs in parallel and independently from the already running Exim daemon. Run as root:

exim -bd -d -oX 26

Then, you can use swaks (from the swaks Debian package) to send a test e-mail to the test instance. The output will be very verbose, so you can easily debug:

swaks --from root@localhost --to root@localhost --port 26

To clarify: This will send an e-mail to the address looked up at “root” in the file /etc/aliases (in our case, The “From:” header address will be looked up at “root” in the file /etc/email-addresses (in our case,

See if you got the e-mail in the target inbox. If yes, then testing with the production Exim daemon should work too.

Testing with the Exim daemon

Observe the output of …

tail -f /var/log/exim4/mainlog

… and then again send a test e-mail using swaks, but this time to the default port 25:

swaks --from root@localhost --to root@localhost

If you got the e-mail, then congratulations! You will now receive e-mails directed at the local root user. You can easily extend this for other, unprivileged users of the system.

To test the entire chain and make sure that you will always be informed by important events, you could send a test e-mail in periodic intervals. But, this is a topic for a future blog post!


You could wrap the above swaks command in a shell script to send e-mail from other scripts. For example:

swaks --from root@localhost --to root@localhost --header "Subject: [$(hostname)] $1" --body "Body: $2"

Warning: You could also use the older tools sendmail or mail to write a similar script, but both directly call exim4 under the hood, which is fine as long as you don’t call such a script from a systemd unit (e.g. a service or a timer). Because seemingly, Exim, when called from the command line to submit e-mail, forks and detaches a short-lived process in order to deliver the e-mail to the target MTA. But systemd kills all sub-processes as soon as the main process exits. The process lives just long enough that the e-mail is put into Exim’s queue, but it is never executed. swaks on the other hand delivers the mail to Exim via SMTP and the delivery process is not subject to be killed by systemd.

You should also rate-limit Exim to protect against DoS attacks, but this is also a topic for a future blog post!

Save the (DOM) trees! Why direct DOM manipulation is not a bad idea

A generic, and so non-binary, unsorted, some labels duplicated, arbitrary diagram of a tree.
A generic, and so non-binary, unsorted, some labels duplicated, arbitrary diagram of a tree. CC BY-SA 4.0

Direct DOM manipulation has gotten a bad reputation in the last decade of web development. From Ruby on Rails to React, the DOM was seen as something to gloriously destroy and re-render from the server or even from the browser. Never mind that the browser already exerted a lot of effort parsing HTML and constructing this tree! Mind-numbingly complex HTML string regular expression tests and manipulations had to deal with low-level details of the HTML syntax to insert, delete and change elements, sometimes on every keystroke! Contrasting to that, functions like createElement, remove and insertBefore from the DOM world were largely unknown and unused, except perhaps in jQuery.

Processing of HTML is destructive: The original DOM is destroyed and garbage collected with a certain time delay. Attached event handlers are detached and garbage collected. A completely new DOM is created from parsing new HTML set via .innerHTML =. Event listeners will have to be re-attached from the user-land (this is no issue when using on* HTML attributes, but this has disadvantages as well).

It doesn’t have to be this way. Do not eliminate, but manipulate!

Save the (DOM) trees!

sanitize-dom crawls a DOM sub-tree (beginning from a given node, all the way down to its ancestral leaves) and filters and manipulates it non-destructively. This is very efficient: The browser doesn’t have to re-render everything; it only re-renders what has been changed (sound familiar from React?).

The benefits of direct DOM manipulation:

  • Nodes stay alive.
  • References to nodes (i.e. stored in a Map or WeakMap) stay alive.
  • Already attached event handlers stay alive.
  • The browser doesn’t have to re-render entire sections of a page; thus no flickering, no scroll jumping, no big CPU spikes.
  • CPU cycles for repeatedly parsing and dumping of HTML are eliminated.


I just released version 4 of my sanitize-dom library which is my tool of choice to sanitize user-generated content. As mentioned in this post, my project takes a different approach: It doesn’t work with HTML, it operates only on DOM nodes.

sanitize-doms further advantages:

  • No dependencies.
  • Small footprint (only about 7 kB minimized).
  • Faster than other HTML sanitizers because there is no HTML parsing and serialization.

Check out sanitize-dom here on Github!

Friction-less login to a tmux session on a suspended host

The Skating Minister by Henry Raeburn – Friction-less! (public domain)

I recently found that more and more I’m using technical solutions only when they are friction-less (I guess that is not that unusual generally, but a bit more unusual for me). This includes my work on a remote machine, which is suspended most of the time, and for which I’ve enabled WOL (Wake-on-LAN, see my related blog post on how I accomplished this).

I wanted to create a way to log into this machine regardless if it is suspended or not, and continue working where I left off (i.e. not losing any state even when it goes to sleep or if the network is interrupted). I’m usually doing complex work in several shell sessions with several files open at once, several processes running at once, etc.

Below, you’ll find a shell script to accomplish this. You can customize the host name and the host’s MAC address directly in the script, and then call it without arguments.

What does the script do?

It will first wake the machine up using wakeonlan . If it is already awake, then wakeonlan will not have any effect, and all is fine. If it is sleeping, then it will come online in a few seconds. Using an exponential back-off strategy, I’m polling the availability of the SSH port 22. If the port comes online, we know that SSH is ready to receive the login. I’m then trying to attach to an already existing tmux session where I can continue to work where I left off after the last ssh disconnection (simulate this by typing sequentially Enter ~ .) or tmux detachment (Ctrl+b d), and if there is no running tmux session, then it will start a new tmux session, which will also keep me logged in using the shell configured for my user (bash or zsh).

And without further ado, here is the script:


# Copyright 2020 Michael Karl Franzl

# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
# associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.


set -e


echoerr() { printf "%s\n" "$*" >&2; }

wakeonlan ${host_mac}

# exponential backoff strategy for the login
for i in 1 2 4 8 16; do
  if [ $i -ge 16 ]; then
    echoerr "Host ${host_name} did not wake up. Giving up."
    exit 1

  echo "Waiting until host ${host_name} is awake..."
  nc -z ${host_name} 22 && break
  sleep $i

echo "Host ${host_name} is now awake. Logging in..."
ssh -t ${host_name} "tmux a || tmux"