跳转至内容

邮件

简介

发送电子邮件不必复杂。Laravel 提供了一个简洁、简单的电子邮件 API,它由流行的 Symfony Mailer 组件驱动。Laravel 和 Symfony Mailer 提供了通过 SMTP、Mailgun、Postmark、Resend、Amazon SES 和 sendmail 发送邮件的驱动程序,使您能够快速开始通过您选择的本地或云服务发送邮件。

配置

Laravel 的电子邮件服务可以通过应用程序的 config/mail.php 配置文件进行配置。在此文件中配置的每个邮件程序都可以拥有其独特的配置,甚至拥有其独特的“传输方式”,从而允许您的应用程序使用不同的电子邮件服务发送特定的电子邮件消息。例如,您的应用程序可以使用 Postmark 发送事务性邮件,同时使用 Amazon SES 发送批量邮件。

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

驱动程序 / 传输方式先决条件

基于 API 的驱动程序(如 Mailgun、Postmark 和 Resend)通常比通过 SMTP 服务器发送邮件更简单、更快捷。在可能的情况下,我们建议您使用这些驱动程序之一。

Mailgun 驱动程序

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

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

接下来,您需要在应用程序的 config/mail.php 配置文件中进行两处更改。首先,将默认邮件程序设置为 mailgun

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

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

1'mailgun' => [
2 'transport' => 'mailgun',
3 // 'client' => [
4 // 'timeout' => 5,
5 // ],
6],

配置好应用程序的默认邮件程序后,将以下选项添加到您的 config/services.php 配置文件中

1'mailgun' => [
2 'domain' => env('MAILGUN_DOMAIN'),
3 'secret' => env('MAILGUN_SECRET'),
4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
5 'scheme' => 'https',
6],

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

1'mailgun' => [
2 'domain' => env('MAILGUN_DOMAIN'),
3 'secret' => env('MAILGUN_SECRET'),
4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
5 'scheme' => 'https',
6],

Postmark 驱动程序

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

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

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

1'postmark' => [
2 'key' => env('POSTMARK_API_KEY'),
3],

如果您想指定特定邮件程序应使用的 Postmark 消息流,可以将 message_stream_id 配置选项添加到邮件程序的配置数组中。此配置数组位于应用程序的 config/mail.php 配置文件中

1'postmark' => [
2 'transport' => 'postmark',
3 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
4 // 'client' => [
5 // 'timeout' => 5,
6 // ],
7],

通过这种方式,您还可以设置多个使用不同消息流的 Postmark 邮件程序。

Resend 驱动程序

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

1composer require resend/resend-php

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

1'resend' => [
2 'key' => env('RESEND_API_KEY'),
3],

SES 驱动程序

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

1composer require aws/aws-sdk-php

接下来,将 config/mail.php 配置文件中的 default 选项设置为 ses,并确认您的 config/services.php 配置文件包含以下选项

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

要通过会话令牌利用 AWS 临时凭证,您可以向应用程序的 SES 配置中添加 token

1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'token' => env('AWS_SESSION_TOKEN'),
6],

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

1/**
2 * Get the message headers.
3 */
4public function headers(): Headers
5{
6 return new Headers(
7 text: [
8 'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic',
9 ],
10 );
11}

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

1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'options' => [
6 'ConfigurationSetName' => 'MyConfigurationSet',
7 'EmailTags' => [
8 ['Name' => 'foo', 'Value' => 'bar'],
9 ],
10 ],
11],

故障转移 (Failover) 配置

有时,您为发送应用程序邮件而配置的外部服务可能会宕机。在这种情况下,定义一个或多个备用邮件发送配置会很有用,以备主要发送驱动程序出现故障时使用。

为此,您应该在应用程序的 mail 配置文件中定义一个使用 failover 传输器的邮件程序。应用程序 failover 邮件程序的配置数组应包含一个 mailers 数组,该数组引用了选择发送邮件时应遵循的顺序

1'mailers' => [
2 'failover' => [
3 'transport' => 'failover',
4 'mailers' => [
5 'postmark',
6 'mailgun',
7 'sendmail',
8 ],
9 'retry_after' => 60,
10 ],
11 
12 // ...
13],

