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:

RuntimeWhere the code runsHow events reach it
webhookYour server, at an HTTPS URL you give at install time.A signed webhook. You call the API with a key revealed once.
hostedSamva, 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.

src/extension.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

FieldRules
name3 to 64 lowercase letters, digits, and hyphens. It identifies the extension in your organization.
displayName1 to 80 characters.
descriptionOptional, up to 500 characters.
versionSemver, such as 1.0.0. A published version is immutable.
runtimehosted or webhook.
eventsOne or more of the mailbox event types, each listed once.
permissionsThe 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 with messages.get, reply, replyAll, forward, and send, and threads.get, list, messages, and update.
  • 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.

extension.test.ts
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
FlagMeaning
--versionA published version. The latest when omitted.
--mailboxRepeat to name mailboxes. Omit it to reach every mailbox the installer reaches.
--namespaceConfine the installation to one namespace.
--permissionRepeat to narrow the manifest's permissions. It can only remove permissions, never add them.
--send-modesend or approval. With approval, every send waits for a person.
--webhook-urlFor 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.

src/server.ts
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

LimitValue
Bundle size1 MiB
CPU time per run1 second
Mailbox API calls per run50
Wall time per run30 seconds
Runs per installation per hour, retries included600
Attempts per event5
Log lines kept per run50
Characters kept per log line500
Runs kept in the run log100

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.

ToCall
Publish a versionPOST /v1/extensions; it answers with the extension and the version.
List extensionsGET /v1/extensions, then GET /v1/extensions/{id}.
List versionsGET /v1/extensions/{id}/versions, one summary per version.
Read one versionGET /v1/extensions/{id}/versions/{versionId}, with the manifest.
Install a versionPOST /v1/extension-installations; the credentials of a webhook extension come back once.
List or read installationsGET /v1/extension-installations and GET /v1/extension-installations/{id}.
Disable, enable, or uninstallPOST /v1/extension-installations/{id}/disable, .../enable, and DELETE on the installation.
List runs or read oneGET /v1/extension-installations/{id}/runs lists runs without logs; .../runs/{runId} adds them.

The API reference lists the fields of each.

Next steps

Related documentation

On this page