Do you find yourself manually emailing the same files every morning?
Maybe it’s a daily sales report, a server status sheet, or data from an automated process.
Doing this by hand is tedious, error‑prone, and, let’s be honest, a waste of our talent.
Fear not, as we will build self‑monitoring email robot on Ubuntu for this exact purpose.
By the end, we’ll have a script that:
- Attaches 2–3 files of any desired extension.
- Emails them at exactly 9:00 AM every day.
- Checks that the files actually exist before sending.
- Creates a safe snapshot so that files can be updated while the script runs.
- Logs everything for easy debugging.
- Notifies you immediately if something fails via email.
And the best part? You’ll understand why every line is written – no black boxes, no copy‑paste without comprehension.
What We’re Building – The Big Picture
Think of the system as four layers:
- Scheduling – Cron wakes up the script at 9 AM (or any time of your choosing).
- Preparation – The script verifies and copies the files to a temporary, isolated folder.
-
Sending –
muttattaches the copies and sends them via Gmail’s secure SMTP server, using Postfix as the local mail relay. - Safety net – If anything breaks, you get an alert email with clear diagnostic information.
Let’s build it step by step.
Prerequisites
- An Ubuntu machine (20.04 or 22.04 or any modern version)
- A Gmail account (free) with 2‑Step Verification enabled and an App Password generated.
- Basic knowledge of bash terminal.
Step 1: Install and Configure Postfix to Relay Through Gmail
Postfix is the engine that actually delivers email from your server to the outside world. By default it only delivers mail to local users. FOr our use-case, we need to teach it to use Gmail as a smart relay for sending emails over the internet.
1.1 Install the packages
sudo apt update
sudo apt install postfix mailutils libsasl2-2 ca-certificates libsasl2-modules
During installation, select Internet Site and use your machine’s hostname (ubuntu-server, mylaptop, etc.) when asked.
1.2 Tell Postfix to use Gmail’s SMTP server
Edit the main configuration file:
sudo nano /etc/postfix/main.cf
Add these lines at the very end:
relayhost = [smtp.gmail.com]:587
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_use_tls = yes
inet_protocols = ipv4
Why this matters:
relayhost tells Postfix: “Don’t try to deliver directly. Send everything through Gmail’s server on port 587.”
The SASL lines enable authentication – Gmail won’t accept mail from strangers.
inet_protocols = ipv4 avoids IPv6 connection problems (common on many servers).
1.3 Store your Gmail App Password securely
Create the file /etc/postfix/sasl_passwd:
sudo nano /etc/postfix/sasl_passwd
Add exactly one line:
[smtp.gmail.com]:587 your.email@gmail.com:your-16-digit-app-password
Now secure it and build the indexed database that Postfix reads:
sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
Why chmod 600? This file contains a plain‑text password – we must ensure only root can read it.
1.4 Reload Postfix and test
sudo systemctl reload postfix
Test with a simple email:
echo "Postfix can talk to Gmail!" | mail -s "Test from Ubuntu" <your_email>@gmail.com
Check your inbox (and spam folder). If it arrives, you’ve successfully turned your Ubuntu machine into an email sender.
No email? Check /var/log/mail.log by typing sudo tail -f /var/log/mail.log. The error message is always there and usually very specific.
Step 2: Install mutt – Because mail Can’t Handle Attachments Well
The basic mail command we just used is great for text, but it struggles with attachments. mutt is a lightweight, reliable email client that handles attachments beautifully.
sudo apt install mutt
Step 3: Write the Email Script
Create a new file called send_daily_report.sh in your home directory:
nano ~/send_daily_report.sh
Now let’s build the script piece by piece. I’ll show you the final script first, then walk through each section.
#!/bin/bash
# Who gets the daily report?
RECIPIENT="manager@company.com"
# Email subject – includes today's date for clarity
SUBJECT="Daily Report - $(date '+%Y-%m-%d')"
# Plain text email body (mutt can do HTML, but plain text is simpler)
BODY="Hello,\n\nPlease find today's report files attached.\n\nBest regards,\nUbuntu Server"
# Full paths to the files you want to send – one per line
ORIGINAL_FILES=(
"/home/<username>/<folder>/<filename1.extension>"
"/home/<username>/<folder>/<filename2.extension>"
)
# Who should be notified if the email fails?
# Use a DIFFERENT email address than the recipient – you don't want failure alerts
# to go to the same place that might be broken!
NOTIFY_RECIPIENT="admin@example.com"
NOTIFY_SUBJECT="ALERT: Daily Report FAILED on $(hostname) - $(date '+%Y-%m-%d')"
# Where to store logs – must be writable by your user
LOG_FILE="$HOME/daily_report.log"
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
log_message "Starting Daily Report Job"
# --- 1. Create a unique temporary directory (staging area) ---
STAGING_DIR="/tmp/daily_report_$$_$(date +%s)"
mkdir -p "$STAGING_DIR"
if [ $? -ne 0 ]; then
log_message "CRITICAL: Cannot create staging directory $STAGING_DIR"
exit 1
fi
log_message "Created staging directory: $STAGING_DIR"
# --- 2. Copy files to staging, verifying each one ---
MISSING_FILES=()
COPIED_FILES=()
for file in "${ORIGINAL_FILES[@]}"; do
if [ -f "$file" ]; then
cp "$file" "$STAGING_DIR/"
if [ $? -eq 0 ]; then
COPIED_FILES+=("$STAGING_DIR/$(basename "$file")")
log_message "Copied $(basename "$file") to staging."
else
log_message "ERROR: Failed to copy $(basename "$file")"
MISSING_FILES+=("$file")
fi
else
log_message "ERROR: File not found: $file"
MISSING_FILES+=("$file")
fi
done
# --- 3. If any file is missing, abort and send a notification ---
if [ ${#MISSING_FILES[@]} -ne 0 ]; then
ERROR_MSG="Missing files: ${MISSING_FILES[*]}"
log_message "ERROR: $ERROR_MSG"
NOTIFY_BODY="The daily report email FAILED at $(date).\n"
NOTIFY_BODY+="Reason: $ERROR_MSG\n"
NOTIFY_BODY+="Host: $(hostname)\n"
NOTIFY_BODY+="Log: $LOG_FILE"
echo -e "$NOTIFY_BODY" | mail -s "$NOTIFY_SUBJECT" "$NOTIFY_RECIPIENT"
log_message "Failure notification sent to $NOTIFY_RECIPIENT."
rm -rf "$STAGING_DIR"
exit 1
fi
# --- 4. Build the mutt command with all attachments ---
MUTT_ARGS=(-s "$SUBJECT")
for file in "${COPIED_FILES[@]}"; do
MUTT_ARGS+=(-a "$file")
done
MUTT_ARGS+=(-- "$RECIPIENT")
# --- 5. Send the email! ---
log_message "Sending email to $RECIPIENT with ${#COPIED_FILES[@]} attachment(s)."
echo -e "$BODY" | mutt "${MUTT_ARGS[@]}"
MAIL_EXIT_CODE=$?
# --- 6. Check if sending succeeded ---
if [ $MAIL_EXIT_CODE -eq 0 ]; then
log_message "SUCCESS: Email sent."
else
log_message "ERROR: mutt failed with exit code $MAIL_EXIT_CODE"
NOTIFY_BODY="The daily report email FAILED at $(date).\n"
NOTIFY_BODY+="Reason: mutt exit code $MAIL_EXIT_CODE\n"
NOTIFY_BODY+="Host: $(hostname)\n"
NOTIFY_BODY+="Check log: $LOG_FILE\n"
NOTIFY_BODY+="Postfix log: /var/log/mail.log"
echo -e "$NOTIFY_BODY" | mail -s "$NOTIFY_SUBJECT" "$NOTIFY_RECIPIENT"
log_message "Failure notification sent."
fi
# --- 7. Clean up temporary directory ---
rm -rf "$STAGING_DIR"
log_message "Removed staging directory."
# --- 8. Exit with appropriate status ---
if [ $MAIL_EXIT_CODE -eq 0 ]; then
log_message "Job finished successfully\n"
exit 0
else
log_message "Job finished with errors\n"
exit $MAIL_EXIT_CODE
fi
Why Each Part Is Written That Way – A Guided Tour
Configuration section
RECIPIENT="manager@company.com"
SUBJECT="Daily Report - $(date '+%Y-%m-%d')"
Using $(date '+%Y-%m-%d') in the subject makes every email uniquely identifiable by the date.
ORIGINAL_FILES=( ... )
Why an array? Because file names might contain spaces in the future. An array handles each element cleanly.
NOTIFY_RECIPIENT="admin@example.com"
Critical : The failure alert should never go to the same address as the report. If the report email fails because of a problem with that domain, your alert would also fail. Use a different email provider or at least a different address.
LOG_FILE="$HOME/daily_report.log"
Keeping logs in your home directory avoids permission headaches with system folders.
Logging function
log_message() {
echo "$(date ...) - $1" | tee -a "$LOG_FILE"
}
tee -a writes the message both to the terminal (so you see it when testing) and appends it to the log file. Timestamps are non‑negotiable. This helps to narrow down the failure when something breaks weeks later.
Staging directory is the secret sauce
STAGING_DIR="/tmp/daily_report_$$_$(date +%s)"
mkdir -p "$STAGING_DIR"
$$ is the process ID of the script. No two runs will ever have the same PID, so this directory is unique.
$(date +%s) adds the Unix timestamp. This will be different even if two scripts run in the same second.
Why copy files at all?
Imagine the files are generated by another program. If that program overwrites a file while your email script is reading it, you might send a half‑written, corrupt file. By copying to a staging area at the very beginning, you capture a consistent snapshot. The original files can be modified, moved, or even deleted. Your email will still contain the version that existed at 9:00:00 AM.
File verification loop
if [ -f "$file" ]; then
cp "$file" "$STAGING_DIR/"
...
else
log_message "ERROR: File not found: $file"
MISSING_FILES+=("$file")
fi
This does two checks: first that the file exists, then that the copy succeeded. Any failure is recorded.
After the loop, we check the length of the MISSING_FILES array:
if [ ${#MISSING_FILES[@]} -ne 0 ]; then
${#array[@]} gives the number of elements. If it’s not zero, something is wrong.
Why not just check exit codes inline? Because we want to report all missing files in one go, not stop at the first error.
Building the mutt command
MUTT_ARGS=(-s "$SUBJECT")
for file in "${COPIED_FILES[@]}"; do
MUTT_ARGS+=(-a "$file")
done
MUTT_ARGS+=(-- "$RECIPIENT")
This creates an array of arguments.
-s sets the subject.
-a is repeated for each attachment.
-- signals the end of options. The recipient comes after the end of options.
Then we execute:
echo -e "$BODY" | mutt "${MUTT_ARGS[@]}"
The "${MUTT_ARGS[@]}" syntax expands each element as a properly quoted word. This is the safe way to handle multiple arguments in bash.
Exit codes and failure notification
MAIL_EXIT_CODE=$?
if [ $MAIL_EXIT_CODE -eq 0 ]; then
...
else
...
fi
Every command in Linux returns an exit code. 0 means success, anything else means failure. mutt returns 0 only if it successfully hands the message to Postfix.
If it fails, we build a detailed error body and send it via the simple mail command (which we already know works). This notification contains:
- The exact error (mutt exit code).
- Hostname – very useful if you run this on multiple servers.
- Paths to the logs so you can investigate immediately.
Cleanup
rm -rf "$STAGING_DIR"
Never leave temporary files lying around. This line runs regardless of success or failure, thanks to its placement.
Step 4: Automate with Cron
Cron is the time‑based job scheduler in Linux. One line is all it takes. But before creating a new cron entry, make the script executable:
chmod +x /home/raafe/send_daily_report.sh
Then, open your personal crontab:
crontab -e
Add this line:
0 9 * * * /home/raafe/send_daily_report.sh
Breakdown:
0 – minute (0 = on the hour)
9 – hour (9 AM)
* * * – every day, every month, every weekday
Save and exit. Cron will now run your script every day at 9:00 AM.
Pro tip: To test quickly, use * * * * * to run every minute, then remove it once you’re confident.
Testing Your Robot
Run the script manually first:
bash ~/send_daily_report.sh
Watch the logs in real time:
tail -f ~/daily_report.log
Check the Postfix log if something goes wrong:
sudo tail -f /var/log/mail.log
You should see your test email arrive within seconds. If not, the log will tell you why; usually an authentication typo or a network issue.
What You’ve Learned
Postfix + Gmail turns your Ubuntu machine into a legitimate email sender.
mutt handles attachments reliably.
Staging directories eliminate race conditions with concurrent file writers.
Exit codes are your friends. They turn success/failure into a numeric value the script can act on.
Failure notifications make your automation self‑aware.
Cron runs it all on schedule, no human needed.
This system is now running on my own ubuntu instance, sending a daily ops report every morning at 9:00 AM sharp. It hasn’t missed a beat in months.
Top comments (0)