DEV Community

Cover image for Email Monitoring with MailWebhook and Zabbix
Pavels Gurskis
Pavels Gurskis

Posted on • Originally published at blog.mailwebhook.com

Email Monitoring with MailWebhook and Zabbix

SMTP checks can tell you whether a server appears healthy. They do not always tell you whether a real message made it through the path your users depend on.

This recipe uses MailWebhook as the arrival detector and Zabbix as the monitoring system. The goal is simple: send a tagged canary email, report success only when that email arrives, and let Zabbix alert when the success heartbeat goes missing.

Pattern

  1. A cron job sends a canary email through an external sender.
  2. The sender resolves MX and delivers to the mailbox under test.
  3. MailWebhook watches that mailbox.
  4. When the canary arrives, MailWebhook calls Zabbix history.push().
  5. Zabbix alerts if no OK heartbeat arrives within the expected window.

The key idea is to alert on missing success, not on every possible delivery failure.

When the canary arrives, MailWebhook calls Zabbix history.push(). Zabbix alerts only if no OK heartbeat arrives within the expected window.

The key idea is to alert on missing success, not on every possible delivery failure.

Canary tagging

Do not send a generic test email. Send a tagged canary:

Subject: MW-CANARY v=1 env=prod mailbox=mx1-prod sender=postmark id=<uuid> ts=<utc>
Enter fullscreen mode Exit fullscreen mode

Each tag has a job:

MW-CANARY      route this email as a canary
env            avoid mixing prod/staging
mailbox        map the OK to the right Zabbix item
sender         identify the external sender path
id             correlate sent vs received canary
ts             keep sent time for troubleshooting or future latency checks
Enter fullscreen mode Exit fullscreen mode

For the first version, Zabbix only needs the OK heartbeat. This canary:

MW-CANARY v=1 env=prod mailbox=mx1-prod sender=postmark id=7f3a9c ts=2026-05-18T12:00:00Z
Enter fullscreen mode Exit fullscreen mode

maps to this Zabbix item key:

mail.canary.ok[prod,mx1-prod]
Enter fullscreen mode Exit fullscreen mode

Zabbix setup

Create a host:

Host: mail-deliverability
Enter fullscreen mode Exit fullscreen mode

Create one Zabbix trapper item per mailbox path:

Name: Canary OK: prod mx1-prod
Key: mail.canary.ok[prod,mx1-prod]
Type: Zabbix trapper
Type of information: Numeric unsigned
Enter fullscreen mode Exit fullscreen mode

Then add a trigger expression using nodata():

nodata(/mail-deliverability/mail.canary.ok[prod,mx1-prod],10m)=1
Enter fullscreen mode Exit fullscreen mode

Example tuning:

Send interval: 1 minute
Warning: nodata(...,5m)=1
Critical: nodata(...,10m)=1
Enter fullscreen mode Exit fullscreen mode

For noisier paths:

Send interval: 5 minutes
Warning: nodata(...,15m)=1
Critical: nodata(...,30m)=1
Enter fullscreen mode Exit fullscreen mode

This avoids flapping because one delayed email does not immediately create an incident. Zabbix only alerts when the OK heartbeat has been missing for longer than the threshold.

MailWebhook endpoint

Create a MailWebhook endpoint pointing to the Zabbix JSON-RPC API:

https://zabbix.example.com/zabbix/api_jsonrpc.php
Enter fullscreen mode Exit fullscreen mode

Add a custom header:

Authorization: Bearer <ZABBIX_API_TOKEN>
Enter fullscreen mode Exit fullscreen mode

MailWebhook route

Use MailWebhook route rules to match only canary emails:

{
  "to_emails": ["deliverability-check@example.com"],
  "from_domains": ["your-canary-sender.example"],
  "subject_regex": [
    "^.*\\bMW-CANARY\\b.*\\benv=[A-Za-z0-9_-]+\\b.*\\bmailbox=[A-Za-z0-9_-]+\\b.*$"
  ]
}
Enter fullscreen mode Exit fullscreen mode

MailWebhook custom JSON payload

Use map.custom_json to emit the Zabbix history.push() request. The expressions below use the MailWebhook JsonLogic-style DSL for regex.replace and cat.

{
  "pipeline": {
    "steps": [
      {
        "name": "map.custom_json",
        "args": {
          "version": "v1",
          "vars": [
            {
              "name": "env",
              "expr": {
                "regex.replace": {
                  "value": { "var": "message.subject" },
                  "pattern": "^.*\\benv=([A-Za-z0-9_-]+)\\b.*$",
                  "with": "\\1"
                }
              }
            },
            {
              "name": "mailbox_id",
              "expr": {
                "regex.replace": {
                  "value": { "var": "message.subject" },
                  "pattern": "^.*\\bmailbox=([A-Za-z0-9_-]+)\\b.*$",
                  "with": "\\1"
                }
              }
            },
            {
              "name": "zabbix_key",
              "expr": {
                "cat": [
                  "mail.canary.ok[",
                  { "var": "vars.env" },
                  ",",
                  { "var": "vars.mailbox_id" },
                  "]"
                ]
              }
            }
          ],
          "output": {
            "jsonrpc": "2.0",
            "method": "history.push",
            "params": [
              {
                "host": "mail-deliverability",
                "key": { "var": "vars.zabbix_key" },
                "value": 1
              }
            ],
            "id": 1
          }
        }
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

For the example subject above, MailWebhook sends this to Zabbix:

{
  "jsonrpc": "2.0",
  "method": "history.push",
  "params": [
    {
      "host": "mail-deliverability",
      "key": "mail.canary.ok[prod,mx1-prod]",
      "value": 1
    }
  ],
  "id": 1
}
Enter fullscreen mode Exit fullscreen mode

Canary sender

Here is a minimal cron sender:

#!/usr/bin/env bash
set -euo pipefail

ENV="prod"
MAILBOX_ID="mx1-prod"
SENDER_ID="postmark"
TO="deliverability-check@example.com"
FROM="canary@your-canary-sender.example"

CANARY_ID="$(uuidgen)"
TS="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

SUBJECT="MW-CANARY v=1 env=${ENV} mailbox=${MAILBOX_ID} sender=${SENDER_ID} id=${CANARY_ID} ts=${TS}"

sleep "$(( RANDOM % 20 ))"

printf 'deliverability canary\n' | mail \
  -s "$SUBJECT" \
  -r "$FROM" \
  "$TO"
Enter fullscreen mode Exit fullscreen mode

Cron:

* * * * * /opt/mail-canary/send-canary.sh
Enter fullscreen mode Exit fullscreen mode

The jitter prevents all checks from landing at the exact same second.

Operational notes

Simple start:

One mailbox path -> one Zabbix trapper item -> one nodata trigger
Enter fullscreen mode Exit fullscreen mode

Add dimensions only when needed:

mail.canary.ok[prod,mx1-prod]
mail.canary.ok[prod,mx1-prod,postmark]
mail.canary.ok[prod,mx1-prod,ses]
Enter fullscreen mode Exit fullscreen mode

Multi-sender checks are useful when you need to distinguish mailbox-path failures from sender-path failures.

One caveat: direct delivery to Zabbix is the simplest implementation, but Zabbix will not validate MailWebhook webhook signatures or a canary HMAC token by itself. For stricter security, put a tiny gateway in front: verify the MailWebhook signature, optionally validate a canary token, then forward history.push() to Zabbix.

Top comments (0)