Build mailbox extensions
Write code that reacts to mailbox events, publish it, and install it with its own permissions on your webhook or on Samva.
An extension is code you publish once and install into an organization. It receives mailbox events and acts through the mailbox API, for example to acknowledge every new message or to label threads. You choose where it runs:
| Runtime | Where the code runs | How events reach it |
|---|---|---|
webhook | Your server, at an HTTPS URL you give at install time. | A signed webhook. You call the API with a key revealed once. |
hosted | Samva, from a bundle you publish. No server to run. | Samva runs your handler for each event. |
Write an extension
Install the authoring package @samva/mailbox, or scaffold a project with the CLI:
samva extensions init ./acknowledge --name acknowledge --runtime hosted--runtime is hosted (the default) or webhook. The directory must be empty. The project contains
src/extension.ts, and a webhook project adds src/server.ts.
import { defineExtension } from "@samva/mailbox";
export default defineExtension({
manifest: {
name: "acknowledge",
displayName: "Acknowledge new mail",
version: "1.0.0",
runtime: "hosted",
events: ["mailbox.message.received"],
permissions: ["read", "send"],
},
on: {
"mailbox.message.received": async (event, { samva, log }) => {
await samva.messages.reply(event.data.mailboxId, event.data.message.id, {
text: "Thanks, we got it.",
});
log("Acknowledged", event.data.message.id);
},
},
});The manifest
| Field | Rules |
|---|---|
name | 3 to 64 lowercase letters, digits, and hyphens. It identifies the extension in your organization. |
displayName | 1 to 80 characters. |
description | Optional, up to 500 characters. |
version | Semver, such as 1.0.0. A published version is immutable. |
runtime | hosted or webhook. |
events | One or more of the mailbox event types, each listed once. |
permissions | The mailbox permissions it may use: read, update, send, quarantine.read. read is required. |
Every key in on must appear in events. An event with no handler is acknowledged and skipped.
What a handler receives
A handler gets the event (id, type, timestamp, data) and a context with:
samva, a mailbox client withmessages.get,reply,replyAll,forward, andsend, andthreads.get,list,messages, andupdate.log(...), which records a line in the run's log.attempt, the delivery attempt number.
Sends made through samva carry an Idempotency-Key of the form <event id>:<method>:<n>, so a
handler that runs again for the same event does not send twice. Other side effects need their own
deduplication. Throwing from a handler fails the event, which Samva then retries.
Test locally
@samva/mailbox/test runs a handler without a network.
import { createTestClient, mailboxEvent, runExtension } from "@samva/mailbox/test";
import extension from "./src/extension";
const client = createTestClient();
const result = await runExtension(extension, mailboxEvent("mailbox.message.received"), { client });
console.log(result.ok, result.logs, result.calls);To run your handlers against live events, use samva extensions dev. It tails the
event stream and runs the extension for each event whose type
is in the manifest. It calls the API with your own credential, so actions are attributed to you, not
to an installation.
samva extensions dev . --mailbox mbx_...Publish a version
Publishing and installing need a person signed in with samva login who can manage mailboxes. The
CLI builds the project before it publishes. A hosted extension builds to one self-contained ES module, which must stay within
the hosted limits.
samva extensions build . --out dist/extension.js
samva extensions publish .publish creates the extension on its first version and prints the version it recorded. Publishing
the same version with the same content again returns it; different content under the same version
is a 409, so bump version.
List what you published with samva extensions list, an extension's versions with
samva extensions versions <name>, and one version with its manifest with
samva extensions version <name> <version>.
Install it
An installation applies a published version to your mailboxes. It is a principal of its own: it acts on the installer's grants, narrowed by what you give it here, and never exceeds them.
samva extensions install acknowledge \
--mailbox mbx_... \
--permission read --permission send \
--send-mode approval| Flag | Meaning |
|---|---|
--version | A published version. The latest when omitted. |
--mailbox | Repeat to name mailboxes. Omit it to reach every mailbox the installer reaches. |
--namespace | Confine the installation to one namespace. |
--permission | Repeat to narrow the manifest's permissions. It can only remove permissions, never add them. |
--send-mode | send or approval. With approval, every send waits for a person. |
--webhook-url | For a webhook extension, the HTTPS URL that receives events. |
An installation receives events only while it is active and its principal can read the event's
mailbox right now. Quarantine events also need quarantine.read. An installation can never approve
a send, release or discard held mail, or change grants and keys. If the installer loses the mailbox,
the installation stops receiving its events.
List installations with samva extensions installations, read one with
samva extensions installation <id>, and manage it with disable, enable, and uninstall.
Disabling stops the installation at once and refuses its credential on every surface; events
published while it was disabled are not delivered after you enable it. Uninstalling deletes the
installation and its key, and the actions it took keep their attribution.
Run on your server
A webhook installation returns an API key and a webhook secret once, when you install it. Store both.
createWebhookHandler verifies each request's signature, runs your handler, and answers 200 on
success or 500 so Samva retries.
import { createWebhookHandler } from "@samva/mailbox/webhook";
import extension from "./extension";
Bun.serve({
fetch: createWebhookHandler(extension, {
apiKey: process.env.SAMVA_API_KEY!,
webhookSecret: process.env.SAMVA_WEBHOOK_SECRET!,
}),
});Delivery, signing, and retries follow the webhook event catalog.
Run on Samva
A hosted extension needs no server and no credential. Samva runs your bundle for each event. The
bundle has no network access: its only way out is samva, the mailbox client, which can do exactly
what the installation's principal can do through the mailbox API.
List an installation's recent runs, then read one to see the lines your handler logged:
samva extensions runs exti_...
samva extensions run exti_... <run-id>The list shows each run's event, attempt, outcome, and duration; the run itself adds its logs. A run's outcome is succeeded, failed, timed_out, budget_exhausted, or revoked.
Hosted limits
| Limit | Value |
|---|---|
| Bundle size | 1 MiB |
| CPU time per run | 1 second |
| Mailbox API calls per run | 50 |
| Wall time per run | 30 seconds |
| Runs per installation per hour, retries included | 600 |
| Attempts per event | 5 |
| Log lines kept per run | 50 |
| Characters kept per log line | 500 |
| Runs kept in the run log | 100 |
An event that arrives after an installation has used its hourly runs is recorded as
budget_exhausted and not run.
Manage extensions over the API
The CLI commands call the API, and the SDK exposes the same operations as the extensions and
extensionInstallations namespaces.
| To | Call |
|---|---|
| Publish a version | POST /v1/extensions; it answers with the extension and the version. |
| List extensions | GET /v1/extensions, then GET /v1/extensions/{id}. |
| List versions | GET /v1/extensions/{id}/versions, one summary per version. |
| Read one version | GET /v1/extensions/{id}/versions/{versionId}, with the manifest. |
| Install a version | POST /v1/extension-installations; the credentials of a webhook extension come back once. |
| List or read installations | GET /v1/extension-installations and GET /v1/extension-installations/{id}. |
| Disable, enable, or uninstall | POST /v1/extension-installations/{id}/disable, .../enable, and DELETE on the installation. |
| List runs or read one | GET /v1/extension-installations/{id}/runs lists runs without logs; .../runs/{runId} adds them. |
The API reference lists the fields of each.