配置好使用 failover 传输器的邮件程序后,您需要在应用程序的 .env 文件中将故障转移邮件程序设置为默认邮件程序,以便使用该功能

1MAIL_MAILER=failover

轮询 (Round Robin) 配置

roundrobin 传输器允许您在多个邮件程序之间分配邮件负载。首先,在应用程序的 mail 配置文件中定义一个使用 roundrobin 传输器的邮件程序。roundrobin 邮件程序的配置数组应包含一个 mailers 数组,引用了应参与发送的邮件程序

1'mailers' => [
2 'roundrobin' => [
3 'transport' => 'roundrobin',
4 'mailers' => [
5 'ses',
6 'postmark',
7 ],
8 'retry_after' => 60,
9 ],
10 
11 // ...
12],

定义好轮询邮件程序后,您应通过将其名称设置为应用程序 mail 配置文件中 default 配置键的值,将其设置为应用程序使用的默认邮件程序

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

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

生成邮件类 (Mailables)

在构建 Laravel 应用程序时,应用程序发送的每种类型的电子邮件都表示为一个“邮件类”(Mailable)。这些类存储在 app/Mail 目录中。如果您在应用程序中没有看到此目录,请不必担心,因为当您第一次使用 make:mail Artisan 命令创建邮件类时,它会自动为您生成

1php artisan make:mail OrderShipped

编写邮件类

生成邮件类后,打开它以探索其内容。邮件类的配置通过多个方法完成,包括 envelopecontentattachments 方法。

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

配置发件人

使用信封 (Envelope)

首先,让我们探索如何配置电子邮件的发件人。或者换句话说,邮件是谁发送的。有两种方法可以配置发件人。首先,您可以在消息的信封中指定“from”地址

1use Illuminate\Mail\Mailables\Address;
2use Illuminate\Mail\Mailables\Envelope;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 from: new Address('[email protected]', 'Jeffrey Way'),
11 subject: 'Order Shipped',
12 );
13}

如果需要,您也可以指定一个 replyTo 地址

1return new Envelope(
2 from: new Address('[email protected]', 'Jeffrey Way'),
3 replyTo: [
4 new Address('[email protected]', 'Taylor Otwell'),
5 ],
6 subject: 'Order Shipped',
7);

使用全局 from 地址

然而,如果您的应用程序的所有电子邮件都使用相同的“from”地址,那么将它添加到您生成的每个邮件类中可能会很麻烦。相反,您可以在 config/mail.php 配置文件中指定一个全局“from”地址。如果邮件类中未指定其他“from”地址,则将使用此地址

1'from' => [
2 'address' => env('MAIL_FROM_ADDRESS', '[email protected]'),
3 'name' => env('MAIL_FROM_NAME', 'Example'),
4],

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

1'reply_to' => [
2 'address' => '[email protected]',
3 'name' => 'App Name',
4],

配置视图

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

1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'mail.orders.shipped',
8 );
9}

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

纯文本电子邮件

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

1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'mail.orders.shipped',
8 text: 'mail.orders.shipped-text'
9 );
10}

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

1return new Content(
2 html: 'mail.orders.shipped',
3 text: 'mail.orders.shipped-text'
4);

视图数据

通过公共属性

通常,您需要向视图传递一些数据,以便在渲染电子邮件 HTML 时使用。有两种方法可以让视图获取数据。首先,邮件类中定义的任何公共属性都将自动提供给视图。因此,例如,您可以将数据传入邮件类的构造函数,并将其设置为类中定义的公共属性

1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 public Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'mail.orders.shipped',
29 );
30 }
31}

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

1<div>
2 Price: {{ $order->price }}
3</div>

通过 with 参数

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

1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 protected Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'mail.orders.shipped',
29 with: [
30 'orderName' => $this->order->name,
31 'orderPrice' => $this->order->price,
32 ],
33 );
34 }
35}

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

1<div>
2 Price: {{ $orderPrice }}
3</div>

附件

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

