[ Laravel 5.3 文档 ] 综合话题 —— 邮件

1、简介

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

邮件驱动预备知识

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

composer require guzzlehttp/guzzle

Mailgun驱动

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

'mailgun' => [
    'domain' => 'your-mailgun-domain',
    'secret' => 'your-mailgun-key',
],

SparkPost驱动

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

'sparkpost' => [
    'secret' => 'your-sparkpost-key',
],

SES驱动

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

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

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

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

2、生成可邮寄类

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

php artisan make:mail OrderShipped

3、编写可邮寄类

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

配置发件人

使用from方法

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

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

使用全局的from地址

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

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

配置视图

你可以在可邮寄类的build方法中使用view方法来指定渲染邮件内容时使用哪个模板,由于每封邮件基本都使用Blade模板来渲染内容,所以你可以在构建邮件HTML时充分使用Blade模板引擎:

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

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

纯文本邮件

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

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

视图数据

通过公共属性

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

<?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;

    /**
     * The order instance.
     *
     * @var Order
     */
    public $order;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

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

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

<div>
    Price: {{ $order->price }}
</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;

    /**
     * The order instance.
     *
     * @var Order
     */
    protected $order;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct(Order $order)
    {
        $this->order = $order;
    }

    /**
     * Build the message.
     *
     * @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');
}

附加文件到消息时,还可以通过传递一个数组作为attach方法第二个参数来指定文件显示名或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>

嵌入原生数据附件

如果你已经有一个想要嵌入邮件模板的原生数据字符串,可以使用$message变量上的embedData方法:

<body>
    Here is an image from raw data:

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

4、发送邮件

要发送一条信息,可以使用Mail门面上的to方法。to方法接收邮箱地址、用户实例或用户集合作为参数。如果传递的是对象或对象集合,在设置邮件收件人的时候邮件会自动使用它们的emailname属性,所以事先要确保这些属性在相应类上有效。指定好收件人以后,传递一个可邮寄类的实例到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”、“cc”以及“bcc”:

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

邮件队列

邮件消息队列

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

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

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

延迟消息队列

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

$when = Carbon\Carbon::now()->addMinutes(10);

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

推入到指定队列

由于通过make:mail命令生成的所有可邮寄类都使用了Illuminate\Bus\Queueabletrait,所以你可以调用可邮寄类实例上的onQueueonConnection方法,以便为消息指定连接和队列名称:

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

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

5、邮件&本地开发

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

日志驱动

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

通用配置

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

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

Mailtrap

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

6、事件

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

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

学院君

学院君 has written 548 articles

资深PHP工程师,Laravel学院院长