Skip to content

邮件 ​

简介 ​

发送电子邮件不必很复杂。Laravel 提供了一个简洁的电子邮件 API,由流行的 Symfony Mailer 组件驱动。Laravel 和 Symfony Mailer 提供了通过 SMTP、Cloudflare、Mailgun、Postmark、Resend、Amazon SES 和 sendmail 发送电子邮件的驱动,让你可以通过本地或云端服务快速开始发送邮件。

配置 ​

Laravel 的电子邮件服务可以通过应用的 config/mail.php 配置文件进行配置。在此文件中配置的每个邮件发送器都可以有自己独特的配置,甚至有自己独特的"传输",允许你的应用使用不同的电子邮件服务来发送特定的邮件消息。例如,你的应用可能使用 Postmark 发送事务性邮件,同时使用 Amazon SES 发送批量邮件。

在 mail 配置文件中,你会找到一个 mailers 配置数组。此数组包含 Laravel 支持的每个主要邮件驱动/传输的示例配置条目,而 default 配置值决定了当你的应用需要发送电子邮件时默认使用哪个邮件发送器。

驱动/传输先决条件 ​

基于 API 的驱动(如 Mailgun、Postmark 和 Resend)通常比通过 SMTP 服务器发送邮件更简单、更快速。我们建议尽可能使用这些驱动之一。

Cloudflare 驱动 ​

要使用 Cloudflare 驱动,请通过 Composer 安装 Symfony 的 HTTP Client:

shell
composer require symfony/http-client

接下来,你需要在应用的 config/mail.php 配置文件中做两处更改。首先,将默认邮件发送器设置为 cloudflare:

php
'default' => env('MAIL_MAILER', 'cloudflare'),

其次,将以下配置数组添加到 mailers 数组中:

php
'cloudflare' => [
    'transport' => 'cloudflare',
],

配置好应用的默认邮件发送器后,将以下选项添加到 config/services.php 配置文件中:

php
'cloudflare' => [
    'account_id' => env('CLOUDFLARE_ACCOUNT_ID'),
    'key' => env('CLOUDFLARE_KEY'),
],

Mailgun 驱动 ​

要使用 Mailgun 驱动,请通过 Composer 安装 Symfony 的 Mailgun Mailer 传输:

shell
composer require symfony/mailgun-mailer symfony/http-client

接下来,你需要在应用的 config/mail.php 配置文件中做两处更改。首先,将默认邮件发送器设置为 mailgun:

php
'default' => env('MAIL_MAILER', 'mailgun'),

其次,将以下配置数组添加到 mailers 数组中:

php
'mailgun' => [
    'transport' => 'mailgun',
    // 'client' => [
    //     'timeout' => 5,
    // ],
],

配置好应用的默认邮件发送器后,将以下选项添加到 config/services.php 配置文件中:

php
'mailgun' => [
    'domain' => env('MAILGUN_DOMAIN'),
    'secret' => env('MAILGUN_SECRET'),
    'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
    'scheme' => 'https',
],

如果你不使用美国 Mailgun 区域,可以在 services 配置文件中定义你所在区域的端点:

php
'mailgun' => [
    'domain' => env('MAILGUN_DOMAIN'),
    'secret' => env('MAILGUN_SECRET'),
    'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
    'scheme' => 'https',
],

Postmark 驱动 ​

要使用 Postmark 驱动,请通过 Composer 安装 Symfony 的 Postmark Mailer 传输:

shell
composer require symfony/postmark-mailer symfony/http-client

接下来,在应用的 config/mail.php 配置文件中将 default 选项设置为 postmark。配置好应用的默认邮件发送器后,确保 config/services.php 配置文件包含以下选项:

php
'postmark' => [
    'key' => env('POSTMARK_API_KEY'),
],

如果你想指定给定邮件发送器应使用的 Postmark 消息流,可以在邮件发送器的配置数组中添加 message_stream_id 配置选项。此配置数组可以在应用的 config/mail.php 配置文件中找到:

php
'postmark' => [
    'transport' => 'postmark',
    'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
    // 'client' => [
    //     'timeout' => 5,
    // ],
],

这样你还可以使用不同的消息流设置多个 Postmark 邮件发送器。

Resend 驱动 ​

要使用 Resend 驱动,请通过 Composer 安装 Resend 的 PHP SDK:

shell
composer require resend/resend-php

接下来,在应用的 config/mail.php 配置文件中将 default 选项设置为 resend。配置好应用的默认邮件发送器后,确保 config/services.php 配置文件包含以下选项:

php
'resend' => [
    'key' => env('RESEND_API_KEY'),
],

SES 驱动 ​

要使用 Amazon SES 驱动,你必须首先安装 Amazon AWS SDK for PHP。你可以通过 Composer 包管理器安装此库:

shell
composer require aws/aws-sdk-php

接下来,在 config/mail.php 配置文件中将 default 选项设置为 ses,并验证 config/services.php 配置文件包含以下选项:

php
'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],

要通过会话令牌使用 AWS 临时凭证,可以在应用的 SES 配置中添加 token 键:

php
'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'token' => env('AWS_SESSION_TOKEN'),
],

要与 SES 的订阅管理功能交互,你可以在邮件消息的 headers 方法返回的数组中返回 X-Ses-List-Management-Options 头:

php
/**
 * 获取消息头。
 */
public function headers(): Headers
{
    return new Headers(
        text: [
            'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic',
        ],
    );
}

要通过 SES tenant 发送电子邮件,可以从 headers 方法返回 X-Ses-Tenant-Name 头。Laravel 在发送消息时会将该头的值作为 TenantName 选项传递给 SES:

