@ 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,网络出错和超时——与焦急的指数倒置
(单位:千美元)
retryBaseMs500~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:
| 类 | status | kind | 被SDK重审 |
|---|---|---|---|
AuthError | 401 (中文(简体) ) | auth | 没有 - clockSkewMs + 如果时钟关闭, 消息中提示 |
ForbiddenError | 403 (韩语) | forbidden | 没有 |
ReputationHoldError (续) ForbiddenError(中文(简体) ) | 403 (韩语) | reputation_hold | 没有 |
NotFoundError | 404 (韩语) | not_found | 没有 |
ConflictError | 409个 | conflict | 没有 |
ValidationError (单位:千美元)field(中文(简体) ) | 422个 | validation | 没有 |
RecipientSuppressedError (单位:千美元)reason, (中文(简体) ), stream, (中文(简体) ), recipient(中文(简体) ) | 422个 | suppressed | 没有 |
PausedError | 423个 | paused | 没有 |
QuotaError (单位:千美元)limit, (中文(简体) ), resetsAt(中文(简体) ) | 429个 quota_exceeded | quota | 没有 |
RateLimitError (续) QuotaError(中文(简体) ) | 429个 rate_limited | rate_limit | 对 |
PartnerApiError | 5 (简体中文) | server | 是(安全请求) |
NetworkError 编号 : TimeoutError | 0 个 | 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 (中文(简体) )
链接
许可证
麻省理工学院