LinuxinfrastructureTested on real hardware

Make Proxmox Actually Email You: msmtp + Gmail Setup

Give your Proxmox node a real outbound mail path with msmtp and a Gmail app password — copy-paste setup, end-to-end test, and the bare-root bounce fix.

DistrosProxmox VE 9, Debian 13
Shellbash
Updated
Script
bash
#!/usr/bin/env bash
# proxmox-email-gmail-msmtp.sh — give ONE Proxmox node a working mail path
# through Gmail. Run as root, on the node itself. Deliberately interactive:
# the app password goes into an editor, never onto the command line.

# ==== MAKE THESE VALUES YOUR OWN ==========================================
GMAIL="you@gmail.com"     # the Gmail account that will do the sending
INBOX="you@gmail.com"     # where root's mail should land (usually the same)
# The Gmail APP PASSWORD is deliberately NOT a variable here — step 2 opens
# an editor so the secret never touches your shell history.
# ==========================================================================

# 1) Install msmtp plus its sendmail hook. Watch apt announce that it is
#    REMOVING postfix — that is expected, not a disaster. Both packages
#    provide the "mail-transport-agent" role, and Proxmox accepts either
#    (pve-manager depends on "postfix | mail-transport-agent").
apt update
apt install -y msmtp msmtp-mta

# 2) Write the config skeleton, lock it down to root-only BEFORE the
#    secret goes in, then open an editor and replace CHANGE_ME with your
#    16-character Gmail app password.
cat > /root/.msmtprc <<'EOF'
defaults
auth on
tls on
tls_starttls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
aliases /etc/aliases

account gmail
host smtp.gmail.com
port 587
from GMAIL_GOES_HERE
user GMAIL_GOES_HERE
password CHANGE_ME

account default : gmail
EOF
chmod 600 /root/.msmtprc
sed -i "s/GMAIL_GOES_HERE/$GMAIL/" /root/.msmtprc
nano /root/.msmtprc   # or your editor: swap CHANGE_ME for the app password

# 3) Catch mail addressed to bare "root" (cron does this constantly).
#    Gmail rejects a recipient with no domain, so map it to a real inbox.
echo "root: $INBOX" >> /etc/aliases
echo "default: $INBOX" >> /etc/aliases

# 4) Point Proxmox's own notifications at a real inbox too.
pveum user modify root@pam --email "$INBOX"

# 5) Test the direct path, then the system path Proxmox and cron use.
printf 'Subject: msmtp test from %s\n\nThe direct path works.\n' "$(hostname)" | msmtp "$INBOX"
printf 'To: root\nSubject: sendmail test from %s\n\nThe system path works.\n' "$(hostname)" | sendmail -t

# 6) Read the log — it records every send and every rejection.
tail -n 5 /var/log/msmtp.log

What this does

First: make these values your ownGMAIL and INBOX at the top of the script are placeholders, CHANGE_ME in the config is where your Gmail app password goes (via the editor, never the command line), and every you@gmail.com below follows the same rule. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy.

Here’s the embarrassing discovery that led to this playbook. After I taught my cluster to only email me about real problems, I went looking for those emails — and found out three of my four nodes had never successfully delivered a single message. The notification system was doing its job perfectly. The mail just never left the building, because a stock Proxmox VE node has no working route to the outside email world. Everything on a Linux box that wants to send mail — Proxmox’s notifications, cron, your own scripts — eventually calls one binary, /usr/sbin/sendmail, and on a fresh node that call quietly dead-ends in a local mailbox nobody reads.

This script rewires that one binary. msmtp is a tiny SMTP client, and its companion package msmtp-mta installs itself as the system’s sendmail — so anything that thinks it’s sending mail the old-fashioned way is actually handing the message to msmtp, which logs in to Gmail over TLS with a dedicated app password and lets Google do the delivering. Proxmox’s own documentation confirms the hook: its default email target “uses the sendmail binary to send emails” — which is now msmtp.

One binary, one pipe: how node mail reaches your phoneProxmox eventbackup FAILEDcron jobmails “root”your script| msmtp you@…/usr/sbin/sendmail→ symlink to msmtpreads /root/.msmtprcTLS :587smtp.gmail.comapp password logininboxEverything that wants to email funnels through sendmail — swap that one name for msmtp,and every sender on the node inherits a working Gmail path at once.Nothing new to teach cron or Proxmox — they never find out the mailman changed.

The payoff is bigger than backup alerts: once this pipe exists, anything on the node can reach you. The single daily digest email I’m writing up next — one 8:30am message that rolls up backups, disk space, and pending updates — rides exactly this path.

Prerequisites

  • A Proxmox VE node — or honestly any Debian-family box — with a root shell. Tested on Proxmox VE 9 (Debian 13 base).
  • A Gmail account with 2-Step Verification turned on. This is not optional: Google only offers app passwords on accounts with 2-Step Verification enabled — “To create an app password, you need 2-Step Verification on your Google Account,” per Google’s own documentation. Create one there before you start; it’s a 16-character code Google generates for you.
  • About ten minutes, most of which is apt.