php
public function headers(): Headers
{
    return new Headers(
        text: [
            'X-Ses-Tenant-Name' => 'tenant-id',
        ],
    );
}

如果你想定义 Laravel 在发送电子邮件时应传递给 AWS SDK 的 SendEmail 方法的额外选项,可以在 ses 配置中定义一个 options 数组:

php
'ses' => [
    'key' => env('AWS_ACCESS_KEY_ID'),
    'secret' => env('AWS_SECRET_ACCESS_KEY'),
    'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
    'options' => [
        'ConfigurationSetName' => 'MyConfigurationSet',
        'EmailTags' => [
            ['Name' => 'foo', 'Value' => 'bar'],
        ],
    ],
],

故障转移配置 ​

有时,你配置用于发送应用邮件的外部服务可能会宕机。在这些情况下,定义一个或多个备用邮件投递配置会很有用,这些配置将在主要投递驱动宕机时使用。

为此,你应该在应用的 mail 配置文件中定义一个使用 failover 传输的邮件发送器。应用的 failover 邮件发送器的配置数组应包含一个 mailers 数组,引用配置的邮件发送器被选择进行投递的顺序:

php
'mailers' => [
    'failover' => [
        'transport' => 'failover',
        'mailers' => [
            'postmark',
            'mailgun',
            'sendmail',
        ],
        'retry_after' => 60,
    ],

    // ...
],

配置好使用 failover 传输的邮件发送器后,你需要在应用的 .env 文件中将故障转移邮件发送器设置为默认邮件发送器,以使用故障转移功能:

ini
MAIL_MAILER=failover

轮询配置 ​

roundrobin 传输允许你将邮件发送工作负载分配到多个邮件发送器上。要开始使用,请在应用的 mail 配置文件中定义一个使用 roundrobin 传输的邮件发送器。应用的 roundrobin 邮件发送器的配置数组应包含一个 mailers 数组,引用应用于投递的已配置邮件发送器:

php
'mailers' => [
    'roundrobin' => [
        'transport' => 'roundrobin',
        'mailers' => [
            'ses',
            'postmark',
        ],
        'retry_after' => 60,
    ],

    // ...
],

定义好轮询邮件发送器后,你应该通过在应用的 mail 配置文件中将 default 配置键的值指定为其名称来将此邮件发送器设置为应用使用的默认邮件发送器:

php
'default' => env('MAIL_MAILER', 'roundrobin'),

轮询传输从配置的邮件发送器列表中随机选择一个邮件发送器,然后为每封后续邮件切换到下一个可用的邮件发送器。与帮助实现高可用性的 failover 传输不同,roundrobin 传输提供负载均衡。

生成 Mailable ​

在构建 Laravel 应用时,应用发送的每种类型的电子邮件都表示为一个"mailable"类。这些类存储在 app/Mail 目录中。如果你在应用中没有看到此目录,不用担心,它会在你使用 make:mail Artisan 命令创建第一个 mailable 类时自动生成:

shell
php artisan make:mail OrderShipped

编写 Mailable ​

生成 mailable 类后,打开它以便我们探索其内容。Mailable 类的配置在几个方法中完成,包括 envelope、content 和 attachments 方法。

envelope 方法返回一个 Illuminate\Mail\Mailables\Envelope 对象,用于定义消息的主题,有时还包括收件人。content 方法返回一个 Illuminate\Mail\Mailables\Content 对象,用于定义生成消息内容时使用的 Blade 模板。

配置发件人 ​

使用 Envelope ​

首先,让我们探索配置电子邮件的发件人。换句话说,就是电子邮件的"from"是谁。有两种方式配置发件人。首先,你可以在消息的 envelope 上指定"from"地址:

php
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Envelope;

/**
 * 获取消息信封。
 */
public function envelope(): Envelope
{
    return new Envelope(
        from: new Address('jeffrey@example.com', 'Jeffrey Way'),
        subject: 'Order Shipped',
    );
}

如果需要,你还可以指定 replyTo 地址:

php
return new Envelope(
    from: new Address('jeffrey@example.com', 'Jeffrey Way'),
    replyTo: [
        new Address('taylor@example.com', 'Taylor Otwell'),
    ],
    subject: 'Order Shipped',
);

使用全局 from 地址 ​

但是,如果你的应用对所有电子邮件使用相同的"from"地址,在每个生成的 mailable 类中添加它可能会很繁琐。相反,你可以在 config/mail.php 配置文件中指定一个全局"from"地址。如果 mailable 类中没有指定其他"from"地址,将使用此地址:

php
'from' => [
    'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
    'name' => env('MAIL_FROM_NAME', 'Example'),
],

此外,你还可以在 config/mail.php 配置文件中定义一个全局"reply_to"地址:

php
'reply_to' => [
    'address' => 'example@example.com',
    'name' => 'App Name',
],

配置视图 ​

在 mailable 类的 content 方法中,你可以定义 view,即渲染电子邮件内容时应使用的模板。由于每封电子邮件通常使用 Blade 模板来渲染其内容,因此你可以在构建电子邮件的 HTML 时充分利用 Blade 模板引擎的强大功能和便利性:

php
/**
 * 获取消息内容定义。
 */
public function content(): Content
{
    return new Content(
        view: 'mail.orders.shipped',
    );
}

NOTE

你可能希望创建一个 resources/views/mail 目录来存放所有的电子邮件模板;但是,你可以自由地将它们放在 resources/views 目录中的任何位置。

纯文本电子邮件 ​

