编程

Laravel 的 Mailbox 简介:一个可直接嵌入应用的本地电子邮件收件箱

3 2026-05-21 17:47:00

每个 Laravel 应用都会发送电子邮件,如欢迎邮件、密码重置邮件、订单确认邮件、发票邮件和通知邮件。每个团队都会遇到同样的尴尬时刻:在真正发送到收件箱之前,需要有人查看这些邮件的实际内容。

常规选项都是折中方案。日志驱动会将编码后的 HTML 和头部信息转储到一个无人愿意阅读的文件中。Mailtrap 和 Mailhog 虽然能工作,但它们意味着每个新开发人员都需要再次注册、获取另一个 API 密钥、搭建另一个 Docker 容器,以及完成另一个入职步骤。测试中的 Mail::fake() 函数只能告诉你一个 Mailable 已排队,却无法告知你渲染后的电子邮件是否包含你所期望的内容。

今天,我们为 Laravel 开源了 Mailbox——一个 Laravel 包,它能够捕获应用发送的邮件,并通过本地自托管的仪表盘现。无需外部服务。无需账户。无需额外流程。只需一个 composer require,你的应用发送的每一封电子邮件都会在 /mailbox 上以收件人看到的样子渲染。

由来

在 Redberry,我们并行运行 30 至 35 个 Laravel 开发项目。每个项目都会发送事务性电子邮件,而每个项目过去都曾遇到过同样的障碍:每当有人首次接触电子邮件流程时,他们都必须停下来配置一个测试工具。有时是 Mailtrap,有时是本地的 Mailhog 容器,有时只是通过日志驱动和大量的眯眼观察。

并不是这些事情本身不好,而是重复性太高。我们有三十多个项目,新开发人员不断轮换,测试环境需要单独设置,持续集成(CI)管道也需要采用不同的方法。我们希望有一个电子邮件收件箱,它位于 Laravel 应用内部,随代码库一起发布,并且在初级开发人员的笔记本电脑上和在测试服务器上的工作方式相同。

因此,我们构建了一个这样的工具。并且,由于我们相信更广泛的 Laravel 社区会从中受益,因此我们将其作为一个开源包发布。

Laravel 的 Mailbox 涵盖了哪些内容(电子邮件测试、预览和断言)

Laravel 的 Mailbox 插件主要做了三件事,并且它力求每一件事都做到最好:

  • 捕获 - 通过 Laravel 的 Mail Facade发送的每一封电子邮件都会被自定义的 Symfony 传输层拦截并本地存储。适用于 Mailable 类、通知和原始的 Mail::raw() 调用。
  • 预览 - /mailbox 上的一个自包含的 Vue 3 仪表板,提供 HTML / 纯文本 / 原始 RFC 822 视图、附件预览和下载、收件人过滤、搜索、已读/未读跟踪以及实时更新轮询功能。
  • 断言 - 一个流畅的测试 API(Mailbox::firstSent()->assertHasSubject()->assertSeeInHtml()...),它针对的是渲染后的消息 - 真实的 HTML、真实的收件人、真实的附件 - 而不仅仅是 Mailable 对象。

但有趣之处并不在于其功能列表,而在于其几乎无需任何设置即可运行。

 

如何在 Laravel 中测试和预览邮件 

让我们将一个新安装的 Laravel 应用从“能发送邮件”的状态,提升至“拥有一个完整的收件箱和一个测试套件,能够针对真实渲染的内容进行断言”的状态。

安装

composer require redberry/mailbox-for-laravel --dev
php artisan mailbox:install

设置完毕。安装命令会发布资源,运行包的迁移,并在 storage/app/mailbox/mailbox.sqlite 中创建一个专用的 SQLite 数据库,该数据库与应用的主数据库完全隔离。该包是自动发现的,因此无需注册服务提供者。

将你的邮件系统指向 mailbox

MAIL_MAILER=mailbox

现在访问 /mailbox。收件箱为空。准备好接收。

 

发送邮件

没什么特别的。现有的代码已经生效:

Mail::to('user@example.com')->send(new WelcomeMail($user));

mailbox 传输会拦截传出的消息,对其进行规范化处理,然后将其交给存储驱动。仪表盘的实时轮询会在几秒钟内获取到该消息。

并行捕获与交付

默认设置为仅捕获,但在过渡阶段,你通常希望同时实现这两项功能——既捕获邮件以供检查,又实际投递邮件。将 MAILBOX_DECORATE 设置为应用已知的任何邮件程序名称:

MAIL_MAILER=mailbox
MAILBOX_DECORATE=smtp

服务提供者解析 smtp 邮件程序底层的 Symfony 传输层并对其进行封装。每封电子邮件首先在本地被捕获,然后转发到真实的传输层。适用于 smtpsespostmarklogconfig/mail.php 中注册的任何传输层。

若要恢复为仅捕获模式,请移除该变量。

仪表盘

仪表盘是一个由 Laravel 应用提供的 Vue 3 应用,它并非一个独立的进程,也不是一个额外的 URL。它位于你配置的任何前缀下,默认是 /mailbox

在内部,你可以得到一个电子邮件客户端所应有的功能:

  • 收件箱列表按最新消息优先排序,支持实时轮询,无需刷新即可显示新消息
  • 每封邮件的 HTML、纯文本和原始 RFC 822 源代码视图
  • 内嵌图像渲染的附件 - 内嵌 cid:重写引用以在浏览器中工作
  • 单条消息删除、全部清除以及已读/未读标记跟踪
  • 收件人筛选与搜索
    一键发送测试邮件按钮,用于进行烟雾测试

仪表盘与宿主应用的前端完全隔离。它运行自己的 Vue 应用,将自有资源构建到 public/vendor/mailbox/ 目录中,并且与你使用的用户界面(UI)——Blade、Livewire、Inertia、React、Vue 或根本不使用任何框架——在运行时没有任何耦合。

