Mail

简介

Laravel 基于 SwiftMailer 库提供了一套干净、清爽的邮件 API。Laravel 为 SMTP、Mailgun、SparkPost、Amazon SES、PHP 的 mail 函数,以及 sendmail 提供了驱动,从而允许你快速通过本地或云服务发送邮件。

邮件驱动预备知识

基于 API 的驱动如 Mailgun 和 SparkPost 通常比 SMTP 服务器更简单、更快,所以如果可以的话,尽可能使用这些服务。所有的 API 驱动要求应用已经安装 Guzzle HTTP 库,你可以通过 Composer 包管理器来安装它:

  1. composer require guzzlehttp/guzzle

Mailgun 驱动

要使用 Mailgun 驱动(Mailgun 前10000封邮件免费,后续收费),首先安装 Guzzle,然后在配置文件 config/mail.php 中设置 driver 选项为 mailgun 。接下来,验证配置文件 config/services.php 包含如下选项:

  1. 'mailgun' => [
  2. 'domain' => 'your-mailgun-domain',
  3. 'secret' => 'your-mailgun-key',
  4. ],

SparkPost 驱动

要使用 SparkPost 驱动,首先安装 Guzzle,然后再配置文件 config/mail.php 中设置 driver 选项值为 sparkpost 。接下来,验证配置文件 config/services.php 包含如下选项:

  1. 'sparkpost' => [
  2. 'secret' => 'your-sparkpost-key',
  3. ],

如果有必要的话,你还可以设置 API 端点 使用:

  1. 'sparkpost' => [
  2. 'secret' => 'your-sparkpost-key',
  3. 'options' => [
  4. 'endpoint' => 'https://api.eu.sparkpost.com/api/v1/transmissions',
  5. ],
  6. ],

SES 驱动

要使用 Amazon SES 驱动(收费),先安装 Amazon AWS 的 PHP SDK,你可以通过添加如下行到 composer.json 文件的 require 部分然后运行 composer update 命令来安装该库:

  1. "aws/aws-sdk-php": "~3.0"

接下来,设置配置文件 config/mail.php 中的 driver 选项为 ses 。然后,验证配置文件 config/services.php 包含如下选项:

  1. 'ses' => [
  2. 'key' => 'your-ses-key',
  3. 'secret' => 'your-ses-secret',
  4. 'region' => 'ses-region', // e.g. us-east-1
  5. ],

生成可邮寄类

在 Laravel 中,应用发送的每一封邮件都可以表示为“可邮寄”类,这些类都存放在 app/Mail 目录。如果没看到这个目录,别担心,它将会在你使用 make:mail 命令创建第一个可邮寄类时生成:

  1. php artisan make:mail OrderShipped

编写可邮寄类

所有的可邮寄类配置都在 build 方法中完成,在这个方法中,你可以调用多个方法,例如 fromsubjectview, 和 attach 来配置邮件的内容和发送。

配置发件人

使用 from 方法

我们来看一下邮件发件人的配置,或者,换句话说,邮件来自于谁。有两种方式来配置发送者,第一种方式是在可邮寄类的 build 方法方法中调用 from 方法:

  1. /**
  2. * 构建消息.
  3. *
  4. * @return $this
  5. */
  6. public function build()
  7. {
  8. return $this->from('example@example.com')
  9. ->view('emails.orders.shipped');
  10. }

使用全局的 from 地址

不过,如果你的应用在所有邮件中都使用相同的发送地址,在每个生成的可邮寄类中都调用 from 方法就显得很累赘。取而代之地,你可以在配置文件 config/mail.php 中指定一个全局的发送地址,该地址可用于在所有可邮寄类中没有指定其它发送地址的场景下(即作为默认发件人):

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

配置视图

你可以在可邮寄类的 build 方法中使用 view 方法来指定渲染邮件内容时使用哪个视图模板,由于每封邮件通常使用 Blade 模板来渲染内容,所以你可以在构建邮件 HTML 时使用 Blade 模板引擎提供的所有功能:

  1. /**
  2. * 构建消息.
  3. *
  4. * @return $this
  5. */
  6. public function build()
  7. {
  8. return $this->view('emails.orders.shipped');
  9. }

