发送邮件

未匹配的标注

发送邮件#

介绍#

发送邮件并不复杂。Laravel 基于 Symfony Mailer 组件提供了一个简洁、简单的邮件 API。Laravel 和 Symfony Mailer 为通过 SMTP、Mailgun、Postmark、Resend、Amazon SES、 及 sendmail 等方式发送邮件提供了驱动,允许你通过本地或者云服务来快速发送邮件。

配置#

Laravel 的邮件系统可以通过应用的 config/mail.php 配置文件进行配置。在该文件中配置的每个邮件程序都可能有自己独特的配置,甚至有自己唯一的「传输方式」,允许你的应用使用不同的电子邮件服务发送某些电子邮件。例如,你的应用可能在使用 Amazon SES 批量发送邮件的同时,使用 Postmark 发送事务性邮件。

mail 配置文件中,你可以看到一个 mailers 配置数组。此数组包含 Laravel 支持的每个主要邮件驱动程序 / 传输的示例配置条目,而 defalut 配置值决定了当应用需要发送电子邮件时,默认情况下将使用哪个邮件程序。

驱动 / 传输的前提#

基于 API 的驱动,比如 Mailgun、Postmark、Resend 和 MailerSend,通常比 SMTP 服务器更简单快速。如果可以的话,我们推荐你使用下面这些驱动。

Mailgun 驱动#

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

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

如果使用的不是 US Mailgun region 区域终端 ,你需要在 services 配置文件中配置区域终端:

'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 传输库:

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

接下来,在应用程序的 config/mail.php 配置文件中将 default 选项设置为 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 配置文件中将 default 选项设置为 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 配置文件中将 default 选项设置为 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'),
],

为了通过 session token 来使用 AWS temporary credentials,你需要向应用的 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

安装扩展包后,需要将 MAILERSEND_API_KEY 环境变量添加到应用程序的 .env 文件中。另外,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 配置文件中的 mailers 数组中

'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 传输允许你将邮件工作负载分布在多个邮件程序中。首先,要在应用程序的 mail 配置文件中定义一个使用 roundrobin 传输的邮件程序。应用程序的 roundrobin 邮件程序的配置数组应包含一个 mailers 数组,该数组引用应使用哪些配置的邮件程序进行传递:

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

    // ...
],

在成功配置了循环轮询邮件发送程序后,你应当将其设置为默认的邮件发送器。这一步骤简单而关键,只需在应用的 mail 配置文件中,将 default 键对应的值更新为你所创建的循环轮询邮件发送器的名称即可。

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

循环轮询传输机制从已配置的邮件发送器列表中随机选取一个进行邮件发送,并在后续的每一封邮件发送中切换至下一个可用的邮件发送器。与 failover 传输对比。failover 旨在实现 高可用性,而 roundrobin 传输则提供负载平衡

生成 Mailables#

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

php artisan make:mail OrderShipped

编写 Mailables#

生成 mailable 类后,就打开它,这样我们就可以探索它的内容。邮件类的配置可以通过多种方法完成,包括 envelopecontentattachments 方法。

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

配置发件人#

使用 Envelope#

首先,让我们来看下如何配置电子邮件的发件人。电子邮件的「发件人」。有两种方法可以配置发送者。首先,你可以在邮件信封上指定「发件人」地址:

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: '订单发货',
    );
}

除此之外,还可以指定 replyTo 地址:

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

使用全局 from 地址#

当然,如果你的应用在任何邮件中使用的「发件人」地址都一致的话,在你生成的每一个 mailable 类中调用 from 方法可能会很麻烦。因此,你可以在 config/mail.php 文件中指定一个全局的「发件人」地址。当某个 mailable 类没有指定「发件人」时,它将使用该全局「发件人」:

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

配置视图#

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

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

[技巧]
你可以创建一个 resources/views/emails 目录来存放所有的邮件模板;当然,也可以将其置于 resources/views 目录下的任何位置。

纯文本邮件#

如果你想定义电子邮件的纯文本版本,你可以在创建邮件的 Content 定义时指定纯文本模板。与 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'
);

视图数据#

通过 Public 属性#

通常,你希望将一些数据传递给视图,以便在呈现电子邮件的 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 类的构造函数来传递数据;但是,你应该把这些数据设置为 protectedprivate 属性,这样数据就不会自动提供给模板:

<?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>
    Here is an image:

    <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 方法。 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: 'Order Shipped',
        tags: ['shipment'],
        metadata: [
            'order_id' => $this->order->id,
        ],
    );
}

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

如果你的应用程序使用 Amazon SES 发送电子邮件,你应该使用 metadata 方法将 SES “标签” 附加到邮件上。

自定义 Symfony 消息#

Laravel 的邮件功能是由 Symfony Mailer 提供支持的。Laravel 允许注册自定义回调,这些回调将在发送消息之前与 Symfony Message 实例一起调用。你可以在发送消息之前深入自定义消息。为此,在你的 Envelope 定义中定义一个 using 参数:

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

/**
 * Get the message envelope.
 */
public function envelope(): Envelope
{
    return new Envelope(
        subject: 'Order Shipped',
        using: [
            function (Email $message) {
                // ...
            },
        ]
    );
}

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;

/**
 * Get the message content definition.
 */
public function content(): Content
{
    return new Content(
        markdown: 'mail.orders.shipped',
        with: [
            'url' => $this->orderUrl,
        ],
    );
}

编写 Markdown 消息#

Markdown mailable 类整合了 Markdown 语法和 Blade 组件,让你能够非常方便的使用 Laravel 预置的 UI 组件来构建邮件消息:

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

你的订单已发货!

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

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

提示
在编写 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 命令时加上 --tag=laravel-mail:

php 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 表示中。

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

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

发送邮件#

若要发送邮件,使用 Mail facade 的方法。该 to 方法接受 邮件地址、用户实例或用户集合。如果传递一个对象或者对象集合,mailer 在设置收件人时将自动使用它们的 emailname 属性,因此请确保对象的这些属性可用。一旦指定了收件人,就可以将 mailable 类实例传递给 send 方法:

<?php

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
{
    /**
     * 发送给定的订单信息。
     */
    public function store(Request $request): RedirectResponse
    {
        $order = Order::findOrFail($request->order_id);

        // 发货订单。。。

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

        return redirect('/orders');
    }
}

在发送消息时不止可以指定「收件人」。还可以通过链式调用「to」、「cc」、「bcc」一次性指定抄送和密送收件人:

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

遍历收件人列表#

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

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

通过特定的 Mailer 发送邮件#

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

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

邮件队列#

将邮件消息加入队列#

由于发送电子邮件消息可能会对应用程序的响应时间产生负面影响,许多开发人员选择将电子邮件消息加入队列以进行后台发送。Laravel 使用其内置的统一队列 API 使这变得容易。要排将邮件消息加入队列,你可以在指定收件人后使用 Mail facade 上的 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 trait,因此你可以在任何 mailable 类实例上调用 onQueueonConnection 方法来指定消息的连接和队列名称:

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

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

默认队列#

如果你希望你的邮件类始终使用队列,你可以给邮件类实现 ShouldQueue 契约,现在即使你调用了 send 方法,邮件依旧使用队列的方式发送:

use Illuminate\Contracts\Queue\ShouldQueue;

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

邮件队列和数据库事务#

当在数据库事务中分发邮件队列时,队列可能在数据库事务提交之前处理邮件。 发生这种情况时,在数据库事务期间对模型或数据库记录所做的任何更新可能都不会反映在数据库中。另外,在事务中创建的任何模型或数据库记录都可能不存在于数据库中。如果你的邮件基于以上这些模型数据,则在处理邮件发送时,可能会发生意外错误。

如果队列连接的 after_commit 配置选项设置为 false,那么仍然可以通过在邮件类上定义 after_commit 属性来设置提交所有打开的数据库事务之后再调度特定的邮件队列:

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

或者,你可以在邮件类的构造函数中调用 afterCommit 方法:

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

技巧
要了解如何解决这些问题的更多信息,请查看队列任务和数据库事务

渲染邮件#

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

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

$invoice = Invoice::find(1);

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

在浏览器中预览邮件#

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

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

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

本地化邮件#

Laravel 允许你在与请求的当前语言环境不同的语言环境中发送邮件,如果邮件被队列化,它甚至会记住这个语言环境。