存储驱动

开箱即用,消息会被存入一个专用的 SQLite 数据库中,该数据库是由安装命令为你创建的。无需进行任何配置。但如果你更愿意使用现有的 MySQL 或 Postgres 连接,你可以:

MAILBOX_STORE_DRIVER=database
MAILBOX_STORE_DATABASE_CONNECTION=mysql

如果这些都不合适,那么就为你的后端(Redis、S3,无论你需要什么)实现 MessageStoreAttachmentStore 接口。驱动对中的两部分都是通过 CaptureService 连接的,因此你无需重复清理逻辑,也无需从存储代码中接触 HTTP 层。

测试

这是最令人兴奋的部分。Mail::fake() 虽然可以验证某项任务是否已排进入队列,但它无法告诉你渲染后的电子邮件是否包含了你所期望的内容。

如果你正在考虑更广泛的测试方法,我们撰写了更多关于 Laravel 测试策略以及如何构建可靠测试套件的内容。

一个损坏的 Blade 模板,一个缺失的变量,一个使用了错误语言环境的主题行—— Mail::fake() 无法捕捉到任何这些问题。测试通过了。客户却收到了一封损坏的电子邮件。

在 Laravel 完成渲染后,Mailbox 会对捕获到的消息进行断言。真实的主题。真实的 HTML。真实的附件。这些字节与原本会发送到 SMTP 服务器的字节完全相同。

InteractsWithMailbox trait 添加到你的测试中——它会在每次测试前清空邮箱,并公开 $this->mailbox() 以供断言使用:

Mailbox Facade 上的集合级断言:

use Redberry\MailboxForLaravel\Facades\Mailbox;
use Redberry\MailboxForLaravel\DTO\MailboxMessageData;

Mailbox::assertSentCount(2);
Mailbox::assertSentTo('user@example.com');
Mailbox::assertNotSentTo('admin@example.com');

Mailbox::assertSent(
    fn (MailboxMessageData $m) => str_contains($m->subject, 'Newsletter'),
    expectedCount: 3,
);

通过 firstSent() 按消息 fluent 断言:

Mailbox::firstSent()
    ->assertHasSubject('Order Confirmation')
    ->assertFrom('noreply@shop.com')
    ->assertHasTo('buyer@example.com')
    ->assertSeeInHtml('Order #12345')
    ->assertDontSeeInHtml('error')
    ->assertHasAttachment('invoice.pdf', 'application/pdf')
    ->assertAttachmentCount(1);

一个注册流程的端到端测试读起来更像是一个规范说明,而非测试本身:

it('sends welcome email with getting started guide', function () {
    $this->post('/register', [
        'name' => 'John Doe',
        'email' => 'john@example.com',
        'password' => 'secret123',
    ]);

    Mailbox::assertSentCount(1);
    Mailbox::assertSentTo('john@example.com');

    Mailbox::firstSent()
        ->assertHasSubject('Welcome, John!')
        ->assertFrom('noreply@myapp.com')
        ->assertSeeInOrderInHtml(['Welcome', 'Getting Started', 'Support'])
        ->assertHasAttachment('getting-started.pdf');
});

授权和生产

捕获的消息可能包含密码重置令牌、发票详情和个人数据,因此在生产环境中,邮箱功能默认是关闭的。主开关是 MAILBOX_ENABLED,它仅在非生产环境中自动启用。

仪表盘访问权限是通过 Laravel 的 Gate 系统进行控制的,具体来说是通过 viewMailbox 功能。默认情况下,它允许在本地环境中进行访问。你可以定义自己的门禁来锁定它:

use Illuminate\Support\Facades\Gate;

public function boot(): void
{
    Gate::define('viewMailbox', fn ($user) => $user?->isAdmin());
}

该包不会覆写你自定义的门卫系统。对于暂存,这为你提供了仅限经过身份验证的访问权限,无需任何额外的中间件。

保留

默认情况下,捕获的消息会在 24 小时后被清除。该包会在 Laravel 的调度器上自动注册一个名为日常(daily) mailbox:clear --outdated 的任务,因此你无需手动配置。你可以通过设置 MAILBOX_RETENTION(以秒为单位)来调整保留窗口,或者如果你更愿意自己运行清除操作,可以设置 MAILBOX_RETENTION_SCHEDULE=false

你可以用它来建造什么

我们构建它是为了满足我们自己的开发和暂存工作流程,但其中出现了一些模式:

  • 试运行 QA - 在测试环境中,以捕获并转发模式运行邮箱。质保(QA)人员可以在其真实收件箱和仪表板中查看每一封电子邮件,且在测试过程中,面向客户的地址绝不会意外收到垃圾邮件。
  • 回归测试套件 - 将仅检查邮件发送的 Mail::fake() 断言替换为针对渲染消息的断言。在上线之前,及时发现模板错误、变量缺失和编码问题。
  • 客户支持工具——为你的支持团队提供一个内部环境的只读仪表板视图,以便他们能够准确查看客户会收到什么。
  • 事务性电子邮件的设计审核 - 设计师可以在实时预览环境中查看渲染后的 HTML 电子邮件,而无需使用邮件客户端或自己的收件箱。
  • CI 冒烟测试 - 使用 sent() 集合 API 来编写端到端流程脚本,在 CI 中对实际渲染的邮件进行发送、检查和断言。

Give It a Try

Mailbox for Laravel 需要 PHP 8.3+ 并支持 Laravel 10、11 和 12。一分钟安装:

composer require redberry/mailbox-for-laravel --dev
php artisan mailbox:install

然后,设置 MAIL_MAILER=mailbox 并且在浏览器中打开 /mailbox