如果你想定义电子邮件的纯文本版本,可以在创建消息的 Content 定义时指定纯文本模板。与 view 参数一样,text 参数应该是一个模板名称,用于渲染电子邮件的内容。你可以自由定义消息的 HTML 版本和纯文本版本:

php
/**
 * 获取消息内容定义。
 */
public function content(): Content
{
    return new Content(
        view: 'mail.orders.shipped',
        text: 'mail.orders.shipped-text'
    );
}

为了清晰起见,html 参数可以用作 view 参数的别名:

php
return new Content(
    html: 'mail.orders.shipped',
    text: 'mail.orders.shipped-text'
);

视图数据 ​

通过公共属性 ​

通常,你会希望将一些数据传递给视图,以便在渲染电子邮件的 HTML 时使用。有两种方式可以使数据对视图可用。首先,mailable 类上定义的任何公共属性都将自动对视图可用。例如,你可以将数据传入 mailable 类的构造函数,并将该数据设置为类上定义的公共属性:

php
<?php

namespace App\Mail;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Queue\SerializesModels;

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * 创建一个新的消息实例。
     */
    public function __construct(
        public Order $order,
    ) {}

    /**
     * 获取消息内容定义。
     */
    public function content(): Content
    {
        return new Content(
            view: 'mail.orders.shipped',
        );
    }
}

一旦数据被设置为公共属性,它将自动在视图中可用,因此你可以像访问 Blade 模板中的任何其他数据一样访问它:

blade
<div>
    Price: {{ $order->price }}
</div>

通过 with 参数 ​

如果你想在数据发送到模板之前自定义电子邮件数据的格式,可以通过 Content 定义的 with 参数手动将数据传递给视图。通常,你仍会通过 mailable 类的构造函数传递数据;但是,你应该将此数据设置为 protected 或 private 属性,这样数据就不会自动对模板可用:

php
<?php

namespace App\Mail;

use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Queue\SerializesModels;

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * 创建一个新的消息实例。
     */
    public function __construct(
        protected Order $order,
    ) {}

    /**
     * 获取消息内容定义。
     */
    public function content(): Content
    {
        return new Content(
            view: 'mail.orders.shipped',
            with: [
                'orderName' => $this->order->name,
                'orderPrice' => $this->order->price,
            ],
        );
    }
}

一旦数据通过 with 参数传递,它将自动在视图中可用,因此你可以像访问 Blade 模板中的任何其他数据一样访问它:

blade
<div>
    Price: {{ $orderPrice }}
</div>

附件 ​

要向电子邮件添加附件,你需要将附件添加到消息的 attachments 方法返回的数组中。首先,你可以通过向 Attachment 类提供的 fromPath 方法提供文件路径来添加附件:

php
use Illuminate\Mail\Mailables\Attachment;

/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromPath('/path/to/file'),
    ];
}

向消息附加文件时,你还可以使用 as 和 withMime 方法指定附件的显示名称和/或 MIME 类型:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromPath('/path/to/file')
            ->as('name.pdf')
            ->withMime('application/pdf'),
    ];
}

从磁盘附加文件 ​

如果你已将文件存储在某个文件系统磁盘上,可以使用 fromStorage 附件方法将其附加到电子邮件:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromStorage('/path/to/file'),
    ];
}

当然,你也可以指定附件的名称和 MIME 类型:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromStorage('/path/to/file')
            ->as('name.pdf')
            ->withMime('application/pdf'),
    ];
}

如果你需要指定默认磁盘之外的存储磁盘,可以使用 fromStorageDisk 方法:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromStorageDisk('s3', '/path/to/file')
            ->as('name.pdf')
            ->withMime('application/pdf'),
    ];
}

原始数据附件 ​

fromData 附件方法可用于将原始字节字符串作为附件附加。例如,如果你在内存中生成了 PDF 并希望将其附加到电子邮件而不写入磁盘,可以使用此方法。fromData 方法接受一个解析原始数据字节的闭包以及应分配给附件的名称:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [
        Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
            ->withMime('application/pdf'),
    ];
}

内联附件 ​

将内联图片嵌入到电子邮件中通常很繁琐;但是,Laravel 提供了一种便捷的方式来将图片附加到电子邮件。要嵌入内联图片,请在电子邮件模板中的 $message 变量上使用 embed 方法。Laravel 会自动使 $message 变量对所有电子邮件模板可用,因此你无需担心手动传递它:

blade
<body>
    这是一张图片:

    <img src="{{ $message->embed($pathToImage) }}">
</body>

WARNING

$message 变量在纯文本消息模板中不可用,因为纯文本消息不使用内联附件。

嵌入原始数据附件 ​

如果你已经有一个想要嵌入到电子邮件模板中的原始图片数据字符串,可以在 $message 变量上调用 embedData 方法。调用 embedData 方法时,你需要提供一个应分配给嵌入图片的文件名:

blade
<body>
    这是一张来自原始数据的图片:

    <img src="{{ $message->embedData($data, 'example-image.jpg') }}">
</body>

可附加对象 ​

虽然通过简单的字符串路径将文件附加到消息通常就足够了,但在许多情况下,应用中的可附加实体由类表示。例如,如果你的应用将照片附加到消息,你的应用可能还有一个代表该照片的 Photo 模型。在这种情况下,简单地将 Photo 模型传递给 attach 方法不是很方便吗?可附加对象允许你这样做。

要开始使用,请在将可附加到消息的对象上实现 Illuminate\Contracts\Mail\Attachable 接口。此接口要求你的类定义一个 toMailAttachment 方法,该方法返回一个 Illuminate\Mail\Attachment 实例:

php
<?php

