邮件

简介

Laravel 为流行的 SwiftMailer 库提供了简洁易用的 API,支持 SMTP,Mailgun,SparkPost,Amazon SES,PHP 的 mail 函数和 sendmail 驱动,让您可以快速通过本地服务或者所选的云服务发送邮件。

驱动前提

基于 API 的驱动(例如 Mailgun 和 SparkPost)通常比 SMTP 服务器更简单更快速。如果可能,应该使用这些驱动之一。所有 API 驱动都需要 Guzzle HTTP 库,可以通过 Composer 包管理器进行安装:

composer require guzzlehttp/guzzle

Mailgun 驱动

使用 Mailgun 驱动,首先安装 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',
],

如有需要,还可以配置使用的 API 端点

'sparkpost' => [
    'secret' => 'your-sparkpost-key',
    'options' => [
        'endpoint' => 'https://api.eu.sparkpost.com/api/v1/transmissions',
    ],
],

SES 驱动

使用 Amazon SES 驱动,首先要安装 PHP 专用的 Amazon AWS 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',  // 例如 us-east-1
],

如果要在发起 SES SendRawEmail 请求时引入 其它选项,可以在 ses 配置项中定义一个 options 数组:

'ses' => [
    'key' => 'your-ses-key',
    'secret' => 'your-ses-secret',
    'region' => 'ses-region',  // 例如 us-east-1
    'options' => [
        'ConfigurationSetName' => 'MyConfigurationSet',
        'Tags' => [
            [
                'Name' => 'foo',
                'Value' => 'bar',
            ],
        ],
    ],
],

生成邮件

在 Laravel 中,应用发送的每种类型的邮件都表示为「Mailable」类。这些类都存储在 app/Mail 目录中。如果在应用中没有看到此目录也不用担心,因为它会在使用 make:mail 命令创建第一个邮件类时自动生成:

php artisan make:mail OrderShipped

编写邮件

所有邮件类的配置都在 build 方法中完成。在此方法中,您可以调用各种方法,如 fromsubjectviewattach 来配置邮件内容和发件人。

配置发件人

使用 from 方法

首先,我们来研究下如何配置邮件的发件人。或者,换句话说,邮件是由谁发出的。有两种方法配置发件人。第一种,在邮件类的 build 方法中使用 from 方法:

/**
 * 构建邮件信息
 *
 * @return $this
 */
public function build()
{
    return $this->from('example@example.com')
                ->view('emails.orders.shipped');
}

使用全局 from 地址

然而,如果应用使用同一地址来发送所有邮件,在每个生成的邮件类中调用 from 方法会很麻烦。因此,可以在 config/mail.php 配置文件中指定一个全局的「from」地址。如果在邮件类中没有指定其它的「from」地址,就会使用此全局地址:

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

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

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

配置视图

在邮件类的 build 方法中,可以使用 view 方法指定渲染邮件内容时使用的模板。由于每封邮件通常都使用 Blade 模板 来渲染内容,因此在构建邮件 HTML 时可以充分利用 Blade 模板引擎的强大功能和便利:

/**
 * 构建邮件信息
 *
 * @return $this
 */
public function build()
{
    return $this->view('emails.orders.shipped');
}

您可能希望创建一个 resources/views/emails 目录来存放所有邮件模板;然而,还可以自由选择将其放在 resources/views 目录中的任何地方。

纯文本邮件

如果要定义纯文本版本的邮件,可以使用 text 方法。与 view 方法一样,text 方法接收用于渲染邮件内容的模板名。可以自由定义 HTML 和纯文本两个版本的内容:

/**
 * 构建邮件信息
 *
 * @return $this
 */
public function build()
{
    return $this->view('emails.orders.shipped')
                ->text('emails.orders.shipped_plain');
}

视图数据

通过公有属性

通常情况下,会传递一些数据到视图,以便在渲染邮件 HTML 时使用。有两种方式可以让视图获取数据。第一种,任何定义在邮件类中的公有属性会自动在视图中可用。因此,可以将数据传递到邮件类的构造函数中,然后赋值给类中定义的公有属性:

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
     */
    public $order;

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

    /**
     * 构建信息
     *
     * @return $this
     */
    public function build()
    {
        return $this->view('emails.orders.shipped');
    }
}

数据赋值给公有属性后,它会自动在视图中可用,因此可以在 Blade 模板中像访问任何其它数据一样访问它:

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

通过 with 方法

如果要在将数据发送给模板之前自定义邮件数据格式,可以通过 with 方法手动将数据传递到视图。通常情况下,仍会通过邮件类的构造函数传递数据;但是,应该将数据赋值给受保护或私有属性,这样数据就不会自动在视图中可用。然后,调用 with 方法时,传递一个希望在视图中可用的数组数据:

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 方法接收文件的完整路径作为其第一个参数:

/**
 * 构建信息
 *
 * @return $this
 */
public function build()
{
    return $this->view('emails.orders.shipped')
                ->attach('/path/to/file');
}

当添加文件到信息时,还可以将一个数组作为第二个参数传递给 attach 方法,指定显示的文件名和/或 MIME 类型:

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

原始数据附件

attachData 方法可用于将原始字节数据作为附件添加。例如,如果在内存中生成了一个 PDF,要将其添加到邮件中但不想写入到磁盘,就可以使用此方法。attachData 方法接收原始字节数据作为其第一个参数,文件名作为第二个参数,以及一个数组选项作为其第三个参数:

/**
 * 构建信息
 *
 * @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 变量在 Markdown 信息中不可使用。

嵌入原始数据附件

如果有要嵌入到邮件模板中的原始字节数据,可以在 $message 变量上使用 embedData 方法:

<body>
    Here is an image from raw data:

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

自定义 SwiftMailer 信息

Mailable 基类的 withSwiftMessage 方法允许您注册一个回调,在发送信息前调用,回调的参数是一个原始的 SwiftMailer 信息实例。让您在信息发送前可以自定义信息:

/**
 * 构建信息
 *
 * @return $this
 */
public function build()
{
    $this->view('emails.orders.shipped');

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

Markdown 邮件

Markdown 邮件信息允许您利用邮件中预先构建的模板和邮件通知组件。由于信息是用 Markdown 编写的,因此 Laravel 能够将信息渲染为漂亮的、响应式 HTML 模板,还会自动生成纯文本副本。

生成 Markdown 邮件

要生成一个邮件和对应的 Markdown 模板,可以在使用 Artisan 命令 make:mail--markdown 选项:

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

然后,在 build 方法中配置邮件时,调用 markdown 方法而不是 view 方法。markdown 方法接收 Markdown 模板名和一个可选的在模板中可用的数组数据:

/**
 * 构建信息
 *
 * @return $this
 */
public function build()
{
    return $this->from('example@example.com')
                ->markdown('emails.orders.shipped');
}

编写 Markdown 信息

Markdown 邮件将 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

在编写 Markdown 邮件时不要使用多余的缩进。Markdown 解析器会将其渲染为代码块。

按钮组件

按钮组件渲染为一个居中对齐的按钮链接。此组件接收两个参数,一个 url 和一个可选的 color。支持的颜色有 primarysuccesserror。可以根据需要添加多个按钮组件:

@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 邮件组件进行自定义。要导出组件,可以使用 Artisan 命令 vendor:publish
发布 laravel-mail 的资源:

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

此命令会将 Markdown 邮件组件发布到 resources/views/vendor/mail 目录中。mail 目录包含一个 html 和一个 markdown 目录,每个目录中包含每个组件相应的内容。html 目录中的组件用于生成 HTML 版本的邮件,markdown 目录对应的副本用于生成纯文本版本。可以随意自定义这些组件。

自定义 CSS

导出组件后,resources/views/vendor/mail/html/themes 目录会包含一个 default.css 文件。可以自定义此文件中的 CSS,样式会自动内嵌到 Markdown 邮件信息的 HTML 内容中。

如果要为 Markdown 组件构建一个全新的主题,可以在 html/themes 目录中编写一个新的 CSS 文件并更改 mail 配置文件中的 theme 选项。

发送邮件

发送邮件,可以使用 Mail Facadeto 方法。to 方法接收邮件地址,用户实例或用户集合。如果传递一个对象或对象的集合,在设置邮件收件人时,会自动使用他们的 emailname 属性,因此确保这些属性在对象中可用。指定收件人后,可以传递一个邮件类的实例给 send 方法:

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
{
    /**
     * 配送给定订单
     *
     * @param  Request  $request
     * @param  int  $orderId
     * @return Response
     */
    public function ship(Request $request, $orderId)
    {
        $order = Order::findOrFail($orderId);

        // 配送订单

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

当然,不仅限于在发送邮件时指定收件人。可以调用单个链式方法指定「收件人」,「抄送」和「私密抄送」:

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 Facade 的 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 Contract。现在,即使只在发送邮件时使用 send 方法,邮件也会通过队列执行,因为它实现了此 Contract:

use Illuminate\Contracts\Queue\ShouldQueue;

class OrderShipped extends Mailable implements ShouldQueue
{
    //
}

本地化邮件

Laravel 允许发送邮件时使用当前语言以外的语言,甚至邮件在队列中也会记住此语言。

为此,Illuminate\Mail\Mailable 类提供了一个 locale 方法用来设置要使用的语言。在格式化邮件时应用会自动切换到该语言,格式化完成后恢复为之前的语言:

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

用户首选语言

有时,应用存储了每个用户的首选语言。通过在一个模型或更多模型上实现 HasLocalePreference Contract,可以指示 Laravel 在发送邮件时使用存储的首选语言:

use Illuminate\Contracts\Translation\HasLocalePreference;

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

实现此接口后,Laravel 会在发送邮件和通知给模型时自动使用首选语言。因此,当使用此接口时就不用调用 locale 方法了:

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

邮件 & 本地开发

当开发发送邮件的应用时,您可能不想实际向邮件地址发送邮件。Laravel 提供了几种方法,在本地开发时「禁用」实际发送邮件。

日志驱动

log 邮件驱动会将所有邮件信息写入到日志文件以供检查,而不会发送邮件。有关应用环境配置的更多信息,可以查看 配置文档

通用收件人

Laravel 提供的另一个解决方案是设置框架发送的所有邮件的通用收件人。这样,应用生成的所有邮件都会发送到指定的地址,而不是发送信息时实际指定的地址。可以通过 config/mail.php 配置文件的 to 选项完成此操作:

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

Mailtrap

最后,可以使用像 Mailtrap 的服务和 smtp 驱动将邮件信息发送到一个「虚拟的」邮箱,在真实的邮件客户端中查看它们。此方法的好处是可以让您在 Mailtrap 的信息查看器中实际检查最终的邮件。

事件

Laravel 在发送邮件信息时触发了两个事件。信息发送前触发 MessageSending 事件,信息发送后触发 MessageSent 事件。注意,这些事件在邮件被发送时触发,而不是加入队列时。可以在 EventServiceProvider 中注册事件监听器来监听它们:

/**
 * 应用的事件监听器映射
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Mail\Events\MessageSending' => [
        'App\Listeners\LogSendingMessage',
    ],
    'Illuminate\Mail\Events\MessageSent' => [
        'App\Listeners\LogSentMessage',
    ],
];