How to send an email with attachment through Shell script
— ny_wk

To send an email with an attachment from a Linux shell script, the quickest reliable methods are mail -A file (mailutils/mailx), mutt -a file, or piping a MIME message built with uuencode into sendmail. Each works from any Bash script and slots cleanly into a cron job for automated disk, CPU, or backup alerts.
Server admins constantly need a machine to email out a report, a log file, or a CSV without a human in the loop. The good news is that sending an email with an attachment from a Linux shell script is built into the standard mail tooling, so you rarely need anything beyond a one-line command once a working mail transfer agent is in place. The catch is that the exact flag differs between tools, and a script that runs perfectly by hand can silently fail under cron if delivery is not configured. The methods below cover every common case and the pitfalls that trip people up.
Goal: automated email with a file attached
The end state is a script you can run on demand or from cron that composes a message with a subject, a body, one or more attachments, and a recipient, then hands it to the system mailer for delivery. You want it to be idempotent, quiet on success, and loud on failure, so it is safe to schedule.
Throughout, the primary keyword is straightforward: you are learning to send an email with an attachment from a shell script using real, tested commands. Pick whichever tool is already installed on your distribution, or install one of the three below.
Prerequisites: install a mail client and an MTA
Two pieces are involved, and people often confuse them:
- A mail client / sender — the command you call in the script:
mail,mailx, ormutt. This builds the message. - A mail transfer agent (MTA) or relay — the service that actually pushes the mail off the box:
postfix,sendmail,msmtp, orssmtp. Without one, the message has nowhere to go.
Install the client
On Debian/Ubuntu:
sudo apt updatesudo apt install -y mailutils(provides themailcommand with attachment support)- or
sudo apt install -y muttfor themuttclient
On RHEL/CentOS/Rocky/AlmaLinux:
sudo dnf install -y mailx(the package iss-nail/mailxdepending on version)- or
sudo dnf install -y mutt
Verify the client is present with which mail or which mutt. A missing client is the single most common reason a copied-from-the-internet snippet does nothing.
Provide an MTA or relay
If the host is a real mail server you may already have Postfix running. If not, the lightest path is a send-only relay that forwards through an existing SMTP provider (your company server, Gmail, SendGrid, Amazon SES). Good options:
- msmtp — a tiny SMTP client, ideal for send-only boxes. Install with
sudo apt install -y msmtp msmtp-mta. - ssmtp — older and simpler, still common on legacy systems.
- Postfix in relay mode — full-featured, the right choice when many local services need to send mail.
Confirm something is listening or that a sendmail-compatible binary exists with which sendmail. On a healthy system that path resolves to your MTA.
Method 1: mailutils / mailx with the -A flag
This is the cleanest modern approach. The mail command from mailutils attaches a file directly with -A, while the body comes from standard input:
- Compose and send in one line:
echo "Nightly backup report attached." | mail -s "Backup report $(date +%F)" -A /var/log/backup.log ops@example.com -ssets the subject,-Aadds the attachment, and the address at the end is the recipient.- The text piped via
echo(or a here-doc) becomes the message body.
A subtle but important point: on the traditional BSD/Heirloom mailx, lowercase -a means "add a custom header," whereas on that same tool an attachment is also -a in some builds. On GNU mailutils, uppercase -A is the attachment flag and lowercase -a is a header. Because this varies by distribution, check your manual with man mail before scheduling. When in doubt, test once interactively and read what arrives.
For a multi-line body, use a here-document, which is far more readable than chained echo calls:
mail -s "Disk alert on $(hostname)" -A /tmp/df_report.txt admin@example.com <<EOFDisk usage crossed the threshold. Full report attached.Generated at $(date).EOF
Method 2: mutt with the -a flag (most reliable for attachments)
Many administrators reach for mutt specifically because its attachment handling is rock solid and its flag is consistent across systems. The -a option attaches a file, and crucially you must place -- before the recipient so mutt knows where the attachment list ends:
echo "See the attached CPU report." | mutt -s "CPU alert $(hostname)" -a /tmp/cpu.txt -- alerts@example.com- The body again comes from standard input (the piped
echoor a here-doc). - The
--separator is mandatory; without it mutt may treat the email address as another attachment and fail.
mutt's biggest advantage is multiple attachments in a single message. List several files after -a, ending with -- and the recipient:
echo "Reports attached." | mutt -s "Daily reports" -a /tmp/cpu.txt /tmp/mem.txt /tmp/disk.txt -- ops@example.com
For a fully unattended run with no terminal, suppress mutt's interactive prompts by feeding the body via -i or piping it, and consider a minimal ~/.muttrc for the user the script runs as so mutt does not block waiting on configuration.
Method 3: the robust uuencode approach
On minimal servers where neither mailutils nor mutt is available, you can still attach a file using uuencode (from the sharutils package) piped into the plain mail or sendmail command. This is the old-school, dependency-light technique:
- Install the tool if needed:
sudo apt install -y sharutils - Encode and pipe into mail:
uuencode /tmp/report.csv report.csv | mail -s "CSV report" finance@example.com - The first argument to
uuencodeis the source path; the second is the name the attachment will have in the email.
The limitation is that uuencode attachments are not true MIME and some modern mail clients (especially webmail) display them poorly or inline. It still works for server-to-server delivery and for recipients that decode uuencoded payloads, but for anything customer-facing prefer mailutils or mutt, which produce standards-compliant MIME messages.
Sending HTML email and multiple attachments
Plain text covers most alerts, but reports sometimes look better as HTML. With mutt, set the content type explicitly:
echo "<h3>Status: OK</h3><p>All checks passed.</p>" | mutt -e "set content_type=text/html" -s "Health check" -a /tmp/health.log -- ops@example.com
With mailutils you can pass the header directly:
mail -s "Health check" -a "Content-Type: text/html" -A /tmp/health.log ops@example.com < /tmp/body.html
For multiple attachments with mailutils, repeat the -A flag once per file:
mail -s "Nightly reports" -A /tmp/cpu.txt -A /tmp/mem.txt -A /tmp/disk.txt admin@example.com < /tmp/body.txt
The table below summarizes the three core tools at a glance:
| Tool | Attachment flag | Multiple files | Best for |
| mailutils (mail) | -A file | repeat -A | Modern Debian/Ubuntu, clean MIME |
| mailx / s-nail | -a file (check man) | repeat -a | RHEL/CentOS family |
| mutt | -a file -- addr | list files before -- | Most reliable, HTML, many files |
| uuencode + mail | pipe encoded data | concatenate encodings | Minimal hosts, no MIME client |
Put it in a script and schedule with cron
Here is a complete, production-minded script that generates a disk-usage report and emails it as an attachment. It fails loudly, uses absolute paths, and is safe to run from cron:
#!/usr/bin/env bashset -euo pipefailSUBJECT="Disk report for $(hostname) on $(date +%F)"TO_ADDRESS="admin@example.com"ATTACHMENT="/tmp/disk_report_$(date +%F).txt"df -h > "$ATTACHMENT"echo "Automated disk usage report is attached." | mail -s "$SUBJECT" -A "$ATTACHMENT" "$TO_ADDRESS"rm -f "$ATTACHMENT"
Save it as /usr/local/bin/disk_report.sh, then make it executable with chmod +x /usr/local/bin/disk_report.sh. The set -euo pipefail line is what turns a fragile snippet into a dependable job: it aborts on any error, on undefined variables, and on a failed pipe stage.
Add the cron entry
Open the crontab with crontab -e and add a line. To run at 6 a.m. daily:
0 6 * * * /usr/local/bin/disk_report.sh >> /var/log/disk_report.log 2>&1
Redirecting both stdout and stderr to a log file is essential under cron, because cron emails its own output and a script that prints to the terminal will otherwise generate noisy mail of its own. Logging to a file gives you a place to look when a run misbehaves.
Pitfalls: spam, SPF, missing MTA, and attachment size
The commands are the easy part; reliable delivery is where most time gets lost. Watch for these:
- No MTA installed. If
mailreturns instantly with no error but nothing arrives, there is probably no working mail transfer agent. Installpostfix,msmtp-mta, orssmtpand configure a relay host. - Mail lands in spam (SPF/DKIM/DMARC). Messages sent straight from a random server IP are frequently flagged. Route through a legitimate relay whose domain has valid SPF, DKIM, and DMARC records. Set a real, deliverable
Fromaddress with-r sender@yourdomain.com(or in the MTA config) rather than the defaultuser@hostname.localdomain. - PATH and environment under cron. Cron runs with a minimal environment, so commands that work in your shell may not be found. Use absolute paths (for example
/usr/bin/mail) or setPATHat the top of the script. - Attachment size limits. Most providers cap messages around 10–25 MB. Large logs should be compressed first with
gzip, and very large files should be uploaded somewhere and emailed as a link instead. Base64/MIME encoding also inflates the payload by roughly a third, so a 20 MB file can exceed a 25 MB limit once encoded. - Wrong attachment flag. As noted,
-Aversus-adiffers by tool and distribution. Always confirm withmanon the target host. - Missing the
--separator in mutt, which causes the recipient address to be misread as an attachment.
Verification: confirm the mail actually went out
Never assume delivery. Verify each layer:
- Run it by hand first to an address you control, then check the inbox and the spam folder.
- Watch the mail log while sending:
sudo tail -f /var/log/mail.log(Debian/Ubuntu) or/var/log/maillog(RHEL family). A successful relay shows astatus=sentline. - Inspect the queue with
mailq(orpostqueue -p). Stuck messages reveal relay or DNS problems. - Open the received message and confirm the attachment downloads and opens correctly — uuencoded payloads in particular may need a sanity check in a real client.
- Force a cron-style run with a stripped environment to catch PATH issues before scheduling:
env -i /usr/local/bin/disk_report.sh.
Key Takeaways
- Use
mail -A file(mailutils),mutt -a file -- addr, oruuencode | mail— pick the tool already installed and confirm its attachment flag withman. - A mail client is not enough; you also need a working MTA or relay (Postfix, msmtp, or ssmtp) for anything to actually leave the box.
- mutt is the most consistent for multiple attachments and HTML email, and always requires the
--separator before the recipient. - Harden scripts for cron with
set -euo pipefail, absolute paths, and output redirected to a log file. - Protect deliverability with a valid From address plus SPF/DKIM/DMARC, and compress or link large attachments to stay under size limits.
Frequently Asked Questions
Why does my mail command run with no error but no email arrives?
Almost always there is no functioning mail transfer agent, or it cannot reach a relay. The mail command happily hands the message to the local MTA and exits; if that MTA is missing or misconfigured the message is dropped or queued. Check mailq and the mail log, and install or configure Postfix/msmtp.
What is the difference between -a and -A?
It depends on the tool. On GNU mailutils, -A attaches a file and -a adds a header. On mutt, -a attaches a file. On traditional mailx/s-nail it varies by build. Run man mail or man mutt on the actual server rather than trusting a snippet from another distribution.
How do I attach multiple files in one email?
With mutt, list all files after -a and end with -- and the recipient. With mailutils, repeat the -A flag once per file. With uuencode, encode each file and concatenate the output into the message stream.
How can I stop my automated emails from going to spam?
Send through a reputable relay (your provider, SES, SendGrid) whose domain publishes valid SPF, DKIM, and DMARC, and set a genuine, deliverable From address instead of the default hostname-based one. Avoid sending directly from a bare server IP, which most receivers distrust.
For more practical Linux, DevOps, and home-lab walkthroughs, subscribe to @explorenystream on YouTube.