namespace App\Models;

use Illuminate\Contracts\Mail\Attachable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Mail\Attachment;

class Photo extends Model implements Attachable
{
    /**
     * 获取模型的可附加表示。
     */
    public function toMailAttachment(): Attachment
    {
        return Attachment::fromPath('/path/to/file');
    }
}

一旦你定义了可附加对象,就可以在构建电子邮件消息时从 attachments 方法返回该对象的实例:

php
/**
 * 获取消息的附件。
 *
 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
 */
public function attachments(): array
{
    return [$this->photo];
}

当然,附件数据可能存储在远程文件存储服务(如 Amazon S3)上。因此,Laravel 还允许你从应用的文件系统磁盘上存储的数据生成附件实例:

php
// 从默认磁盘上的文件创建附件...
return Attachment::fromStorage($this->path);

// 从特定磁盘上的文件创建附件...
return Attachment::fromStorageDisk('backblaze', $this->path);

此外,你可以通过内存中的数据创建附件实例。为此,请向 fromData 方法提供一个闭包。该闭包应返回表示附件的原始数据:

php
return Attachment::fromData(fn () => $this->content, 'Photo Name');

Laravel 还提供了你可以用来自定义附件的其他方法。例如,你可以使用 as 和 withMime 方法来自定义文件的名称和 MIME 类型:

php
return Attachment::fromPath('/path/to/file')
    ->as('Photo Name')
    ->withMime('image/jpeg');

头信息 ​

有时你可能需要向发出的消息附加额外的头信息。例如,你可能需要设置自定义的 Message-Id 或其他任意文本头。

为此,请在 mailable 上定义一个 headers 方法。headers 方法应返回一个 Illuminate\Mail\Mailables\Headers 实例。此类接受 messageId、references 和 text 参数。当然,你可以只提供特定消息所需的参数:

php
use Illuminate\Mail\Mailables\Headers;

/**
 * 获取消息头。
 */
public function headers(): Headers
{
    return new Headers(
        messageId: 'custom-message-id@example.com',
        references: ['previous-message@example.com'],
        text: [
            'X-Custom-Header' => 'Custom Value',
        ],
    );
}

标签和元数据 ​

一些第三方电子邮件提供商(如 Mailgun 和 Postmark)支持消息"标签"和"元数据",可用于对应用发送的电子邮件进行分组和跟踪。你可以通过 Envelope 定义向电子邮件消息添加标签和元数据:

php
use Illuminate\Mail\Mailables\Envelope;

/**
 * 获取消息信封。
 *
 * @return \Illuminate\Mail\Mailables\Envelope
 */
public function envelope(): Envelope
{
    return new Envelope(
        subject: 'Order Shipped',
        tags: ['shipment'],
        metadata: [
            'order_id' => $this->order->id,
        ],
    );
}

如果你的应用使用 Mailgun 驱动,你可以查阅 Mailgun 的文档以获取有关标签和元数据的更多信息。同样,你也可以查阅 Postmark 文档以获取有关其标签和元数据支持的更多信息。

如果你的应用使用 Amazon SES 发送电子邮件,你应该使用 metadata 方法将 SES "标签"附加到消息。

自定义 Symfony 消息 ​

Laravel 的邮件功能由 Symfony Mailer 驱动。Laravel 允许你注册自定义回调,这些回调将在发送消息之前使用 Symfony Message 实例调用。这给了你在发送消息之前深度自定义消息的机会。为此,请在 Envelope 定义上定义一个 using 参数:

php
use Illuminate\Mail\Mailables\Envelope;
use Symfony\Component\Mime\Email;

/**
 * 获取消息信封。
 */
public function envelope(): Envelope
{
    return new Envelope(
        subject: 'Order Shipped',
        using: [
            function (Email $message) {
                // ...
            },
        ]
    );
}

Markdown Mailable ​

Markdown mailable 消息允许你在 mailable 中利用邮件通知的预构建模板和组件。由于消息是用 Markdown 编写的,Laravel 能够为消息渲染美观、响应式的 HTML 模板,同时自动生成纯文本对应版本。

生成 Markdown Mailable ​

要生成带有对应 Markdown 模板的 mailable,你可以使用 make:mail Artisan 命令的 --markdown 选项:

shell
php artisan make:mail OrderShipped --markdown=mail.orders.shipped

然后,在 content 方法中配置 mailable 的 Content 定义时,使用 markdown 参数代替 view 参数:

php
use Illuminate\Mail\Mailables\Content;

/**
 * 获取消息内容定义。
 */
public function content(): Content
{
    return new Content(
        markdown: 'mail.orders.shipped',
        with: [
            'url' => $this->orderUrl,
        ],
    );
}

编写 Markdown 消息 ​

Markdown mailable 使用 Blade 组件和 Markdown 语法的组合,让你可以轻松构建邮件消息,同时利用 Laravel 的预构建电子邮件 UI 组件:

blade
<x-mail::message>
# Order Shipped

Your order has been shipped!

<x-mail::button :url="$url">
View Order
</x-mail::button>

Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

NOTE

编写 Markdown 电子邮件时不要使用过多缩进。根据 Markdown 标准,Markdown 解析器会将缩进内容渲染为代码块。

Button 组件 ​

按钮组件渲染一个居中的按钮链接。该组件接受两个参数,一个 url 和一个可选的 color。支持的颜色有 primary、success 和 error。你可以向消息中添加任意数量的按钮组件:

blade
<x-mail::button :url="$url" color="success">
View Order
</x-mail::button>

Panel 组件 ​

