Skip to content

Sending Email by SMTP

The smtp transport sends email directly over SMTP — the Processor speaks the protocol itself, using only Go's standard library (no third-party SMTP or SASL package). It's the right choice for your own mail server, for a Gmail or Google Workspace mailbox, and — via its oauth2 authentication mode — for Exchange Online's SMTP AUTH OAuth2 replacement, the Azure Communication Services SMTP relay, or Google's OAuth2-authenticated SMTP endpoints. It is also how pg_relay reaches most third-party email providers — ready-made profiles for Amazon SES, SendGrid, Postmark, Mailgun, SMTP2GO, and Resend are in this book's provider recipe chapters.

Profile fields

Field Type Required Default Notes
host string yes SMTP server hostname
port integer no 587
security string no starttls one of starttls, tls, none — see below
auth string no plain if username is set, otherwise none one of plain, login, none, oauth2
username string for auth: plain/login ignored under auth: oauth2 — see the oauth2 block below
password string for auth: plain/login use _env:VAR_NAME — see Keeping Secrets Out of the Database
from string yes the message's sender address
timeout_seconds integer no 30 (capped at 120)
oauth2 object for auth: oauth2 see below

The oauth2 block

When auth is oauth2, the flat username/password fields above are ignored, and this nested object carries everything needed instead: an OAuth2 grant, exchanged for a bearer token and sent to the server as SASL XOAUTH2. The block's optional grant_type field selects which of two grants that is:

  • client_credentials (the default when grant_type is absent) — an Entra ID (Microsoft's identity platform) application grant. This is the mode for the Azure Communication Services SMTP relay and for Exchange Online's SMTP AUTH OAuth2 replacement.
  • refresh_token — a per-mailbox consent grant. This is the mode for Gmail and Google Workspace SMTP (smtp.gmail.com, smtp-relay.gmail.com), where the credential is a refresh token produced by a one-time OAuth consent from the sending mailbox rather than a service-wide secret.

With grant_type: client_credentials (or omitted):

Field Required Notes
username yes the SMTP username presented in the XOAUTH2 exchange — this can differ from any login name you'd otherwise use (for example, an Azure Communication Services–provisioned SMTP username)
tenant_id yes the Entra tenant id
client_id yes the Entra app registration's client id
client_secret yes use _env:VAR_NAME
scope yes the OAuth2 scope to request for the token

scope being a profile field rather than something fixed in the Processor is what lets this one grant serve two different purposes with no transport-specific code: point it at the Azure Communication Services SMTP relay's scope, or at Exchange Online's own SMTP AUTH OAuth2 scope, and the identical mechanism handles either.

With grant_type: refresh_token:

Field Required Notes
username yes the mailbox address this refresh token authorises — for Gmail, the account itself
token_url yes the provider's token endpoint — for Google, https://oauth2.googleapis.com/token
client_id yes the OAuth client id (for Google, ....apps.googleusercontent.com)
client_secret yes use _env:VAR_NAME
refresh_token yes use _env:VAR_NAME — the mailbox's own consent grant

token_url is a profile field (rather than something fixed in the Processor) because refresh-token providers each have their own flat token endpoint — any provider whose SMTP servers accept XOAUTH2 and whose token endpoint answers a standard refresh-token grant works through this same block, with Google simply being the one this mode was built for.

Every field in the block — username included — accepts _env: references, since they all go through the same secret-resolution pass described in Keeping Secrets Out of the Database. Access tokens are cached and refreshed automatically; the refresh token itself stays valid until the mailbox owner revokes it, at which point sends fail permanently until an operator re-runs the consent flow and updates the referenced variable.

Gmail and Google Workspace over SMTP

Three ways into a Google mailbox, simplest first:

  1. An App Password — for a mailbox with 2-Step Verification, generate an App Password and use the ordinary auth: plain mode against smtp.gmail.com:587 with security: starttls. No OAuth2 involved; this is the zero-ceremony option unless your Workspace admin policy disables App Passwords.
  2. The Workspace SMTP relay by source IPsmtp-relay.gmail.com can be configured (in the Workspace admin console) to accept mail from your fixed egress IP with no per-message credential at all: auth: none, security: starttls. The simplest option for a fixed-IP deployment.
  3. OAuth2auth: oauth2 with grant_type: refresh_token as above, for deployments required to use OAuth2. Same XOAUTH2 mechanism, Google's token endpoint.

Whichever you use, from should be the authenticated mailbox itself or one of its configured Send As aliases. For the Gmail REST API alternative to SMTP entirely, see Sending Email through Gmail.

Message fields

Field Required Notes
to at least one address
cc no
bcc no envelope-only — never appears in the message headers a recipient sees
reply_to no
subject no
body_text / body_html at least one supplying both sends multipart/alternative, so mail clients pick whichever they render better
attachments no array of {filename, content_type, content_base64}

TLS and security

security Behaviour
starttls (default) Connects in plain text, then negotiates STARTTLS before authenticating
tls Negotiates TLS immediately, before any SMTP conversation happens
none No TLS at all

Whichever mode you use, credentials are never sent over a connection that has not negotiated TLS — the only exception is a server at localhost, which is allowed to accept plain-text authentication for local testing. A LOGIN or oauth2 authentication attempt against a non-TLS, non-local connection is refused outright before anything is sent.

Message-ID and duplicate-resistant retries

Every send carries a Message-ID header derived deterministically from the notification's own primary key — never a random one. If a send genuinely succeeds at the mail server but the Processor's transaction never commits (a crash at exactly the wrong moment), the retried attempt carries the exact same Message-ID. A receiving server or client that de-duplicates by Message-ID then recognises the retry as the message it already has, rather than showing the recipient a duplicate.

Rejected recipients

If the mail server rejects a recipient at the RCPT TO step, the failure is identified by category and position — for example to[2/3] — never by the address itself, which is message content and is never written to pgrelay.log, to set_status's own detail text, or to any Processor log line. The position is enough to look the real address up in pg_relay_notifier's own record of the message.

Failure classification

A 5xx SMTP reply (bad recipient, rejected authentication, a policy refusal) is a permanent failure — no retry. Everything else — a 4xx reply, a dial failure, a timeout, a TLS interruption — is transient and goes through the normal retry chain.


Continue to Sending Email through Microsoft 365.