The mail command is the oldest and most portable way to send an email from a shell. It is what cron uses, what countless monitoring scripts call, and what the Postfix guides in this section use for quick tests:
echo "Backup finished" | mail -s "backup: OK" [email protected]
The catch is that mail is not one program. Several unrelated implementations install a binary called mail (and mailx), and the flags beyond -s are not compatible. The most common way people get bitten is attachments: -a attaches a file in one implementation and adds a header in another.
Which mail do I have?¶
| Implementation | Typically found on | Package | Attach a file | -a means |
|---|---|---|---|---|
| s-nail (successor to Heirloom mailx) | RHEL 9+, Fedora, Arch, Alpine | s-nail |
-a FILE |
attach |
| Heirloom mailx | RHEL/CentOS 7–8 | mailx |
-a FILE |
attach |
| GNU Mailutils | Debian/Ubuntu (mailutils, pulled in by Postfix's suggested packages) |
mailutils |
-A FILE |
add a header |
| bsd-mailx | Debian/Ubuntu (alternative) | bsd-mailx |
not supported | add a header |
Find out which one is installed:
readlink -f "$(command -v mail)" # resolves the alternatives symlink chain
mail --version 2>/dev/null | head -1 # GNU Mailutils answers this
mail -V 2>/dev/null # s-nail answers this
On Debian/Ubuntu, update-alternatives --display mailx shows which provider currently owns mailx.
The classic mistake.
mail -s "VPN" -a archive.zip [email protected]attachesarchive.zipwith s-nail or Heirloom mailx. With GNU Mailutils it tries to add a header line literally readingarchive.zip, and with bsd-mailx it does the same. The message is sent with no attachment and often no error. On Mailutils the attach flag is-A.
Everyday sending¶
The common subset works the same in every implementation:
# Body from stdin
echo "Disk usage at 91%" | mail -s "disk alert: $(hostname -s)" [email protected]
# Body from a file
mail -s "Nightly report" [email protected] < /var/tmp/report.txt
# Body from a heredoc
mail -s "Deploy complete" [email protected] <<EOF
Version: ${VERSION}
Host: $(hostname -f)
Time: $(date -Is)
EOF
# Multiple recipients, Cc and Bcc
mail -s "Status" -c [email protected] -b [email protected] [email protected] [email protected] < status.txt
# Pipe command output straight in
df -h | mail -s "df on $(hostname -s)" [email protected]
Setting the From address¶
By default the sender is $USER@$(hostname) after rewriting by the local MTA (myorigin in Postfix). To override it:
# s-nail / Heirloom mailx / GNU Mailutils
mail -r "Backups <[email protected]>" -s "backup: OK" [email protected] < log.txt
-r sets both the From: header and the envelope sender, which is what most smart hosts check. If your relay only accepts specific senders (Gmail, Proton SMTP submission, Mailgun domains), this is how you satisfy it from a script. For a null client, a cleaner fix is to rewrite addresses centrally with smtp_generic_maps (see Relaying Outbound Mail), so scripts don't have to care.
Attachments¶
# s-nail / Heirloom mailx
echo "Report attached." | mail -s "Q3 report" -a report.pdf -a data.csv [email protected]
# GNU Mailutils (capital -A; repeat for multiple files)
echo "Report attached." | mail -s "Q3 report" -A report.pdf -A data.csv [email protected]
# Anything with Mutt installed (portable fallback; note the -- before recipients)
echo "Report attached." | mutt -s "Q3 report" -a report.pdf data.csv -- [email protected]
bsd-mailx cannot attach files. Install Mutt or s-nail, or build the MIME message yourself with sendmail -t (below).
Sending something sensitive¶
Email is not a secure channel. Sending an encrypted archive is reasonable, but send the passphrase out of band (phone, Signal, in person), never in the same email or a follow-up email. Better still, encrypt to the recipient's key (gpg --encrypt --recipient … or age -r …) so no shared password exists at all. Password-protected ZIP files use weak encryption in many tools; prefer gpg or age.
HTML mail¶
Implementations disagree on HTML flags too. The portable method is to write the MIME headers yourself and hand the message to sendmail:
/usr/sbin/sendmail -t <<'EOF'
From: Reports <[email protected]>
To: [email protected]
Subject: Weekly summary
MIME-Version: 1.0
Content-Type: text/html; charset=UTF-8
<h2>Weekly summary</h2>
<p>All jobs <strong>passed</strong>.</p>
EOF
GNU Mailutils also accepts --content-type=text/html (it applies to the body and any following -A attachments).
sendmail -t: skipping the mail front end¶
Every MTA (Postfix, Exim, msmtp-mta, OpenSMTPD) installs a sendmail-compatible binary at /usr/sbin/sendmail. This is the lowest-level, most predictable interface. You write the complete message (headers, a blank line, the body) and the MTA queues it.
| Flag | Meaning |
|---|---|
-t |
Read recipients from the To:/Cc:/Bcc: headers (Bcc is stripped before sending) |
-f ADDR |
Envelope sender (where bounces go) |
-F "Name" |
Full name for the From header |
-oi |
A line containing only . does not end the message (safe for arbitrary content) |
-bv ADDR |
Postfix: don't send. Verify how the address would be delivered and mail the report back to you |
-v |
Verbose: Postfix mails you a transcript of the delivery |
printf 'To: [email protected]\nSubject: test\n\nhello\n' | /usr/sbin/sendmail -oi -t -f [email protected]
sendmail -bv [email protected] is underused. It asks Postfix to resolve the address through all its aliases, virtual maps and transports and tell you where it would go, without sending anything to the recipient.
Configuring the client to use a remote SMTP server directly¶
By default, mail hands messages to the local /usr/sbin/sendmail, so it needs a local MTA (or msmtp-mta). Both s-nail and GNU Mailutils can instead speak SMTP themselves.
s-nail (~/.mailrc, or /etc/s-nail.rc system-wide):
set v15-compat
set from="Your Name <[email protected]>"
set mta=smtp://user%[email protected]:587 smtp-use-starttls
# Pull the password from a credentials store rather than writing it here:
# see `man s-nail` for netrc-lookup and password-* variables
GNU Mailutils (/etc/mailutils.conf or ~/.mailutils):
mailer {
url "smtp://user%[email protected]:587;auth=+login";
};
Option names here change between releases, so check man s-nail or mailutils info for your version. For anything more than a single box, the better pattern is to keep SMTP credentials in one place, a local null-client Postfix or msmtp (see Relaying Outbound Mail), and let every tool keep using plain sendmail.
Reading local mail with mail¶
Run with no arguments, mail opens your local spool (/var/mail/$USER or /var/spool/mail/$USER). That's where cron output and system notices land on boxes that deliver locally.
| Command | Action |
|---|---|
h |
List message headers |
3 or p 3 |
Print message 3 |
d 3 / d 1-5 |
Delete message(s) |
r 3 |
Reply to message 3 |
s 3 ~/saved.mbox |
Save message 3 to a file |
q |
Quit, committing changes (read messages may move to ~/mbox) |
x |
Exit without committing changes |
On a null client there is no local spool. Route root and other system accounts to a real mailbox with /etc/aliases or smtp_generic_maps instead.
Cron and mail¶
Cron mails any output a job produces to the job's owner, or to MAILTO if set:
MAILTO=ops@domain.tld
MAILFROM=cron@domain.tld # cronie (RHEL/Fedora) supports this; check `man 5 crontab` elsewhere
0 3 * * * /usr/local/bin/backup.sh
MAILTO=""disables mail for the crontab entirely.- Jobs that are noisy on success should print only on failure (
backup.sh > /dev/nulland let stderr through), or you'll train yourself to ignore the mail. - systemd timers do not send mail. Use
OnFailure=with a small unit that pipessystemctl status/journalctloutput intomail.
Testing SMTP with swaks¶
swaks (Swiss Army Knife for SMTP) is the right tool for testing a server, as opposed to just sending a message. It lets you control the envelope, TLS mode and AUTH exactly, and prints the full SMTP transcript.
sudo dnf install swaks # or: sudo apt install swaks
# Plain delivery test against a specific server
swaks --to [email protected] --server mx.domain.tld
# Submission with STARTTLS + AUTH (prompts for the password if --auth-password is omitted)
swaks --to [email protected] --from [email protected] \
--server smtp.domain.tld --port 587 --tls \
--auth LOGIN --auth-user [email protected]
# Implicit TLS on 465
swaks --to [email protected] --server smtp.domain.tld --port 465 --tls-on-connect --auth LOGIN --auth-user [email protected]
# Open-relay probe: stop after RCPT so nothing is actually sent
swaks --server your-mx.domain.tld --from [email protected] --to [email protected] --quit-after RCPT
# With an attachment (the @ means "read this file")
swaks --to [email protected] --server smtp.domain.tld --attach @report.pdf
Avoid --auth-password on the command line on shared machines, because it ends up in shell history and ps output.
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
mail returns instantly, nothing arrives |
It worked locally; check the MTA log (journalctl -u postfix / /var/log/maillog) and postqueue -p |
send-mail: fatal: parameter inet_interfaces: no local interface found for ::1 |
Postfix's inet_protocols includes IPv6 but the host has none; set inet_protocols = ipv4 |
/usr/sbin/sendmail: No such file or directory |
No MTA installed. Install Postfix (null client) or msmtp-mta |
| Attachment missing, odd header present | Wrong attach flag for your implementation (see the table at the top) |
| Mail rejected by the relay for sender address | Set -r, or fix it centrally with smtp_generic_maps / sender_canonical_maps |