面板组件将给定的文本块渲染在一个与消息其余部分背景色略有不同的面板中。这使你可以引起对特定文本块的注意:

blade
<x-mail::panel>
This is the panel content.
</x-mail::panel>

Table 组件 ​

表格组件允许你将 Markdown 表格转换为 HTML 表格。该组件接受 Markdown 表格作为其内容。支持使用默认的 Markdown 表格对齐语法进行表格列对齐:

blade
<x-mail::table>
| Laravel       | Table         | Example       |
| ------------- | :-----------: | ------------: |
| Col 2 is      | Centered      | $10           |
| Col 3 is      | Right-Aligned | $20           |
</x-mail::table>

自定义组件 ​

你可以将所有 Markdown 邮件组件导出到你自己的应用中进行自定义。要导出组件,请使用 vendor:publish Artisan 命令发布 laravel-mail 资产标签:

shell
php artisan vendor:publish --tag=laravel-mail

此命令会将 Markdown 邮件组件发布到 resources/views/vendor/mail 目录。mail 目录将包含一个 html 和一个 text 目录,每个目录包含每个可用组件的相应表示。你可以自由地按需自定义这些组件。

自定义 CSS ​

导出组件后,resources/views/vendor/mail/html/themes 目录将包含一个 default.css 文件。你可以自定义此文件中的 CSS,你的样式将自动转换为 Markdown 邮件消息的 HTML 表示中的内联 CSS 样式。

如果你想为 Laravel 的 Markdown 组件构建一个全新的主题,可以在 html/themes 目录中放置一个 CSS 文件。命名并保存 CSS 文件后,更新应用的 config/mail.php 配置文件的 theme 选项以匹配新主题的名称。

要为单个 mailable 自定义主题,你可以将 mailable 类的 $theme 属性设置为发送该 mailable 时应使用的主题名称。

发送邮件 ​

要发送消息,请使用 Mail facade 上的 to 方法。to 方法接受电子邮件地址、用户实例或用户集合。如果你传递一个对象或对象集合,邮件发送器将自动使用它们的 email 和 name 属性来确定电子邮件的收件人,因此请确保这些属性在你的对象上可用。指定收件人后,你可以将 mailable 类的实例传递给 send 方法:

php
<?php

namespace App\Http\Controllers;

use App\Mail\OrderShipped;
use App\Models\Order;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;

class OrderShipmentController extends Controller
{
    /**
     * 发货给定的订单。
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // 发货订单...

        Mail::to($request->user())->send(new OrderShipped($order));

        return redirect('/orders');
    }
}

发送消息时,你不仅限于指定"to"收件人。你可以自由地通过链式调用相应方法来设置"to"、"cc"和"bcc"收件人:

php
Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->send(new OrderShipped($order));

遍历收件人 ​

有时,你可能需要通过遍历收件人/电子邮件地址数组来向一系列收件人发送 mailable。但是,由于 to 方法会将电子邮件地址追加到 mailable 的收件人列表中,因此循环的每次迭代都会向之前的所有收件人发送另一封电子邮件。因此,你应该始终为每个收件人重新创建 mailable 实例:

php
foreach (['taylor@example.com', 'dries@example.com'] as $recipient) {
    Mail::to($recipient)->send(new OrderShipped($order));
}

通过特定邮件发送器发送邮件 ​

默认情况下,Laravel 将使用应用的 mail 配置文件中配置为 default 的邮件发送器来发送电子邮件。但是,你可以使用 mailer 方法通过特定的邮件发送器配置发送消息:

php
Mail::mailer('postmark')
    ->to($request->user())
    ->send(new OrderShipped($order));

队列邮件 ​

将邮件消息加入队列 ​

由于发送电子邮件消息可能会对应用的响应时间产生负面影响,许多开发者选择将电子邮件消息加入队列以在后台发送。Laravel 使用其内置的统一队列 API使这变得简单。要将邮件消息加入队列,请在指定消息收件人后在 Mail facade 上使用 queue 方法:

php
Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->queue(new OrderShipped($order));

此方法将自动负责将任务推送到队列中,以便在后台发送消息。在使用此功能之前,你需要配置你的队列。

延迟消息队列 ​

如果你希望延迟发送队列中的电子邮件消息,可以使用 later 方法。later 方法的第一个参数接受一个 DateTime 实例,指示消息应何时发送:

php
Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->later(now()->plus(minutes: 10), new OrderShipped($order));

推送到特定队列 ​

由于所有使用 make:mail 命令生成的 mailable 类都使用了 Illuminate\Bus\Queueable trait,你可以在任何 mailable 类实例上调用 onQueue 和 onConnection 方法,从而指定消息的连接和队列名称:

php
$message = (new OrderShipped($order))
    ->onConnection('sqs')
    ->onQueue('emails');

Mail::to($request->user())
    ->cc($moreUsers)
    ->bcc($evenMoreUsers)
    ->queue($message);

或者,你也可以在 mailable 类上使用 Connection 和 Queue 属性来指定连接和队列:

php
use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Queue;

#[Connection('sqs')]
#[Queue('emails')]
class OrderShipped extends Mailable
{
    // ...
}

默认队列 ​

如果你的 mailable 类希望始终加入队列,你可以在类上实现 ShouldQueue 契约。现在,即使你在发送邮件时调用 send 方法,mailable 仍然会被加入队列,因为它实现了该契约:

php
use Illuminate\Contracts\Queue\ShouldQueue;

class OrderShipped extends Mailable implements ShouldQueue
{
    // ...
}

队列 Mailable 和数据库事务 ​

当队列 mailable 在数据库事务中被分发时,它们可能会在数据库事务提交之前就被队列处理。发生这种情况时,你在数据库事务期间对模型或数据库记录所做的任何更新可能尚未反映在数据库中。此外,在事务中创建的任何模型或数据库记录可能不存在于数据库中。如果你的 mailable 依赖于这些模型,当处理发送队列 mailable 的任务时可能会出现意外错误。

如果你的队列连接的 after_commit 配置选项设置为 false,你仍然可以通过在发送邮件消息时调用 afterCommit 方法来指示特定的队列 mailable 应在所有打开的数据库事务提交后分发:

php
Mail::to($request->user())->send(
    (new OrderShipped($order))->afterCommit()
);

或者,你可以从 mailable 的构造函数中调用 afterCommit 方法:

php
<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class OrderShipped extends Mailable implements ShouldQueue
{
    use Queueable, SerializesModels;

    /**
     * 创建一个新的消息实例。
     */
    public function __construct()
    {
        $this->afterCommit();
    }
}

