Source: https://cli.nylas.com/guides/grafana-alert-emails-cli

# Send Grafana Alert Emails without SMTP

Route Grafana alert emails to recipients by pointing a webhook contact point at a handler that runs nylas email send, instead of configuring Grafana's built-in SMTP server.

Written by [Caleb Geene](https://cli.nylas.com/authors/caleb-geene) Director, Site Reliability Engineering

Reviewed by [Qasim Muhammad](https://cli.nylas.com/authors/qasim-muhammad)

Updated June 9, 2026

> **TL;DR:** Add a `webhook` contact point in Grafana that POSTs to a small HTTP handler. The handler parses the alert JSON and calls [`nylas email send`](https://cli.nylas.com/docs/commands/email-send). No SMTP host in `grafana.ini`, no app password. The payoff: one credential to rotate, shown at the end.

## Why skip Grafana's built-in SMTP server?

Grafana's email contact point needs a working SMTP relay configured in the `[smtp]` section of `grafana.ini`: a host, port 587, a user, and a password that lives in the config or an environment variable. Google disabled less-secure-app SMTP access in May 2022, and Microsoft 365 turned off basic SMTP auth, so that password is now an app password or a relay you maintain yourself. The Nylas CLI moves email delivery out of Grafana.

With a webhook contact point, Grafana only speaks HTTP to a local handler, and the handler owns delivery. According to the Grafana [webhook notifier docs](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/), the integration takes a single URL plus optional auth headers, then POSTs a JSON body on every alert. That is the whole contract. You stop maintaining the `[smtp]` block, a TLS setting, and a password 2 systems both depend on. The CLI authenticates with one API key and refreshes OAuth tokens on its own.

## How do I configure the Grafana webhook contact point?

A Grafana webhook contact point is a notification target that POSTs the alert group as JSON to a URL you control. You create it under Alerting then Contact points, choose the Webhook integration, and set the URL to your handler. Grafana groups and batches alerts through its notification policy, so the handler receives grouped events rather than one request per evaluation.

You can define the contact point in the UI or as file provisioning. The provisioning file below registers a contact point named `nylas-email` pointing at a handler on `localhost:9095`. Grafana applies provisioned contact points on startup and on a config reload, and the default notification policy waits a 30-second group wait before the first POST, so a burst of 40 related alerts arrives as 1 batched payload.

```yaml
apiVersion: 1

contactPoints:
  - orgId: 1
    name: nylas-email
    receivers:
      - uid: nylas_email_webhook
        type: webhook
        settings:
          url: http://localhost:9095/alert
          httpMethod: POST
```

Save the file under `provisioning/alerting/` and restart Grafana, or use the Alerting API to apply it without downtime. Send a test notification from the contact point page to confirm the handler receives the POST before you wire it into a live notification policy.

## What does the webhook handler look like?

The handler is a short HTTP server that decodes the Grafana payload and shells out to the CLI once per batch. Grafana's webhook body is documented JSON: a top-level `status` field plus an `alerts` array, where each alert carries `labels` and `annotations` maps and a `generatorURL`. The example below uses about 30 lines of Python and the standard library only.

The `nylas email send` command sends a message across providers with no 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.

```python
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} ({a.get('generatorURL', '')})")
        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()
```

Run the handler with `NYLAS_API_KEY`, `NYLAS_GRANT_ID`, and `ALERT_EMAIL_TO` exported in its environment. Install the CLI first with `brew install nylas/nylas-cli/nylas` (see [getting started](https://cli.nylas.com/guides/getting-started) for other install methods).

## How do I route Grafana alerts by severity?

Severity routing sends critical pages to one address and warnings to another, using Grafana's notification policy tree rather than handler logic. You add a nested policy that matches on the `severity` label and assigns a different contact point per branch. Each contact point points at the same handler on a distinct path, so the handler reads which recipient list to use from the URL it was hit on.

The provisioning below splits traffic: alerts labeled `severity = critical` route to the `nylas-critical` contact point at `/alert/critical`, while 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.

```yaml
apiVersion: 1

policies:
  - orgId: 1
    receiver: nylas-email
    routes:
      - receiver: nylas-critical
        object_matchers:
          - ['severity', '=', 'critical']

contactPoints:
  - orgId: 1
    name: nylas-critical
    receivers:
      - uid: nylas_critical_webhook
        type: webhook
        settings:
          url: http://localhost:9095/alert/critical
          httpMethod: POST
```

## Why does the handler need to track send failures?

A failed email send must not look like a healthy Grafana alert pipeline. If the handler returns HTTP 200 after `nylas email send` exits non-zero, Grafana records the notification as delivered and moves on. Returning a 5xx on send failure makes Grafana retry the webhook, and the contact point's notification log dedupes firing-and-resolved pairs so a brief outage on your side does not produce duplicate emails once delivery recovers.

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 in Grafana's state history. Pair email with a second channel for the highest-severity alerts so one delivery problem never hides a production incident. Rotating the sender means updating exactly 1 secret, the `NYLAS_API_KEY` the handler reads, not an SMTP password in `grafana.ini`.

```python
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 Grafana to retry the webhook
    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 Sentry Issue Alerts by Email (CLI)](https://cli.nylas.com/guides/sentry-email-alerts-cli) — Turn a Sentry issue alert webhook into an enriched email with…
- [Email Prometheus alerts without SMTP](https://cli.nylas.com/guides/prometheus-alertmanager-email) – the same webhook pattern for Alertmanager receivers
- [Terraform email alerts](https://cli.nylas.com/guides/terraform-email-alerts) – provision the alert sender and recipients as code
- [Debug email delivery from the CLI](https://cli.nylas.com/guides/debug-email-delivery-cli) – trace why an alert email never arrived
- [Email to GitHub issues](https://cli.nylas.com/guides/email-to-github-issues) – turn high-severity alert mail into tracked issues
- [Email to Snowflake](https://cli.nylas.com/guides/email-to-snowflake) – archive alert emails into a data warehouse
- [Command reference](https://cli.nylas.com/docs/commands) – `nylas email send` flags and JSON output
- [Grafana contact points](https://grafana.com/docs/grafana/latest/alerting/configure-notifications/manage-contact-points/) – integrations, provisioning, and test notifications
- [Grafana Alerting overview](https://grafana.com/docs/grafana/latest/alerting/) – alert rules, notification policies, and grouping
- [RFC 5322](https://datatracker.ietf.org/doc/html/rfc5322) – the internet message format the CLI emits
