Skip to content

SMTP presets

z4j sends invitation and password-reset emails through the project's notification channels, not through global env vars. Each email channel is a record in the brain's notification_channels table with the SMTP config inline. Create one per project. z4j tries the project's active email channels in order until a delivery succeeds; for a password reset it does that across every project the user belongs to.

An email channel's config JSON looks like this:

{
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "you@gmail.com",
"smtp_pass": "xxxx-xxxx-xxxx-xxxx",
"smtp_tls": true,
"from_addr": "z4j <you@gmail.com>",
"to_addrs": ["ops@example.com"]
}
Field Meaning
smtp_host SMTP hostname. Resolution is checked during validation and again for delivery; loopback, private, link-local, and other blocked address classes are rejected. There is no private-network opt-in for SMTP channels.
smtp_port One of the allow-listed SMTP ports (25, 465, 587, 2525).
smtp_user Username.
smtp_pass Password.
smtp_tls Defaults to true. Port 465 is implicit TLS whatever this says; on 25, 587 and 2525, true means STARTTLS and false sends plaintext.
from_addr From: header. RFC 5322 mailbox or display <addr@host> format.
to_addrs Default recipient list. For invitation and reset emails the brain overrides this with the recipient address.

Create the channel via API (POST /api/v1/projects/{slug}/notifications/channels) or via the dashboard's Notifications page.

Gmail requires an app password (not your account password). Enable 2FA, mint an app password at myaccount.google.com/apppasswords, then create an email channel with:

{
"smtp_host": "smtp.gmail.com",
"smtp_port": 587,
"smtp_user": "you@gmail.com",
"smtp_pass": "xxxx-xxxx-xxxx-xxxx",
"smtp_tls": true,
"from_addr": "z4j <you@gmail.com>"
}

OAuth2 against Gmail is not supported; SMTP + app password only.

{
"smtp_host": "smtp.mailgun.org",
"smtp_port": 587,
"smtp_user": "postmaster@mg.yourdomain",
"smtp_pass": "<mailgun smtp password>",
"smtp_tls": true,
"from_addr": "z4j <noreply@yourdomain>"
}
{
"smtp_host": "smtp-relay.brevo.com",
"smtp_port": 587,
"smtp_user": "<your brevo smtp login>",
"smtp_pass": "<your smtp key>",
"smtp_tls": true,
"from_addr": "z4j <noreply@yourdomain>"
}
{
"smtp_host": "email-smtp.us-east-1.amazonaws.com",
"smtp_port": 587,
"smtp_user": "<SES SMTP username>",
"smtp_pass": "<SES SMTP password>",
"smtp_tls": true,
"from_addr": "z4j <noreply@verified-domain>"
}

SES requires a verified sender domain.

Without an active email channel, invitation creation still returns its single-use accept URL, so an administrator can deliver that link out of band.

Password-reset requests are different: the public endpoint always returns the same generic accepted response and never exposes the reset token or URL. Without an email channel, self-service password reset cannot deliver its link. Use the fresh-MFA admin password-reset route or z4j changepassword user@example.com --password-stdin for operator-assisted recovery.