测试邮件投递(邮件沙盒)
邮件沙盒可以在不向真实收件箱发送的情况下测试 webhook 处理器、抑制逻辑和投递结果。通过正常的 API 向 messagebird.dev 上的地址发送即可。本地部分决定结果:bounce@messagebird.dev 会退信,delivered@messagebird.dev 会成功投递。
沙盒发送使用正常的接收、事件和 webhook 路径。它返回相同的 202 响应,并生成与生产发送相同的按收件人维度的事件载荷结构。载荷中没有测试标志。消息不会到达外部投递基础设施或真实收件箱。模拟的退信和投诉不会影响发送信誉,也不会写入抑制列表,因此你可以重复使用这些地址。
沙盒无需任何设置:不需要开关、测试模式或特殊的 API 密钥。它纯粹根据收件人地址触发,作用于正常的发送端点(POST /v1/email/messages 和 POST /v1/email/batches)以及群发:受众中地址为沙盒地址的联系人会被模拟而非实际发送,这就是你在不发送任何邮件的情况下演练活动的方式。模拟收件人仍计入你的发送配额,因此演练消耗的配额与真实发送相同。
魔术地址
所有地址都在 @messagebird.dev 上。本地部分选择结果:
| 地址 | 模拟结果 | Webhook 序列 | 备注 |
|---|---|---|---|
| delivered@ | 接收邮件服务器接受了消息 | email.accepted → email.processed → email.delivered | 正常路径 |
| bounce@ / hardbounce@ | 硬退信:SMTP 550,5.1.1 Unknown User,退信类别 10 | email.accepted → email.processed → email.bounced,附带 bounce_type: "hard" | 不写入抑制列表,因此地址可重复使用 |
| softbounce@ | 软退信:SMTP 451,4.3.0 Temporary failure, please retry,类别 20 | email.accepted → email.processed → email.bounced,附带 bounce_type: "soft" | 软退信无论真实还是模拟都不会触发抑制 |
| deferred@ / delay@ | 延迟投递:SMTP 451,4.2.1 Mailbox temporarily unavailable, will retry,类别 21 | email.accepted → email.processed → email.deferred | 模拟的延迟投递是终态:不会进行重试,因此收件人保持 deferred |
| complaint@ / spam@ | 收件人将消息举报为垃圾邮件(feedback_type: "abuse") | email.accepted → email.processed → email.complained | 不写入抑制列表;可重复使用 |
| suppressed@ | 收件人被视为已在你的抑制列表中 | email.accepted → email.rejected,附带 rejection_reason: "recipient_suppressed" | 在处理过程中短路,与真实被抑制的收件人完全相同:没有 email.processed,没有投递事件 |
| reject@ | 消息在任何投递尝试之前被拒绝 | email.accepted → email.rejected,附带 rejection_reason: "transmission_failed" | 不会有 email.processed 或投递事件 |
以上序列是单次发送产生的 webhook。在群发中,去掉开头的 email.accepted:群发收件人本身就被接收,但我们记录该事件时不会为其发送 webhook,因此每个序列从后续事件开始,投递路径上是 email.processed,suppressed@ 和 reject@ 则是 email.rejected。之后的内容完全相同,事件参考中有完整的规则说明。
一次发送可以混合沙盒和真实收件人。每个收件人拥有独立的生命周期:真实收件人正常投递,沙盒收件人被模拟。
地址规则
- 检测仅基于本地部分,且仅限于 messagebird.dev 域。 bounce@yourdomain.com 是一个普通地址。
- 只有魔术地址表中的本地部分才是魔术地址。 messagebird.dev 上的任何其他地址都是普通收件人。当你从共享的入门域名发送时,该地址必须属于已验证的工作区成员。
- 匹配不区分大小写:Bounce@messagebird.dev 和 bounce@messagebird.dev 行为完全相同。
- +label 子地址标签在匹配前被剥离:bounce+signup-flow@messagebird.dev 仍然会退信。使用标签来关联测试用例;完整地址(包括标签)会出现在你的事件和 webhook 中,因此每次测试运行可以标记自己的收件人。
演练:端到端模拟退信
确保你有一个订阅了邮件事件的 webhook 端点(参见 Webhooks),然后发送:
const msg = await bird.email.send({
from: { email: "onboarding@messagebird.dev", name: "Bird" },
to: ["bounce+signup-flow@messagebird.dev"],
subject: "Sandbox bounce test",
html: "<p>This message will hard-bounce.</p>",
tags: [{ name: "flow", value: "signup" }],
metadata: { test_run: "docs-capture-1" },
});
console.log(msg.id, msg.status); // "em_…", "accepted"msg = client.email.send(
from_={"email": "onboarding@messagebird.dev", "name": "Bird"},
to=["bounce+signup-flow@messagebird.dev"],
subject="Sandbox bounce test",
html="<p>This message will hard-bounce.</p>",
tags=[{"name": "flow", "value": "signup"}],
metadata={"test_run": "docs-capture-1"},
)
print(msg.id, msg.status)package main
import (
"context"
"fmt"
"log"
"os"
bird "github.com/messagebird/bird-sdk-go"
"github.com/messagebird/bird-sdk-go/option"
)
func main() {
client, err := bird.NewClient(option.WithAPIKey(os.Getenv("BIRD_API_KEY")))
if err != nil {
log.Fatal(err)
}
msg, err := client.Email.Send(context.Background(), bird.EmailSendParams{
From: "onboarding@messagebird.dev",
To: []string{"bounce+signup-flow@messagebird.dev"},
Subject: "Sandbox bounce test",
HTML: "<p>This message will hard-bounce.</p>",
Tags: []bird.Tag{{Name: "flow", Value: "signup"}},
Metadata: map[string]any{"test_run": "docs-capture-1"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(msg.Id, *msg.Status)
}$message = $bird->email->send(
from: 'Bird <onboarding@messagebird.dev>',
to: ['bounce+signup-flow@messagebird.dev'],
subject: 'Sandbox bounce test',
html: '<p>This message will hard-bounce.</p>',
);
echo $message->getId(), ' ', $message->getStatus();bird email send \
--from onboarding@messagebird.dev \
--html '<p>This message will hard-bounce.</p>' \
--metadata '{"test_run":"docs-capture-1"}' \
--subject 'Sandbox bounce test' \
--tag flow=signup \
--to bounce+signup-flow@messagebird.devcurl -X POST "https://us1.platform.bird.com/v1/email/messages" \
-H "Authorization: Bearer $BIRD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"from": "onboarding@messagebird.dev",
"to": ["bounce+signup-flow@messagebird.dev"],
"subject": "Sandbox bounce test",
"html": "<p>This message will hard-bounce.</p>",
"tags": [{ "name": "flow", "value": "signup" }],
"metadata": { "test_run": "docs-capture-1" }
}'如果你的密钥以 bk_eu1_ 开头,请改为调用 https://eu1.platform.bird.com。
API 响应 202 Accepted,附带一个 em_* 消息 ID,与生产发送无法区分。这正是要点:你正在测试的代码路径就是你的真实路径。你的 webhook 端点随后接收 email.accepted、email.processed,最后是 email.bounced。每个事件都回显发送的 tags 和 metadata(发送时未提供则为 null),email.bounced 载荷包含完整的退信分类:
代码示例
{
"type": "email.bounced",
"timestamp": "2026-07-23T14:51:00.362Z",
"data": {
"email_id": "em_01ky7qanhrejer0bn34v38hrxh",
"recipient_id": "er_01ky7qanhrejds4decpk83q5qq",
"workspace_id": "ws_01ky7m21hjffh9s8kq76gyb1xf",
"recipient": "bounce+signup-flow@messagebird.dev",
"recipient_role": "to",
"bounce_type": "hard",
"bounce_class": 10,
"bounce_code": "550",
"bounce_description": "5.1.1 Unknown User",
"sending_ip": null,
"tags": [{ "name": "flow", "value": "signup" }],
"metadata": { "test_run": "docs-capture-1" },
"broadcast_id": null
}
}字段含义请参阅事件参考。载荷中没有任何模拟器标志:事件类型和结构与真实硬退信完全相同。唯一标识其为模拟的是收件人地址本身,因此如果你的处理器需要区分测试流量,请基于 messagebird.dev 收件人域名进行判断。
要验证已被抑制的收件人永远不会被发送,请使用 suppressed@messagebird.dev 重复发送。断言你收到了 email.accepted,然后收到附带 rejection_reason: "recipient_suppressed" 的 email.rejected。你不应收到任何 email.processed 或投递事件。这与真实被抑制的收件人完全一致:消息被接收,然后在发送之前的处理阶段被短路。
沙盒能做什么和不能做什么
- 不写入抑制列表。 模拟的硬退信和投诉不会将收件人添加到你的抑制列表;这正是保持地址可重复使用的原因。你的 webhook 仍会触发(email.bounced、email.complained),因此你自己的抑制逻辑得到了完整验证。要测试已被抑制的拒绝路径,请使用专用的 suppressed@ 地址。
- 绝不会真实投递。 沙盒收件人在消息到达投递基础设施之前就被拦截。不会传输任何内容,不涉及任何收件箱,你的发送信誉不受影响。
- 请求验证仍然生效。 沙盒发送使用正常端点,因此 schema 检查、大小限制和请求头规则会照常拒绝无效请求。沙盒跳过的是交接之后的所有内容:渲染和该点之后的投递行为不会被验证。
- 打开和点击不会被模拟。 魔术地址模拟的是传输结果,没有人会打开消息,因此 email.opened 和 email.clicked 只来自真实邮件。
- 统计数据包含沙盒流量。 沙盒发送计入你工作区的汇总统计数据以及退信率和投诉率。大量沙盒退信会使你的仪表盘数据偏差,但不会影响你的信誉。
后续步骤
- 事件:完整的事件词汇表和按收件人维度的生命周期
- Webhooks:订阅、签名验证和重试
- 发送你的第一封邮件:本演练所基于的入门域名快速入门
- 抑制:真实抑制列表的工作方式
- 测试邮件发送而不打扰任何人:一段视频,演示沙盒地址及其各自产生的事件