@easybits.cloud/mailmask

中文文档
0.9.2发布于 2026/11/12MITTypeScriptESM + CJS零依赖近 30 天下载 2043

Official SDK for the MailMask email alias/forwarding API

原始使用文档
文档目录9 节
  1. Install
  2. Quick start
  3. MCP
  4. More
  5. Partners
  6. TypeScript
  7. Docs
  8. Links
  9. License

@easybits.cloud/mailmask

Official TypeScript/JavaScript SDK for the MailMask email address, mailbox and forwarding API.

Install

npm i @easybits.cloud/mailmask

Quick start

import { MailMask } from "@easybits.cloud/mailmask";

const mm = new MailMask({ apiKey: "mk_..." });

// Get your domains
const domains = await mm.domains.list();
const domainId = domains[0].id;

// Create an address — emails to [email protected] forward to your inbox
const address = await mm.addresses.create(domainId, {
  alias: "hello",
  destinations: ["[email protected]"],
});

// Send an email (routed through MailMask for high deliverability)
await mm.send.send(domainId, {
  from: "hello",              // must be an active address; defaults to `noreply`
  fromName: "Acme",           // optional display name: Acme <[email protected]>
  to: "[email protected]",
  subject: "Welcome!",
  html: "<p>Thanks for signing up.</p>", // or `body` (plain text) / `markdown`
});

// List addresses for a domain
const addresses = await mm.addresses.list(domainId);

// Add a domain — returns the DNS records to configure (MX, TXT, DKIM, SPF)
const { domain, dnsRecords } = await mm.domains.create("yourdomain.com");

// An address with an IMAP mailbox (activated domain): password is returned once
const inbox = await mm.addresses.create(domainId, { alias: "sales", mailbox: true });
// `mm.aliases` still works: it is the same resource under its old name.
console.log(inbox.buzon?.password, inbox.buzon?.imap);

MCP

The same actions are available to AI agents at https://www.mailmask.studio/mcp (Streamable HTTP, Authorization: Bearer mk_...). See https://www.mailmask.studio/docs#mcp.

More

// Attachments, copies, threading, idempotency
const pdf = await mm.attachments.upload(domainId, { filename: "invoice.pdf", contentType: "application/pdf", data: bytes });
await mm.send.send(domainId, {
  from: "billing", to: "[email protected]", cc: ["[email protected]"],
  subject: "Re: Invoice 42", markdown: "Attached.", attachments: [pdf],
  inReplyTo: "<[email protected]>", references: "<[email protected]>",
}, { idempotencyKey: `invoice-42` });

// Account: identity + usage, and a full JSON export
const me = await mm.account.me();
const backup = await mm.account.export();

// Upload an image to embed in an outgoing email (returns a public url)
const { url } = await mm.domains.uploadImage(domainId, imageBlob);

// Suppression list (hard bounces / complaints)
await mm.suppressions.list(domainId);
await mm.suppressions.remove(domainId, "[email protected]");

// Webhooks (activated domain) — secret is shown once
const wh = await mm.webhooks.create(domainId, { url: "https://app.example.com/hooks/mailmask", events: ["email.received", "email.delivered", "email.bounced"] });

// On your server: verify the signature with the raw body
import { verifyWebhookSignature } from "@easybits.cloud/mailmask";
const ok = await verifyWebhookSignature(secret, { signature: req.headers["x-mailmask-signature"], timestamp: req.headers["x-mailmask-timestamp"] }, rawBody);

Partners

For apps that send email on behalf of their own customers (each customer is a tenant with its own domain, quota and reputation). Uses a partner credential (mmk_… + mms_… secret), not an mk_ key. Every request is signed with HMAC-SHA256 automatically; send/batch get an idempotencyKey when you don't pass one (the same key on every retry).