1use Illuminate\Mail\Mailables\Attachment;
2 
3/**
4 * Get the attachments for the message.
5 *
6 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
7 */
8public function attachments(): array
9{
10 return [
11 Attachment::fromPath('/path/to/file'),
12 ];
13}

在向消息添加附件时,您还可以使用 aswithMime 方法指定附件的显示名称和/或 MIME 类型

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromPath('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

从磁盘添加文件附件

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

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file'),
10 ];
11}

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

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

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

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorageDisk('s3', '/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

原始数据附件

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

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
10 ->withMime('application/pdf'),
11 ];
12}

内联附件

在电子邮件中嵌入内联图像通常很麻烦;不过,Laravel 提供了一种方便的方法将图像附加到电子邮件中。要嵌入内联图像,请在电子邮件模板中使用 $message 变量上的 embed 方法。Laravel 会自动使 $message 变量在所有电子邮件模板中可用,因此您无需担心手动传递它

1<body>
2 Here is an image:
3 
4 <img src="{{ $message->embed($pathToImage) }}">
5</body>

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

嵌入原始数据附件

如果您已经拥有希望嵌入到电子邮件模板中的原始图像数据字符串,则可以调用 $message 变量上的 embedData 方法。调用 embedData 方法时,您需要提供应分配给嵌入图像的文件名

1<body>
2 Here is an image from raw data:
3 
4 <img src="{{ $message->embedData($data, 'example-image.jpg') }}">
5</body>

可附件对象 (Attachable Objects)

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

首先,在可附加到消息的对象上实现 Illuminate\Contracts\Mail\Attachable 接口。此接口规定您的类必须定义一个 toMailAttachment 方法,该方法返回一个 Illuminate\Mail\Attachment 实例

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Mail\Attachable;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Mail\Attachment;
8 
9class Photo extends Model implements Attachable
10{
11 /**
12 * Get the attachable representation of the model.
13 */
14 public function toMailAttachment(): Attachment
15 {
16 return Attachment::fromPath('/path/to/file');
17 }
18}

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

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [$this->photo];
9}

当然,附件数据可以存储在 Amazon S3 等远程文件存储服务上。因此,Laravel 也允许您从存储在应用程序 文件系统磁盘 之一上的数据生成附件实例

1// Create an attachment from a file on your default disk...
2return Attachment::fromStorage($this->path);
3 
4// Create an attachment from a file on a specific disk...
5return Attachment::fromStorageDisk('backblaze', $this->path);

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

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

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

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

邮件头 (Headers)

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

为此,请在您的邮件类中定义一个 headers 方法。headers 方法应返回一个 Illuminate\Mail\Mailables\Headers 实例。此类接受 messageIdreferencestext 参数。当然,您只需提供特定消息所需的参数即可

1use Illuminate\Mail\Mailables\Headers;
2 
3/**
4 * Get the message headers.
5 */
6public function headers(): Headers
7{
8 return new Headers(
9 messageId: '[email protected]',
10 references: ['[email protected]'],
11 text: [
12 'X-Custom-Header' => 'Custom Value',
13 ],
14 );
15}

标签与元数据

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

1use Illuminate\Mail\Mailables\Envelope;
2 
3/**
4 * Get the message envelope.
5 *
6 * @return \Illuminate\Mail\Mailables\Envelope
7 */
8public function envelope(): Envelope
9{
10 return new Envelope(
11 subject: 'Order Shipped',
12 tags: ['shipment'],
13 metadata: [
14 'order_id' => $this->order->id,
15 ],
16 );
17}

如果您的应用程序正在使用 Mailgun 驱动程序,您可以查阅 Mailgun 文档以获取有关 标签元数据 的更多信息。同样,也可以查阅 Postmark 文档以获取有关他们对 标签元数据 支持的更多信息。

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

自定义 Symfony 邮件消息

Laravel 的邮件功能由 Symfony Mailer 提供支持。Laravel 允许您注册自定义回调,这些回调将在发送消息之前与 Symfony Message 实例一起调用。这使您有机会在消息发送之前对其进行深入自定义。为此,请在 Envelope 定义中定义一个 using 参数

