Sending Mail From a Server: msmtp, Postfix Relay and Cron Alerts

Sending Mail From a Server: msmtp, Postfix Relay and Cron Alerts

You want a cron job to email you when a backup fails. Running a mail server for that is badly disproportionate, and our self-hosted email post explains why receiving mail is genuinely hard. Sending is not, provided you relay through somebody who already does it.

Why relaying rather than sending directly

A server sending mail straight to recipients gets filtered, because it looks like every spam source:

  • The IP has no reputation, or a bad one if it is in a cloud range
  • No reverse DNS matching the sending domain
  • No SPF record authorising it
  • No DKIM signature
  • Cloud providers block port 25 outbound as a matter of policy

Relaying through a provider whose domain is already authenticated sidesteps all of it. The mail arrives from an address with a reputation, and you configured one file.

msmtp, the minimal option

sudo apt install msmtp msmtp-mta      # Debian and Ubuntu
sudo dnf install msmtp                # Fedora
sudo pacman -S msmtp                  # Arch

msmtp-mta is the important second package. It provides /usr/sbin/sendmail as a symlink, which is what cron, mail, logwatch, fail2ban and most other software look for.

# system wide
sudo tee /etc/msmtprc <<'EOF'
defaults
auth           on
tls            on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile        /var/log/msmtp.log

account        default
host           smtp.example.com
port           587
from           server@example.com
user           server@example.com
passwordeval   "cat /etc/msmtp-password"
EOF

sudo chmod 600 /etc/msmtprc
sudo chown root:root /etc/msmtprc
# the password, separately
printf '%s' 'your-app-password' | sudo tee /etc/msmtp-password >/dev/null
sudo chmod 600 /etc/msmtp-password

msmtp refuses to run if the config is group or world readable, which is a genuinely useful guard rather than an annoyance. If it complains about permissions, it is right.

passwordeval runs a command to obtain the password, which is better than password inline because the secret lives in its own file. It can also pull from a password store:

passwordeval   "pass show servers/smtp"
passwordeval   "gpg --batch -d /etc/msmtp-password.gpg"

Our pass guide covers the first, though note that a password store needing a passphrase does not work unattended, which is the whole point of a server. An age or gpg file with a key on the machine is the usual compromise, and our secrets management guide covers the trade-off.

Test it

printf 'Subject: test from %s\n\nIt works.\n' "$(hostname)" \
  | msmtp -a default you@example.com

# verbose, when it does not
printf 'Subject: test\n\nbody\n' | msmtp --debug you@example.com

tail /var/log/msmtp.log

--debug prints the entire SMTP conversation, which tells you whether authentication failed, TLS failed, or the server rejected the sender. Those are three different fixes.

Cron

crontab -e
MAILTO=you@example.com
PATH=/usr/local/bin:/usr/bin:/bin

0 3 * * * /usr/local/bin/backup.sh
*/5 * * * * /usr/local/bin/check-disk.sh

Cron mails you only when a job produces output. A silent success sends nothing, which is exactly right: a job running every five minutes that mails on success is a job you will filter into a folder and never read.

So write scripts that are quiet when they work and loud when they fail:

#!/usr/bin/env bash
set -euo pipefail

if ! output=$(restic backup /data 2>&1); then
    printf 'backup failed on %s\n\n%s\n' "$(hostname)" "$output"
    exit 1
fi

Output on failure, nothing on success, non-zero exit. Our bash error handling guide covers set -euo pipefail, and our cron guide covers why PATH needs setting there.

MAILTO="" disables mail for that crontab, which is how you silence a noisy job without editing it.

systemd timers, which do not mail

A difference that surprises people migrating from cron: systemd timers do not email anything. Output goes to the journal.

That is better for most purposes, and it means you need an explicit notification path for failures.

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
OnFailure=notify-failure@%n.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/notify-failure@.service
[Unit]
Description=Mail about %i failing

[Service]
Type=oneshot
ExecStart=/usr/local/bin/notify-failure %i
#!/usr/bin/env bash
# /usr/local/bin/notify-failure
unit="$1"
{
    printf 'Subject: FAILED: %s on %s\n\n' "$unit" "$(hostname)"
    systemctl status --full --no-pager "$unit" || true
    printf '\n--- last 50 log lines ---\n'
    journalctl -u "$unit" -n 50 --no-pager
} | msmtp you@example.com

OnFailure= with a templated unit is the idiomatic systemd approach, and it gives you the status and the logs in the mail rather than just a notification that something broke. Our systemd timers guide and service files guide cover the units.

Postfix as a relay

Use Postfix instead of msmtp when you want queueing. If the relay is unreachable, msmtp fails and the message is gone; Postfix holds it and retries.

