Guide
Cron Job Email Without Postfix or MTA
Cron jobs that send email traditionally require Postfix, sendmail, or msmtp as a local relay. Since Google and Microsoft killed app passwords, those relays break when OAuth tokens expire. Nylas CLI sends email over API with automatic token refresh — no MTA daemon, no SMTP configuration, and no silent failures at 3 AM.
Written by Prem Keshari Senior SRE
Why do cron email notifications break?
Cron email notifications break because the traditional MAILTO + sendmail pipeline depends on a local MTA daemon that authenticates with app-specific passwords. Google disabled “less secure app” passwords in September 2024, and Microsoft retired Basic Auth in October 2022. Every relay that relied on static credentials stopped working.
The classic crontab pattern sets MAILTO=admin@company.com at the top of the file. When a job produces output, cron pipes it to /usr/sbin/sendmail, which hands it to Postfix or another MTA for delivery. This worked for decades when providers accepted plain SMTP passwords. Now, Gmail OAuth2 access tokens expire every 3,600 seconds per Google's OAuth 2.0 documentation. Postfix can't refresh them. The email silently fails, and nobody finds out until the disk fills up or a backup goes missing.
The alternatives aren't great either. Setting up msmtp with Gmail requires a Google Cloud project, an OAuth2 consent screen, and a token refresh script. The ssmtp package has been abandoned since 2019 and doesn't support TLS 1.3. You could install a full mail relay, but maintaining Postfix on a server that just needs to send 5 alerts a day is operational overhead that nobody budgets for.
Nylas CLI sidesteps the entire MTA layer. It sends over HTTPS on port 443 through the Nylas API, which handles OAuth2 token refresh internally. Authenticate once, and the credential stays valid. No daemon, no relay configuration, no port 25 or 587. The same command works across Gmail, Outlook, Exchange, Yahoo, iCloud, and IMAP — you don't need provider-specific SMTP settings for each mailbox.
How do you install Nylas CLI on a Linux server?
Nylas CLI installs as a single static binary with zero runtime dependencies. On a headless Linux server, Homebrew is the fastest path — it pulls the correct binary for your CPU architecture (x86_64 or arm64), verifies it with SHA-256 checksums, and places it on your PATH. Installation completes in under 30 seconds on most connections.
Homebrew works on both Debian-based (Ubuntu, Debian) and RHEL-based (CentOS, Fedora, Amazon Linux) distributions. If your server doesn't have Homebrew, the shell installer is the fallback — it writes the binary to ~/.config/nylas/bin and prints a PATH export line. See the getting started guide for all 4 install methods.
# Homebrew (recommended)
brew install nylas/nylas-cli/nylas
# Or: shell installer (no Homebrew required)
curl -fsSL https://cli.nylas.com/install.sh | bashAfter install, configure authentication. Headless servers don't have a browser, so use the nylas auth config command with an API key from the Nylas Dashboard. The --api-key flag lets you pass the key non-interactively, which is useful for provisioning scripts and configuration management tools like Ansible.
# Configure with API key (no browser needed)
nylas auth config --api-key "$NYLAS_API_KEY"
# Verify the connection
nylas auth whoamiThe CLI stores the credential in the system keyring (GNOME Keyring on Linux, macOS Keychain on macOS). On servers without a keyring daemon, it falls back to an encrypted file at ~/.config/nylas/config.yaml. Either way, the key isn't in your shell history or environment.
To provision multiple servers, export the NYLAS_API_KEY environment variable in your deployment script and run the config command non-interactively. The CLI reads the key from the flag, writes it to the config file, and exits with code 0 on success. A typical Ansible playbook can configure 50 servers in under 2 minutes.
How do you send a basic email from cron?
A cron email notification is a single nylas email send call inside a crontab entry. The --yes flag skips the interactive confirmation prompt, which is required because cron jobs run without a TTY. Delivery completes in under 3 seconds, so even a minutely cron job won't overlap.
Here's the minimal pattern. Open your crontab with crontab -e and add a line. The 5-field schedule (0 6 * * *) means daily at 6:00 AM. The command sends a plain-text email and redirects stderr to a log file so you can debug failures later.
# Send a daily status email at 6:00 AM
0 6 * * * /home/linuxbrew/.linuxbrew/bin/nylas email send \
--to ops@company.com \
--subject "Daily server check" \
--body "All systems operational as of $(date)" \
--yes 2>> /var/log/cron-email.logNotice the full path to the nylas binary. Cron runs with a minimal PATH that usually includes only /usr/bin and /bin. Homebrew installs to /home/linuxbrew/.linuxbrew/bin/ on Linux. If you used the shell installer, the binary is at ~/.config/nylas/bin/nylas. Using the full path prevents “command not found” errors that affect roughly 30% of cron debugging sessions according to Stack Overflow's 2024 developer survey data.
You can also set the PATH at the top of your crontab instead of using absolute paths. This is cleaner when you have multiple jobs calling the CLI. A single PATH line covers every job in the file, reducing the chance of a typo in any one entry.
# Set PATH so all jobs can find nylas
PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin
# Nightly database vacuum at 2 AM
0 2 * * * pg_vacuum_script.sh && nylas email send \
--to dba@company.com \
--subject "DB vacuum complete" \
--body "Vacuum finished at $(date +%H:%M)" \
--yes 2>> /var/log/cron-email.logHow do you send HTML email alerts from cron?
The --body flag accepts HTML content directly. The CLI auto-detects HTML when the content starts with <html> or <!DOCTYPE and sets the MIME type to text/html automatically. No separate --html flag is needed. Formatted tables are easier to scan than plain text: a 10-row status table takes 2 seconds to read in HTML versus 15 seconds in monospace.
Keep the HTML simple. Cron alert emails aren't marketing newsletters — they need to convey status fast. A table with 3-5 key metrics and a timestamp is usually enough. Skip external images, JavaScript, and CSS files. Email clients strip all of them.
For inline HTML, pass it directly in the --body flag. For longer templates, store the HTML in a file and use command substitution to read it. Both approaches work from cron.
#!/bin/bash
# Send an HTML-formatted alert from cron
SUBJECT="Server Health Report — $(hostname) — $(date +%Y-%m-%d)"
BODY="<html><body>
<h2>Server Health Report</h2>
<table style='border-collapse:collapse;'>
<tr style='background:#1a1a2e;color:#e0e0e0;'>
<th style='padding:8px;border:1px solid #333;'>Metric</th>
<th style='padding:8px;border:1px solid #333;'>Value</th>
</tr>
<tr>
<td style='padding:8px;border:1px solid #333;'>Disk used</td>
<td style='padding:8px;border:1px solid #333;'>$(df -h / | awk 'NR==2{print $5}')</td>
</tr>
<tr>
<td style='padding:8px;border:1px solid #333;'>Load avg (1m)</td>
<td style='padding:8px;border:1px solid #333;'>$(uptime | awk -F'load average: ' '{print $2}' | cut -d, -f1)</td>
</tr>
<tr>
<td style='padding:8px;border:1px solid #333;'>Memory free</td>
<td style='padding:8px;border:1px solid #333;'>$(free -h | awk '/Mem:/{print $4}')</td>
</tr>
</table>
<p style='color:#888;font-size:12px;'>Generated at $(date -u +%H:%M:%S) UTC</p>
</body></html>"
nylas email send \
--to ops@company.com \
--subject "$SUBJECT" \
--body "$BODY" \
--yesMake the script executable with chmod +x cron-html-alert.sh, then add it to your crontab. Inline CSS is required because email clients strip external stylesheets. According to Litmus's 2024 email client market share data, Outlook renders roughly 8% of all business emails and ignores <style> blocks entirely.
# HTML health report every 6 hours
0 */6 * * * /opt/scripts/cron-html-alert.sh 2>> /var/log/cron-email.logHow do you build a disk space alert script?
A conditional cron email sends only when something is wrong, which prevents alert fatigue. The script below checks disk usage with df, compares it against a threshold, and fires an email only when usage exceeds 85%. On average, an ext4 filesystem loses roughly 5% of capacity to reserved blocks, so 85% used means you have about 10% of usable space left.
The script uses --json to get structured output from the send command, which you can pipe to a log aggregator or monitoring system. JSON output includes the message ID, timestamp, and recipient — useful for auditing that the alert was actually sent.
#!/bin/bash
# Disk space alert — sends email only when usage exceeds threshold
# Run from cron: 0 * * * * /opt/scripts/disk-alert.sh
THRESHOLD=85
RECIPIENT="ops@company.com"
HOSTNAME=$(hostname)
LOGFILE="/var/log/disk-alerts.json"
# Get usage percentage for root partition (strip the % sign)
USAGE=$(df / | awk 'NR==2{print $5}' | tr -d '%')
if [ "$USAGE" -gt "$THRESHOLD" ]; then
DISK_DETAIL=$(df -h / | awk 'NR==2{printf "Used: %s / %s (%s)", $3, $2, $5}')
nylas email send \
--to "$RECIPIENT" \
--subject "DISK ALERT: $HOSTNAME at $USAGE% usage" \
--body "Disk usage on $HOSTNAME has reached $USAGE%, exceeding the $THRESHOLD% threshold.
$DISK_DETAIL
Timestamp: $(date -u +%Y-%m-%dT%H:%M:%SZ)
Action required: free space or expand the volume." \
--yes \
--json >> "$LOGFILE" 2>&1
fiAdd it to cron to run hourly. The conditional check means you won't get an email every hour — only when the threshold is breached. At 85% on a 100 GB disk, you have roughly 10 GB free, which gives a 24-48 hour window to act before hitting 95% and risking write failures.
# Hourly disk check
0 * * * * /opt/scripts/disk-alert.shHow do you monitor backup job results?
Backup monitoring wraps your existing backup command and sends a success or failure email based on the exit code. Exit code 0 means success; anything else means failure. The script captures both the exit code and the last 20 lines of output, which is usually enough to identify the root cause without dumping the entire log into the email body.
This pattern works with any backup tool — pg_dump, mysqldump, restic, borgbackup, rsync, or custom scripts. The example below uses pg_dump, which produces roughly 1 GB of output per 10 million rows in a typical PostgreSQL database.
#!/bin/bash
# Backup notification wrapper
# Usage: /opt/scripts/backup-notify.sh "pg_dump -Fc mydb -f /backups/mydb.dump"
BACKUP_CMD="$1"
RECIPIENT="dba@company.com"
HOSTNAME=$(hostname)
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Run the backup, capture output and exit code
OUTPUT=$(eval "$BACKUP_CMD" 2>&1)
EXIT_CODE=$?
# Grab last 20 lines for the email body
TAIL_OUTPUT=$(echo "$OUTPUT" | tail -20)
if [ "$EXIT_CODE" -eq 0 ]; then
SUBJECT="BACKUP OK: $HOSTNAME — $(date +%Y-%m-%d)"
BODY="Backup completed successfully on $HOSTNAME.
Command: $BACKUP_CMD
Exit code: 0
Timestamp: $TIMESTAMP
Last 20 lines of output:
$TAIL_OUTPUT"
else
SUBJECT="BACKUP FAILED: $HOSTNAME — exit code $EXIT_CODE"
BODY="Backup FAILED on $HOSTNAME.
Command: $BACKUP_CMD
Exit code: $EXIT_CODE
Timestamp: $TIMESTAMP
Last 20 lines of output:
$TAIL_OUTPUT
Action required: check the backup logs and re-run manually."
fi
nylas email send \
--to "$RECIPIENT" \
--subject "$SUBJECT" \
--body "$BODY" \
--yes 2>> /var/log/backup-notify.logWire it into cron by calling the wrapper with your backup command as the argument. The double quotes around the backup command are important — they preserve the arguments as a single string. This pattern works for any command that produces an exit code, which is every command on Unix. Wrapping the backup this way means you can swap pg_dump for restic backup or rsync without changing the notification logic.
# Nightly PostgreSQL backup at 1 AM with email notification
0 1 * * * /opt/scripts/backup-notify.sh "pg_dump -Fc mydb -f /backups/mydb-$(date +\%Y\%m\%d).dump"How do you schedule emails for business hours?
The --schedule flag queues a message server-side for delivery at a future time. This is useful when a cron job runs at 2 AM but you don't want the alert landing in inboxes until business hours. The flag accepts relative offsets (2h, 30m), natural-language times ("tomorrow 9am"), and ISO 8601 timestamps. Scheduled messages support delays from 1 minute to 30 days.
Combine the --schedule flag with cron to separate data collection from delivery. A cron job at 2 AM collects the report, and the CLI holds the email until 9 AM. Recipients get a fresh report at the start of their day instead of a 7-hour-old notification buried in overnight email. This is especially useful for teams spread across time zones — a single cron job can queue reports that arrive at each recipient's local morning.
# Collect report at 2 AM, deliver at 9 AM
0 2 * * 1-5 /opt/scripts/weekly-metrics.sh | nylas email send \
--to team@company.com \
--subject "Daily metrics report — $(date +\%Y-\%m-\%d)" \
--body "$(cat)" \
--schedule "9am" \
--yes 2>> /var/log/cron-email.logHow do you debug cron email failures?
Cron email failures are invisible by default because cron discards stdout and stderr unless you redirect them. The first debugging step is always to capture output. About 80% of cron failures fall into 3 categories: PATH issues (command not found), authentication expired, and permission errors on the log file.
Start by checking whether the CLI is accessible and authenticated. The nylas auth whoami command shows the current authentication status, connected email, and provider. If it returns an error, re-run nylas auth config.
# 1. Verify the CLI is on PATH and authenticated
which nylas
nylas auth whoami
# 2. Test the exact cron command manually
/home/linuxbrew/.linuxbrew/bin/nylas email send \
--to your-email@company.com \
--subject "Cron test $(date)" \
--body "Manual test from $(hostname)" \
--yes
# 3. Check the cron log for errors
tail -20 /var/log/cron-email.logIf manual sends work but cron fails, the problem is almost always the environment. Cron runs with a stripped-down environment — no $HOME, minimal $PATH, and no shell profile. Add environment variables explicitly in your crontab.
# Fix common cron environment issues
SHELL=/bin/bash
HOME=/home/deploy
PATH=/home/linuxbrew/.linuxbrew/bin:/usr/local/bin:/usr/bin:/bin
# Now cron jobs can find nylas and read its config
0 6 * * * nylas email send --to ops@company.com --subject "Morning check" --body "OK" --yesFor persistent debugging, redirect both stdout and stderr to a log file with timestamps. The pattern below prepends a UTC timestamp to each line, which makes it easy to correlate failures with specific cron runs. UTC avoids daylight saving time confusion — a log gap that looks like a missed run might just be a DST clock change if you use local time.
# Full debug logging with timestamps
0 * * * * /opt/scripts/disk-alert.sh 2>&1 | while read line; do echo "$(date -u +\%Y-\%m-\%dT\%H:\%M:\%SZ) $line"; done >> /var/log/cron-debug.logCommon errors and their fixes:
| Error | Cause | Fix |
|---|---|---|
command not found: nylas | Cron PATH doesn't include install directory | Use full path or set PATH= in crontab |
no API key configured | CLI can't find config file from cron's HOME | Set HOME=/home/deploy in crontab |
401 Unauthorized | API key was rotated or revoked | Re-run nylas auth config --api-key |
Permission denied on log file | Cron user can't write to the log path | Use touch /var/log/cron-email.log && chmod 664 |
If everything looks correct and the email still doesn't arrive, check the recipient's spam folder. Automated emails from new senders sometimes get flagged during the first few sends. Using a real, verified sender address (not a noreply@) and keeping the subject line informational rather than promotional helps deliverability. Once the recipient marks 2-3 messages as “not spam,” subsequent alerts should land in the inbox.
How do you verify emails were delivered?
Sending an email is half the job — knowing it arrived is the other half. The nylas email list command shows recent sent messages when you pass --folder Sent. You can verify the last alert landed by checking the sent folder from the same terminal where the cron job runs. This takes under 2 seconds.
For automated verification, pipe the send output through --json and extract the message ID. A non-empty message ID confirms the API accepted the message for delivery. You can store these IDs in a log file and cross-reference them against the sent folder for a full audit trail. On a server sending 10 alerts per day, the audit log grows by roughly 5 KB per month — small enough to keep indefinitely.
# Check the sent folder for recent alerts
nylas email list --folder Sent --limit 5
# Programmatic verification: capture the message ID
MSG_ID=$(nylas email send \
--to ops@company.com \
--subject "Test alert" \
--body "Delivery verification test" \
--yes \
--json 2>/dev/null | grep -o '"id":"[^"]*"' | head -1)
echo "Sent message: $MSG_ID" >> /var/log/alert-audit.logHow does this compare to other cron email methods?
There are 5 common ways to send email from a cron job, each with different setup complexity and failure modes. The table below compares them on setup time, OAuth survival, and daemon requirements. Setup times are based on a fresh Ubuntu 24.04 server.
| Method | Setup time | OAuth support | Daemon required | Multi-provider |
|---|---|---|---|---|
| Postfix + MAILTO | 30-60 min | No | Yes (postfix daemon) | No (one relay) |
| msmtp + Gmail | 15-30 min | Partial (manual refresh) | No | No |
| ssmtp (abandoned) | 10-15 min | No | No | No |
| curl + SMTP API | 5-10 min | Varies by API | No | No |
| Nylas CLI | 2 min | Yes (auto-refresh) | No | Yes (6 providers) |
The trade-off is that the CLI requires a Nylas API key and an internet connection. For air-gapped servers that can't reach external APIs, Postfix with a local relay is still the right choice. For every other case — cloud VMs, CI runners, containers, and development servers — the API-based approach eliminates the maintenance overhead of running a mail daemon.
Next steps
You now have cron email notifications that don't depend on Postfix, sendmail, or app passwords. The setup survives OAuth token changes, server reboots, and provider authentication updates.
- Send email from terminal — the full reference for all send options including CC, BCC, scheduling, and tracking
- GitHub Actions email notifications — the same pattern applied to CI/CD pipelines with encrypted secrets
- Send email from PowerShell — Windows Task Scheduler equivalent using Nylas CLI and PowerShell
nylas email sendcommand reference — all flags, including--track-opens,--track-links, and--metadata- Full command reference — all 72+ CLI commands for email, calendar, contacts, and authentication
- Getting started — install methods for macOS, Linux, Windows, and Go