Notes

  • apt will announce it is REMOVING postfix. Let it. I’ll admit I hovered over Ctrl+C the first time. But both packages claim the same mail-transport-agent role — msmtp-mta declares Provides, Conflicts, and Replaces on it, so installing one evicts the other by design. Proxmox genuinely doesn’t mind: pve-manager depends on postfix | mail-transport-agent, meaning any provider satisfies it, and the notification system’s sendmail target just calls whatever /usr/sbin/sendmail is. I checked the symlink on my own node after install: ls -l /usr/sbin/sendmail../bin/msmtp. The package planted it for me — no manual symlinking required.
  • The bare-root bounce is the gotcha that ate my cron mail. Cron addresses its mail to plain root — no domain. Gmail flatly rejects that with SMTP error 553 5.1.3: “The recipient address is not a valid RFC 5321 address,” per Google’s SMTP error reference. Step 3’s aliases file is the fix — msmtp replaces local recipients (any address without a domain part) using /etc/aliases, and the default: line catches every other local name. One honest wrinkle from the msmtp manual: aliasing rewrites the envelope only, so a delivered message may still display “To: root”. That’s cosmetic — it’s in your inbox, which is the point.
  • The app password is a real secret — handle it like one. It’s a 16-character credential that opens your mailbox for sending. The script’s whole choreography exists for it: the config file goes to chmod 600 before the password goes in, and the password arrives via your editor so it never lands in shell history or a process listing. If it ever leaks, revoke that one app password — your main password stays untouched, which is exactly why you use a dedicated one. Bonus fact from the same page: changing your main Google password auto-revokes existing app passwords, so if mail mysteriously stops months from now, that’s the first thing to check.
  • msmtp does not queue. Postfix would hold mail and retry if Gmail were unreachable; msmtp tries once, and a failure is a logged failure, not a deferred delivery. For a homelab this is usually a fine trade — the next backup or cron run generates fresh mail anyway — but it’s worth knowing what you gave up along with postfix. Silence still isn’t proof of health, which is why an external watcher like Uptime Kuma belongs alongside email alerts.
  • Cluster-wide settings, per-node delivery. This is the one that got me. The notification matchers live in cluster-wide config — set them once, done. But delivery happens on whichever node fires the event, using that node’s own mail setup. A cluster where one node has msmtp and three don’t is a cluster where three nodes’ failures go unreported. Run this playbook on every node — the config file is identical on each, so it’s the same ten minutes on repeat.
The trap: one configured node looks like a working clusternode1no mail pathnode2no mail pathnode3no mail pathnode4msmtp → Gmailthe wall — no route out✉ ✓Matchers are cluster-wide; mail paths are per-node. Three of these four failures die at the wall.
  • The log file is your proof, not your memory. Every attempt — success or rejection, with Gmail’s exact response — lands in /var/log/msmtp.log, one line per mail. A healthy send looks like this (a real line from my node, identifiers swapped for placeholders):
Aug 16 08:30:16 host=smtp.gmail.com tls=on auth=on user=you@gmail.com from=you@gmail.com recipients=you@gmail.com mailsize=15854 smtpstatus=250 smtpmsg='250 2.0.0 OK ... - gsmtp' exitcode=EX_OK
  • smtpstatus=250 and exitcode=EX_OK are the two fields that mean delivered to Gmail; a bounce shows the rejection code and message in the same place (this is where the 553 from the earlier note shows up). When I audited my own cluster, that log is how I learned what had actually been sent versus what I assumed had been. After any change here, send a test and read the log; “the command didn’t error” and “the mail arrived” are two different facts. (Mine has quietly grown since July — it’s a plain text file, so glance at its size when you’re in the neighborhood.)
  • Why not Proxmox’s built-in SMTP target instead? Fair question — the notification system can speak SMTP directly since the 8.x era, no msmtp involved. But that route carries only Proxmox notification events. The sendmail-symlink route carries everything: Proxmox events, cron output, and any script or tool that pipes to sendmail or msmtp — one pipe, one log, one credential to manage. If a second thing on your node ever wants to email you (it will), this is the setup that already handles it.
  • Ran it twice? The alias echos append, so a re-run duplicates those two lines in /etc/aliases. Harmless — first match wins — but tidy it if it bothers you. Everything else is safely re-runnable.
  • Tested for real: this exact recipe has been running in production on my own Proxmox VE 9 cluster since late July, delivering its backup-failure alerts and a daily report. Before publishing I re-verified the package relationships (Provides/Conflicts/Replaces: mail-transport-agent), the shipped sendmail symlink, and the pve-manager dependency on a live node, and every external claim above against the linked official docs.