1use Illuminate\Mail\Mailables\Envelope;
2use Symfony\Component\Mime\Email;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 subject: 'Order Shipped',
11 using: [
12 function (Email $message) {
13 // ...
14 },
15 ]
16 );
17}

Markdown 邮件类

Markdown 邮件消息允许您在邮件类中利用 邮件通知 的预构建模板和组件。由于消息是用 Markdown 编写的,Laravel 能够为这些消息呈现美观、响应式的 HTML 模板,同时自动生成纯文本对应内容。

生成 Markdown 邮件类

要生成带有相应 Markdown 模板的邮件类,可以使用 make:mail Artisan 命令的 --markdown 选项

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

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

1use Illuminate\Mail\Mailables\Content;
2 
3/**
4 * Get the message content definition.
5 */
6public function content(): Content
7{
8 return new Content(
9 markdown: 'mail.orders.shipped',
10 with: [
11 'url' => $this->orderUrl,
12 ],
13 );
14}

编写 Markdown 消息

Markdown 邮件类结合了 Blade 组件和 Markdown 语法,使您可以轻松构建电子邮件消息,同时利用 Laravel 的预构建电子邮件 UI 组件

1<x-mail::message>
2# Order Shipped
3 
4Your order has been shipped!
5 
6<x-mail::button :url="$url">
7View Order
8</x-mail::button>
9 
10Thanks,<br>
11{{ config('app.name') }}
12</x-mail::message>

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

按钮组件

按钮组件呈现一个居中的按钮链接。该组件接受两个参数,一个 url 和一个可选的 color。支持的颜色有 primarysuccesserror。您可以在消息中添加任意数量的按钮组件

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

面板组件

面板组件在面板中呈现给定的文本块,其背景颜色与邮件的其他部分略有不同。这允许您引起对特定文本块的注意

1<x-mail::panel>
2This is the panel content.
3</x-mail::panel>

表格组件

表格组件允许您将 Markdown 表格转换为 HTML 表格。该组件接受 Markdown 表格作为其内容。表格列对齐方式支持使用标准的 Markdown 表格对齐语法

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

自定义组件

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

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

此命令将 Markdown 邮件组件发布到 resources/views/vendor/mail 目录。mail 目录将包含 htmltext 目录,每个目录都包含每个可用组件的相应表示形式。您可以随意自定义这些组件。

自定义 CSS

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

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

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

发送邮件

要发送消息,请使用 Mail 外观 (facade) 上的 to 方法。to 方法接受电子邮件地址、用户实例或用户集合。如果您传递对象或对象集合,邮件程序将自动使用其 emailname 属性来确定电子邮件的收件人,因此请确保这些属性在您的对象上可用。指定收件人后,您可以将邮件类的实例传递给 send 方法

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Mail\OrderShipped;
6use App\Models\Order;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9use Illuminate\Support\Facades\Mail;
10 
11class OrderShipmentController extends Controller
12{
13 /**
14 * Ship the given order.
15 */
16 public function store(Request $request): RedirectResponse
17 {
18 $order = Order::findOrFail($request->order_id);
19 
20 // Ship the order...
21 
22 Mail::to($request->user())->send(new OrderShipped($order));
23 
24 return redirect('/orders');
25 }
26}

发送消息时,您不限于仅指定“to”收件人。您可以通过链式调用各自的方法来自由设置“to”、“cc”和“bcc”收件人

1Mail::to($request->user())
2 ->cc($moreUsers)
3 ->bcc($evenMoreUsers)
4 ->send(new OrderShipped($order));

循环处理收件人

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

1foreach (['[email protected]', '[email protected]'] as $recipient) {
2 Mail::to($recipient)->send(new OrderShipped($order));
3}

通过特定邮件程序发送邮件

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

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

邮件队列

将邮件消息加入队列

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

1Mail::to($request->user())
2 ->cc($moreUsers)
3 ->bcc($evenMoreUsers)
4 ->queue(new OrderShipped($order));

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

延迟消息队列

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

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