NOTE

要了解有关解决这些问题的更多信息,请查看有关队列任务和数据库事务的文档。

队列邮件失败 ​

当队列邮件失败时,如果队列 mailable 类上定义了 failed 方法,该方法将被调用。导致队列邮件失败的 Throwable 实例将传递给 failed 方法:

php
<?php

namespace App\Mail;

use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Throwable;

class OrderDelayed extends Mailable implements ShouldQueue
{
    use SerializesModels;

    /**
     * 处理队列邮件的失败。
     */
    public function failed(Throwable $exception): void
    {
        // ...
    }
}

渲染 Mailable ​

有时你可能希望捕获 mailable 的 HTML 内容而不发送它。为此,你可以调用 mailable 的 render 方法。此方法将以字符串形式返回 mailable 的已评估 HTML 内容:

php
use App\Mail\InvoicePaid;
use App\Models\Invoice;

$invoice = Invoice::find(1);

return (new InvoicePaid($invoice))->render();

在浏览器中预览 Mailable ​

设计 mailable 的模板时,像典型的 Blade 模板一样在浏览器中快速预览渲染的 mailable 会很方便。因此,Laravel 允许你直接从路由闭包或控制器返回任何 mailable。当返回 mailable 时,它将被渲染并在浏览器中显示,让你可以快速预览其设计,无需将其发送到实际的电子邮件地址:

php
Route::get('/mailable', function () {
    $invoice = App\Models\Invoice::find(1);

    return new App\Mail\InvoicePaid($invoice);
});

本地化 Mailable ​

Laravel 允许你使用请求当前语言环境以外的语言环境发送 mailable,并且即使邮件被加入队列也会记住此语言环境。

为此,Mail facade 提供了一个 locale 方法来设置所需的语言。应用将在评估 mailable 的模板时切换到此语言环境,然后在评估完成后恢复到之前的语言环境:

php
Mail::to($request->user())->locale('es')->send(
    new OrderShipped($order)
);

用户首选语言环境 ​

有时,应用会存储每个用户的首选语言环境。通过在一个或多个模型上实现 HasLocalePreference 契约,你可以指示 Laravel 在发送邮件时使用此存储的语言环境:

php
use Illuminate\Contracts\Translation\HasLocalePreference;

class User extends Model implements HasLocalePreference
{
    /**
     * 获取用户的首选语言环境。
     */
    public function preferredLocale(): string
    {
        return $this->locale;
    }
}

一旦你实现了该接口,Laravel 将在向该模型发送 mailable 和通知时自动使用首选语言环境。因此,使用此接口时无需调用 locale 方法:

php
Mail::to($request->user())->send(new OrderShipped($order));

测试 ​

测试 Mailable 内容 ​

Laravel 提供了多种方法来检查 mailable 的结构。此外,Laravel 还提供了几个便捷方法来测试 mailable 是否包含你期望的内容:

php
use App\Mail\InvoicePaid;
use App\Models\User;

test('mailable content', function () {
    $user = User::factory()->create();

    $mailable = new InvoicePaid($user);

    $mailable->assertFrom('jeffrey@example.com');
    $mailable->assertTo('taylor@example.com');
    $mailable->assertHasCc('abigail@example.com');
    $mailable->assertHasBcc('victoria@example.com');
    $mailable->assertHasReplyTo('tyler@example.com');
    $mailable->assertHasSubject('Invoice Paid');
    $mailable->assertHasTag('example-tag');
    $mailable->assertHasMetadata('key', 'value');

    $mailable->assertSeeInHtml($user->email);
    $mailable->assertDontSeeInHtml('Invoice Not Paid');
    $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);

    $mailable->assertSeeInText($user->email);
    $mailable->assertDontSeeInText('Invoice Not Paid');
    $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);

    $mailable->assertHasAttachment('/path/to/file');
    $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
    $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
    $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
    $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
});
php
use App\Mail\InvoicePaid;
use App\Models\User;

public function test_mailable_content(): void
{
    $user = User::factory()->create();

    $mailable = new InvoicePaid($user);

    $mailable->assertFrom('jeffrey@example.com');
    $mailable->assertTo('taylor@example.com');
    $mailable->assertHasCc('abigail@example.com');
    $mailable->assertHasBcc('victoria@example.com');
    $mailable->assertHasReplyTo('tyler@example.com');
    $mailable->assertHasSubject('Invoice Paid');
    $mailable->assertHasTag('example-tag');
    $mailable->assertHasMetadata('key', 'value');

    $mailable->assertSeeInHtml($user->email);
    $mailable->assertDontSeeInHtml('Invoice Not Paid');
    $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);

    $mailable->assertSeeInText($user->email);
    $mailable->assertDontSeeInText('Invoice Not Paid');
    $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);

    $mailable->assertHasAttachment('/path/to/file');
    $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
    $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
    $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
    $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
}