{注:} 你可以创建一个 resources/views/emails 目录来存放所有邮件模板,当然,你也可以将邮件模板放到 resources/views 目录下任意其它位置。

纯文本邮件

如果你想要定义一个纯文本格式的邮件,可以使用 text 方法。和 view 方法一样, text 方法接收一个用于渲染邮件内容的模板名,你既可以定义纯文本消息也可以定义 HTML 消息:

  1. /**
  2. * 构建消息.
  3. *
  4. * @return $this
  5. */
  6. public function build()
  7. {
  8. return $this->view('emails.orders.shipped')
  9. ->text('emails.orders.shipped_plain');
  10. }

视图数据

通过公共属性

通常,我们需要传递一些数据到渲染邮件的 HTML 视图以供使用。有两种方式将数据传递到视图,第一种是可邮寄类的公共(public)属性在视图中自动生效,举个例子,我们将数据传递给可邮寄类的构造器并将数据设置给该类的公共属性:

  1. <?php
  2. namespace App\Mail;
  3. use App\Order;
  4. use Illuminate\Bus\Queueable;
  5. use Illuminate\Mail\Mailable;
  6. use Illuminate\Queue\SerializesModels;
  7. class OrderShipped extends Mailable
  8. {
  9. use Queueable, SerializesModels;
  10. /**
  11. * 订单实例.
  12. *
  13. * @var Order
  14. */
  15. public $order;
  16. /**
  17. * 创建一个新的消息实例.
  18. *
  19. * @return void
  20. */
  21. public function __construct(Order $order)
  22. {
  23. $this->order = $order;
  24. }
  25. /**
  26. * 构建消息.
  27. *
  28. * @return $this
  29. */
  30. public function build()
  31. {
  32. return $this->view('emails.orders.shipped');
  33. }
  34. }

数据被设置给公共属性后,将会在视图中自动生效,所以你可以像在 Blade 模板中访问其它数据一样访问它们:

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

通过 with 方法

如果你想要在数据发送到模板之前自定义邮件数据的格式,可以通过 with 方法手动传递数据到视图。一般情况下,你还是需要通过可邮寄类的构造器传递数据,不过,这次你需要设置数据为 protectedprivate 属性,这样,这些数据就不会在视图中自动生效。然后,当调用 with 方法时,传递数组数据到该方法以便数据在视图模板中生效:

<?php

namespace App\Mail;

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

class OrderShipped extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * 订单实例.
     *
     * @var Order
     */
    protected $order;

    /**
     * 创建一个新的实例.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * 构建消息.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.orders.shipped')
                    ->with([
                        'orderName' => $this->order->name,
                        'orderPrice' => $this->order->price,
                    ]);
    }
}

数据通过 with 方法传递到视图后,将会在视图中自动生效,因此你也可以像在 Blade 模板访问其它数据一样访问传递过来的数据:

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

附件

要在邮件中加入附件,在 build 方法中使用 attach 方法。attach 方法接受文件的绝对路径作为它的第一个参数:

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.orders.shipped')
                    ->attach('/path/to/file');
    }

附加文件到消息时,你也可以传递 数组attache 方法作为第二个参数,以指定显示名称和 / 或是 MIME 类型:

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.orders.shipped')
                    ->attach('/path/to/file', [
                        'as' => 'name.pdf',
                        'mime' => 'application/pdf',
                    ]);
    }

原始数据附件

attachData 可以使用字节数据作为附件。例如,你可以使用这个方法将内存中生成而没有保存到磁盘中的 PDF 附加到邮件中。attachData 方法第一个参数接收原始字节数据,第二个参数为文件名,第三个参数接受一个数组以指定其他参数:

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.orders.shipped')
                    ->attachData($this->pdf, 'name.pdf', [
                        'mime' => 'application/pdf',
                    ]);
    }

内部附件

在邮件中嵌入内部图标通常是件麻烦事;然而 Laravel 提供了一个便利的方法,让你在邮件中附件图片并获取适当的 CID 。要嵌入内部图片,在邮件模板的 $message 变量上调用 embed 方法,Laravel 会自动让你所有邮件模板都能够获取到 $message 变量,所以不必担心如何手动传递它:

<body>
    Here is an image:

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

{node} $message 变量在 markdown 消息中是不可用。

嵌入原始数据附件

如果你已经有想嵌入原始数据,希望嵌入邮件模板,你可以调用 $message 变量的 embedData 方法:

<body>
    Here is an image from raw data:

    <img src="{{ $message->embedData($data, $name) }}">
</body>

自定义 SwiftMailer 消息

Mailable 基类的 withSwiftMessage 方法允许你注册一个回调,该回调将在邮件发送之前调用,参数是原始 SwiftMailer 消息实例。这让你有机会在邮件发送之前自定义消息:

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        $this->view('emails.orders.shipped');

        $this->withSwiftMessage(function ($message) {
            $message->getHeaders()
                    ->addTextHeader('Custom-Header', 'HeaderValue');
        });
    }

Markdown 格式的 Mailable 类

Markdown 格式的 Mailable 消息允许你从预编译的模板和你的 Mailable 类中的邮件提醒组件中受益。因为消息是用 Markdown 格式写的, Laravel 能为消息体渲染出漂亮、响应式的 HTML 模板,也能自动生成一个纯文本的副本。

生成 Markdown 格式的 Mailable

要生成一个包含友好的 Markdown 模板的 Mailable 类,你在使用 make:mail 这个 Artisan 命令时,要加上 —markdown 选项:

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

然后,在使用 build 方法配置 Mailable 时,用 markdown 方法来换掉 view 方法, markdown 方法接受一个 Markdown 模板的名称和一个将在模板中可用的选项数组:

/**
 * Build the message.
 *
 * @return $this
 */
