On this page:
- spider.mail: Sending mail through a provider's HTTP API (Brevo, Resend, Postmark) — see modules/mail/mail.zig.
spider.mail
Sending mail (spider.mail). The app builds a Mail and hands it to a Mailer; the Mailer's backend decides how it leaves the process:
var mailer = try spider.mail.Mailer.fromEnv(); // once, at startup
_ = try mailer.send(c, .{
.to = &.{.{ .address = "ada@example.com" }},
.subject = "Welcome",
.html = "<h1>Welcome!</h1>",
.text = "Welcome!",
});
This is a client of mail providers (their HTTP APIs), not a mail server: it does not deliver to inboxes itself and does not receive mail.
Backends: .brevo, .resend, .postmark (real delivery), .log (development), .memory (tests, with an Outbox), .custom (the app's own Transport).
spider.mail.Mail
pub const Mail = struct
A message to send. from may be left out when the Mailer has a default. It needs at least one recipient among to, cc and bcc, and a non-empty html or text; subject may be empty.
Fields
from: ?Mailbox = nullto: []const Mailbox = &.{}cc: []const Mailbox = &.{}bcc: []const Mailbox = &.{}reply_to: ?Mailbox = nullsubject: []const u8 = ""html: ?[]const u8 = nulltext: ?[]const u8 = null
spider.mail.Mailbox
pub const Mailbox = struct
An address with an optional display name.
Fields
name: ?[]const u8 = nulladdress: []const u8
mail.Mailbox.parse
pub fn parse(s: []const u8) !Mailbox
Parses Ada <ada@example.com>, "Lovelace, Ada" <ada@example.com> or a bare ada@example.com. The result borrows from s. Fails like validate: error.MailInvalidAddress, error.MailInvalidHeader.
mail.Mailbox.toHeader
pub fn toHeader(self: Mailbox, arena: std.mem.Allocator) ![]const u8
Name <address> (the name quoted when it needs to be), allocated in arena, or the bare address itself (not a copy) when there is no name.
mail.Mailbox.validate
pub fn validate(self: Mailbox) !void
error.MailInvalidAddress when the address is not well formed, error.MailInvalidHeader when the name has a control character.
spider.mail.Receipt
pub const Receipt = struct
What a transport returns once it accepted a message. Acceptance is not inbox delivery.
Fields
message_id: ?[]const u8 = null: The id the transport gave the message, when it gave one.
spider.mail.validate
pub fn validate(mail: Mail) !void
Checks a message before it reaches a transport: a sender, at least one recipient, a body, well-formed addresses, and no control characters (CR/LF above all) in anything that ends up in a header.
spider.mail.Brevo
pub const Brevo = struct
Delivery through Brevo: the value of Backend.brevo.
Fields
api_key: []const u8: The API key, from the provider's dashboard (Mailer.fromEnv:BREVO_API_KEY). Not copied.base_url: []const u8 = "https://api.brevo.com": The API's address, without a trailing slash. Change it for a mock server or a regional endpoint (Mailer.fromEnv:MAIL_BASE_URL).timeout_ms: u32 = 30_000: How long one delivery may take, in milliseconds; past that it fails witherror.MailDeliveryFailed. 0: no limit.
spider.mail.Resend
pub const Resend = struct
Delivery through Resend: the value of Backend.resend. Recipients and reply_to are sent as bare addresses: their display names are dropped.
Fields
api_key: []const u8: The API key, from the provider's dashboard (Mailer.fromEnv:RESEND_API_KEY). Not copied.base_url: []const u8 = "https://api.resend.com": The API's address, without a trailing slash. Change it for a mock server or a regional endpoint (Mailer.fromEnv:MAIL_BASE_URL).timeout_ms: u32 = 30_000: How long one delivery may take, in milliseconds; past that it fails witherror.MailDeliveryFailed. 0: no limit.
spider.mail.Postmark
pub const Postmark = struct
Delivery through Postmark: the value of Backend.postmark.
Fields
api_key: []const u8: The server token, sent asX-Postmark-Server-Token(Mailer.fromEnv:POSTMARK_SERVER_TOKEN). Not copied.base_url: []const u8 = "https://api.postmarkapp.com": The API's address, without a trailing slash. Change it for a mock server or a regional endpoint (Mailer.fromEnv:MAIL_BASE_URL).timeout_ms: u32 = 30_000: How long one delivery may take, in milliseconds; past that it fails witherror.MailDeliveryFailed. 0: no limit.
spider.mail.Log
pub const Log = struct
The value of Backend.log: each mail becomes one info log line (scope mail) with sender, recipients and subject, and nothing is delivered. The receipt has no message id.
Fields
body: bool = true: Also log the body (the text one when there is one, else the HTML).
mail.Log.line
pub fn line(self: Log, arena: std.mem.Allocator, mail: Mail) ![]const u8
The log line for a validated mail.
spider.mail.Outbox
pub const Outbox = struct
Where the memory transport keeps messages. Safe to send to from several threads (handlers) while a test reads it.
Fields
arena: std.heap.ArenaAllocatormails: std.ArrayList(Mail) = .emptynext_id: u64 = 1lock: std.atomic.Mutex = .unlocked
mail.Outbox.init
pub fn init(allocator: std.mem.Allocator) Outbox
An empty Outbox. The copies of the mails are allocated with allocator; call deinit() when done.
var outbox: spider.mail.Outbox = .init(std.testing.allocator);
defer outbox.deinit();
const mailer: spider.mail.Mailer = .{ .backend = .{ .memory = &outbox } };
mail.Outbox.deinit
pub fn deinit(self: *Outbox) void
Frees every stored mail.
mail.Outbox.send
pub fn send(self: *Outbox, arena: std.mem.Allocator, mail: Mail) !Receipt
Stores a copy of mail: it stays readable after the sender's arena is gone. The receipt's id is allocated in arena.
mail.Outbox.sent
pub fn sent(self: *Outbox) []const Mail
Everything sent so far, oldest first. Valid until clear() or deinit().
mail.Outbox.count
pub fn count(self: *Outbox) usize
How many mails were sent since init() or the last clear().
mail.Outbox.last
pub fn last(self: *Outbox) ?Mail
The most recent message, or null when nothing was sent.
mail.Outbox.clear
pub fn clear(self: *Outbox) void
Forgets every message; slices handed out before are no longer valid.
spider.mail.Transport
pub const Transport = struct
A transport written by the app (another provider, a queue, ...): .{ .custom = .{ .ptr = &my_state, .sendFn = mySend } }. sendFn gets a message that already passed validate, with from set.
Fields
ptr: *anyopaque: The transport's own state, handed back tosendFn. Not owned by the Mailer.sendFn: *const fn (ptr: *anyopaque, arena: std.mem.Allocator, io: std.Io, mail: Mail) anyerror!Receipt: Deliversmail.arenaandioare the ones of the send call (the request's, forMailer.send). An error it returns is the send's error.
spider.mail.Backend
pub const Backend = union(enum)
How a Mailer delivers. One of these goes in Mailer.backend:
const mailer: spider.mail.Mailer = .{
.backend = .{ .memory = &outbox },
.from = .{ .address = "app@example.com" },
};
Fields
brevo: Brevo: Brevo's HTTP API.resend: Resend: Resend's HTTP API.postmark: Postmark: Postmark's HTTP API.log: Log: Nothing is delivered: the mail is written to the log (development).memory: *Outbox: Nothing is delivered: the mail is copied into the Outbox (tests). The Outbox must outlive the Mailer.custom: Transport: The app's own delivery function.
spider.mail.Settings
pub const Settings = struct
What Mailer.fromSettings reads; Mailer.fromEnv fills it from the environment.
Fields
transport: []const u8 = "log": "brevo", "resend", "postmark" or "log".from: ?[]const u8 = null: The default sender: "Name <address>" or a bare address.api_key: ?[]const u8 = null: The provider's API key (Postmark: the server token). Required unless the transport is "log".base_url: ?[]const u8 = null: Replaces the provider's API URL (a mock server, a regional endpoint).timeout_ms: u32 = 30_000: How long one delivery may take, in milliseconds, before it fails witherror.MailDeliveryFailed. 0: no limit. A provider that accepts the connection and then stays silent used to hold the request that was sending for as long as it liked.
spider.mail.Mailer
pub const Mailer = struct
Sends mail through one backend. Create it once at startup (fromEnv, fromSettings, or a literal with a Backend) and keep it: it is a plain value, safe to copy and to use from several requests.
Fields
backend: Backend: Where the mail goes.from: ?Mailbox = null: The sender of a mail that declares none.
mail.Mailer.send
pub fn send(self: Mailer, c: *Ctx, mail: Mail) anyerror!Receipt
Sends from a handler.
Errors: MailMissingFrom, MailMissingRecipients, MailMissingBody, MailInvalidAddress, MailInvalidHeader (nothing was sent); MailUnauthorized (the provider refused the API key), MailRejected (it refused this message), MailDeliveryFailed (network, rate limit or provider failure: worth retrying). A .custom transport returns its own errors. The request to the provider is given up after the transport's timeout_ms (30 seconds unless set otherwise: also MailDeliveryFailed) and is not retried. The receipt's id is allocated in the request arena.
mail.Mailer.sendWith
pub fn sendWith(self: Mailer, arena: std.mem.Allocator, io: std.Io, mail: Mail) anyerror!Receipt
Sends outside a request (jobs, boot): mailer.sendWith(arena, hub.io, mail). Same checks and errors as send. arena holds the request sent to the provider, its answer and the receipt's id: use an arena, nothing is freed one by one.
mail.Mailer.fromSettings
pub fn fromSettings(settings: Settings) !Mailer
A Mailer for settings. The result borrows the strings of settings: they must stay valid as long as the Mailer. Errors: MailTransportUnknown (a transport other than brevo, resend, postmark or log), MailApiKeyMissing (a provider without a key, or an empty one), MailInvalidAddress / MailInvalidHeader (a bad from).
mail.Mailer.fromEnv
pub fn fromEnv() !Mailer
Reads MAIL_TRANSPORT (brevo | resend | postmark | log, default log), MAIL_FROM, MAIL_BASE_URL, MAIL_TIMEOUT_MS (default 30000; 0 for no limit) and the transport's key: BREVO_API_KEY, RESEND_API_KEY or POSTMARK_SERVER_TOKEN. Call it once at startup and keep the Mailer (each call copies the variables it reads).
mail.Mailer.fromLookup
pub fn fromLookup(lookup: *const fn (key: []const u8) ?[]const u8) !Mailer
fromEnv() over any variable lookup.