Skip to main content
Version: 0.1.0

raisin.email

Transactional email for server-side functions. Available identically in QuickJS and Starlark.

Configuration — which providers exist, which is default, what address they send as — is covered in the Outbound Email guide.

send(message)

Sends one message and returns the provider's receipt.

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

Message

FieldTypeNotes
tostring | string[]One address or several. At most 20.
subjectstringRequired. CR/LF is refused (header splitting).
textstringRequired, even alongside html. An HTML-only message is a spam signal and unreadable in a text client.
htmlstring?Optional alternative body, sent alongside the text.
providerstring?Which configured sender to use. Omit for the default.

There is no from. The sender identity comes from the configuration, so a function cannot send as an address the tenant never verified. A function chooses which configured account to use, never who it is.

provider names an entry from providers(). An unknown name throws; it never falls back to the default. null, "" and whitespace all mean "the default", so an unset template variable behaves as you would expect.

Receipt

{ "message_id": "4bJ1x…", "provider": "resend", "sender": "transactional" }
FieldNotes
message_idThe provider's id — what a later bounce or webhook correlates against
providerThe provider API: resend, brevo or smtp
senderThe configured name it went through

Acceptance is not delivery.

Errors

Every error carries a stable machine code in its message.

CodeMeans
email:policy_deniedThe function's email_policy does not permit a recipient
email:configNot enabled, no provider, unknown provider name, ambiguous default, or an incomplete entry
email:invalid_messageMissing/oversized/malformed message — refused before a socket opens
email:auth_failedThe provider rejected the credential (401/403). Rotate the secret.
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

The auth_failed / invalid_message split is the one that matters operationally: the first is your credential failing (an operator problem), the second is your message failing (a caller problem).

providers()

Lists what this tenant has configured, so a function can discover the names send accepts rather than hardcoding one it cannot verify.

const { enabled, providers } = await 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 master switch — off means no sender works, however many are listed. Disabled entries are included so you can tell "not configured" from "switched off"; they cannot be selected.

Carries no credential and no credential_ref: a function that may send does not thereby get to enumerate the secret store.

Permissions

Both calls need the function's email_policy, which denies by default:

email_policy:
enabled: true
allowed_recipients: ["*@example.com"] # or ["*"] for mail to your users
secret_policy:
enabled: true
allowed_names: ["email/*"]

The policy runs first — before the configuration is read and before any credential is decrypted — so a denied send never causes a key to be decrypted.