public function build()
{
    return $this->from('example@example.com')
                ->markdown('emails.orders.shipped');
}

编写 Markdown 格式的消息

Markdown Mailable 使用 Blade 组件和 Markdown 语法的组合,允许你轻松地构建邮件消息,同时利用 Laravel 的预制组件:

@component('mail::message')
# Order Shipped

Your order has been shipped!

@component('mail::button', ['url' => $url])
View Order
@endcomponent

Thanks,<br>
{{ config('app.name') }}
@endcomponent

{tip} 在写 Markdown 邮件时不要使用过多的缩进。Markdown 解析器将把缩进的内容作为代码块呈现出来。

按钮组件

按钮组件渲染一个居中的连接按钮,组件接受两个参数,一个 url 和一个可选的 color 。支持的颜色有 bluegreen 、 和 red 。你可以在邮件消息体中加入任意多个你想要的按钮:

@component('mail::button', ['url' => $url, 'color' => 'success'])
View Order
@endcomponent

面板组件

面板组件将面板中给定的一块文字的背景渲染成跟其他的消息体背景稍微不同。这样可以让这块文字引起人们的注意:

@component('mail::panel')
This is the panel content.
@endcomponent

表格组件

表格组件允许你将一个 Markdown 格式的表格转换成一个 HTML 格式的表格。组件接受 Markdown 格式的表格作为它的内容。表格列对齐方式按照默认的 Markdown 对齐风格而定:

@component('mail::table')
| Laravel       | Table         | Example  |
| ------------- |:-------------:| --------:|
| Col 2 is      | Centered      | $10      |
| Col 3 is      | Right-Aligned | $20      |
@endcomponent

自定义组件

你或许会为了自定义而导出你应用中所有的 Markdown 邮件组件。要导出这些组件,使用 vendor:publish 这个 Artisan 命令来发布资源文件标签:

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

这个命令会发布 Markdown 邮件组件到 resources/views/vendor/mail 文件夹。而 mail 文件夹会包含一个 html 文件夹和一个 markdown 文件夹,每个文件夹都包含他们的可用组件的描述。你可以按照你的意愿自定义这些组件。

自定义样式表 CSS

在导出组件之后,resources/views/vendor/mail/html/themes 文件夹将包含一个默认的 default.css 文件。你可以在这个文件中自定义 CSS ,你定义的这些样式将会在 Markdown 格式消息体转换成 HTML 格式时自动得到应用。

{tip} 如果你想为 Markdown 组件构建一个全新的主题,只要写一个新的 CSS 文件,放在 html/themes 文件夹,然后在你的 mail 配置文件中改变 theme 选项就可以了。