推送到特定队列

由于使用 make:mail 命令生成的所有邮件类都使用了 Illuminate\Bus\Queueable 特征 (trait),因此您可以在任何邮件类实例上调用 onQueueonConnection 方法,从而指定消息的连接和队列名称

1$message = (new OrderShipped($order))
2 ->onConnection('sqs')
3 ->onQueue('emails');
4 
5Mail::to($request->user())
6 ->cc($moreUsers)
7 ->bcc($evenMoreUsers)
8 ->queue($message);

默认加入队列

如果您有希望始终加入队列的邮件类,可以实现该类上的 ShouldQueue 合约。现在,即使您在发送邮件时调用了 send 方法,由于它实现了该合约,邮件仍会被加入队列

1use Illuminate\Contracts\Queue\ShouldQueue;
2 
3class OrderShipped extends Mailable implements ShouldQueue
4{
5 // ...
6}

排队的邮件类与数据库事务

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

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

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

或者,您可以从邮件类的构造函数中调用 afterCommit 方法

1<?php
2 
3namespace App\Mail;
4 
5use Illuminate\Bus\Queueable;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Mail\Mailable;
8use Illuminate\Queue\SerializesModels;
9 
10class OrderShipped extends Mailable implements ShouldQueue
11{
12 use Queueable, SerializesModels;
13 
14 /**
15 * Create a new message instance.
16 */
17 public function __construct()
18 {
19 $this->afterCommit();
20 }
21}

要了解更多关于解决这些问题的信息,请查看关于排队作业与数据库事务的文档。

排队邮件失败

当排队的电子邮件失败时,如果已定义,则将调用排队邮件类上的 failed 方法。导致排队电子邮件失败的 Throwable 实例将被传递给 failed 方法

1<?php
2 
3namespace App\Mail;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Mail\Mailable;
7use Illuminate\Queue\SerializesModels;
8use Throwable;
9 
10class OrderDelayed extends Mailable implements ShouldQueue
11{
12 use SerializesModels;
13 
14 /**
15 * Handle a queued email's failure.
16 */
17 public function failed(Throwable $exception): void
18 {
19 // ...
20 }
21}

渲染邮件类

有时您可能希望在不发送的情况下捕获邮件的 HTML 内容。为此,您可以调用邮件类的 render 方法。此方法将以字符串形式返回邮件的已求值 HTML 内容

1use App\Mail\InvoicePaid;
2use App\Models\Invoice;
3 
4$invoice = Invoice::find(1);
5 
6return (new InvoicePaid($invoice))->render();

在浏览器中预览邮件类

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

1Route::get('/mailable', function () {
2 $invoice = App\Models\Invoice::find(1);
3 
4 return new App\Mail\InvoicePaid($invoice);
5});

邮件本地化

Laravel 允许您以非请求当前区域设置的其他区域设置发送邮件,即使邮件被排队,它也会记住此区域设置。

为此,Mail 外观提供了一个 locale 方法来设置所需的语言。当评估邮件的模板时,应用程序将切换到此区域设置,并在评估完成后恢复到之前的区域设置

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

用户偏好区域设置

有时,应用程序会存储每个用户的首选区域设置。通过在一个或多个模型上实现 HasLocalePreference 合约,您可以指示 Laravel 在发送邮件时使用此存储的区域设置

1use Illuminate\Contracts\Translation\HasLocalePreference;
2 
3class User extends Model implements HasLocalePreference
4{
5 /**
6 * Get the user's preferred locale.
7 */
8 public function preferredLocale(): string
9 {
10 return $this->locale;
11 }
12}

实现接口后,Laravel 在向模型发送邮件和通知时会自动使用首选区域设置。因此,使用此接口时无需调用 locale 方法

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

测试

测试邮件内容

Laravel 提供了多种方法来检查邮件的结构。此外,Laravel 还提供了几种方便的方法来测试您的邮件是否包含您预期的内容