如你所料,"HTML"断言会断言 mailable 的 HTML 版本包含给定的字符串,而"text"断言则断言 mailable 的纯文本版本包含给定的字符串。

测试 Mailable 发送 ​

我们建议将 mailable 内容的测试与断言给定 mailable 已"发送"给特定用户的测试分开。通常,mailable 的内容与你正在测试的代码无关,只需简单地断言 Laravel 被指示发送了给定的 mailable 就足够了。

你可以使用 Mail facade 的 fake 方法来阻止邮件发送。调用 Mail facade 的 fake 方法后,你可以断言 mailable 被指示发送给了用户,甚至检查 mailable 接收到的数据:

php
<?php

use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

test('orders can be shipped', function () {
    Mail::fake();

    // 执行订单发货...

    // 断言没有发送任何 mailable...
    Mail::assertNothingSent();

    // 断言发送了一个 mailable...
    Mail::assertSent(OrderShipped::class);

    // 断言 mailable 发送了两次...
    Mail::assertSent(OrderShipped::class, 2);

    // 断言 mailable 发送到了某个邮箱地址...
    Mail::assertSent(OrderShipped::class, 'example@laravel.com');

    // 断言 mailable 发送到了多个邮箱地址...
    Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']);

    // 断言没有发送某个 mailable...
    Mail::assertNotSent(AnotherMailable::class);

    // 断言 mailable 发送了两次...
    Mail::assertSentTimes(OrderShipped::class, 2);

    // 断言 mailable 恰好发送了一次...
    Mail::assertSentOnce(OrderShipped::class);

    // 断言总共发送了 3 个 mailable...
    Mail::assertSentCount(3);
});
php
<?php

namespace Tests\Feature;

use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;

class ExampleTest extends TestCase
{
    public function test_orders_can_be_shipped(): void
    {
        Mail::fake();

        // 执行订单发货...

        // 断言没有发送任何 mailable...
        Mail::assertNothingSent();

        // 断言发送了一个 mailable...
        Mail::assertSent(OrderShipped::class);

        // 断言 mailable 发送了两次...
        Mail::assertSent(OrderShipped::class, 2);

        // 断言 mailable 发送到了某个邮箱地址...
        Mail::assertSent(OrderShipped::class, 'example@laravel.com');

        // 断言 mailable 发送到了多个邮箱地址...
        Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']);

        // 断言没有发送某个 mailable...
        Mail::assertNotSent(AnotherMailable::class);

        // 断言 mailable 发送了两次...
        Mail::assertSentTimes(OrderShipped::class, 2);

        // 断言 mailable 恰好发送了一次...
        Mail::assertSentOnce(OrderShipped::class);

        // 断言总共发送了 3 个 mailable...
        Mail::assertSentCount(3);
    }
}

如果你在后台将 mailable 加入队列发送,应使用 assertQueued 方法而不是 assertSent:

php
Mail::assertQueued(OrderShipped::class);
Mail::assertQueuedOnce(OrderShipped::class);
Mail::assertNotQueued(OrderShipped::class);
Mail::assertNothingQueued();
Mail::assertQueuedCount(3);

你还可以使用 assertOutgoingCount 方法断言已发送或已入队的 mailable 总数:

php
Mail::assertOutgoingCount(3);

你可以向 assertSent、assertNotSent、assertQueued 或 assertNotQueued 方法传递一个闭包,以断言发送的 mailable 通过了给定的"真值测试"。如果至少有一个 mailable 通过了给定的真值测试,则断言将成功:

php
Mail::assertSent(function (OrderShipped $mail) use ($order) {
    return $mail->order->id === $order->id;
});

调用 Mail facade 的断言方法时,提供的闭包接受的 mailable 实例暴露了检查 mailable 的有用方法:

php
Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) {
    return $mail->hasTo($user->email) &&
           $mail->hasCc('...') &&
           $mail->hasBcc('...') &&
           $mail->hasReplyTo('...') &&
           $mail->hasFrom('...') &&
           $mail->hasSubject('...') &&
           $mail->hasMetadata('order_id', $mail->order->id) &&
           $mail->usesMailer('ses');
});

mailable 实例还包含几个检查 mailable 附件的有用方法:

php
use Illuminate\Mail\Mailables\Attachment;

Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
    return $mail->hasAttachment(
        Attachment::fromPath('/path/to/file')
            ->as('name.pdf')
            ->withMime('application/pdf')
    );
});

Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
    return $mail->hasAttachment(
        Attachment::fromStorageDisk('s3', '/path/to/file')
    );
});

Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) {
    return $mail->hasAttachment(
        Attachment::fromData(fn () => $pdfData, 'name.pdf')
    );
});

你可能已经注意到有两个方法用于断言邮件未发送:assertNotSent 和 assertNotQueued。有时你可能想断言没有邮件被发送或加入队列。为此,你可以使用 assertNothingOutgoing 和 assertNotOutgoing 方法:

php
Mail::assertNothingOutgoing();

Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
    return $mail->order->id === $order->id;
});

邮件与本地开发 ​

在开发发送电子邮件的应用时,你可能不希望实际将电子邮件发送到真实的电子邮件地址。Laravel 提供了几种在本地开发期间"禁用"实际发送电子邮件的方法。

Log 驱动 ​

log 邮件驱动不会发送你的电子邮件,而是将所有电子邮件消息写入日志文件以供检查。通常,此驱动仅在本地开发期间使用。有关按环境配置应用的更多信息,请查看配置文档。

