@easybits.cloud/mailmask

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

MailMask 电子邮件别名/转发 API 的官方 SDK

中文使用文档
文档目录9 节
  1. 安装
  2. 快开始
  3. MCP 磁共振
  4. 再来
  5. 合作伙伴
  6. 类型脚本
  7. 文档
  8. 链接
  9. 许可证

@ easybits. cloud/ mailmask (英语)

正式类型SDK 邮件掩码 电子邮件地址,邮箱和转发 API.

安装

npm i @easybits.cloud/mailmask

快开始

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 磁共振

同样的行动也提供给大赦国际的代理人 https://www.mailmask.studio/mcp (Streamable HTTP, , ) (St, Authorization: Bearer mk_...) (中文(简体) ). 见 https://www.mailmask.studio/docs#mcp 互联网档案馆的存檔,存档日期2013-12-02。 。 。 .

再来

// 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);

合作伙伴

对于发送电子邮件的应用程序 代表自己的客户 (每个客户都是 租户 联合国 (b) 拥有所有权、配额和声誉。 使用合伙人证书(mmk_… + 键 mms_… 秘密),而不是一个 mk_ 键。 每一份申请均自动与HMAC-SHA256签署; send编号 :batch 得到一个 idempotencyKey 当你不通过一个(每一次重试上相同的钥匙)时.

运输(0.6):

  • 检索 5xx,网络出错和超时——与焦急的指数倒置 (单位:千美元)retryBaseMs 500~250~500 ms,500~1000 ms,.),荣誉 Retry-After。只有安全请求 重审:获得/PUT/DELETE, tenants.pause编号 :resume, (中文(简体) ), domains.add 财务报告和审定财务报表 send编号 :batch (他们总是) (a) 携带一能键. webhook.rotateSecret 财务报告和审定财务报表 webhook.test 从来没有被重审。 若为 Retry-After 要求超过 retryMaxDelayMs (默认 30 s) 错误随 retryAfterMs 所以你决定. maxRetries 默认 3,最大 10; onRetry 钩取出你的日志.
  • 超时 每次尝试 AbortController (单位:千美元)timeoutMs,默认为30 s) ∗∗ TimeoutError。 。 。 .
  • 秘密远离日志:秘密不是客户的财产(console.log(mm) 没办法 打印),没有错误消息随附.
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),
});

例:Formmy从客户域发送通讯.

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 处理器——单线(工人,德诺, Bun, Next.js 路由处理器,Hono...): 在生体上签名,请回答 401 / 200,如果您 onEvent 抛出( MailMask 重试) :

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 */ },
});

或者用手(Remix/React Router, 任何带有 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");
}

错误:全部延伸 MailMaskError (单位:千美元)status, (中文(简体) ), code, (中文(简体) ), requestId, (中文(简体) ), retryable和 PartnerApiError (单位:千美元)body, (中文(简体) ), retryAfterMs (单位:千美元) kind 为平地而歧视的人 switch:

类statuskind被SDK重审
AuthError401 (中文(简体) )auth没有 - clockSkewMs + 如果时钟关闭, 消息中提示
ForbiddenError403 (韩语)forbidden没有
ReputationHoldError (续) ForbiddenError(中文(简体) )403 (韩语)reputation_hold没有
NotFoundError404 (韩语)not_found没有
ConflictError409个conflict没有
ValidationError (单位:千美元)field(中文(简体) )422个validation没有
RecipientSuppressedError (单位:千美元)reason, (中文(简体) ), stream, (中文(简体) ), recipient(中文(简体) )422个suppressed没有
PausedError423个paused没有
QuotaError (单位:千美元)limit, (中文(简体) ), resetsAt(中文(简体) )429个 quota_exceededquota没有
RateLimitError (续) QuotaError(中文(简体) )429个 rate_limitedrate_limit对
PartnerApiError5 (简体中文)server是(安全请求)
NetworkError 编号 : TimeoutError0 个network 编号 : timeout是(安全请求)

requestId 来自 X-Request-Id 编号 : Fly-Request-Id:请求支持时引用.

管理路线(您的 mk_ 车主钥匙,不是合伙人证书: MailMaskPartnerAdmin —— 说 listPartners, (中文(简体) ), listCredentials, (中文(简体) ), createCredential, (中文(简体) ), rotateCredential, (中文(简体) ), revokeCredential, (中文(简体) ), getFallbackZone 和只为 MailMask 的工作人员, createPartner, (中文(简体) ), setReputationPolicy, (中文(简体) ), resumeTenant, (中文(简体) ), setFallbackZone (注册合伙人倒扣区等. marketing.denik.me时返回 4个NS改为代表;然后 domains.add(id, "<label>.marketing.denik.me") 撰写其DNS并核查 此外,《公约》第2条第1款(a)项也规定: 发行和轮换 从未被重审(失去的反应会隐藏一个没人看见的秘密).

类型脚本

公共宣传品 脚本类型 4.9 en adelante,塔姆比昂孔 moduleResolution: node (l clásico,罪过) exports) (中文(简体) ). El paquete se compila con TypeScript 7, pero (英语) 建造 " 国家公园 " export type *, de TS 5.0) a su (中文(简体) ) 等额(scripts/postbuild-dts.mjs:罪以索,非自相残杀 协进会 TS1383 y desde aquí no se nota. [ y des ade as as equí] 互联网档案馆的存檔,存档日期2014-12-02. npm run check:consumer 阿尔马·埃尔·帕克特孔 npm pack y o compila en un proyecto con TypeScript 4.9.5 para virarlo.

文档

完整 API 参考和实例 : https://mailmask.studio/docs (中文(简体) )

许可证

麻省理工学院

版本与发布记录

@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 是什么?

MailMask 电子邮件别名/转发 API 的官方 SDK

如何安装 @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 和提交记录判断。