1use App\Mail\InvoicePaid;
2use App\Models\User;
3 
4test('mailable content', function () {
5 $user = User::factory()->create();
6 
7 $mailable = new InvoicePaid($user);
8 
9 $mailable->assertFrom('[email protected]');
10 $mailable->assertTo('[email protected]');
11 $mailable->assertHasCc('[email protected]');
12 $mailable->assertHasBcc('[email protected]');
13 $mailable->assertHasReplyTo('[email protected]');
14 $mailable->assertHasSubject('Invoice Paid');
15 $mailable->assertHasTag('example-tag');
16 $mailable->assertHasMetadata('key', 'value');
17 
18 $mailable->assertSeeInHtml($user->email);
19 $mailable->assertDontSeeInHtml('Invoice Not Paid');
20 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
21 
22 $mailable->assertSeeInText($user->email);
23 $mailable->assertDontSeeInText('Invoice Not Paid');
24 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);
25 
26 $mailable->assertHasAttachment('/path/to/file');
27 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
28 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
29 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
30 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
31});
1use App\Mail\InvoicePaid;
2use App\Models\User;
3 
4public function test_mailable_content(): void
5{
6 $user = User::factory()->create();
7 
8 $mailable = new InvoicePaid($user);
9 
10 $mailable->assertFrom('[email protected]');
11 $mailable->assertTo('[email protected]');
12 $mailable->assertHasCc('[email protected]');
13 $mailable->assertHasBcc('[email protected]');
14 $mailable->assertHasReplyTo('[email protected]');
15 $mailable->assertHasSubject('Invoice Paid');
16 $mailable->assertHasTag('example-tag');
17 $mailable->assertHasMetadata('key', 'value');
18 
19 $mailable->assertSeeInHtml($user->email);
20 $mailable->assertDontSeeInHtml('Invoice Not Paid');
21 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
22 
23 $mailable->assertSeeInText($user->email);
24 $mailable->assertDontSeeInText('Invoice Not Paid');
25 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);
26 
27 $mailable->assertHasAttachment('/path/to/file');
28 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
29 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
30 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
31 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
32}

正如您所料,“HTML”断言断言邮件的 HTML 版本包含给定字符串,而“text”断言断言邮件的纯文本版本包含给定字符串。

测试邮件发送

我们建议将邮件内容的测试与断言特定邮件已“发送”给特定用户的测试分开。通常,邮件内容与您正在测试的代码无关,只需断言 Laravel 已被指示发送给定的邮件就足够了。

您可以使用 Mail 外观的 fake 方法来阻止发送邮件。调用 Mail 外观的 fake 方法后,您可以断言邮件已被指示发送给用户,甚至可以检查邮件收到的数据

1<?php
2 
3use App\Mail\OrderShipped;
4use Illuminate\Support\Facades\Mail;
5 
6test('orders can be shipped', function () {
7 Mail::fake();
8 
9 // Perform order shipping...
10 
11 // Assert that no mailables were sent...
12 Mail::assertNothingSent();
13 
14 // Assert that a mailable was sent...
15 Mail::assertSent(OrderShipped::class);
16 
17 // Assert a mailable was sent twice...
18 Mail::assertSent(OrderShipped::class, 2);
19 
20 // Assert a mailable was sent to an email address...
21 Mail::assertSent(OrderShipped::class, '[email protected]');
22 
23 // Assert a mailable was sent to multiple email addresses...
24 Mail::assertSent(OrderShipped::class, ['[email protected]', '...']);
25 
26 // Assert a mailable was not sent...
27 Mail::assertNotSent(AnotherMailable::class);
28 
29 // Assert a mailable was sent twice...
30 Mail::assertSentTimes(OrderShipped::class, 2);
31 
32 // Assert 3 total mailables were sent...
33 Mail::assertSentCount(3);
34});
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Mail\OrderShipped;
6use Illuminate\Support\Facades\Mail;
7use Tests\TestCase;
8 
9class ExampleTest extends TestCase
10{
11 public function test_orders_can_be_shipped(): void
12 {
13 Mail::fake();
14 
15 // Perform order shipping...
16 
17 // Assert that no mailables were sent...
18 Mail::assertNothingSent();
19 
20 // Assert that a mailable was sent...
21 Mail::assertSent(OrderShipped::class);
22 
23 // Assert a mailable was sent twice...
24 Mail::assertSent(OrderShipped::class, 2);
25 
26 // Assert a mailable was sent to an email address...
27 Mail::assertSent(OrderShipped::class, '[email protected]');
28 
29 // Assert a mailable was sent to multiple email addresses...
30 Mail::assertSent(OrderShipped::class, ['[email protected]', '...']);
31 
32 // Assert a mailable was not sent...
33 Mail::assertNotSent(AnotherMailable::class);
34 
35 // Assert a mailable was sent twice...
36 Mail::assertSentTimes(OrderShipped::class, 2);
37 
38 // Assert 3 total mailables were sent...
39 Mail::assertSentCount(3);
40 }
41}

