Standalone Python (FastAPI + Jinja2) service that receives NetBeez webhook notifications, renders event-specific email templates, and sends via SMTP with retry. Includes a web dashboard and optional Docker deployment.
- nb-api sends webhook POSTs (JSON:API) to this service.
- The service validates the payload, deduplicates by event ID, renders the matching Jinja2 template, and sends email via SMTP.
- A built-in dashboard shows template inventory, event history, config (redacted), and test tools.
Important: If you use both nb-api’s built-in email notifications (Settings → Notification Integrations → SMTP) and this webhook-based service, you will get duplicate emails for every alert/incident. Disable the built-in SMTP notification channel when using this service (or the reverse).
- Python 3.11+
- Docker Engine 20.10+ and Docker Compose v2 (for containerized run)
- SMTP relay or mail server reachable from the service
- Network reachability from the Beezkeeper server (nb-api) to this service
- A shared webhook auth token (configured in nb-api and in this service’s config)
-
Download the project and go to the project root.
Git clone (recommended):
git clone https://github.com/netbeez/nb-email-templating.git cd nb-email-templatingTarball (optional, if you prefer not to use Git): download the archive for your branch or tag, then extract and enter the directory (GitHub archives unpack to a folder named
<repo>-<ref>):curl -fsSL -o nb-email-templating.tar.gz 'https://github.com/netbeez/nb-email-templating/archive/refs/heads/main.tar.gz' tar xzf nb-email-templating.tar.gz cd nb-email-templating-main
-
If you run with Docker Compose, create an empty
.envin the project root so Compose does not fail on theenv_file: .enventry indocker-compose.yml(you can add real variables to this file later):touch .env
-
Copy the example config and set token + SMTP via env or edit:
cp config/config.example.yaml config/config.yaml export NB_EMAIL_WEBHOOK_TOKEN=your-secret-token export SMTP_USERNAME=your-smtp-user export SMTP_PASSWORD=your-smtp-password
-
Run with Docker (Compose reads the project
.envand passes values into the container; seedocker-compose.yml):docker compose up -d
Or run locally (create a venv first):
pip install -e . uvicorn nb_email_templating.main:app --host 0.0.0.0 --port 8025 -
Check health:
GET http://localhost:8025/health
See config/config.example.yaml for all options. Main sections:
- server: host, port, optional
public_base_url(for webhook URL hints on the dashboard when the browser host differs from Beezkeeper’s), shutdown timeout, max webhook payload size. - auth:
webhook_token(use${NB_EMAIL_WEBHOOK_TOKEN}or${VAR:-default}), session cookie name and max age. - smtp: host, port, STARTTLS, username/password (use
${SMTP_USERNAME},${SMTP_PASSWORD}), from address, max connections. - dedup:
window_seconds. - data_retention:
days,cleanup_hour. - retry: max attempts, backoff, recovery timeout for stuck events.
- rendering: template render timeout.
- test_tools: rate limit per minute.
- logging: log directory, rotation (
max_bytes,backup_count), level, format (e.g. JSON). - template_context: optional string key/value map merged into every email render context (e.g.
staff_sop_url,netbeez_dashboard_url). The dashboard footer and incident buttons can use these;netbeez_dashboard_urlalso drives therewrite_url_originJinja filter to swap the NetBeez host inattributes.url. - templates: per-event-type file, subject, recipients (to/cc/bcc), active flag.
Environment variable resolution: ${VAR} and ${VAR:-default} are replaced from the environment before Pydantic validation. Missing required vars (no default) cause startup failure.
- Inbound: Beezkeeper nb-api → this service (e.g. TCP 8025), and admin browser → this service (8025).
- Outbound: This service → SMTP server (e.g. 587 or 465).
Configure the webhook URL in nb-api to point at this service (same host or separate). Ensure TCP 8025 is reachable from nb-api to the email service host (IP or FQDN).
- Settings → Notification Integrations → Webhooks.
- Create an alert webhook: URL
http://<email-service-host>:8025/webhook?token=<your-secret-token>, notification type alert, serializer Integrations::JsonApiAlertSerializer. - Create an incident webhook with the same URL, notification type incident, serializer Integrations::JsonApiIncidentSerializer.
- Assign both webhooks to the desired agents/targets/wifi profiles.
Payloads follow JSON:API from nb-api:
- Single alert:
{ "data": { "id", "type": "alert", "attributes": { "event_type", "message", "agent", "destination", "alert_ts", ... } } } - Aggregate alerts:
{ "data": [ { "id", "type": "alert", "attributes": { ... } }, ... ] } - Incident:
{ "data": { "id", "type": "incident", "attributes": { "event", "message", "url", "incident_ts", ... } } }
Templates receive event_type, event_id, data_type, attributes, and alerts (list; one item for single alert/incident). Aggregate alert payloads (data as an array) are routed to ALERT_AGGREGATE; attributes matches the first alert, aggregate_count is the array length, is_aggregate is true, and each item in alerts keeps its own lifecycle event_type (ALERT_OPEN or ALERT_CLEARED). Aggregate templates also receive aggregation_entity_type, aggregation_entity_type_label, aggregation_entity_name, and top-level test_counts derived from the first alert. Keys from template_context in config are merged into the same context. All *_ts fields are milliseconds since epoch.
Incident templates can render a tests table when attributes.tests is present and is a list (shape depends on your NetBeez incident serializer; if the webhook does not include that array, only the summary rows and message will appear).
Templates live in email_templates/ (e.g. alert_open.html.j2, incident_open.html.j2, _fallback.html.j2). Use the dashboard at /templates to list, edit (with validation), and preview. Subject lines are configured per event type in config.yaml.
- Token in URL: The webhook token appears in the webhook URL. Prefer internal networks, HTTPS, and token rotation.
- Secrets: Use env vars for
NB_EMAIL_WEBHOOK_TOKEN,SMTP_USERNAME,SMTP_PASSWORD; the service redacts them in logs and config view. - Dashboard: Protected by the same token (query param or session cookie). Mutation endpoints use CSRF.
- Templates: Rendered in a Jinja2 sandbox; template names validated to prevent path traversal.
- SMTP test: Send a test email to a given address.
- Render + send: Choose event type, optional JSON payload, preview HTML, or send a test email. Rate-limited (default 5/min).
docker compose stop nb-email-templating- Update image tag to the previous version
docker compose up -d nb-email-templating
SQLite data and template volumes are unchanged by rollback.
GET /healthreturns 200 with DB and logs ok.- Dashboard at
GET /(with?token=...) shows template inventory. - Send a test email from
/test. - Trigger a test webhook and confirm the event appears under
/eventswith delivery status.
| Symptom | Likely cause | Check |
|---|---|---|
| nb-api logs "non 200" | Unreachable or wrong token | Network and ?token= match |
| No emails | Wrong SMTP or relay reject | /test SMTP tool, logs |
| 401 on webhook | Token mismatch | Webhook URL token vs config.auth.webhook_token |
| Duplicate emails | Both built-in SMTP and this service on | Disable one channel |