Guide
Email Prometheus Alerts without SMTP
Route Prometheus Alertmanager alerts to email by pointing a webhook receiver at a handler that runs nylas email send, instead of configuring Alertmanager's built-in SMTP relay.
Written by Nick Barraclough Product Manager
Reviewed by Qasim Muhammad
Why skip Alertmanager's built-in SMTP?
Alertmanager ships an email_config receiver, but it needs a working SMTP relay: a host, port 587, a username, and a password stored in the config file. Google disabled less-secure-app SMTP access in May 2022 and Microsoft 365 turned off basic SMTP auth, so that password is usually an app password or an OAuth relay you maintain separately. The Nylas CLI moves email out of Alertmanager entirely.
With a webhook receiver, Alertmanager only speaks HTTP to a local handler, and the handler owns delivery. According to the Alertmanager receiver settings docs, a webhook_config takes a single url and an optional bearer token. That is the whole contract. You stop maintaining a smtp_smarthost, a TLS config block, and a secret that 2 systems both depend on. The CLI authenticates with one API key and refreshes OAuth tokens on its own.
How do I configure the Alertmanager webhook receiver?
An Alertmanager webhook receiver is a route target that POSTs alert groups as JSON to a URL you control. You define it under receivers with a webhook_config block, then point a route at it. Alertmanager batches alerts and retries failed POSTs, so your handler receives grouped, deduplicated events rather than one request per firing rule.
The config below sends every alert to a handler listening on localhost:9095. Alertmanager groups alerts by alertname and waits group_wait (30 seconds by default) before the first POST, which means a burst of 50 related alerts arrives as 1 batched payload. Keep send_resolved: true so the handler can email both firing and resolved states.
route:
receiver: nylas-email
group_by: ['alertname']
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
receivers:
- name: nylas-email
webhook_configs:
- url: 'http://localhost:9095/alert'
send_resolved: trueValidate the file before reloading: amtool check-config alertmanager.yml parses the syntax, and a reload via SIGHUP or POST /-/reload applies it without restarting the process.
What does the webhook handler look like?
The handler is a short HTTP server that decodes the Alertmanager payload and shells out to the CLI once per batch. Alertmanager's webhook payload is documented JSON: a top-level status field plus an alerts array, each alert carrying labels and annotations maps. The example uses about 30 lines of Python and the standard library only.
The nylas email send command sends a message across providers without any SMTP relay. It reads NYLAS_API_KEY from the environment, takes the grant ID as its first argument, and the --yes flag skips the interactive confirmation so it runs unattended. The handler builds the subject from the alert name and status, then passes the rendered summary as the body.
import json, os, subprocess
from http.server import BaseHTTPRequestHandler, HTTPServer
GRANT = os.environ["NYLAS_GRANT_ID"]
TO = os.environ["ALERT_EMAIL_TO"]
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
body = self.rfile.read(int(self.headers["Content-Length"]))
data = json.loads(body)
status = data.get("status", "firing")
names = sorted({a["labels"].get("alertname", "alert") for a in data["alerts"]})
subject = f"[{status.upper()}] {', '.join(names)}"
lines = []
for a in data["alerts"]:
summary = a["annotations"].get("summary", a["labels"].get("alertname", ""))
lines.append(f"- {a['labels'].get('severity', 'none')}: {summary}")
subprocess.run([
"nylas", "email", "send", GRANT,
"--to", TO,
"--subject", subject,
"--body", "\n".join(lines),
"--yes", "--json",
], check=True)
self.send_response(200)
self.end_headers()
HTTPServer(("127.0.0.1", 9095), Handler).serve_forever()Alertmanager posts to this handler through a webhook_configs receiver, so run it with NYLAS_API_KEY, NYLAS_GRANT_ID, and ALERT_EMAIL_TO in its environment, behind your routing tree. Install the CLI with brew install nylas/nylas-cli/nylas (see getting started for other methods).
How do I route alerts by severity?
Severity routing sends critical pages to one address and warnings to another, using Alertmanager's route tree rather than handler logic. You match on the severity label and assign a different receiver per branch. Each receiver points at the same handler on a distinct path or port, so the handler reads which recipient list to use from the URL it was hit on.
The route below splits traffic: alerts labeled severity: critical hit /alert/critical, everything else falls through to the default. This keeps the 2 audiences separate without duplicating alert rules. A critical database-down alert reaches the on-call rotation in under 60 seconds, while a disk-at-80-percent warning goes to a lower-priority inbox the team reviews daily.
route:
receiver: nylas-default
routes:
- matchers: [ 'severity="critical"' ]
receiver: nylas-critical
receivers:
- name: nylas-default
webhook_configs:
- url: 'http://localhost:9095/alert'
send_resolved: true
- name: nylas-critical
webhook_configs:
- url: 'http://localhost:9095/alert/critical'
send_resolved: trueWhy does the handler need to track send failures?
A failed email send must not look like a healthy alert pipeline. If the handler returns HTTP 200 after nylas email send exits non-zero, Alertmanager marks the notification delivered and never retries. Returning a 5xx on send failure makes Alertmanager retry on its next attempt, governed by the queue and the route's repeat_interval of 4 hours in the earlier example.
The CLI prints a JSON object with the sent message ID when --json is passed, which gives you an audit record per alert email. Log that ID, then have the handler raise a 502 when the subprocess exits non-zero so the failure surfaces. Alertmanager's notification log dedupes resolved-and-firing pairs, so a brief outage on your side does not produce duplicate emails once delivery recovers. Pair email with a second channel for the highest-severity alerts so one delivery problem never hides a production incident.
result = subprocess.run([
"nylas", "email", "send", GRANT,
"--to", TO, "--subject", subject, "--body", "\n".join(lines),
"--yes", "--json",
], capture_output=True, text=True)
if result.returncode != 0:
self.send_response(502) # tell Alertmanager to retry
self.end_headers()
return
msg_id = json.loads(result.stdout).get("id", "unknown")
print(f"sent alert email: {msg_id}")
self.send_response(200)
self.end_headers()Next steps
- Send Grafana Alert Emails without SMTP — Point a Grafana webhook contact point at a handler that runs nylas…
- Send Sentry Issue Alerts by Email (CLI) — Turn a Sentry issue alert webhook into an enriched email with…
- Terraform email alerts – provision the alert sender and recipients as code
- Kubernetes CronJob email – run the handler or scheduled checks inside a cluster
- Debug email delivery from the CLI – trace why an alert email never arrived
- Automate email reports from terminal – daily digests alongside real-time alerts
- Email to Mattermost notifications – mirror the same alerts into chat
- Email to Telegram notifications – a second channel for critical pages
- Command reference –
nylas email sendflags and JSON output - Alertmanager webhook_config reference – receiver fields and payload format
- Alertmanager overview – grouping, routing, and silencing
- RFC 5322 – the internet message format the CLI emits