Sending mail from a function
substrio.mail.smtp(options) sends one message over SMTP and resolves with { ok, code, message }: a
server refusal never throws, it settles ok: false with the SMTP code and the server's message. Only
a call with no options object, or one missing message, throws (a TypeError or a MISSING_FIELD error).
export default async function (request) {
const result = await substrio.mail.smtp({
host: "smtp.example.com",
port: 587,
user: env.SMTP_USER,
pass: env.SMTP_PASS,
from: "[email protected]",
to: "[email protected]",
message: "Subject: Your order shipped\r\nFrom: [email protected]\r\nTo: [email protected]\r\n\r\nOn its way.\r\n",
});
return Response.json(result);
}
| Option | Type | Default | Meaning |
|---|---|---|---|
host |
string | (required) | the SMTP server |
port |
number | 587, or 465 when secure |
the SMTP port |
secure |
boolean | false |
TLS from the first byte (port 465) instead of STARTTLS |
user, pass |
string | none | SMTP login |
from |
string | (required) | the envelope sender |
to |
string or string[] | (required) | one or more recipients, each its own RCPT |
message |
string | (required) | the whole message, headers and body (RFC 5322) |
requireTLS |
boolean | true |
refuse a server with no STARTTLS when secure is false |
verifyTLS |
boolean | true |
verify the server's certificate |
encode |
boolean | true |
normalize line endings to CRLF |
timeoutMs |
number | none | the send's timeout |
host, from and at least one to address are all required: missing any of them settles ok: false
with code: 0. The send runs behind your app's outbound fence, the same private-address allowlist a
fetch() call runs behind, and counts against the call's outbound fetch limit: a loop of sends can
exhaust that call's fetch budget. message is limited to 16MB; a larger one is refused with a
VALUE_TOO_LARGE error. Store the SMTP login as secrets and read it from env, rather than
writing it into the function's source.
The mail your app's sign-in flow sends
Substrio does not send mail on its own behalf. Name one of your functions as email_function in the
[auth] table of substrio.toml, and it is called whenever a sign-in flow needs a mail sent: a password
reset, an email verification, or an account invite.
[auth]
email_function = "send_auth_mail"
reset_url = "https://app.example/reset?token={token}"
verify_url = "https://app.example/verify?token={token}"
export default async function send_auth_mail(event) {
// event: { kind, email, token, link, expires_at, app, user_id }
await substrio.mail.smtp({
host: "smtp.example.com",
user: env.SMTP_USER,
pass: env.SMTP_PASS,
from: "[email protected]",
to: event.email,
message: `Subject: Confirm your action\r\nFrom: [email protected]\r\nTo: ${event.email}\r\n\r\n${event.link}\r\n`,
});
}
| Field | Meaning |
|---|---|
kind |
password_reset, email_verify or account_invite |
email |
where to send it |
token |
the single-use token, redeemed by POST /v1/_auth/password/reset or /v1/_auth/email/verify |
link |
reset_url or verify_url with {token} filled in (an invite's link comes from invite_url); empty when the setting is empty |
expires_at |
when the token stops working, in Unix seconds |
app |
the app's slug |
user_id |
the user the token was issued to (absent for account_invite) |
account_name, inviter_email, role |
present only for account_invite, in place of user_id |
What email_function returns is not read, and a failure inside it does not reach the caller: a reset
request answers 202 {"accepted": true} at once whether or not the address has an account, so an address
cannot be probed. Read a failed send from the function's own logs. With no email_function set, a
password reset or verification request answers 409.
curl -s -X POST https://my-app.substrio.com/v1/_auth/password/reset_request \
-H "Content-Type: application/json" -d '{"email": "[email protected]"}'
See authentication for the full sign-in, reset and invite flow, and the routes that redeem a token.