Home server email, by way of Gmail
Sending PHP’s mail() through msmtp and a Gmail app password, when your ISP won’t let a home server send mail
My home server is allowed almost everything. My ISP lets SSH, HTTP and HTTPS in and out, so the websites I run from a Mac Mini on a shelf work just like hosted ones. Almost. The one thing it won’t let out is mail: the port that mail servers use to hand messages to each other is blocked for home connections. That’s fair! A home internet connection is exactly where a hijacked machine would start blasting spam from.
But my sites still need to send mail. Contact forms, password resets, the occasional “something broke” alert. So instead of running a mail server, I handed the whole job to one I already had: Gmail. Every message the server sends goes to Gmail first, signed in like any other email app, and Gmail delivers it from there.
Why msmtp
msmtp isn’t a mail server. It’s a small program that takes a message and passes it to a real mail server, then exits. There’s no service running in the background, no queue, and nothing listening for connections, so there’s nothing new for anyone to attack. It also stands in for sendmail, the command Linux programs traditionally hand their mail to, which means PHP and everything else on the box use it without knowing it’s there.
A full mail server like Postfix can do this too, with Gmail as its “relay host”. It’s just a lot more machinery for a server that only ever sends a handful of messages a day.
A word on the examples. Everything below is what’s running on my server: Debian 13 (trixie), Apache 2.4.69 with PHP 8.4 running as PHP-FPM, and msmtp 1.8.28. Package names and paths will differ on other distributions, and running PHP as an Apache module instead of through FPM changes Step 5 a little. The addresses use example.com, so swap in your own.
Step 1: a Gmail app password
msmtp signs in to Gmail like any email app would, but you don’t want your real Google password sitting in a file on a server. An app password is a separate 16-character password for one app or device. If it ever leaks, you revoke just that one, and your real password and every other device stay as they are.
- Turn on 2-Step Verification for the Google account, if it isn’t already. App passwords don’t exist without it.
- Go to myaccount.google.com/apppasswords and sign in.
- Give it a name you’ll recognize later, like “msmtp on my home server”, and create it.
- Copy the password straight away. Google shows it once, and if you lose it, you just make a new one.
If the page says the option isn’t available, it’s usually one of three things: 2-Step Verification uses security keys only, the account has Advanced Protection turned on, or it’s a work or school account whose administrator doesn’t allow app passwords. One more thing worth knowing up front: changing your Google password revokes every app password. If mail from the server stops one day, that’s the first thing to check.
Step 2: install msmtp
sudo apt install msmtp msmtp-mtamsmtp is the program itself. msmtp-mta is the part that makes it stand in for sendmail: it points /usr/sbin/sendmail at msmtp, which is what saves you from configuring PHP later.
$ ls -l /usr/sbin/sendmail
lrwxrwxrwx 1 root root 12 Apr 16 2025 /usr/sbin/sendmail -> ../bin/msmtpDuring the install, Debian asks whether to turn on msmtp’s AppArmor profile, which limits the files msmtp is allowed to read and write. I said no. The profile allows /etc/msmtprc and a log at /var/log/msmtp, but not the password file or the log name I use below, so it would quietly block both. If you want it on, keep your files inside what /etc/apparmor.d/usr.bin.msmtp allows, or add your paths to it.
Step 3: the password file and the config
The app password goes in a file of its own, so the config itself holds nothing secret. On my server PHP runs as www-data, and it has to read both files, so they belong to root with the www-data group, and nobody else can read them:
sudo mkdir /var/www/msmtp
sudo nano /var/www/msmtp/msmtp-gmail.pass # paste the app password, save
sudo chown -R root:www-data /var/www/msmtp
sudo chmod 750 /var/www/msmtp
sudo chmod 640 /var/www/msmtp/msmtp-gmail.passAn editor keeps the password out of your shell history, which an echo wouldn’t. I keep it under /var/www next to the sites, but make sure it’s not inside any site’s document root, or it could end up one URL away from the internet.
Then the config, in /etc/msmtprc:
# System-wide defaults
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
# Gmail account
account gmail
host smtp.gmail.com
port 587
from server@example.com
user you@example.com
passwordeval "cat /var/www/msmtp/msmtp-gmail.pass"
# Use this account as default
account default : gmailPort 587 is mail submission: an email app signing in to its own provider to send, rather than one mail server talking to another. That’s the difference my ISP cares about, and why this gets through when a mail server on the box wouldn’t. tls on encrypts the connection before the password is sent. user is the Google account you made the app password for, and from is the address messages say they’re from (more on that in Step 6). passwordeval runs a command and uses its output as the password, which is how the secret stays in its own file.
sudo chown root:www-data /etc/msmtprc
sudo chmod 640 /etc/msmtprcStep 4: a log PHP can write to
msmtp writes one line per message to its log: who it went to, and what Gmail said back. It’s the first place to look when a contact form goes quiet, and it’s easy to set up so that PHP can’t write to it. If you send your first test as root, msmtp creates the log owned by root, and later messages sent by www-data can’t add their line. Create it up front with the same group as everything else:
sudo touch /var/log/msmtp.log
sudo chown root:www-data /var/log/msmtp.log
sudo chmod 660 /var/log/msmtp.logIt grows by one short line per message, so on a quiet server it can go a long time without rotation. On a busy one, give it a logrotate rule that recreates it with the same owner and mode.
Step 5: test it, then point PHP at it
Send a test from the command line first, then the same thing as www-data, because that’s who PHP will be:
printf 'To: you@example.com\nSubject: msmtp test\n\nIt works.\n' | msmtp -t
sudo -u www-data sh -c "printf 'To: you@example.com\nSubject: msmtp test as www-data\n\nStill works.\n' | msmtp -t"
tail -n 2 /var/log/msmtp.logA good send ends its log line with smtpstatus=250 and exitcode=EX_OK. A 535 means Gmail turned the login down: check the user line and the app password, and whether the Google password has changed since you made it.
Now PHP. Its mail() function doesn’t send mail itself. It hands each message to the command in its sendmail_path setting, which on Debian defaults to /usr/sbin/sendmail -t -i. Thanks to msmtp-mta, that’s already msmtp, so on my server PHP needed no changes at all. Check the copy of PHP your sites actually use, though. PHP-FPM reads its own settings, separate from the command line’s:
php-fpm8.4 -i | grep sendmail_path
# sendmail_path => /usr/sbin/sendmail -t -i => /usr/sbin/sendmail -t -iIf yours says something else, or you’d rather not rely on the link, set it directly in a file of its own and restart PHP-FPM:
; /etc/php/8.4/fpm/conf.d/99-msmtp.ini
sendmail_path = "/usr/bin/msmtp -t"sudo systemctl restart php8.4-fpm # mod_php instead of FPM? restart apache2Then a real mail() call, as the web server’s user:
sudo -u www-data php -r 'var_dump(mail("you@example.com", "PHP test", "Sent by mail(), through msmtp."));'bool(true) and a new line in the log mean PHP is done. Be aware that true only means msmtp accepted the message. The log line is what tells you Gmail did.
Step 6: who the mail says it’s from
Gmail is picky about the From address, and it has a right to be. Unless the address is the account you signed in with, or one you’ve added to that account under Settings › Accounts › Send mail as, Gmail replaces it with the account’s own address. So if you want messages from a server@ or no-reply@ address on your own domain, add it there first.
For contact forms, there’s a neat trick that keeps all of this simple. Send the message from your own address and put the visitor’s address in Reply-To. You never pretend to be someone you’re not, so nothing gets flagged as spoofed, and hitting Reply in your inbox still writes straight back to them:
$headers = [
'From: Contact form <server@example.com>',
'Reply-To: ' . $visitorEmail, // validated with filter_var(..., FILTER_VALIDATE_EMAIL)
'Content-Type: text/plain; charset=UTF-8',
];
mail('you@example.com', 'New message from the site', $message, implode("\r\n", $headers));Validate that address before it goes anywhere near a header. A visitor who can put a line break in it can add headers of their own.
What this is good for, and what it isn’t
This is the right size for a server that sends a few messages a day: contact forms, password resets, alerts. Gmail limits how much one account can send each day, and mail that all leaves through one personal account isn’t what you want for newsletters or anything bulk. That needs a proper sending service.
And if the server ever stops sending, the checklist is short: is there a fresh line in /var/log/msmtp.log, what does it say, and has anyone changed the Google password lately?
Need a hand?
If your server can’t send mail and you’d rather not wrestle with it, I do this kind of work. Tell me what you’re running.