sudo apt install postfix
# choose "Satellite system" at the prompt
# /etc/postfix/main.cf
myhostname = server.example.com
relayhost = [smtp.example.com]:587
inet_interfaces = loopback-only
inet_protocols = ipv4

smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt

# rewrite local addresses to something deliverable
sender_canonical_maps = regexp:/etc/postfix/sender_canonical
smtp_generic_maps = regexp:/etc/postfix/generic
echo '[smtp.example.com]:587 server@example.com:your-app-password' \
  | sudo tee /etc/postfix/sasl_passwd
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd

echo '/.+/  server@example.com' | sudo tee /etc/postfix/sender_canonical
echo '/.+/  server@example.com' | sudo tee /etc/postfix/generic
sudo postmap /etc/postfix/sender_canonical /etc/postfix/generic

sudo systemctl restart postfix

inet_interfaces = loopback-only is the security-relevant line. It stops Postfix listening for incoming connections, which is what you want on a send-only host. Without it you have a mail server on the internet, and an open relay is worse than no mail at all.

The canonical and generic maps rewrite root@server.localdomain into your real address. Relays reject mail from addresses they do not recognise, and this is the most common reason a relay setup fails after the credentials are correct.

# queue inspection, the reason you chose Postfix
mailq
postqueue -f              # retry now
postsuper -d ALL          # discard everything, carefully
tail -f /var/log/mail.log

Providers

ProviderHost and portNotes
Gmailsmtp.gmail.com:587App password required, sending limits
Fastmailsmtp.fastmail.com:587App password
Proton127.0.0.1:1025Needs the local bridge
Amazon SESemail-smtp.<region>.amazonaws.com:587Separate SMTP credentials
Postmark, Mailgun, ResendProvider specificBuilt for this

Use an app-specific password, never your account password. It can be revoked without changing anything else, and it usually cannot read your mail.

For personal alerts, your own mail provider is fine. For anything sending to other people, a transactional provider handles reputation, bounces and deliverability as their product, and the free tiers cover a small server comfortably.

Port 465 with tls_starttls off is worth trying when 587 is blocked, which some networks do.

The address mail comes from

# route root's mail to you
sudo tee -a /etc/aliases <<'EOF'
root: you@example.com
EOF
sudo newaliases

Plenty of software mails root by default, and root@yourhostname is not deliverable anywhere. This one line catches all of it.

For msmtp, the equivalent is an alias file:

# /etc/aliases, with msmtp
aliases /etc/aliases
root: you@example.com
default: you@example.com

Testing properly

# does the tooling find a sendmail
ls -l /usr/sbin/sendmail
which sendmail mail mailx

# send as the software would
echo "test body" | mail -s "test subject" you@example.com

# as a specific user, since cron runs as the job owner
sudo -u www-data sh -c 'echo test | mail -s "as www-data" you@example.com'

# simulate a cron job producing output
sudo tee /etc/cron.d/mailtest <<'EOF'
MAILTO=you@example.com
* * * * * root echo "cron can mail"
EOF
# then remove it

Test as the user that will actually send. A system-wide msmtp config with mode 600 owned by root is unreadable by www-data, so mail from a web application fails while your test as root succeeds. Either widen the group carefully or give that user its own ~/.msmtprc.

Check /var/log/msmtp.log or /var/log/mail.log rather than assuming silence means success.

Frequently Asked Questions

What is the simplest way to send email from a Linux server?

msmtp configured as a relay to an existing mail provider. It is a single small program with one config file, it provides a sendmail compatible interface so cron and other tools find it, and it does not listen on any port. For sending only, it is all you need.

Why does mail from my server go to spam?

Because a residential or cloud IP address with no SPF, DKIM or reverse DNS record looks exactly like a spam source. Relaying through a provider whose domain is already authenticated solves it, since the mail then comes from an address with a reputation rather than from your machine.

Do I need Postfix if I only want to send mail?

No. Postfix is a full mail transfer agent that listens for incoming mail and manages a queue, which is more than sending requires. Its advantage is queueing: if the relay is unreachable, Postfix retries later, whereas msmtp fails immediately unless you add a queue wrapper.

How do I get cron to email me its output?

Set MAILTO at the top of the crontab and make sure a sendmail compatible binary exists, which msmtp provides. Cron only sends mail when a job produces output, so a silent job sends nothing, and that is the behaviour you want for anything that runs every minute.

Is it safe to put my email password in a config file?

Only with the file locked down, and msmtp refuses to run if it is group or world readable, which is a useful guard. Use an application specific password rather than your account password so it can be revoked on its own, and better still have msmtp read the secret from a password store at send time.

Can I use Gmail to relay server mail?

Yes, with an app password rather than your account password, since plain password login is not accepted. Providers also apply sending limits and may treat a new server as suspicious. A transactional mail provider is a better fit for anything beyond occasional personal alerts.