mail

介绍

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

配置

Laravel 的邮件服务可以通过应用程序的 config/mail.php 配置文件进行配置。该文件中配置的每个邮件发送器(mailer)可以有自己独特的配置,甚至可以使用独立的 “传输” 方式(transport),这使得您的应用能够使用不同的邮件服务来发送特定的邮件。例如,您的应用可能使用 Postmark 发送事务性邮件,同时使用 Amazon SES 发送批量邮件。

mail 配置文件中,您会找到一个 mailers 配置数组。这个数组包含了每个主要邮件驱动程序/传输(driver/transport)的示例配置项,而 default 配置值则决定了在应用需要发送邮件时,默认使用哪个邮件发送器。

驱动程序要求

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

Mailgun 驱动程序

要使用 Mailgun 驱动程序,请通过 Composer 安装 Symfony 的 Mailgun 邮件传输:

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

接下来,将应用程序 config/mail.php 配置文件中的 default 选项设置为 mailgun,并向 mailers 配置数组中添加以下配置:

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

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

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

如果您不使用美国的 【Mailgun 区域】,您可以在服务配置文件中定义区域的端点:

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

Postmark 驱动程序

要使用 Postmark 驱动程序,请通过 Composer 安装 Symfony 的 Postmark 邮件传输:

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

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

'postmark' => [
    'token' => env('POSTMARK_TOKEN'),
],

如果您希望为特定邮件发送器指定要使用的 Postmark 消息流,您可以将 message_stream_id 配置选项添加到邮件发送器的配置数组中。这个配置数组可以在应用程序的 config/mail.php 配置文件中找到:

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

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

Resend 驱动程序

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

composer require resend/resend-php

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

'resend' => [
    'key' => env('RESEND_KEY'),
],

SES 驱动程序

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

composer require aws/aws-sdk-php

接下来,将应用程序 config/mail.php 配置文件中的默认选项设置为 ses,并确保在 config/services.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 键:

'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 头:

/**
 * Get the message headers.
 */
public function headers(): Headers
{
    return new Headers(
        text: [
            'X-Ses-List-Management-Options' => 'contactListName=MyContactList;topicName=MyTopic',
        ],
    );
}

如果您希望定义【更多选项】,在发送电子邮件时 Laravel 应该将这些选项传递给 AWS SDK 的 SendEmail 方法,您可以在 SES 配置中定义一个 options 数组:

'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'],
        ],
    ],
],

MailerSend 驱动程序

MailerSend 是一个事务性邮件和 SMS 服务,提供了适用于 Laravel 的 API 邮件驱动程序。您可以通过 Composer 包管理器安装该驱动程序:

composer require mailersend/laravel-driver

安装该包后,在应用程序的 .env 文件中添加 MAILERSEND_API_KEY 环境变量。此外,MAIL_MAILER 环境变量应设置为 mailersend

MAIL_MAILER=mailersend
MAIL_FROM_ADDRESS=app@yourdomain.com
MAIL_FROM_NAME="App Name"

MAILERSEND_API_KEY=your-api-key

最后,将 MailerSend 添加到应用程序 config/mail.php 配置文件中的邮件发送器数组:

'mailersend' => [
    'transport' => 'mailersend',
],

要了解更多关于 MailerSend 的信息,包括如何使用托管模板,请查阅 【MailerSend 驱动程序的文档】。

故障转移配置

有时,您配置的外部邮件服务可能会出现故障。在这种情况下,定义一个或多个备用邮件传输配置非常有用,以便在您的主传输驱动程序出现故障时使用。

为此,您需要在应用程序的 mail 配置文件中定义一个使用 failover 传输的邮件发送器。应用程序的 failover 邮件发送器的配置数组应包含一个 mailers 数组,指明在传输失败时应该按顺序选择哪些配置的邮件发送器:

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

    // ...
],

一旦定义了故障转移邮件发送器,您应该通过在应用程序的 mail 配置文件中将其名称作为 default 配置键的值,将该邮件发送器设置为应用程序使用的默认邮件发送器:

'default' => env('MAIL_MAILER', 'failover'),

轮询配置

roundrobin 传输允许您将邮件工作负载分配到多个邮件发送器上。要开始使用,您需要在应用程序的邮件配置文件中定义一个使用 roundrobin 传输的邮件发送器。应用程序的 roundrobin 邮件发送器的配置数组应包含一个邮件发送器数组,指明应使用哪些已配置的邮件发送器来进行邮件发送:

'mailers' => [
    'roundrobin' => [
        'transport' => 'roundrobin',
        'mailers' => [
            'ses',
            'postmark',
        ],
    ],

    // ...
],

一旦定义了 roundrobin 邮件发送器,您应该通过在应用程序的邮件配置文件中将其名称作为默认配置键的值,将该邮件发送器设置为应用程序使用的默认邮件发送器:

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

roundrobin 传输会从已配置的邮件发送器列表中随机选择一个邮件发送器,然后对于每封后续邮件,它会切换到下一个可用的邮件发送器。与故障转移传输(https://en.wikipedia.org/wiki/High_availability[用于实现高可用性])不同,roundrobin 传输提供 负载均衡 功能。

生成邮件

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

php artisan make:mail OrderShipped

编写邮件

一旦您生成了一个 mailable 类,打开它以便我们一起探索其内容。Mailable 类的配置通过多个方法完成,包括 envelopecontentattachments 方法。

  • envelope 方法返回一个 Illuminate\Mail\Mailables\Envelope 对象,用于定义邮件的主题以及有时定义邮件的收件人。

  • content 方法返回一个 Illuminate\Mail\Mailables\Content 对象,用于定义将用于生成邮件内容的 【Blade 模板】。

配置发件人

使用 Envelope

首先,让我们探讨如何配置邮件的发件人,换句话说,邮件的 “发件人” 是谁。有两种方法可以配置发件人。首先,您可以在邮件的 envelope 中指定 “from” 地址:

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

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

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

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

使用全局的发件人地址

然而,如果您的应用程序对所有邮件使用相同的 “from” 地址,那么每次生成 mailable 类时都手动添加这个地址会变得很繁琐。您可以在 config/mail.php 配置文件中指定一个全局的 “from” 地址。如果在 mailable 类中没有指定其它 “from” 地址,Laravel 会使用这个地址:

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

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

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

配置视图

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

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

您可能希望在 resources/views 目录下创建一个 emails 子目录,用于存放所有的邮件模板;但是,您可以根据需要将它们放置在 resources/views 目录中的任何位置。

纯文本邮件

如果您希望定义邮件的纯文本版本,可以在创建邮件内容定义时指定纯文本模板。与 view 参数类似,text 参数应是一个模板名称,用于渲染邮件的内容。您可以同时定义邮件的 HTML 版本和纯文本版本:

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

为了更加清晰,html 参数也可以作为 view 参数的别名:

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

视图数据

通过公共属性传递数据

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

<?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 模板中访问其它数据一样访问它:

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

通过 with 参数传递数据

如果您希望在数据发送到模板之前自定义其格式,可以通过 Content 定义中的 with 参数手动将数据传递给视图。通常,您仍然会通过 mailable 类的构造函数传递数据;但是,您应该将这些数据设置为受保护或私有属性,以便数据不会自动传递给模板:

<?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 模板中访问其它数据一样访问它:

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

附件

添加附件到邮件

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

use Illuminate\Mail\Mailables\Attachment;

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

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

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

从磁盘附加文件

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

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

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

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

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

/**
 * 获取邮件的附件。
 *
 * @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 方法接受一个闭包,该闭包解析原始数据字节以及附件应分配的名称:

/**
 * 获取邮件的附件。
 *
 * @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 变量提供给所有邮件模板,因此您不需要手动传递它:

<body>
    这里是一张图片:

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

请注意,$message 变量在纯文本邮件模板中不可用,因为纯文本邮件不使用内联附件。

嵌入原始数据附件

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

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

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

可附加对象

使用可附加对象

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

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

<?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 方法返回该对象的实例:

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

当然,附件数据可能存储在像 Amazon S3 这样的远程文件存储服务上。因此,Laravel 也允许您从存储在应用程序某个【文件系统磁盘】上的数据生成附件实例:

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

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

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

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

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

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

头部

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

为此,您需要在您的 mailable 类中定义一个 headers 方法。该方法应返回一个 Illuminate\Mail\Mailables\Headers 实例。此类接受 messageIdreferencestext 参数。当然,您可以根据您的具体邮件需求,只提供必要的参数:

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 定义将标签和元数据添加到电子邮件消息中:

use Illuminate\Mail\Mailables\Envelope;

/**
 * 获取邮件的信封信息。
 *
 * @return \Illuminate\Mail\Mailables\Envelope
 */
public function envelope(): Envelope
{
    return new Envelope(
        subject: '订单已发货',
        tags: ['shipment'],
        metadata: [
            'order_id' => $this->order->id,
        ],
    );
}

如果您的应用程序使用的是 Mailgun 驱动程序,您可以参考 Mailgun 的文档了解有关 标签元数据 的更多信息。同样,您也可以参考 Postmark 的文档,了解它们如何支持 标签元数据

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

自定义 Symfony 消息

Laravel 的邮件功能由 Symfony Mailer 提供支持。Laravel 允许您注册自定义回调函数,这些回调会在发送消息之前被调用,并传入 Symfony Message 实例。这样,您可以在消息发送之前深度定制邮件内容。为此,您可以在 Envelope 定义中设置 using 参数:

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

/**
 * 获取邮件的信封信息。
 */
public function envelope(): Envelope
{
    return new Envelope(
        subject: '订单已发货',
        using: [
            function (Email $message) {
                // ...
            },
        ]
    );
}

通过这种方式,您可以在邮件发送前对 Email 实例进行定制。

Markdown 邮件

Markdown 格式的邮件消息允许您在邮件中利用内置的通知模板和组件。由于消息是用 Markdown 编写的,Laravel 可以为这些邮件渲染漂亮的响应式 HTML 模板,同时自动生成一份纯文本版本。

生成 Markdown 邮件

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

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

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

use Illuminate\Mail\Mailables\Content;

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

编写 Markdown 消息

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

<x-mail::message>
# 订单已发货

您的订单已经发货!

<x-mail::button :url="$url">
查看订单
</x-mail::button>

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

在编写 Markdown 邮件时,请避免过多的缩进。根据 Markdown 标准,Markdown 解析器会将缩进的内容渲染为代码块。

按钮组件

按钮组件渲染一个居中的按钮链接。该组件接受两个参数,一个是 url,另一个是可选的 color(颜色)。支持的颜色有 primarysuccesserror。您可以根据需要在消息中添加多个按钮组件:

<x-mail::button :url="$url" color="success">
查看订单
</x-mail::button>

面板组件

面板组件将给定的文本块呈现为一个面板,面板的背景颜色与消息的其它部分略有不同。这使得您可以将某个文本块突出显示:

<x-mail::panel>
这是面板内容。
</x-mail::panel>

表格组件

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

<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 资产标签:

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

此命令将会把 Markdown 邮件组件发布到 resources/views/vendor/mail 目录下。mail 目录将包含 htmltext 子目录,每个子目录包含相应组件的 HTML 或纯文本版本。您可以根据需要自定义这些组件。

自定义 CSS

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

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

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

发送邮件

要发送邮件,请使用 Mail 【门面】上的 to 方法。to 方法接受一个电子邮件地址、一个用户实例或一个用户集合。如果您传递一个对象或对象集合,邮件发送器将自动使用这些对象的电子邮件和名称属性来确定邮件的收件人,因此请确保这些属性在您的对象中是可用的。指定了收件人后,您可以将 mailable 类的实例传递给 send 方法:

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
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
{
    /**
     * Ship the given order.
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // Ship the order...

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

        return redirect('/orders');
    }
}

发送邮件时,您不仅可以指定 "to" 收件人,还可以通过链式调用设置 "to"、"cc" 和 "bcc" 收件人:

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

遍历收件人列表

有时,您可能需要通过迭代收件人/电子邮件地址数组来发送邮件。然而,由于 to 方法会将电子邮件地址附加到 mailable 的收件人列表中,因此每次迭代都会将邮件发送给之前的所有收件人。因此,您应该在每次迭代时重新创建 mailable 实例:

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

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

默认情况下,Laravel 会使用在应用程序的邮件配置文件中配置为默认邮件发送器的邮件发送器来发送邮件。然而,您可以使用 mailer 方法通过特定的邮件发送器配置发送邮件:

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

队列邮件

邮件消息排队

由于发送邮件可能会影响应用程序的响应时间,许多开发人员选择将邮件消息排入后台队列进行发送。Laravel 使用内置的统一队列 API,使得这一过程变得非常简单。要将邮件消息排队,只需在指定消息的收件人后,使用 queue 方法:

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

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

延迟邮件排队

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

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

推送到特定队列

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

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

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

默认排队

如果您希望某些 mailable 类总是被排入队列,可以在类中实现 ShouldQueue 接口。现在,即使您调用 send 方法发送邮件,只要实现了该接口,mailable 仍然会被排入队列:

use Illuminate\Contracts\Queue\ShouldQueue;

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

排队的邮件与数据库事务

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

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

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

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

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();
    }
}

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

渲染邮件

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

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

$invoice = Invoice::find(1);

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

在浏览器中预览邮件

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

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

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

本地化邮件

Laravel 允许您在与请求的当前语言环境不同的语言环境中发送邮件,并且即使邮件被排队,系统也会记住这个语言环境。

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

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

用户首选语言环境

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

use Illuminate\Contracts\Translation\HasLocalePreference;

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

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

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

测试

测试邮件内容

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

正如您所预期的那样,"HTML" 断言用于检查 mailable 的 HTML 版本是否包含给定字符串,而 "text" 断言则用于检查 mailable 的纯文本版本是否包含给定字符串:

  • Pest

  • PHPUnit

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->assertSeeInHtml('Invoice Paid');
    $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);

    $mailable->assertSeeInText($user->email);
    $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']);
});
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->assertSeeInHtml('Invoice Paid');
    $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);

    $mailable->assertSeeInText($user->email);
    $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']);
}

测试邮件发送

我们建议将测试 mailable 内容与测试特定 mailable 是否“发送”到特定用户的测试分开。通常,mailable 的内容与您正在测试的代码无关,通常只需断言 Laravel 被指示发送给定的 mailable 即可。

您可以使用 Mail facade 的 fake 方法来防止邮件被实际发送。在调用 Mail facade 的 fake 方法后,您可以断言 mailable 是否被指示发送给用户,甚至检查 mailable 接收到的数据:

  • Pest

  • PHPUnit

<?php

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

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

    // Perform order shipping...

    // Assert that no mailables were sent...
    Mail::assertNothingSent();

    // Assert that a mailable was sent...
    Mail::assertSent(OrderShipped::class);

    // Assert a mailable was sent twice...
    Mail::assertSent(OrderShipped::class, 2);

    // Assert a mailable was sent to an email address...
    Mail::assertSent(OrderShipped::class, 'example@laravel.com');

    // Assert a mailable was sent to multiple email addresses...
    Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']);

    // Assert a mailable was not sent...
    Mail::assertNotSent(AnotherMailable::class);

    // Assert 3 total mailables were sent...
    Mail::assertSentCount(3);
});
<?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();

        // Perform order shipping...

        // Assert that no mailables were sent...
        Mail::assertNothingSent();

        // Assert that a mailable was sent...
        Mail::assertSent(OrderShipped::class);

        // Assert a mailable was sent twice...
        Mail::assertSent(OrderShipped::class, 2);

        // Assert a mailable was sent to an email address...
        Mail::assertSent(OrderShipped::class, 'example@laravel.com');

        // Assert a mailable was sent to multiple email addresses...
        Mail::assertSent(OrderShipped::class, ['example@laravel.com', '...']);

        // Assert a mailable was not sent...
        Mail::assertNotSent(AnotherMailable::class);

        // Assert 3 total mailables were sent...
        Mail::assertSentCount(3);
    }
}

如果您正在将 mailable 排入队列以便在后台发送,应该使用 assertQueued 方法,而不是 assertSent

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

您可以将一个闭包传递给 assertSentassertNotSentassertQueuedassertNotQueued 方法,以断言发送的 mailable 满足给定的 “真值测试”。如果至少有一个 mailable 满足给定的真值测试,则该断言将成功。

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

在调用 Mail facade 的断言方法时,通过提供的闭包接受的 mailable 实例暴露了一些有用的方法,用于检查该 mailable。

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

mailable 实例还包含了几个有用的方法,用于检查 mailable 上的附件。

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')
    );
});

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

Mail::assertNothingOutgoing();

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

邮件和本地开发

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

日志驱动(Log Driver)

通过使用 log 邮件驱动(Log Mail Driver),你可以将所有电子邮件消息写入日志文件进行检查,而不是实际发送邮件。通常,这种驱动程序只会在本地开发环境中使用。有关如何根据环境配置应用程序的更多信息,请参考【配置文档】。

HELO / Mailtrap / Mailpit

另外,你可以使用像 HELOMailtrap 这样的服务,并使用 smtp 驱动将邮件发送到一个 “虚拟” 邮箱,在这里你可以在真实的电子邮件客户端中查看邮件。使用这种方法的好处是,你可以在 Mailtrap 的消息查看器中实际检查最终的电子邮件内容。

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

使用全局收件地址

最后,你可以通过调用 Mail facade 提供的 alwaysTo 方法来指定一个全局的 “收件人” 地址。通常,这个方法应该在你的应用程序服务提供者的 boot 方法中调用:

use Illuminate\Support\Facades\Mail;

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

事件

Laravel 在发送邮件消息时会触发两个事件。MessageSending 事件在邮件发送之前触发,而 MessageSent 事件在邮件发送之后触发。请注意,这些事件是在邮件发送时触发的,而不是在邮件被加入队列时。你可以为这些事件在应用程序中创建 事件监听器

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() 方法:

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 方法中完成。提供给 extend 方法的闭包将接收一个 $config 参数,这个参数包含了应用程序的 config/mail.php 配置文件中为邮件传输方式定义的配置数组:

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

/**
 * 引导应用程序的任何服务。
 */
public function boot(): void
{
    Mail::extend('mailchimp', function (array $config = []) {
        return new MailchimpTransport(/* ... */);
    });
}

定义并注册了自定义的传输方式后,你可以在应用程序的 config/mail.php 配置文件中创建一个邮件传输方式的定义,使用你刚刚创建的自定义传输:

'mailchimp' => [
    'transport' => 'mailchimp',
    // ...
],

附加的 Symfony 传输

Laravel 支持一些现有的由 Symfony 维护的邮件传输方式,如 Mailgun 和 Postmark。然而,你可能希望扩展 Laravel,以支持更多由 Symfony 维护的传输方式。你可以通过通过 Composer 安装必要的 Symfony 邮件传输包并将其注册到 Laravel 中来实现。例如,你可以安装并注册 "Brevo"(前身为 "Sendinblue")的 Symfony 邮件传输:

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

安装了 Brevo 邮件传输包后,你可以在应用程序的服务配置文件中添加 Brevo API 凭证的条目:

'brevo' => [
    'key' => 'your-api-key',
],

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

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 配置文件中创建一个新的邮件传输方式定义,使用你刚刚注册的自定义传输:

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