The SMTP driver sends a real email per event — multipart/alternative
with both text/plain and text/html parts, so Gmail/Outlook render
the rich layout and terminal MUAs see a usable fallback. It works
with any SMTP relay: Gmail (with an app password), Amazon SES,
Mailgun, Postmark, SendGrid, or your own Postfix on 127.0.0.1.
id, type, host, from, and at least one to are required.
Credentials are optional — leave username and password unset to
talk to a local relay (Postfix listening on 127.0.0.1:25, an
internal MTA, etc.). Set them together or not at all. Use
${...} substitution to pull the
password from an env var or a file —
storing the secret.
Key
Required
What it does
host
yes
SMTP relay hostname (e.g. smtp.gmail.com).
port
no
TCP port. Defaults to 25; common values: 587 (STARTTLS), 465 (implicit TLS).
tls
no
"starttls" (default for 25/587), "implicit" (default for 465), "none".
tls_skip_verify
no
Skip TLS certificate verification. Do not set this in production.
username
no
SMTP AUTH username. Required when sending creds.
password
no
SMTP AUTH password — inline, ${VAR}, or ${file:path}.
from
yes
From: header. Accepts "name@example.com" or "Name <name@example.com>".
SendGrid uses a fixed username (apikey) and an API key as
the password.
[[notifier]]
id = "email-ops"
type = "smtp"
host = "smtp.sendgrid.net"
port = 587
from = "RunWisp <alerts@yourdomain.com>"
to = ["ops@yourdomain.com"]
username = "apikey"
password = "${RUNWISP_SENDGRID_API_KEY}"
SES uses dedicated SMTP credentials — generate them in the
SES console under SMTP settings, not your IAM password.
[[notifier]]
id = "email-ops"
type = "smtp"
host = "email-smtp.eu-west-1.amazonaws.com"
port = 587
from = "RunWisp <alerts@yourdomain.com>"
to = ["ops@yourdomain.com"]
username = "AKIA..."
password = "${RUNWISP_SES_SMTP_PASSWORD}"
Same shape — pick the region-appropriate host and use the
credentials they issue.
# Mailgun
[[notifier]]
id = "email-ops"
type = "smtp"
host = "smtp.eu.mailgun.org"
port = 587
from = "RunWisp <alerts@yourdomain.com>"
to = ["ops@yourdomain.com"]
username = "postmaster@yourdomain.com"
password = "${RUNWISP_MAILGUN_PASSWORD}"
# Postmark
[[notifier]]
id = "email-ops"
type = "smtp"
host = "smtp.postmarkapp.com"
port = 587
from = "RunWisp <alerts@yourdomain.com>"
to = ["ops@yourdomain.com"]
username = "<server-token>"
password = "${RUNWISP_POSTMARK_TOKEN}"
A relay you already trust on the same host — no auth, no
TLS. The daemon refuses to send credentials over cleartext,
so tls = "none" only works alongside an auth-less relay.
The simplest option for any deployment — Docker, systemd,
bare metal. Set the variable in whatever already manages
your environment.
Terminal window
exportRUNWISP_SMTP_PASSWORD='your-app-password'
runwispdaemon
[[notifier]]
id = "email-ops"
type = "smtp"
host = "smtp.example.com"
from = "runwisp@example.com"
to = ["ops@example.com"]
username = "apikey"
password = "${RUNWISP_SMTP_PASSWORD}"
Useful when a secrets manager (Vault agent, sops, Docker
secrets at /run/secrets/...) writes the password to a
known path for you. Relative paths resolve next to
runwisp.toml; ~/ works too.
Identical to Slack and Telegram — the routing layer does not care
which driver is on the other end:
# On one task:
[tasks.backup-postgres]
notify_on_failure = ["email-ops"]
# …
# Or in a notification rule:
[[notification_route]]
match = { kind = ["run.failed", "run.timeout", "run.crashed"] }
notify = ["email-ops"]
Inline recipient overrides reuse one credential set across many
destinations — notify_on_failure = ["email-ops:db-team@example.com"]
sends through the email-ops relay but to db-team@example.com
instead of the parent notifier’s to list:
runwispexecsmoke-test# something that exits non-zero
Emails typically arrive in a few seconds. If yours doesn’t:
Check the bell for a notify.delivery_failed event with the
underlying SMTP error.
Common causes: wrong port for the chosen tls mode (Gmail
needs 587 + STARTTLS; SES on 465 needs tls = "implicit"),
expired app password, recipient address rejected by the relay
(550 No such user).
Spam folder: the first message from a new sender often lands
in spam. Whitelist the from address.
The default template renders one message per event. A failure looks
like:
❌ backup-postgres failed
Exited with code 1 after 0.3s.
Scheduled run · 14 May, 17:11.
Error: connection refused
dial tcp 127.0.0.1:5432: connect:
connection refused
[View full run]
from runwisp · bright-falcon
The captured-output block appears on run.failed and
run.timeout only, capped at three lines / 300 bytes.
The View run link points at
<external_url>/tasks/<task>/<id>, taken from
[daemon] external_url.
When external_url is unset the link is omitted — no broken
anchors.
The fingerprint footer identifies the specific daemon that
emitted the event.
The plain-text alternative is derived from the HTML body, so
terminal MUAs (mutt, mail) see the same content without markup.
template_path overrides the embedded template if you need a
different layout. Copy
smtp.tmpl.html
as your starting point. The helpers statusEmoji, statusVerb,
humanTime, humanDuration, runDuration, triggerPhrase,
eventSentence, eventTrigger, linkLabel, runURL, taskURL,
outputTail, htmlEsc, and fingerprint are available inside it.