Back to blog
6 min read

Ubuntu Unattended Upgrades with Mailtrap Email Notifications

Set up automatic security updates on Ubuntu with email notifications via Mailtrap API. Bypass SMTP port blocks on cloud providers like DigitalOcean.

Keeping your Ubuntu servers up to date with security patches is critical, but manually logging into each server to run updates doesn't scale. Ubuntu's unattended-upgrades package automates this, and with email notifications you'll always know what's happening on your servers.

There's a catch though: cloud providers like DigitalOcean block outbound SMTP ports (25, 587, 465), making it impossible to send emails the traditional way. This guide shows you how to set up unattended upgrades with a custom sendmail wrapper that uses Mailtrap's HTTP API to bypass SMTP restrictions entirely.

Install Packages

Start by installing the required packages:

sudo apt update
sudo apt install unattended-upgrades apt-listchanges ca-certificates
  • unattended-upgrades - Handles automatic package updates
  • apt-listchanges - Sends changelogs of upgraded packages
  • ca-certificates - Ensures HTTPS connections work properly for the Mailtrap API

Create a Sendmail Wrapper Using Mailtrap API

Since SMTP ports are blocked on most cloud providers, we'll create a custom sendmail script that routes emails through Mailtrap's HTTP API over HTTPS (port 443).

Create the script:

sudo nano /usr/local/bin/sendmail

Paste the following Python script:

#!/usr/bin/env python3
import sys
import json
import urllib.request
import quopri
import email
from email import policy

API_TOKEN = "your-mailtrap-api-token"
FROM_EMAIL = "info@panka.net"

# Read full message from stdin
raw_message = sys.stdin.read()

# Parse the email properly
msg = email.message_from_string(raw_message, policy=policy.default)

subject = msg.get("subject", "Unattended Upgrades")
to_email = msg.get("to", "").strip()

# Get decoded body
body = msg.get_body(preferencelist=("plain",))
if body:
    body_text = body.get_content()
else:
    parts = raw_message.split("\n\n", 1)
    body_text = parts[1] if len(parts) > 1 else ""
    try:
        body_text = quopri.decodestring(body_text.encode()).decode()
    except Exception:
        pass

# Clean up: only keep the summary, skip the verbose log
lines = body_text.split("\n")
summary_lines = []
for line in lines:
    summary_lines.append(line)
    if line.strip().startswith("Unattended-upgrades log:"):
        summary_lines.append("[Full log available at /var/log/unattended-upgrades/unattended-upgrades.log]")
        break
else:
    summary_lines = lines

body_text = "\n".join(summary_lines)

# Fall back to command line arg if To header not found
if not to_email:
    for arg in sys.argv[1:]:
        if "@" in arg:
            to_email = arg
            break

if not to_email:
    print("Error: No recipient found", file=sys.stderr)
    sys.exit(1)

# Send via API with HTML for better formatting
html_body = f"""<html><body>
<h2 style="color: #2c3e50;">{subject}</h2>
<pre style="background: #f4f4f4; padding: 15px; border-radius: 5px; font-size: 14px; line-height: 1.6;">{body_text}</pre>
<hr style="border: 1px solid #ddd;">
<p style="color: #888; font-size: 12px;">Sent from {subject.split('for ')[-1].split(':')[0] if 'for ' in subject else 'server'} via unattended-upgrades</p>
</body></html>"""

payload = json.dumps({
    "from": {"email": FROM_EMAIL, "name": "SEO Tools Server"},
    "to": [{"email": to_email}],
    "subject": subject,
    "text": body_text,
    "html": html_body
}).encode("utf-8")

req = urllib.request.Request(
    "https://send.api.mailtrap.io/api/send",
    data=payload,
    headers={
        "Authorization": f"Bearer {API_TOKEN}",
        "Content-Type": "application/json",
        "User-Agent": "curl/8.0"
    },
    method="POST"
)

try:
    with urllib.request.urlopen(req) as resp:
        print(resp.read().decode())
except Exception as e:
    print(f"Error: {e}", file=sys.stderr)
    sys.exit(1)

How the Script Works

The script acts as a drop-in replacement for the system's sendmail binary. When unattended-upgrades tries to send an email:

  1. It reads the raw email message from stdin
  2. Parses the email headers (subject, to, body) using Python's built-in email module
  3. Trims verbose upgrade logs down to a clean summary
  4. Sends the email via Mailtrap's REST API over HTTPS
  5. Formats the email as HTML for better readability

Set Permissions and Symlink

Make the script executable and create a symlink so the system can find it:

sudo chmod +x /usr/local/bin/sendmail
sudo ln -sf /usr/local/bin/sendmail /usr/sbin/sendmail

The symlink to /usr/sbin/sendmail is important because that's where unattended-upgrades looks for the sendmail binary by default.

Configure Unattended Upgrades

Edit the main configuration file:

sudo nano /etc/apt/apt.conf.d/50unattended-upgrades

Uncomment and set the following options:

Unattended-Upgrade::Mail "info@example.org";
Unattended-Upgrade::MailReport "always";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";          // optional
Unattended-Upgrade::Automatic-Reboot-Time "03:00";    // optional

Configuration Breakdown

OptionDescription
MailThe email address to send notifications to
MailReportWhen to send emails: "always", "on-change", or "only-on-error"
Remove-Unused-DependenciesAutomatically remove packages that are no longer needed
Automatic-RebootAllow automatic reboots when kernel updates require it
Automatic-Reboot-TimeSchedule reboots at a low-traffic time

Enable Services

Enable and start all the required systemd services:

sudo systemctl enable --now unattended-upgrades
sudo systemctl enable --now apt-daily.timer
sudo systemctl enable --now apt-daily-upgrade.timer
  • apt-daily.timer - Runs apt update to refresh package lists (default: daily)
  • apt-daily-upgrade.timer - Runs the actual upgrade (default: daily, after apt-daily)
  • unattended-upgrades - The service that processes the upgrades

Testing

Test the Sendmail Wrapper Directly

echo -e "Subject: Test\n\nTest body" | /usr/local/bin/sendmail info@example.com

Test as Root

Since unattended-upgrades runs as root, verify it works with root permissions:

sudo bash -c 'echo -e "Subject: Test\n\nTest body" | /usr/local/bin/sendmail info@example.org'

Full Test with Unattended Upgrades

Run a dry-run to verify the entire pipeline:

sudo unattended-upgrades --debug 2>&1 | grep -i mail

Expected output: mail returned: 0

A return value of 0 means the email was sent successfully through your sendmail wrapper.

Notes

  • Mailtrap API token is found under Sending Domains > SMTP/API Settings in the Mailtrap dashboard
  • This approach bypasses SMTP port blocks by using HTTPS (port 443) instead of traditional SMTP ports
  • Update the FROM_EMAIL and server name in the script for each server
  • Change MailReport to "on-change" if you only want emails when packages are actually upgraded
  • Full upgrade logs are available at /var/log/unattended-upgrades/unattended-upgrades.log

DigitalOcean Referral Badge