HELO / Mailtrap / Mailpit ​

或者,你可以使用 HELO 或 Mailtrap 等服务和 smtp 驱动将你的电子邮件消息发送到一个"虚拟"邮箱,你可以在真正的电子邮件客户端中查看它们。这种方法的好处是允许你在 Mailtrap 的消息查看器中实际检查最终的电子邮件。

如果你使用 Laravel Sail,你可以使用 Mailpit 预览你的消息。当 Sail 运行时,你可以在以下地址访问 Mailpit 界面:http://localhost:8025。

使用全局 to 地址 ​

最后,你可以通过调用 Mail facade 提供的 alwaysTo 方法指定一个全局"to"地址。通常,此方法应从应用的某个服务提供者的 boot 方法中调用:

php
use Illuminate\Support\Facades\Mail;

/**
 * 引导任何应用服务。
 */
public function boot(): void
{
    if ($this->app->environment('local')) {
        Mail::alwaysTo('taylor@example.com');
    }
}

使用 alwaysTo 方法时,邮件消息上的任何额外"cc"或"bcc"地址将被移除。

事件 ​

Laravel 在发送邮件消息时分发两个事件。MessageSending 事件在消息发送之前分发,而 MessageSent 事件在消息发送之后分发。请记住,这些事件在邮件被发送时分发,而不是在它被加入队列时。你可以在应用中为这些事件创建事件监听器:

php
use Illuminate\Mail\Events\MessageSending;
// use Illuminate\Mail\Events\MessageSent;

class LogMessage
{
    /**
     * 处理事件。
     */
    public function handle(MessageSending $event): void
    {
        // ...
    }
}

自定义传输 ​

Laravel 包含多种邮件传输;但是,你可能希望编写自己的传输以通过 Laravel 不支持的其他服务来投递电子邮件。要开始使用,请定义一个继承 Symfony\Component\Mailer\Transport\AbstractTransport 类的类。然后,在你的传输上实现 doSend 和 __toString 方法:

php
<?php

namespace App\Mail;

use MailchimpTransactional\ApiClient;
use Symfony\Component\Mailer\SentMessage;
use Symfony\Component\Mailer\Transport\AbstractTransport;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\MessageConverter;

class MailchimpTransport extends AbstractTransport
{
    /**
     * 创建一个新的 Mailchimp 传输实例。
     */
    public function __construct(
        protected ApiClient $client,
    ) {
        parent::__construct();
    }

    /**
     * {@inheritDoc}
     */
    protected function doSend(SentMessage $message): void
    {
        $email = MessageConverter::toEmail($message->getOriginalMessage());

        $this->client->messages->send(['message' => [
            'from_email' => $email->getFrom(),
            'to' => collect($email->getTo())->map(function (Address $email) {
                return ['email' => $email->getAddress(), 'type' => 'to'];
            })->all(),
            'subject' => $email->getSubject(),
            'text' => $email->getTextBody(),
        ]]);
    }

    /**
     * 获取传输的字符串表示。
     */
    public function __toString(): string
    {
        return 'mailchimp';
    }
}

定义好自定义传输后,你可以通过 Mail facade 提供的 extend 方法注册它。通常,这应该在应用的 AppServiceProvider 的 boot 方法中完成。一个 $config 参数将传递给提供给 extend 方法的闭包。此参数将包含在应用的 config/mail.php 配置文件中为邮件发送器定义的配置数组:

php
use App\Mail\MailchimpTransport;
use Illuminate\Support\Facades\Mail;
use MailchimpTransactional\ApiClient;

/**
 * 引导任何应用服务。
 */
public function boot(): void
{
    Mail::extend('mailchimp', function (array $config = []) {
        $client = new ApiClient;

        $client->setApiKey($config['key']);

        return new MailchimpTransport($client);
    });
}

定义并注册自定义传输后,你可以在应用的 config/mail.php 配置文件中创建使用新传输的邮件发送器定义:

php
'mailchimp' => [
    'transport' => 'mailchimp',
    'key' => env('MAILCHIMP_API_KEY'),
    // ...
],

额外的 Symfony 传输 ​

Laravel 包含对一些现有的 Symfony 维护的邮件传输(如 Mailgun 和 Postmark)的支持。但是,你可能希望扩展 Laravel 以支持额外的 Symfony 维护的传输。你可以通过 Composer 引入必要的 Symfony 邮件发送器并将传输注册到 Laravel 来实现。例如,你可以安装并注册"Brevo"(前身为"Sendinblue")Symfony 邮件发送器:

shell
composer require symfony/brevo-mailer symfony/http-client

安装 Brevo 邮件发送器包后,你可以在应用的 services 配置文件中添加 Brevo API 凭证条目:

php
'brevo' => [
    'key' => env('BREVO_API_KEY'),
],

接下来,你可以使用 Mail facade 的 extend 方法将传输注册到 Laravel。通常,这应该在服务提供者的 boot 方法中完成:

php
use Illuminate\Support\Facades\Mail;
use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
use Symfony\Component\Mailer\Transport\Dsn;

/**
 * 引导任何应用服务。
 */
public function boot(): void
{
    Mail::extend('brevo', function () {
        return (new BrevoTransportFactory)->create(
            new Dsn(
                'brevo+api',
                'default',
                config('services.brevo.key')
            )
        );
    });
}

传输注册后,你可以在应用的 config/mail.php 配置文件中创建使用新传输的邮件发送器定义:

php
'brevo' => [
    'transport' => 'brevo',
    // ...
],