如果您正在将邮件排队以在后台发送,则应使用 assertQueued 方法而不是 assertSent

1Mail::assertQueued(OrderShipped::class);
2Mail::assertNotQueued(OrderShipped::class);
3Mail::assertNothingQueued();
4Mail::assertQueuedCount(3);

您还可以使用 assertOutgoingCount 方法断言已发送或排队的邮件总数

1Mail::assertOutgoingCount(3);

您可以向 assertSentassertNotSentassertQueuedassertNotQueued 方法传递一个闭包,以断言发送的邮件通过了给定的“真值测试”。如果至少有一封发送的邮件通过了给定的真值测试,则断言将成功

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

调用 Mail 外观的断言方法时,所提供闭包接收到的邮件实例公开了有助于检查邮件的有用方法

1Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) {
2 return $mail->hasTo($user->email) &&
3 $mail->hasCc('...') &&
4 $mail->hasBcc('...') &&
5 $mail->hasReplyTo('...') &&
6 $mail->hasFrom('...') &&
7 $mail->hasSubject('...') &&
8 $mail->hasMetadata('order_id', $mail->order->id);
9 $mail->usesMailer('ses');
10});

邮件实例还包括几个有助于检查邮件附件的有用方法

1use Illuminate\Mail\Mailables\Attachment;
2 
3Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
4 return $mail->hasAttachment(
5 Attachment::fromPath('/path/to/file')
6 ->as('name.pdf')
7 ->withMime('application/pdf')
8 );
9});
10 
11Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
12 return $mail->hasAttachment(
13 Attachment::fromStorageDisk('s3', '/path/to/file')
14 );
15});
16 
17Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) {
18 return $mail->hasAttachment(
19 Attachment::fromData(fn () => $pdfData, 'name.pdf')
20 );
21});

您可能已经注意到有两个用于断言邮件未发送的方法:assertNotSentassertNotQueued。有时您可能希望断言没有邮件被发送 排队。为此,您可以使用 assertNothingOutgoingassertNotOutgoing 方法

1Mail::assertNothingOutgoing();
2 
3Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
4 return $mail->order->id === $order->id;
5});

邮件与本地开发

在开发发送电子邮件的应用程序时,您可能不想真正向真实的电子邮件地址发送邮件。Laravel 提供了几种在本地开发过程中“禁用”实际发送电子邮件的方法。

日志驱动程序

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

HELO / Mailtrap / Mailpit

或者,您可以使用 HELOMailtrap 等服务以及 smtp 驱动程序将您的电子邮件消息发送到“虚拟”邮箱,在那里您可以在真正的电子邮件客户端中查看它们。这种方法的好处是允许您在 Mailtrap 的消息查看器中实际检查最终的电子邮件。

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

使用全局 to 地址

最后,您可以通过调用 Mail 外观提供的 alwaysTo 方法来指定全局“to”地址。通常,此方法应从应用程序服务提供商之一的 boot 方法中调用

1use Illuminate\Support\Facades\Mail;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 if ($this->app->environment('local')) {
9 Mail::alwaysTo('[email protected]');
10 }
11}

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

活动

Laravel 在发送邮件消息时会分发两个事件。MessageSending 事件在消息发送之前分发,而 MessageSent 事件在消息发送之后分发。请记住,这些事件是在邮件 正在发送 时分发的,而不是在邮件被排队时分发的。您可以在应用程序中为这些事件创建 事件监听器

