Skip to main content
Version: 0.3.36

raisin.email

Transactional email for server-side functions, available in every runtime.

Which providers exist, which is the default and what address they send as is configured per tenant; see the Outbound Email guide.

send(message)​

Sends one message and returns the provider's receipt.

const receipt = raisin.email.send({
to: ["user@example.com"],
subject: "Your sign-in link",
text: "Open this link to sign in: https://app.example.com/…",
html: "<a href='https://app.example.com/…'>Sign in</a>",
});

Message​

FieldTypeNotes
tostring | string[]Required. to, cc and bcc together may name at most 20 recipients.
cc, bccstring | string[]Optional.
subjectstringRequired. A CR, LF or NUL character is refused.
textstringRequired, even alongside html.
htmlstringOptional alternative body, sent alongside the text.
attachmentsEmailAttachment[]Optional. At most 20, 10 MiB each and 10 MiB in total by default.
providerstringWhich configured sender to use. Omit for the default.
correlation{ kind: string, id: string }Optional. Stored on the message record and copied to its delivery events, so you can link them back to your own node.

There is no from field. The sender identity comes from the tenant's email configuration, so a function chooses which configured account to use, not what address it sends as.

provider names an entry from providers(). An unknown name is an error; it does not fall back to the default. null, "" and whitespace all mean "the default".

Receipt​

{ "message_id": "4bJ1x…", "provider": "resend", "sender": "transactional" }
FieldNotes
message_idThe provider's id, which a later bounce or webhook correlates against
providerThe provider API: resend or brevo (smtp is configurable but not yet implemented)
senderThe configured account it went through

Acceptance by the provider is not delivery. Each accepted send is recorded as a raisin:EmailMessage, and with delivery tracking enabled for a Brevo or Resend sender its later events (delivered, bounced, blocked, complained) arrive as raisin:EmailEvent nodes. See Delivery tracking.

Errors​

Every error carries a stable code in its message.

CodeMeans
email:policy_deniedThe function has no email_policy, or it does not permit a recipient
email:configEmail is not enabled for the tenant, no provider is configured, the provider name is unknown, or the entry is incomplete
email:invalid_messageMissing, oversized or malformed message; refused before any connection is opened
email:auth_failedThe provider rejected the credential (401 or 403)
email:rate_limitedThe provider is throttling (429)
email:provider_errorAny other provider response
email:transportDNS, TCP or TLS failure
email:timeoutThe send exceeded 30 seconds
email:unsupportedThe configured provider type has no implementation yet (SMTP)

A function that never declared a policy sees, for example:

Permission denied: [email:policy_denied] cannot send to ["a@example.com"]: this function
has no email_policy (sending is denied by default). Grant it by adding the recipient
domain to the function's email_policy.allowed_recipients.

auth_failed means the credential is wrong (an operator problem); invalid_message means the message is wrong (a caller problem).

providers()​

Lists what this tenant has configured, so a function can discover the names send accepts instead of hardcoding one.

const { enabled, providers } = raisin.email.providers();
// {
// enabled: true,
// providers: [
// { name: "transactional", provider: "resend",
// from_address: "no-reply@example.com", enabled: true, default: true },
// { name: "relay", provider: "smtp",
// from_address: "no-reply@example.com", enabled: false, default: false },
// ],
// }

enabled at the top level is the tenant switch: when it is off no sender works, however many are listed. Disabled entries are included so you can tell "not configured" from "switched off"; they cannot be selected. The listing carries no credentials.

Permissions​

Both calls require the function's email_policy, which denies by default. allowed_recipients is a list of domain patterns matched against the part after @ in each recipient: example.com, *.example.com, or * for any domain. The provider's credential is read from the tenant's email configuration under the function's secret_policy, so declare that too:

email_policy:
enabled: true
allowed_recipients: ["example.com", "*.example.com"]
secret_policy:
enabled: true
allowed_names: ["email/*"]

The recipient check runs first, before the configuration is read and before any credential is decrypted.