# 从 MailerSend 迁移

本页将 MailerSend 的发送载荷、屏蔽列表和 webhook 映射到 Bird。请按顺序执行[主迁移指南](/docs/guides/email/migrate)，并在步骤 1、3 和 4 中使用这些映射。

MailerSend 维护五个屏蔽列表，其中一个在设计上是临时的。请在步骤 3 之前阅读[导出屏蔽列表](#导出屏蔽列表)：导入该列表会将 72 小时的暂停变为永久屏蔽。

## 交给你的 agent

将此内容粘贴到 Claude Code、Cursor 或 Codex 中。Agent 会针对你自己的代码仓库逐步处理本页内容，使用它已有的任何 Bird 接口：已连接的 MCP 服务器，或已安装并登录的 CLI。

```text
I am moving an email integration from MailerSend to Bird. Route through it with me.
1. Check what you already have before setting anything up. If Bird's MCP server is connected, use its tools. If the Bird CLI is installed and signed in, use that. Either one is enough, and every step below is an action you take with whichever you have. Only if neither is present, follow https://bird.com/docs/ai/set-up-your-agent.md to set one up and sign me in. Every Bird docs page serves Markdown at its own URL with `.md` appended, so fetch that rather than the HTML.
2. Read https://bird.com/docs/guides/email/migrate/mailersend.md for the payload, suppression and webhook mapping, and https://bird.com/docs/guides/email/migrate.md for the order the steps go in.
3. Find and list my MailerSend usage in this repository before you change anything: calls to /v1/email and any SDK wrappers around them, whether I send with template_id and personalization or with html, the webhook handler and the URL it is registered at, and every domain I send from. Tell me the list before you edit anything.
4. Register each of those sending domains with Bird and give me the DNS records to publish, following https://bird.com/docs/guides/email/sending-domains.md. Leave every DNS record MailerSend uses exactly as it is: Bird's records are published alongside them and both providers authenticate side by side until I switch traffic. Publishing DNS affects mail for the whole domain, so show me the records and let me publish them.
5. Export my suppressions and import them into Bird before any production traffic goes through Bird, so my first sends do not reach addresses that already bounced or complained. MailerSend keeps five lists under /v1/suppressions: hard bounces, spam complaints, unsubscribes and the blocklist are the four to carry over. Do NOT import the On Hold list: it is a temporary 72-hour hold that MailerSend clears by itself, and importing it would suppress those addresses permanently. The Bird import takes one address per request and is idempotent, so a partial re-run is safe. https://bird.com/docs/guides/email/suppressions.md has the reason taxonomy.
6. Port the send call and the webhook handler using the mapping tables on the provider page. Both providers sign with HMAC-SHA256, so this is a re-implementation rather than new code: MailerSend sends one Signature header, Bird follows the Standard Webhooks scheme with webhook-id, webhook-timestamp and webhook-signature, and the signed string is constructed differently. Keep the constant-time comparison. https://bird.com/docs/guides/webhooks.md and https://bird.com/docs/guides/email/events.md.
7. Run my whole integration against Bird's mail sandbox before any production traffic, following https://bird.com/docs/guides/email/testing-sandbox.md. Sandbox sends run the real pipeline without reaching an inbox or touching my sending reputation.
8. Stop and ask me wherever a step needs a decision. Do not point production traffic at Bird until I have seen the sandbox results and replied with the words cut over to Bird. Retiring the MailerSend path is a separate step that comes later: ask me again and wait for me to reply with the words retire the MailerSend path. A reply that agrees without naming what it is authorising is not authorisation. Finish by telling me what is left that only a person can do.
```

## 映射发送调用

MailerSend 的 `POST /v1/email` 和我们的 [`POST /v1/email/messages`](/docs/api/reference/create-email-message) 结构相似。值得修改代码的差异在于标签上限和每个收件人数据的传递方式。

| 功能         | MailerSend                                                  | Bird                                              |
| ------------ | ----------------------------------------------------------- | ------------------------------------------------- |
| 发件人       | `from` (`{email, name}`)                                    | `from`                                            |
| 收件人       | `to`（最多 50 个）、`cc` / `bcc`（各最多 10 个）            | `to` / `cc` / `bcc`（数组）                       |
| 主题         | `subject`（最多 998 个字符）                                | `subject`                                         |
| 正文         | `html` / `text`                                             | `html` / `text`（至少一个）                       |
| Reply-to     | `reply_to`（`{email, name}`）                               | `reply_to`（数组）                                |
| 自定义请求头 | `headers`（`{name, value}`，更高级别套餐）                  | `headers`（string → string 对象）                 |
| 可筛选标签   | `tags`（字符串数组，最多 5 个）                             | `tags`: `{name, value}` 键值对                    |
| 已存储模板   | `template_id` + `personalization`                           | `template` + `template.parameters`（见下文）      |
| 附件         | `attachments`（`content`、`filename`、`disposition`、`id`） | `attachments`                                     |
| 定时发送     | `send_at`（最多提前 72 小时）                               | `scheduled_at`                                    |
| 批量优先级   | `precedence_bulk`                                           | （无对应字段，见下文）                            |
| 类别         | （无）                                                      | `category`: `marketing`（默认）或 `transactional` |
| 线程         | `in_reply_to`                                               | `headers`                                         |

字段上限和默认值（收件人数量、标签和元数据限制）详见[发送邮件](/docs/guides/email/sending-email)。

迁移说明：

- **MailerSend 中地址是对象，这里是字符串。** `{"email": "a@x.com", "name": "A"}` 变为 `"A <a@x.com>"` 或直接写 `"a@x.com"`，适用于 `from`、`to`、`cc`、`bcc` 和 `reply_to`。
- **`tags` 是裸字符串，最多五个。我们的标签是键值对。** 类似 `"welcome"` 的标签变为 `{"name": "category", "value": "welcome"}`。选择一个稳定的 `name`，这样你的仪表盘筛选方式与之前 MailerSend 标签统计一致。
- **`personalization` 是每个收件人的模板数据。** 它是一个以收件人邮箱为键的数组。我们的 `template.parameters` 应用于整个发送，因此内容确实因收件人而异的消息需要为每个收件人单独发送，或分别作为 [batch](/docs/guides/email/sending-bulk) 中的一个条目。如果你的 `personalization` 数组对每个收件人都携带相同的值，则可以合并为一个 `template.parameters` 对象。
- **往返上下文在 MailerSend 中没有可复制的对应物。** 如果你之前从 `tags` 重建上下文，请改用 [`metadata`](/docs/guides/email/sending-email)：我们会在每个 webhook 事件中将其与 `email_id`/`recipient_id` 一起回传，并且不受五个条目的上限。
- **`precedence_bulk` 没有对应字段，`category` 也不是。** `precedence_bulk` 设置一个 `Precedence: bulk` 请求头，要求自动回复和外出回复代理保持静默。我们的 `category` 决定哪些屏蔽记录和退订偏好可以拦截消息，仅此而已。用 `category` 代替它会改变屏蔽行为，对自动回复没有任何作用。如果你需要该请求头，请注意 `headers` 会拒绝地址和平台名称，但不会拒绝此请求头。
- **自定义请求头和 `list_unsubscribe` 在 MailerSend 受套餐限制。** 如果你的套餐不包含它们，在这里可以直接使用；参见[发送邮件](/docs/guides/email/sending-email)和[类别](/docs/guides/email/categories)了解我们如何在营销邮件中处理 list-unsubscribe。

## 导出屏蔽列表

MailerSend 在 `/v1/suppressions` 下维护五个列表，每个列表一个端点。四个是永久的，需要迁移；第五个不可迁移。

导出后通过[导入流程](/docs/guides/email/migrate#3-import-suppressions)处理：

- **硬退信**，例如 `GET https://api.mailersend.com/v1/suppressions/hard-bounces`
- **垃圾邮件投诉**
- **退订**
- **黑名单**，即你手动添加的地址和模式

**不要导入 On Hold 列表。** MailerSend 自己的描述是：它保存的地址 "have soft bounced 5 times within 30 days"、"these emails will be blocked for 72 hours"，之后 "automatically removed from the list"。这是一个由提供商自行清除的冷却期，因此导入它会将临时暂停转为永久屏蔽，并悄然停止向即将被释放的地址发送邮件。我们对应的行为是[延迟投递](/docs/guides/email/events)，我们根据实时投递结果来处理，而不是从导入的列表中处理。

MailerSend 的列表类型映射到我们的 `hard_bounce`、`complaint` 和 `manual` 原因；[屏蔽列表](/docs/guides/email/suppressions)中有完整的分类。

## 转换 webhook 事件

| 结果          | MailerSend                                     | Bird                                             |
| ------------- | ---------------------------------------------- | ------------------------------------------------ |
| 已接受/已处理 | `activity.sent`                                | `email.accepted` → `email.processed`             |
| 已送达        | `activity.delivered`                           | `email.delivered`                                |
| 临时失败      | `activity.soft_bounced` / `activity.deferred`  | `email.deferred`                                 |
| 永久退信      | `activity.hard_bounced`                        | `email.bounced` / `email.out_of_band_bounce`     |
| 垃圾邮件投诉  | `activity.spam_complaint`                      | `email.complained`                               |
| 打开          | `activity.opened` / `activity.opened_unique`   | `email.opened`                                   |
| 点击          | `activity.clicked` / `activity.clicked_unique` | `email.clicked`                                  |
| 退订          | `activity.unsubscribed`                        | `email.unsubscribed` / `email.list_unsubscribed` |
| 临时暂停      | `recipient.on_hold_added` / `..._removed`      | （无对应事件；见上文）                           |

两个差异决定了你的处理程序需要改动多少。

**双方都使用 HMAC-SHA256 签名，因此这是重新实现而非编写全新代码。** MailerSend 发送一个 `Signature` 请求头，其中包含用该 webhook 签名密钥计算的载荷哈希。我们遵循 [Standard Webhooks](https://www.standardwebhooks.com) 方案，使用 `webhook-id`、`webhook-timestamp` 和 `webhook-signature`，对由 id、时间戳和正文构建的字符串进行签名，因此时间戳还提供了重放保护。保留你已有的常量时间比较逻辑，替换构造方式即可；具体做法参见 [Webhooks 和事件](/docs/guides/webhooks)。

**MailerSend 区分打开/点击与其唯一变体，我们不区分。** `activity.opened` 和 `activity.opened_unique` 都以 `email.opened` 形式到达，因此之前只计唯一变体的处理程序需要自行基于 `recipient_id` 去重。我们的投递事件以收件人为范围，因此发送给三个收件人会产生三个投递结果，而不是一个。

## 切换上线

按照主指南完成[域名和 DNS](/docs/guides/email/migrate#2-re-point-domains-and-dns) 以及[沙箱冒烟测试](/docs/guides/email/migrate#5-verify-in-the-sandbox-before-cutover)。两者均与提供商无关。

## 后续步骤

- [发送域名](/docs/guides/email/sending-domains)：注册、验证生命周期，以及你正在发布的 DNS 记录
- [Webhooks 和事件](/docs/guides/webhooks)：端点设置和 Standard Webhooks 验证
- [测试沙箱](/docs/guides/email/testing-sandbox)：在切换上线前对新集成进行冒烟测试
- [屏蔽列表](/docs/guides/email/suppressions)：确认导入的列表以及后续我们如何维护它

## Related resources

- [Getting started with email](/learn/email/getting-started-with-email) (video)
- [Email](/products/email) (product)
- [Build your first integration](/learn/paths/integration) (course)
- [Send your first email](/docs/get-started/send-your-first-email) (docs)

[Get an implementation brief](/learn/workspace?topic=email)