为了实现这一点,Mail facade 提供了一个 locale 方法来设置所需的语言。当邮件的模板被评估时,应用程序将会改变到这个语言环境,然后在评估完成后恢复到之前的语言环境:

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 提供了多种方法来检查你的邮件的结构。此外,Laravel 提供了几个方便的方法来测试你的邮件是否包含你期望的内容。这些方法包括:assertSeeInHtmlassertDontSeeInHtmlassertSeeInOrderInHtmlassertSeeInTextassertDontSeeInTextassertSeeInOrderInTextassertHasAttachmentassertHasAttachedDataassertHasAttachmentFromStorage 以及 assertHasAttachmentFromStorageDisk

正如你所期望的,「HTML」断言会断言你的邮件的 HTML 版本是否包含给定的字符串,而「text」断言断言邮件的纯文本版本是否包含给定的字符串:

//译者注:Pest 示例
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']);
});
//译者注:PHPUnit 示例
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']);
}

测试邮件发送#

我们建议将邮件内容和判断指定的邮件「发送」给特定用户的测试分开进行测试。通常来讲,邮件的内容与你正在测试的代码无关,只要能简单地判断 Laravel 能够发送指定的邮件就足够了。

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

<?php
//译者注:Pest 示例
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;

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

    // 执行订单发货...

    // 断言没有邮件被发送...
    Mail::assertNothingSent();

    // 断言已发送邮件...
    Mail::assertSent(OrderShipped::class);

    // 断言已发送两次的邮件...
    Mail::assertSent(OrderShipped::class, 2);

    // 断言邮件未发送...
    Mail::assertNotSent(AnotherMailable::class);

    // 断言总共发送了3封邮件...
    Mail::assertSentCount(3);
});
<?php
//译者注:PHPUnit 示例
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();

        // 执行订单发货...

        // 断言没有发送邮件...
        Mail::assertNothingSent();

        // 断言已发送邮件...
        Mail::assertSent(OrderShipped::class);

        // 断言已发送两次的邮件...
        Mail::assertSent(OrderShipped::class, 2);

        // 断言邮件未发送...
        Mail::assertNotSent(AnotherMailable::class);

        // 断言总共发送了3封邮件...
        Mail::assertSentCount(3);
    }
}

如果你在后台排队等待邮件的发送,则应该使用 assertQueued 方法而不是 assertSent 方法。

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

你可以向 assertSent, assertNotSent, assertQueued, 或 assertNotQueued 方法传递一个闭包,以来判断发送的邮件是否通过给定的 「真值检验」。如果至少有一个通过给定真值测试的邮件被发送,那么断言将是成功的:

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

当调用 Mail facade 的断言方法时,所提供的闭包接受的邮件实例提供了检查邮件的有用方法:

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

邮件实例还包括几个有用的方法来检查邮件上的附件:

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

你可能已经注意到,有 2 种方法可以判断邮件是否发送,即:assertNotSentassertNotQueued 。有时你可能希望判断邮件没有被发送或排队。如果要实现这一点,你可以使用 assertNothingOutgoingassertNotOutgoing 方法。

Mail::assertNothingOutgoing();

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

邮件和本地开发#

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

日志驱动#

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

HELO / Mailtrap / Mailpit#

或者,你可以使用像 HELOMailtrap 这样的服务,并使用 smtp 驱动将你的电子邮件消息发送到「虚拟」邮箱中,你可以通过在真正的电子邮件客户端中查看它们。这种方法的好处在于,它允许你在 Mailtrap 的消息查看器中实际检查最终的电子邮件。

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

使用全局 to 地址#

最后,你可以通过调用 Mail 门面提供的 alwaysTo 方法来指定一个全局的「收件人」地址。 通常,应该从应用程序的服务提供者之一的 boot 方法调用此方法:

use Illuminate\Support\Facades\Mail;

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

事件#

Laravel 在发送邮件消息的过程中会触发 2 个事件。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 方法中完成。$config 参数将提供给 extend 方法的闭包。该参数将包含在应用程序中的 config/mail.php 来配置文件中为 mailer 定义的配置数组。

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 中注册传输来实现这一点。例如,你可以安装并注册 Symfony 的「Brevo」 (原 「Sendinblue」) 邮件:

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

安装好 Brevo 邮件器包后,你可以在应用程序的 services 配置文件中为你的 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',
    // ...
],

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/laravel/11.x/ma...

译文地址:https://learnku.com/docs/laravel/11.x/ma...

上一篇 下一篇
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
贡献者:11
讨论数量: 0
发起讨论 只看当前版本


暂无话题~