发送邮件

发送一个邮件,使用 Mail facadeto 方法。 to 方法接受一个邮件地址,一个 user 实现或一个 users 集合。如果传递一个对象或集合,mailer 将自动使用 emailname 属性来设置邮件收件人,所以确保你的对象里有这些属性。一旦指定收件人,你可以传递一个实现到 Mailable 类的 send 方法:

<?php

namespace App\Http\Controllers;

use App\Order;
use App\Mail\OrderShipped;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Http\Controllers\Controller;

class OrderController extends Controller
{
    /**
     * Ship the given order.
     *
     * @param  Request  $request
     * @param  int  $orderId
     * @return Response
     */
    public function ship(Request $request, $orderId)
    {
        $order = Order::findOrFail($orderId);

        // Ship order...

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

当然,不局限于只使用「to」给收件人发送邮件,你可以通过一个单一的链式调用来自由的设置 「to」,「cc」和 「bcc」接收者:

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

渲染可邮寄类

有时候你可能想要在不发送可邮寄类的情况下捕获其 HTML 内容,要实现这个功能,可以调用可邮寄类的 render 方法,该方法会将处理后的邮件内容以字符串方式返回:

$invoice = App\Invoice::find(1);

return (new App\Mail\InvoicePaid($invoice))->render();

在浏览器中预览邮件

设计邮件的模板时,如果可以像普通的 Blade 模板一样快速预览渲染后的邮件是很方便的。因此,Laravel 允许你从路由闭包或控制器中直接返回可邮寄类。当可邮寄类返回时,就会在浏览器渲染并展示,从而方便你快速预览而不必真的发送邮件后查看:

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

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

邮件队列

邮件消息队列

由于发送邮件消息可能会大幅度延长应用的响应时间,许多开发者选择将邮件发送放到队列中在后台发送,Laravel 中可以使用内置的 统一队列 API 来实现这一功能。要将邮件消息推送到队列,可以在指定消息的接收者后使用 Mail 门面上的 queue 方法:

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

该方法自动将邮件任务推送到队列以便在后台发送。当然,你需要在使用该特性前 配置队列

延迟消息队列

如果你想要延迟队列中邮件消息的发送,可以使用 later 方法。 later 方法的第一个参数接收一个 DateTime 实例来表示邮件发送时间:

$when = now()->addMinutes(10);

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

推送到指定队列

由于通过 make:mail 命令生成的所有可邮寄类都使用了 Illuminate\Bus\Queueable trait,所以你可以调用可邮寄类实例上的 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
{
    //
}

邮件 & 本地开发

本地环境开发发送邮件的应用时,你可能不想要真的发送邮件到有效的电子邮件地址,而只是想要做下测试。为此,Laravel 提供了几种方式“取消”邮件的实际发送。

日志驱动

一种解决方案是在本地开发时使用 log 邮件驱动。该驱动将所有邮件信息写到日志文件中以备查看,想要了解更多关于每个环境的应用配置信息,查看 配置文档

通用配置

Laravel 提供的另一种解决方案是为框架发送的所有邮件设置通用收件人,这样的话,所有应用生成的邮件将会被发送到指定地址,而不是实际发送邮件指定的地址。这可以通过在配置文件 config/mail.php 中设置 to 选项来实现:

'to' => [
    'address' => 'example@example.com',
    'name' => 'Example'
],

Mailtrap

最后,你可以使用 Mailtrap 服务和 smtp 驱动发送邮件信息到“虚拟”邮箱,这种方法允许你在 Mailtrap 的消息查看器中查看最终的邮件。

事件

Laravel 会在发送邮件消息前触发两个事件, MessageSending 事件在消息发送前触发, MessageSent 事件在消息发送后触发。需要注意的是这两个事件是在邮件被发送前后触发,而不是推送到队列时,你可以在 EventServiceProvider 中注册对应的事件监听器:

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Mail\Events\MessageSending' => [
        'App\Listeners\LogSendingMessage',
    ],
    'Illuminate\Mail\Events\MessageSent' => [
        'App\Listeners\LogSentMessage',
    ],
];

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

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