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/sendmailPaste 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:
- It reads the raw email message from stdin
- Parses the email headers (subject, to, body) using Python's built-in
emailmodule - Trims verbose upgrade logs down to a clean summary
- Sends the email via Mailtrap's REST API over HTTPS
- 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/sendmailThe 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-upgradesUncomment 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"; // optionalConfiguration Breakdown
| Option | Description |
|---|---|
Mail | The email address to send notifications to |
MailReport | When to send emails: "always", "on-change", or "only-on-error" |
Remove-Unused-Dependencies | Automatically remove packages that are no longer needed |
Automatic-Reboot | Allow automatic reboots when kernel updates require it |
Automatic-Reboot-Time | Schedule 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 updateto 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.comTest 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 mailExpected 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_EMAILand server name in the script for each server - Change
MailReportto"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