Transport (0.6):

  • Retries on rate-limit 429, 5xx, network errors and timeouts — exponential backoff with jitter (retryBaseMs 500 → 250–500 ms, 500–1000 ms, …), honoring Retry-After. Only safe requests are retried: GET/PUT/DELETE, tenants.pause/resume, domains.add and send/batch (they always carry an idempotency key). webhook.rotateSecret and webhook.test are never retried. If Retry-After asks for more than retryMaxDelayMs (default 30 s) the error is thrown with retryAfterMs so you decide. maxRetries default 3, max 10; onRetry hook for your logs.
  • Timeout per attempt with AbortController (timeoutMs, default 30 s) → TimeoutError.
  • Secrets stay out of logs: the secret is not a property of the client (console.log(mm) can't print it) and no error message carries it.
const mm = new MailMaskPartner({
  keyId: process.env.MAILMASK_KEY_ID!, secret: process.env.MAILMASK_SECRET!,
  timeoutMs: 10_000, maxRetries: 5,
  onRetry: ({ attempt, delayMs, error }) => console.warn(`mailmask retry ${attempt} in ${delayMs} ms`, error.kind, error.requestId),
});

Example: Formmy sending a newsletter from its customer's domain.

import {
  MailMaskPartner, PausedError, QuotaError, RecipientSuppressedError, ReputationHoldError, ValidationError, verifyWebhook,
} from "@easybits.cloud/mailmask";

const mm = new MailMaskPartner({ keyId: process.env.MAILMASK_KEY_ID!, secret: process.env.MAILMASK_SECRET! });

// 1. One tenant per Formmy customer (idempotent: call it on every sign-up/update)
await mm.tenants.put("formmy-user-42", { name: "Panadería La Espiga" });

// 2. Their domain: show these DNS records to the customer, then poll the status
const { records } = await mm.domains.add("formmy-user-42", "laespiga.mx");
// records: [{ type: "TXT", name: "_amazonses", value: "…", level: "requerido" }, …]
const { state } = await mm.domains.status("formmy-user-42", "laespiga.mx"); // "pending" | "verified" | "failed"

// 3. Send (pass your own idempotencyKey to make retries across processes safe).
//    `stream`: "transactional" (default for send: receipts, reminders; no unsubscribe link) or
//    "broadcast" (newsletters: one-click List-Unsubscribe; an unsubscribe only silences broadcast).
try {
  await mm.send("formmy-user-42", {
    from: "La Espiga <[email protected]>",
    to: "[email protected]",
    subject: "Pan de muerto ya disponible",
    html: "<p>Desde hoy…</p>",
    tags: { campaign: "oct-26" },
    stream: "broadcast",
    idempotencyKey: "campaign-oct-26:[email protected]",
  });
} catch (e) {
  if (e instanceof RecipientSuppressedError) { /* e.reason, e.stream, e.recipient: mark the reader, don't retry */ }
  else if (e instanceof PausedError) { /* tenant paused (bounces/complaints): stop until resume */ }
  else if (e instanceof QuotaError) { /* e.limit, e.resetsAt */ }
  else if (e instanceof ValidationError) { /* e.field */ }
  else throw e;
}

// Up to 10,000 recipients in the background (stream defaults to "broadcast"; suppressed ones are
// skipped). Recipients are plain addresses or { to, vars }: {{name}} in subject/html/text is replaced
// per recipient (escaped inside html). Broadcasts get {{unsubscribe_url}} (or a footer if unused).
const job = await mm.batch("formmy-user-42", {
  from: "[email protected]", subject: "Hola {{nombre}}", html: "<p>{{nombre}}, tu cupón: {{cupon}}</p>",
  recipients: ["[email protected]", { to: "[email protected]", vars: { nombre: "Bea", cupon: "OCT10" } }],
});
const st = await mm.batchStatus("formmy-user-42", job.batchId);
// st.sent / failed / skippedSuppressed, then (for up to 7 days) delivered / bounced / complained / unsubscribed

// Opens and clicks (opt-in per send/batch): `track: true` adds SES's open pixel and rewrites the html
// links. Without it nothing is added. Unique counts + rates + top links come back in batchStatus, and
// `email.opened` / `email.clicked` fire once per message (the first time). Opens are inflated by Apple
// Mail Privacy Protection: decide by clicks.
const tracked = await mm.batch("formmy-user-42", { from: "[email protected]", subject: "Oct", html: "<a href=\"https://laespiga.mx/menu?utm_source=mail\">Menú</a>", recipients: ["[email protected]"], track: true });
const ts = await mm.batchStatus("formmy-user-42", tracked.batchId);
// ts.opened / clicked (unique), ts.openRate / clickRate (0–1 over delivered, or sent while delivered is 0),
// ts.topLinks: [{ url: "https://laespiga.mx/menu", clicks, uniqueClicks }] (utm_*, fbclid, gclid stripped)

// Test mode: validates everything (signature, domain, suppression, quota) without sending or billing
await mm.send("formmy-user-42", { from: "[email protected]", to: "[email protected]", subject: "s", text: "t" }, { test: true });

// Preview exactly what one recipient gets — same function the server uses, no network
import { previewMessage } from "@easybits.cloud/mailmask";
const p = previewMessage({ subject: "Hola {{nombre}}", html: "<p>{{nombre}}</p>", stream: "broadcast", vars: { nombre: "Ana" } });
// p.subject, p.html, p.unsubscribeFooter, p.missingVars, p.unusedVars

// Monthly usage (active tenants = at least one send this month)
await mm.usage({ month: "2026-10" });

// 4. Webhook: the whsec_ secret comes back only the FIRST time (later `set` calls return null:
//    changing the URL doesn't rotate it)
const { secret } = await mm.webhook.set({ url: "https://formmy.app/api/mailmask/events" });
await mm.webhook.test(); // sends a `ping`

// Rotate without losing events: during the grace period (default 24 h) every delivery carries both
// signatures; pass both secrets to verifyWebhook until you've deployed the new one.
const { secret: next, previousValidUntil } = await mm.webhook.rotateSecret({ graceMinutes: 60 });

// Pausing: tenants.resume only lifts a pause YOU set (pausedBy "partner"). A reputation pause
// (pausedBy "mailmask" | "ses") throws ReputationHoldError: MailMask lifts it after review.
try { await mm.tenants.resume("formmy-user-42"); } catch (e) { if (!(e instanceof ReputationHoldError)) throw e; }

Webhook handler — the one-liner (Workers, Deno, Bun, Next.js route handlers, Hono…): verifies the signature on the raw body, answers 401 / 200, and 500 if your onEvent throws (MailMask retries):

import { createWebhookHandler } from "@easybits.cloud/mailmask";
export const POST = createWebhookHandler({
  secret: [process.env.MAILMASK_WEBHOOK_SECRET!, process.env.MAILMASK_WEBHOOK_SECRET_OLD ?? ""],
  onEvent: async (event) => { /* dedupe by event.id */ },
});

Or by hand (Remix/React Router, anything with a Request):

export async function action({ request }: { request: Request }) {
  const rawBody = await request.text(); // the RAW body, before JSON.parse
  const event = await verifyWebhook({ rawBody, headers: request.headers, secret: [process.env.MAILMASK_WEBHOOK_SECRET!, process.env.MAILMASK_WEBHOOK_SECRET_OLD ?? ""] });
  if (!event) return new Response("bad signature", { status: 401 });
  // event.id is the delivery id: use it to drop duplicates
  switch (event.event) {
    case "email.bounced":
    case "contact.unsubscribed":
      // mark the reader as unsubscribed for event.externalId
      break;
    case "tenant.paused":
      // notify the customer
      break;
    case "email.clicked":
      // event.data.url, event.data.recipient, event.data.batchId, event.data.tags (first click only)
      break;
  }
  return new Response("ok");
}

Errors: all extend MailMaskError (status, code, requestId, retryable) and PartnerApiError (body, retryAfterMs and a kind discriminant for a plain switch):

ClassstatuskindRetried by the SDK
AuthError401authno — clockSkewMs + a hint in the message if your clock is off
ForbiddenError403forbiddenno
ReputationHoldError (extends ForbiddenError)403reputation_holdno
NotFoundError404not_foundno
ConflictError409conflictno
ValidationError (field)422validationno
RecipientSuppressedError (reason, stream, recipient)422suppressedno
PausedError423pausedno
QuotaError (limit, resetsAt)429 quota_exceededquotano
RateLimitError (extends QuotaError)429 rate_limitedrate_limityes
PartnerApiError5xxserveryes (safe requests)
NetworkError / TimeoutError0network / timeoutyes (safe requests)

requestId comes from X-Request-Id / Fly-Request-Id: quote it when asking for support.

Admin routes (your mk_ owner key, not the partner credential): MailMaskPartnerAdmin — listPartners, listCredentials, createCredential, rotateCredential, revokeCredential, getFallbackZone and, for MailMask staff only, createPartner, setReputationPolicy, resumeTenant, setFallbackZone (registers the partner's fallback zone, e.g. marketing.denik.me, and returns the 4 NS to delegate; then domains.add(id, "<label>.marketing.denik.me") writes its DNS and verifies on its own). Issuing and rotating are never retried (a lost response would hide a secret nobody saw).

TypeScript

Los tipos publicados se pueden consumir desde TypeScript 4.9 en adelante, también con moduleResolution: node (el clásico, sin exports). El paquete se compila con TypeScript 7, pero el build baja la única sintaxis que un TS viejo no entiende (export type *, de TS 5.0) a su equivalente (scripts/postbuild-dts.mjs): sin eso, un consumidor con TS ≤ 4.9 falla en su propia compilación con TS1383 y desde aquí no se nota. npm run check:consumer arma el paquete con npm pack y lo compila en un proyecto con TypeScript 4.9.5 para confirmarlo.

Docs

Full API reference and examples: https://mailmask.studio/docs

  • MailMask — Email addresses, mailboxes & forwarding on your domain
  • EasyBits — Cloud platform
  • Fixter — Development team

License

MIT

版本与发布记录

@easybits.cloud/mailmask 共发布 31 个版本,首次发布于 2026/03/04;过去 12 个月发布了 31 个版本。

版本发布日期距今
0.9.2latest2026/11/122 天前
0.9.12026/10/2718 天前
0.9.02026/10/2619 天前
0.8.02026/10/2619 天前
0.7.02026/10/2619 天前
0.6.12026/10/2619 天前

关于 @easybits.cloud/mailmask 的常见问题

@easybits.cloud/mailmask 是什么?

Official SDK for the MailMask email alias/forwarding API

如何安装 @easybits.cloud/mailmask?

在项目目录运行 npm install @easybits.cloud/mailmask,或使用 pnpm add / yarn add / bun add。

@easybits.cloud/mailmask 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@easybits.cloud/mailmask 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@easybits.cloud/mailmask 可以商用吗?

@easybits.cloud/mailmask 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@easybits.cloud/mailmask 的下载量有多少?

根据 npm 官方下载统计,@easybits.cloud/mailmask 在 2026/10/13 – 2026/11/11 这 30 天内被下载 2,043 次,日均约 68 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@easybits.cloud/mailmask 还在维护吗?

最新版本 0.9.2 发布于 2026/11/12(2 天前);至今共发布 31 个版本,其中过去 12 个月发布了 31 个;首次发布于 2026/03/04。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。