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.
The id (as the table key), 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_mode
no
"starttls" (default for 25/587), "implicit" (default for 465), "off".
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}.
A relay you already trust on the same host — no auth, no
TLS. The daemon refuses to send credentials over cleartext,
so tls_mode = "off" only works alongside an auth-less relay.
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:
[[route]]
match = { kinds = ["run.failed", "run.timeout", "run.crashed"] }
notifiers = ["email-ops"]
Inline recipient overrides reuse one credential set across many
destinations — notify_on_failure = ["email-ops:[email protected]"]
sends through the email-ops relay but to [email protected]
instead of the parent notifier’s to list:
runwisprunsmoke-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_mode = "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.