1use Illuminate\Mail\Events\MessageSending;
2// use Illuminate\Mail\Events\MessageSent;
3 
4class LogMessage
5{
6 /**
7 * Handle the event.
8 */
9 public function handle(MessageSending $event): void
10 {
11 // ...
12 }
13}

自定义传输方式

Laravel 包含了多种邮件传输器;但是,您可能希望编写自己的传输器,通过 Laravel 开箱即不支持的其他服务发送电子邮件。首先,定义一个继承 Symfony\Component\Mailer\Transport\AbstractTransport 类的类。然后,在您的传输器上实现 doSend__toString 方法

1<?php
2 
3namespace App\Mail;
4 
5use MailchimpTransactional\ApiClient;
6use Symfony\Component\Mailer\SentMessage;
7use Symfony\Component\Mailer\Transport\AbstractTransport;
8use Symfony\Component\Mime\Address;
9use Symfony\Component\Mime\MessageConverter;
10 
11class MailchimpTransport extends AbstractTransport
12{
13 /**
14 * Create a new Mailchimp transport instance.
15 */
16 public function __construct(
17 protected ApiClient $client,
18 ) {
19 parent::__construct();
20 }
21 
22 /**
23 * {@inheritDoc}
24 */
25 protected function doSend(SentMessage $message): void
26 {
27 $email = MessageConverter::toEmail($message->getOriginalMessage());
28 
29 $this->client->messages->send(['message' => [
30 'from_email' => $email->getFrom(),
31 'to' => collect($email->getTo())->map(function (Address $email) {
32 return ['email' => $email->getAddress(), 'type' => 'to'];
33 })->all(),
34 'subject' => $email->getSubject(),
35 'text' => $email->getTextBody(),
36 ]]);
37 }
38 
39 /**
40 * Get the string representation of the transport.
41 */
42 public function __toString(): string
43 {
44 return 'mailchimp';
45 }
46}

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

1use App\Mail\MailchimpTransport;
2use Illuminate\Support\Facades\Mail;
3use MailchimpTransactional\ApiClient;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Mail::extend('mailchimp', function (array $config = []) {
11 $client = new ApiClient;
12 
13 $client->setApiKey($config['key']);
14 
15 return new MailchimpTransport($client);
16 });
17}

定义并注册好自定义传输器后,您可以在应用程序的 config/mail.php 配置文件中创建一个使用新传输器的邮件程序定义

1'mailchimp' => [
2 'transport' => 'mailchimp',
3 'key' => env('MAILCHIMP_API_KEY'),
4 // ...
5],

其他 Symfony 传输方式

Laravel 包含对一些现有的由 Symfony 维护的邮件传输器(如 Mailgun 和 Postmark)的支持。但是,您可能希望扩展 Laravel 以支持其他由 Symfony 维护的传输器。您可以通过 Composer 要求必要的 Symfony 邮件程序并向 Laravel 注册该传输器来做到这一点。例如,您可以安装并注册“Brevo”(前身为“Sendinblue”)Symfony 邮件程序

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

安装 Brevo 邮件程序包后,您可以将 Brevo API 凭证的条目添加到应用程序的 services 配置文件中

1'brevo' => [
2 'key' => env('BREVO_API_KEY'),
3],

接下来,您可以使用 Mail 外观的 extend 方法向 Laravel 注册该传输器。通常,这应该在服务提供商的 boot 方法中完成

1use Illuminate\Support\Facades\Mail;
2use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
3use Symfony\Component\Mailer\Transport\Dsn;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Mail::extend('brevo', function () {
11 return (new BrevoTransportFactory)->create(
12 new Dsn(
13 'brevo+api',
14 'default',
15 config('services.brevo.key')
16 )
17 );
18 });
19}

注册好传输器后,您可以在应用程序的 config/mail.php 配置文件中创建一个使用新传输器的邮件程序定义

1'brevo' => [
2 'transport' => 'brevo',
3 // ...
4],