DEV Community

Cover image for How to Automate Daily Email Reports on Ubuntu: A Beginner’s Guide
Raafe Asad
Raafe Asad

Posted on • Edited on

How to Automate Daily Email Reports on Ubuntu: A Beginner’s Guide

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.
  • Sendingmutt attaches 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Add exactly one line:

[smtp.gmail.com]:587 your.email@gmail.com:your-16-digit-app-password
Enter fullscreen mode Exit fullscreen mode

Now secure it and build the indexed database that Postfix reads:

sudo chmod 600 /etc/postfix/sasl_passwd
sudo postmap /etc/postfix/sasl_passwd
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Test with a simple email:

echo "Postfix can talk to Gmail!" | mail -s "Test from Ubuntu" <your_email>@gmail.com
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Step 3: Write the Email Script

Create a new file called send_daily_report.sh in your home directory:

nano ~/send_daily_report.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Why Each Part Is Written That Way – A Guided Tour

Configuration section

RECIPIENT="manager@company.com"
SUBJECT="Daily Report - $(date '+%Y-%m-%d')"
Enter fullscreen mode Exit fullscreen mode

Using $(date '+%Y-%m-%d') in the subject makes every email uniquely identifiable by the date.

ORIGINAL_FILES=( ... )
Enter fullscreen mode Exit fullscreen mode

Why an array? Because file names might contain spaces in the future. An array handles each element cleanly.

NOTIFY_RECIPIENT="admin@example.com"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Keeping logs in your home directory avoids permission headaches with system folders.

Logging function

log_message() {
    echo "$(date ...) - $1" | tee -a "$LOG_FILE"
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

$$ 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

${#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")
Enter fullscreen mode Exit fullscreen mode

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[@]}"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Then, open your personal crontab:

crontab -e
Enter fullscreen mode Exit fullscreen mode

Add this line:

0 9 * * * /home/raafe/send_daily_report.sh
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Watch the logs in real time:

tail -f ~/daily_report.log
Enter fullscreen mode Exit fullscreen mode

Check the Postfix log if something goes wrong:

sudo tail -f /var/log/mail.log
Enter fullscreen mode Exit fullscreen mode

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)