郵件

簡介

傳送郵件不會很複雜。Laravel 提供簡潔的 API,並由熱門的 Symfony Mailer 驅動。Laravel 與 Symfony Mailer 提供使用 SMTP、Mailgun、Postmark、Amazon SES、sendmail 等方式寄信的 Driver,可讓我們使用偏好的本機或雲端服務來快速開始傳送郵件。

設定

可以使用專案的 config/mail.php 設定檔來設定 Laravel 的郵件服務。在這個檔案中,每個 Mailer(郵件傳送程式) 都可以有不同的設定,甚至還可以設定不同的「Transport」設定,這樣我們就可以在程式中使用不同的電子郵件服務來寄送不同的訊息。舉例來說,我們可以使用 Postmark 來寄送交易電子郵件,並使用 Amazon SES 來傳送大量寄送的電子郵件。

mail 設定檔中,可以看到一個 mailers 設定陣列。這個陣列中包含了 Laravel 支援的各個主要郵件 Driver / Transport 範例設定,而其中 default 設定值用來判斷專案預設要使用哪個 Mailer 來傳送電子郵件訊息。

Driver / Transport 的前置要求

如 Mailgun、Postmark,與 MailerSend 等基於 API 的 Driver 與使用 SMTP 伺服器寄送郵件比起來通常會比較簡單快速。若可能的話,我們推薦儘量使用這類 Driver。

Mailgun Driver

若要使用 Mailgun Driver,請使用 Composer 安裝 Symfony 的 Mailgun Mailer Transport:

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

接著,請在 config/mail.php 設定檔中將 default 選項設為 mailgun。設定好預設 Mailer 後,請確認一下 config/services.php 設定檔中是否包含下列選項:

1'mailgun' => [
2 'transport' => 'mailgun',
3 'domain' => env('MAILGUN_DOMAIN'),
4 'secret' => env('MAILGUN_SECRET'),
5],
1'mailgun' => [
2 'transport' => 'mailgun',
3 'domain' => env('MAILGUN_DOMAIN'),
4 'secret' => env('MAILGUN_SECRET'),
5],

若你使用的 Mailgun 地區不是美國的話,請在 services 設定檔中定義該地區的 Endpoint:

1'mailgun' => [
2 'domain' => env('MAILGUN_DOMAIN'),
3 'secret' => env('MAILGUN_SECRET'),
4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
5],
1'mailgun' => [
2 'domain' => env('MAILGUN_DOMAIN'),
3 'secret' => env('MAILGUN_SECRET'),
4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),
5],

Postmark Driver

若要使用 Postmark Driver,請使用 Composer 安裝 Symfony 的 Postmark Mailer Transport:

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

接著,請在 config/mail.php 設定檔中將 default 選項設為 postmark。設定好預設 Mailer 後,請確認一下 config/services.php 設定檔中是否包含下列選項:

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

若想為給定 Mailer 指定 Postmark 訊息串流(Message Stream),請在該 Mailer 的設定陣列中加上 message_stream_id 設定選項。該設定陣列可在 config/mail.php 設定檔中找到:

1'postmark' => [
2 'transport' => 'postmark',
3 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
4],
1'postmark' => [
2 'transport' => 'postmark',
3 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
4],

這樣一來,我們就能設定多個 Postmark Mailer,並給不同 Mailer 設定不同的訊息串流。

SES Driver

若要使用 Amazon SES Driver,必須先安裝 PHP 版的 Amazon SDK。可使用 Composer 套件管理員來安裝這個函式庫:

1composer require aws/aws-sdk-php
1composer require aws/aws-sdk-php

接著,請在 config/mail.php 設定檔中將 default 選項設為 ses,然後確認一下 config/services.php 設定檔中是否包含下列選項:

1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5],
1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5],

若要通過 Session Token 使用 AWS 的 Temporary Credential,請在專案的 SES 設定中加上 token 索引鍵:

1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'token' => env('AWS_SESSION_TOKEN'),
6],
1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'token' => env('AWS_SESSION_TOKEN'),
6],

若想定義要讓 Laravel 在寄送郵件時要傳給 AWS SDK 之 SendEmail 方法的額外的選項,可在 ses 設定中定義一個 options 陣列:

1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'options' => [
6 'ConfigurationSetName' => 'MyConfigurationSet',
7 'EmailTags' => [
8 ['Name' => 'foo', 'Value' => 'bar'],
9 ],
10 ],
11],
1'ses' => [
2 'key' => env('AWS_ACCESS_KEY_ID'),
3 'secret' => env('AWS_SECRET_ACCESS_KEY'),
4 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
5 'options' => [
6 'ConfigurationSetName' => 'MyConfigurationSet',
7 'EmailTags' => [
8 ['Name' => 'foo', 'Value' => 'bar'],
9 ],
10 ],
11],

MailerSend Driver

MailerSend,是一個交易式 (Transactional) 的 Email 與簡訊服務。MailerSend 自行維護了用於 Laravel 的、基於 API 的 Mail Driver。可以使用 Composer 套件管理員來安裝包含 MailerSend Driver 的套件:

1composer require mailersend/laravel-driver
1composer require mailersend/laravel-driver

安裝好套件後,請在專案的 .env 檔案中新增 MAILERSEND_API_KEY 環境變數。此外,頁請將 MAIL_MAILER 環境變數定義為 mailersend

1MAIL_MAILER=mailersend
2MAIL_FROM_ADDRESS=[email protected]
3MAIL_FROM_NAME="App Name"
4 
5MAILERSEND_API_KEY=your-api-key
1MAIL_MAILER=mailersend
2MAIL_FROM_ADDRESS=[email protected]
3MAIL_FROM_NAME="App Name"
4 
5MAILERSEND_API_KEY=your-api-key

欲瞭解更多有關 MailerSend 的資訊,包含如何使用 Hosted Template (託管的樣板),請參考 MailerSend Driver 的說明文件

Failover 設定

有時候,我們設定要用來寄送郵件的外部服務可能沒辦法用。因為這種情況,所以最好定義一個或多個備用的郵件寄送設定,以免主要寄送 Driver 無法使用。

若要定義備用 Mailer,請在 mail 設定檔中定義一個使用 failover Transport的 Mailer。failover Mailer的設定值呢列應包含一個 mailers 的陣列,並在其中參照用來寄送郵件之各個 Driver 的順序:

1'mailers' => [
2 'failover' => [
3 'transport' => 'failover',
4 'mailers' => [
5 'postmark',
6 'mailgun',
7 'sendmail',
8 ],
9 ],
10 
11 // ...
12],
1'mailers' => [
2 'failover' => [
3 'transport' => 'failover',
4 'mailers' => [
5 'postmark',
6 'mailgun',
7 'sendmail',
8 ],
9 ],
10 
11 // ...
12],

定義好 Failover Mailer 後,請將 mail 設定檔中的 default 設定索引鍵設為該 Failover Mailer 的名稱,以將其設為預設 Mailer。

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

產生 Mailable

在撰寫 Laravel 專案時,程式所寄出的所有郵件都以「Mailable」類別的形式呈現。這些類別保存在 app/Mail 目錄中。若沒看到這個目錄,請別擔心。使用 make:mail Artisan 指令初次建立 Mailable 類別時會自動產生該目錄:

1php artisan make:mail OrderShipped
1php artisan make:mail OrderShipped

撰寫 Mailable

產生 Mailable 類別後,請先開啟該類別,讓我們來看看該類別的內容。Mailable 類別可通過多個方法來進行設定,包含 envelopecontent、與 attachments 方法。

evelope 方法回傳 Illuminate\Mail\Mailables\Envelope 物件,用來定義標題,而有的時候也會用來定義收件者與訊息。content 方法回傳 Illuminate\Mail\Mailables\Content 物件,該物件定義用來產生訊息內容的 Blade 樣板

設定寄件人

使用 Evelope

首先,我們先來看看如何設定寄件人。或者,換句話說,也就是郵件要「從 (From)」誰那裡寄出。要設定寄件人,有兩種方法。第一種方法,我們可以在訊息的 Evelope 上指定「from」位址:

1use Illuminate\Mail\Mailables\Address;
2use Illuminate\Mail\Mailables\Envelope;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 from: new Address('[email protected]', 'Jeffrey Way'),
11 subject: 'Order Shipped',
12 );
13}
1use Illuminate\Mail\Mailables\Address;
2use Illuminate\Mail\Mailables\Envelope;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 from: new Address('[email protected]', 'Jeffrey Way'),
11 subject: 'Order Shipped',
12 );
13}

若有需要的話,可以指定 replyTo 位址:

1return new Envelope(
2 from: new Address('[email protected]', 'Jeffrey Way'),
3 replyTo: [
4 new Address('[email protected]', 'Taylor Otwell'),
5 ],
6 subject: 'Order Shipped',
7);
1return new Envelope(
2 from: new Address('[email protected]', 'Jeffrey Way'),
3 replyTo: [
4 new Address('[email protected]', 'Taylor Otwell'),
5 ],
6 subject: 'Order Shipped',
7);

使用全域的 from 位址

不過,若你的專案中所有的郵件都使用相同的寄件人位址,在每個產生的 Mailable 類別內都呼叫 from 方法會很麻煩。比起在每個 Mailable 內呼叫 from 方法,我們可以在 config/mail.php 設定檔中指定一個全域的「from」位址。若 Mailable 類別內沒有指定「from」位址,就會使用這個全域的位址:

1'from' => [
2 'address' => env('MAIL_FROM_ADDRESS', '[email protected]'),
3 'name' => env('MAIL_FROM_NAME', 'Example'),
4],
1'from' => [
2 'address' => env('MAIL_FROM_ADDRESS', '[email protected]'),
3 'name' => env('MAIL_FROM_NAME', 'Example'),
4],

​此外,也可以在 config/mail.php 設定檔中定義一個全域的「reply_to」位址:

1'reply_to' => ['address' => '[email protected]', 'name' => 'App Name'],
1'reply_to' => ['address' => '[email protected]', 'name' => 'App Name'],

​設定 View

在 Mailable 類別的 content 方法中,可以定義 view,或者,可以說在 content 方法中指定轉譯郵件內容時要使用哪個樣板。由於一般來說大部分郵件都是使用 [Blade 樣板]來轉譯內容的,因此在建立郵件內容時,我們就可以使用 Blade 樣板引擎的完整功能與便利:

1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'emails.orders.shipped',
8 );
9}
1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'emails.orders.shipped',
8 );
9}
lightbulb

可以建立一個 resources/views/emails 目錄來放置所有的郵件樣板。不過,不一定要放在這個目錄,可以隨意放在 resources/views 目錄下。

純文字郵件

若想為郵件定義純文字版本,可以在定義訊息的 Content 時使用 text 方法。與 view 參數類似,text 參數應為用來轉譯 E-Mail 內容的樣板名稱。可以同時為訊息定義 HTML 與純文字的版本:

1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'emails.orders.shipped',
8 text: 'emails.orders.shipped-text'
9 );
10}
1/**
2 * Get the message content definition.
3 */
4public function content(): Content
5{
6 return new Content(
7 view: 'emails.orders.shipped',
8 text: 'emails.orders.shipped-text'
9 );
10}

為了讓程式碼更清除,可以使用 html 參數。這個參數是 view 參數的別名:

1return new Content(
2 html: 'emails.orders.shipped',
3 text: 'emails.orders.shipped-text'
4);
1return new Content(
2 html: 'emails.orders.shipped',
3 text: 'emails.orders.shipped-text'
4);

View 資料

使用公開屬性

一般來說,在轉譯 HTML 版本的郵件時,我們會需要將資料傳入 View 來在其中使用。要將資料傳入 View 有兩種方法。第一種方法,即是在 Mailable 類別裡的公用變數,在 View 裡面可以直接使用。因此,舉例來說,我們可以將資料傳入 Mailable 類別的 Constructor(建構函式) 內,然後將資料設為該類別中定義的公用變數:

1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 public Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'emails.orders.shipped',
29 );
30 }
31}
1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 public Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'emails.orders.shipped',
29 );
30 }
31}

將資料設為公用變數後,在 View 中就自動可以使用該資料。因此在 Blade 樣板中,我們可以像存取其他資料一樣存取這些資料:

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

通過 with 參數:

若想在資料被傳給樣板前自訂其格式,可使用 Content 定義的 with 參數來手動將資料傳給 View。一般來說,我們還是會使用 Mailable 類別的 Constroctor 來傳入資料。不過,我們可以將該資料設為 protectedprivate 屬性,這樣這些資料才不會被自動暴露到樣板中:

1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 protected Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'emails.orders.shipped',
29 with: [
30 'orderName' => $this->order->name,
31 'orderPrice' => $this->order->price,
32 ],
33 );
34 }
35}
1<?php
2 
3namespace App\Mail;
4 
5use App\Models\Order;
6use Illuminate\Bus\Queueable;
7use Illuminate\Mail\Mailable;
8use Illuminate\Mail\Mailables\Content;
9use Illuminate\Queue\SerializesModels;
10 
11class OrderShipped extends Mailable
12{
13 use Queueable, SerializesModels;
14 
15 /**
16 * Create a new message instance.
17 */
18 public function __construct(
19 protected Order $order,
20 ) {}
21 
22 /**
23 * Get the message content definition.
24 */
25 public function content(): Content
26 {
27 return new Content(
28 view: 'emails.orders.shipped',
29 with: [
30 'orderName' => $this->order->name,
31 'orderPrice' => $this->order->price,
32 ],
33 );
34 }
35}

使用 with 方法傳入資料後,在 View 中就自動可以使用該資料。因此在 Blade 樣板中,我們可以像存取其他資料一樣存取這些資料:

1<div>
2 Price: {{ $orderPrice }}
3</div>
1<div>
2 Price: {{ $orderPrice }}
3</div>

附加檔案

若要將附件加到 E-Mail 中,可以在訊息的 attachments 方法所回傳的陣列內加上附件。首先,我們需要將附件的檔案路徑提供給 Attachment 類別的 fromPath 方法來加上附件:

1use Illuminate\Mail\Mailables\Attachment;
2 
3/**
4 * Get the attachments for the message.
5 *
6 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
7 */
8public function attachments(): array
9{
10 return [
11 Attachment::fromPath('/path/to/file'),
12 ];
13}
1use Illuminate\Mail\Mailables\Attachment;
2 
3/**
4 * Get the attachments for the message.
5 *
6 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
7 */
8public function attachments(): array
9{
10 return [
11 Attachment::fromPath('/path/to/file'),
12 ];
13}

將檔案附加至訊息時,也可以使用 aswithMime 方法來指定附件的顯示名稱與/或 MIME 型別:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromPath('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromPath('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

從 Disk 中附加檔案

若有儲存在檔案系統 Disk中的檔案,可使用 fromStorage 方法來將其附加至郵件中:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file'),
10 ];
11}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file'),
10 ];
11}

當然,也可以指定附件的名稱與 MIME 型別:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorage('/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

若想指定非預設的 Disk,可使用 fromStorageDisk 方法:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorageDisk('s3', '/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromStorageDisk('s3', '/path/to/file')
10 ->as('name.pdf')
11 ->withMime('application/pdf'),
12 ];
13}

原始資料附加檔案

可使用 fromData 方法來將位元組原始字串 (Raw String of Bytes) 形式的值作為附件附加。舉例來說,我們可能會在記憶體內產生 PDF,然後想在不寫入 Disk 的情況下將其附加到郵件上。fromData 方法需傳入一個閉包,Laravel 會使用該閉包用來取得原始資料字串,以及附加檔案的名稱:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
10 ->withMime('application/pdf'),
11 ];
12}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [
9 Attachment::fromData(fn () => $this->pdf, 'Report.pdf')
10 ->withMime('application/pdf'),
11 ];
12}

內嵌的附加檔案

一般來說,要把圖片內嵌到郵件裡面是很麻煩的。不過,Laravel 提供了一個方便的方法可以將圖片附加到郵件裡。若要內嵌圖片,請使用郵件樣板內 $message 變數中的 embed 方法。Laravel 會自動為所有的郵件樣板提供這個 $message 變數,所以我們不需要手動傳入:

1<body>
2 Here is an image:
3 
4 <img src="{{ $message->embed($pathToImage) }}">
5</body>
1<body>
2 Here is an image:
3 
4 <img src="{{ $message->embed($pathToImage) }}">
5</body>
exclamation

$message 變數無法在純文字訊息樣板中使用,因為純文字樣板無法使用內嵌的附加檔案。

內嵌原始資料附件

若有欲嵌入到郵件樣板中的原始圖片字串,可呼叫 $message 變數上的 embedData 方法。呼叫 embedData 方法時,請提供一個欲設定給嵌入圖片的檔案名稱:

1<body>
2 Here is an image from raw data:
3 
4 <img src="{{ $message->embedData($data, 'example-image.jpg') }}">
5</body>
1<body>
2 Here is an image from raw data:
3 
4 <img src="{{ $message->embedData($data, 'example-image.jpg') }}">
5</body>

可附加的物件

雖然一般來說,以簡單的字串路徑來將檔案附加到訊息上通常就夠了。但很多情況下,在專案中,可附加的物件都是以類別形式存在的。舉例來說,若要將照片附加到訊息中,則專案內可能有一個用來代表該照片的 Photo Model。 這時,若可以直接將 PhotoModel 附加到attach` 方法上不是很方便嗎?使用可附加的物件,就可以輕鬆達成。

若要開始定義可附加物件,請在要被附加到訊息的物件上實作 Illuminate\Contracts\Mail\Attachable 介面。該介面會要求這個類別定義 toMailAttachment,且該方法應回傳 Illuminate\Mail\Attachment 實體:

1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Mail\Attachable;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Mail\Attachment;
8 
9class Photo extends Model implements Attachable
10{
11 /**
12 * Get the attachable representation of the model.
13 */
14 public function toMailAttachment(): Attachment
15 {
16 return Attachment::fromPath('/path/to/file');
17 }
18}
1<?php
2 
3namespace App\Models;
4 
5use Illuminate\Contracts\Mail\Attachable;
6use Illuminate\Database\Eloquent\Model;
7use Illuminate\Mail\Attachment;
8 
9class Photo extends Model implements Attachable
10{
11 /**
12 * Get the attachable representation of the model.
13 */
14 public function toMailAttachment(): Attachment
15 {
16 return Attachment::fromPath('/path/to/file');
17 }
18}

定義好可附加的物件後,就可以在建立 E-Mail 訊息時從 attachments 方法中回傳該物件的實體:

1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [$this->photo];
9}
1/**
2 * Get the attachments for the message.
3 *
4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>
5 */
6public function attachments(): array
7{
8 return [$this->photo];
9}

當然,要附加的資料也可能存放在如 Amazon S3 之類的遠端檔案儲存服務上。因此,在 Laravel 中,我們可以從存放在專案檔案系統磁碟上的資料來產生附件實體:

1// 從預設磁碟上的檔案來建立附件...
2return Attachment::fromStorage($this->path);
3 
4// 從指定磁碟上的檔案來建立附件...
5return Attachment::fromStorageDisk('backblaze', $this->path);
1// 從預設磁碟上的檔案來建立附件...
2return Attachment::fromStorage($this->path);
3 
4// 從指定磁碟上的檔案來建立附件...
5return Attachment::fromStorageDisk('backblaze', $this->path);

此外,也可以使用記憶體中的資料來建立附件實體。若要從記憶體中建立,請傳入一個閉包給 fromData 方法。該閉包應回傳代表該附件的原始資料:

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

Laravel 也提供了一些額外的方法,讓我們可以自訂附件。舉例來說,可以使用 aswithMime 方法來自訂檔案名稱與 MIME 型別:

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

標頭 (Header)

有時候,我們會需要在連外訊息中加上額外的標頭。舉例來說,我們可能會需要設定自定的 Message-Id 或其他任意的文字標頭。

若要設定標頭,請在 Mailable 內定義一個 headers 方法。headers 方法應回傳 Illuminate\Mail\Mailables\Headers 實體。該類別接受 messageIdreferences、與 text 參數。當然,我們只需要提供該訊息所需要的參數即可:

1use Illuminate\Mail\Mailables\Headers;
2 
3/**
4 * Get the message headers.
5 */
6public function headers(): Headers
7{
8 return new Headers(
9 messageId: '[email protected]',
10 references: ['[email protected]'],
11 text: [
12 'X-Custom-Header' => 'Custom Value',
13 ],
14 );
15}
1use Illuminate\Mail\Mailables\Headers;
2 
3/**
4 * Get the message headers.
5 */
6public function headers(): Headers
7{
8 return new Headers(
9 messageId: '[email protected]',
10 references: ['[email protected]'],
11 text: [
12 'X-Custom-Header' => 'Custom Value',
13 ],
14 );
15}

Tag 與詮釋資料

有的第三方 E-Mail 提供商,如 Mailgun 或 Postmark 等,支援訊息的「Tag」與「詮釋資料 (Metadata)」,使用 Tag 與詮釋資料,就可以對專案所送出的 E-Mail 進行分組與追蹤。可以通過 Evelope 定義來為 E-Mail 訊息加上 Tag 與詮釋資料:

1use Illuminate\Mail\Mailables\Envelope;
2 
3/**
4 * Get the message envelope.
5 *
6 * @return \Illuminate\Mail\Mailables\Envelope
7 */
8public function envelope(): Envelope
9{
10 return new Envelope(
11 subject: 'Order Shipped',
12 tags: ['shipment'],
13 metadata: [
14 'order_id' => $this->order->id,
15 ],
16 );
17}
1use Illuminate\Mail\Mailables\Envelope;
2 
3/**
4 * Get the message envelope.
5 *
6 * @return \Illuminate\Mail\Mailables\Envelope
7 */
8public function envelope(): Envelope
9{
10 return new Envelope(
11 subject: 'Order Shipped',
12 tags: ['shipment'],
13 metadata: [
14 'order_id' => $this->order->id,
15 ],
16 );
17}

若使用 Mailgun Driver,請參考 Mailgun 說明文件中有關 Tag詮釋資料的更多資訊。同樣地,也請參考 Postmark 說明文件中有關 Tag詮釋資料的更多資料。

若使用 Amazon SES 來寄送 E-Mail,則可使用 metadata 方法來將 SES「Tag」附加到訊息上。

自訂 Symfony Message

Laravel 的郵件是使用 Symfony Mailer 驅動的。在 Laravel 中,我們可以註冊一個在寄送訊息前會被呼叫的回呼,該回呼會收到 Symfony Message 實體。這樣,我們就能在郵件被寄送前深度自定訊息。若要註冊這個回呼,可以在 Evelope 實體上定義一個 using 參數:

1use Illuminate\Mail\Mailables\Envelope;
2use Symfony\Component\Mime\Email;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 subject: 'Order Shipped',
11 using: [
12 function (Email $message) {
13 // ...
14 },
15 ]
16 );
17}
1use Illuminate\Mail\Mailables\Envelope;
2use Symfony\Component\Mime\Email;
3 
4/**
5 * Get the message envelope.
6 */
7public function envelope(): Envelope
8{
9 return new Envelope(
10 subject: 'Order Shipped',
11 using: [
12 function (Email $message) {
13 // ...
14 },
15 ]
16 );
17}

Markdown 的 Mailer

Markdown Mailer 訊息可讓我們在 Mailable 內使用內建樣板與 Mail Notification 的元件。由於使用 Markdown 來撰寫訊息,因此 Laravel 就可為這些郵件轉譯出漂亮的回應式 HTML 樣板,並自動轉譯出純文字版本的郵件。

產生 Markdown 的 Malable

若要產生有對應 Markdown 樣板的 Mailable,請使用 make:mail Artisan 指令的 --markdown 選項:

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

接著,在 content 方法中設定 Mailable 的 Content 定義時,請將 view 參數改成 markdown 參數:

1use Illuminate\Mail\Mailables\Content;
2 
3/**
4 * Get the message content definition.
5 */
6public function content(): Content
7{
8 return new Content(
9 markdown: 'emails.orders.shipped',
10 with: [
11 'url' => $this->orderUrl,
12 ],
13 );
14}
1use Illuminate\Mail\Mailables\Content;
2 
3/**
4 * Get the message content definition.
5 */
6public function content(): Content
7{
8 return new Content(
9 markdown: 'emails.orders.shipped',
10 with: [
11 'url' => $this->orderUrl,
12 ],
13 );
14}

撰寫 Markdown 訊息

Markdown 的 Markdown 使用 Blade 元件與 Markdown 格式的組合,讓我們能輕鬆地使用 Laravel 內建的 E-Mail UI 元件來建立訊息:

1<x-mail::message>
2# Order Shipped
3 
4Your order has been shipped!
5 
6<x-mail::button :url="$url">
7View Order
8</x-mail::button>
9 
10Thanks,<br>
11{{ config('app.name') }}
12</x-mail::message>
1<x-mail::message>
2# Order Shipped
3 
4Your order has been shipped!
5 
6<x-mail::button :url="$url">
7View Order
8</x-mail::button>
9 
10Thanks,<br>
11{{ config('app.name') }}
12</x-mail::message>
lightbulb

在撰寫 Markdown 郵件時請不要增加縮排。依據 Markdown 標準,Markdown 解析程式會將縮排的內容轉譯為程式碼區塊。

Button 元件

Button 元件轉譯一個置中的按鈕連結。這個元件接受兩個引數,一個是 url 網址,另一個則是可選的 color 顏色。支援的顏色有 primarysuccesserror。在訊息中可以加上不限數量的 Button 元件:

1<x-mail::button :url="$url" color="success">
2View Order
3</x-mail::button>
1<x-mail::button :url="$url" color="success">
2View Order
3</x-mail::button>

Panel 元件

Panel 元件將給定的文字區塊轉譯在一個面板中,面板的底色與訊息中其他部分的背景色稍有不同。我們可以使用 Panel 元件來讓給定區塊的文字較為醒目:

1<x-mail::panel>
2This is the panel content.
3</x-mail::panel>
1<x-mail::panel>
2This is the panel content.
3</x-mail::panel>

Table 元件

Table 元件可讓我們將 Markdown 表格轉為 HTML 表格。該元件接受一個 Markdown 表格作為其內容。支援使用預設的 Markdown 表格對其格式來對其表格欄位:

1<x-mail::table>
2| Laravel | Table | Example |
3| ------------- |:-------------:| --------:|
4| Col 2 is | Centered | $10 |
5| Col 3 is | Right-Aligned | $20 |
6</x-mail::table>
1<x-mail::table>
2| Laravel | Table | Example |
3| ------------- |:-------------:| --------:|
4| Col 2 is | Centered | $10 |
5| Col 3 is | Right-Aligned | $20 |
6</x-mail::table>

自訂元件

可以將所有的 Markdown 郵件元件匯出到專案內來自訂這些元件。若要匯出元件,請使用 vendor:publish Artisan 指令來安裝(Publish) laravel-mail 素材標籤:

1php artisan vendor:publish --tag=laravel-mail
1php 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 呈現上會自動被轉換為內嵌的 CSS 樣式:

若想為 Laravel Markdown 元件製作一個全新的主題,可在 html/themes 目錄下放置一個 CSS 檔。命名好 CSS 檔並保存後,請修改專案 config/mail.php 設定檔中的 theme 選項為該新主題的名稱:

若要為個別 Mailable 自訂主題,可在 Mailable 類別上將 $theme 屬性設為傳送該 Mailable 時要使用的主題名稱:

傳送郵件

若要傳送郵件,請使用 Mail Facade上的to方法。可傳入電子郵件位址、使用者實體、或是一組包含使用者的 Collection 給to方法。若傳入物件或一組包含物件的 Collection,則 Mailer 在判斷收件人時會自動使用這些物件的emailname屬性來判斷。因此,請確認這些物件上是否有這兩個屬性。指定好收件人後,就可傳入 Mailable 類別的實體給send` 方法:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use App\Mail\OrderShipped;
7use App\Models\Order;
8use Illuminate\Http\RedirectResponse;
9use Illuminate\Http\Request;
10use Illuminate\Support\Facades\Mail;
11 
12class OrderShipmentController extends Controller
13{
14 /**
15 * Ship the given order.
16 */
17 public function store(Request $request): RedirectResponse
18 {
19 $order = Order::findOrFail($request->order_id);
20 
21 // 訂單出貨...
22 
23 Mail::to($request->user())->send(new OrderShipped($order));
24 
25 return redirect('/orders');
26 }
27}
1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Http\Controllers\Controller;
6use App\Mail\OrderShipped;
7use App\Models\Order;
8use Illuminate\Http\RedirectResponse;
9use Illuminate\Http\Request;
10use Illuminate\Support\Facades\Mail;
11 
12class OrderShipmentController extends Controller
13{
14 /**
15 * Ship the given order.
16 */
17 public function store(Request $request): RedirectResponse
18 {
19 $order = Order::findOrFail($request->order_id);
20 
21 // 訂單出貨...
22 
23 Mail::to($request->user())->send(new OrderShipped($order));
24 
25 return redirect('/orders');
26 }
27}

傳送訊息時,除了「to」方法能用來指定收件人外,還可以指定「CC(副本)」與「BCC(密件副本)」收件人。可將「to」、「cc」、「bcc」等方法串聯使用,以指定這些方法對應的收件人:

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

在收件人中迴圈

有時候,我們會需要迭代一組收件人或 E-Mail 位址的陣列來將 Mailable 傳送給多個收件人。不過,因為 to 方法會將 E-Mail 位址加到 Mailable 的收件人列表上,因此每次循環都會將該郵件再傳送給之前的收件人一次。所以,每個收件人都需要重新建立一個新的 Mailable 實體:

1foreach (['[email protected]', '[email protected]'] as $recipient) {
2 Mail::to($recipient)->send(new OrderShipped($order));
3}
1foreach (['[email protected]', '[email protected]'] as $recipient) {
2 Mail::to($recipient)->send(new OrderShipped($order));
3}

使用指定的 Mailer 來傳送郵件

預設情況下,Laravel 會使用專案 mail 設定中設為 default 的 Mailaer 來寄送郵件。不過,也可以使用 mailer 方法來特定的 Mailer 設定傳送訊息:

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

將郵件放入佇列

將郵件訊息放入佇列

由於傳送郵件訊息可能對程式的 Response 時間造成負面影響,因此許多開發人員都選擇將郵件訊息放入陣列來在背景執行。在 Laravel 中,使用內建的統一佇列 API,就能輕鬆地將郵件放入佇列。若要將郵件訊息放入佇列,請在指定好收件人後使用 Mail Facade 的 queue 方法:

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

這個方法會自動將任務推入佇列,這樣訊息就會在背景傳送。在使用這個功能前,會需要先設定佇列

延遲訊息佇列

若想延遲傳送某個佇列訊息,可使用 later 方法。later 方法的第一個引數是 DateTime 實體,用來表示該訊息何時寄出:

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

推入指定的佇列

由於所有使用 make:mail 指令產生的 Mailable 類別都使用 Illiminate\Bus\Queuable Trait,因此我們可以在任何一個 Mailable 類別實體上呼叫 onQueueonConnection 方法,可讓我們指定該訊息要使用的佇列名稱:

1$message = (new OrderShipped($order))
2 ->onConnection('sqs')
3 ->onQueue('emails');
4 
5Mail::to($request->user())
6 ->cc($moreUsers)
7 ->bcc($evenMoreUsers)
8 ->queue($message);
1$message = (new OrderShipped($order))
2 ->onConnection('sqs')
3 ->onQueue('emails');
4 
5Mail::to($request->user())
6 ->cc($moreUsers)
7 ->bcc($evenMoreUsers)
8 ->queue($message);

預設佇列

若有想要永遠放入佇列的 Mailable 類別,可在該類別上實作 ShouldQueue Contract。接著,即使使用 send 方法來寄送郵件,由於該 Mailable 有實作 ShouldQueue Contract,因此還是會被放入佇列:

1use Illuminate\Contracts\Queue\ShouldQueue;
2 
3class OrderShipped extends Mailable implements ShouldQueue
4{
5 // ...
6}
1use Illuminate\Contracts\Queue\ShouldQueue;
2 
3class OrderShipped extends Mailable implements ShouldQueue
4{
5 // ...
6}

佇列的 Mailable 與資料庫 Transaction

當佇列 Mailable 是在資料庫 Transaction 內分派(Dispatch)的時候,這個 Mailable 可能會在資料庫 Transaction 被 Commit 前就被佇列進行處理了。發生這種情況時,在資料庫 Transaction 期間對 Model 或資料庫記錄所做出的更新可能都還未反應到資料庫內。另外,所有在 Transaction 期間新增的 Model 或資料庫記錄也可能還未出現在資料庫內。若 Mailable 有使用這些 Model 的話,在處理該佇列 Mailable 的任務時可能會出現未預期的錯誤。

若佇列的 after_commit 選項設為 false,則我們還是可以通過在寄送郵件訊息前呼叫 afterCommit 方法來表示出該 Mailable 應在所有資料庫 Transaction 都被 Commit 後才分派:

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

或者,也可以在 Mailable 的 Constructor 上呼叫 afterCommit 方法:

1<?php
2 
3namespace App\Mail;
4 
5use Illuminate\Bus\Queueable;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Mail\Mailable;
8use Illuminate\Queue\SerializesModels;
9 
10class OrderShipped extends Mailable implements ShouldQueue
11{
12 use Queueable, SerializesModels;
13 
14 /**
15 * Create a new message instance.
16 */
17 public function __construct()
18 {
19 $this->afterCommit();
20 }
21}
1<?php
2 
3namespace App\Mail;
4 
5use Illuminate\Bus\Queueable;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Mail\Mailable;
8use Illuminate\Queue\SerializesModels;
9 
10class OrderShipped extends Mailable implements ShouldQueue
11{
12 use Queueable, SerializesModels;
13 
14 /**
15 * Create a new message instance.
16 */
17 public function __construct()
18 {
19 $this->afterCommit();
20 }
21}
lightbulb

要瞭解更多有關這類問題的解決方法,請參考有關佇列任務與資料庫 Transaction 有關的說明文件。

轉譯 Mailable

有時候我們會想在不寄送 Mailable 的情況下截取其 HTML 內容。若要截取其內容,可呼叫 Mailable 的 render 方法。該方法會以字串回傳該 Mailable 的 HTML 取值內容:

1use App\Mail\InvoicePaid;
2use App\Models\Invoice;
3 
4$invoice = Invoice::find(1);
5 
6return (new InvoicePaid($invoice))->render();
1use App\Mail\InvoicePaid;
2use App\Models\Invoice;
3 
4$invoice = Invoice::find(1);
5 
6return (new InvoicePaid($invoice))->render();

在瀏覽器內預覽 Mailable

在設計 Mailable 樣板時,若能像普通的 Blade 樣板一樣在瀏覽器中預覽轉譯後的 Mailable 該有多方便。因為這樣,在 Laravel 中,可以直接在 Route 閉包或 Controller 中回傳任何的 Mailable。若回傳 Mailable,則會轉譯該 Mailable 並顯示在瀏覽器上,讓我們不需將其寄到真實的電子郵件上也能快速檢視其設計:

1Route::get('/mailable', function () {
2 $invoice = App\Models\Invoice::find(1);
3 
4 return new App\Mail\InvoicePaid($invoice);
5});
1Route::get('/mailable', function () {
2 $invoice = App\Models\Invoice::find(1);
3 
4 return new App\Mail\InvoicePaid($invoice);
5});

本土化 Mailable

在 Laravel 中,可以使用與 Request 中不同的語系設定來傳送郵件,且在郵件被放入佇列後依然會使用所設定的語系。

若要設定語系,請使用 Mail Facade 提供的 locale 方法來設定要使用的語言。在轉譯 Mailable 樣板時,程式會先進入這個語系中,轉譯完畢後再回到之前的語系:

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

使用者偏好的語系

有時候,我們的程式會儲存每個使用者偏好的語言。只要在一個或多個 Model 上實作 HasLocalePreference Contract,就可以讓 Laravel 在寄送郵件時使用這些儲存的語系:

1use Illuminate\Contracts\Translation\HasLocalePreference;
2 
3class User extends Model implements HasLocalePreference
4{
5 /**
6 * Get the user's preferred locale.
7 */
8 public function preferredLocale(): string
9 {
10 return $this->locale;
11 }
12}
1use Illuminate\Contracts\Translation\HasLocalePreference;
2 
3class User extends Model implements HasLocalePreference
4{
5 /**
6 * Get the user's preferred locale.
7 */
8 public function preferredLocale(): string
9 {
10 return $this->locale;
11 }
12}

實作好該介面後,向該 Model 寄送 Mailable 或通知時,Laravel 會自動使用偏好的語系。因此,使用該介面時不需呼叫 locale 方法:

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

測試

測試 Mailable 的內容

Laravel 提供了各種可用來檢查 Mailable 結構的方法。此外,Laravel 還提供了多種方便的方法,可讓你測試 Mailable 是否包含預期的內容。這些測試方法有:assertSeeInHtml, assertDontSeeInHtml, assertSeeInOrderInHtml, assertSeeInText, assertDontSeeInText, assertSeeInOrderInText, assertHasAttachment, assertHasAttachedData, assertHasAttachmentFromStorage, 與 assertHasAttachmentFromStorageDisk

就和預期的一樣,有「HTML」的^ Assertion 判斷 HTML 版本的 Mailable 是否包含給定字串,而「Text」版本的 Assertion 則判斷純文字版本的 Mailable 是否包含給定字串:

1use App\Mail\InvoicePaid;
2use App\Models\User;
3 
4public function test_mailable_content(): void
5{
6 $user = User::factory()->create();
7 
8 $mailable = new InvoicePaid($user);
9 
10 $mailable->assertFrom('[email protected]');
11 $mailable->assertTo('[email protected]');
12 $mailable->assertHasCc('[email protected]');
13 $mailable->assertHasBcc('[email protected]');
14 $mailable->assertHasReplyTo('[email protected]');
15 $mailable->assertHasSubject('Invoice Paid');
16 $mailable->assertHasTag('example-tag');
17 $mailable->assertHasMetadata('key', 'value');
18 
19 $mailable->assertSeeInHtml($user->email);
20 $mailable->assertSeeInHtml('Invoice Paid');
21 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
22 
23 $mailable->assertSeeInText($user->email);
24 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);
25 
26 $mailable->assertHasAttachment('/path/to/file');
27 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
28 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
29 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
30 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
31}
1use App\Mail\InvoicePaid;
2use App\Models\User;
3 
4public function test_mailable_content(): void
5{
6 $user = User::factory()->create();
7 
8 $mailable = new InvoicePaid($user);
9 
10 $mailable->assertFrom('[email protected]');
11 $mailable->assertTo('[email protected]');
12 $mailable->assertHasCc('[email protected]');
13 $mailable->assertHasBcc('[email protected]');
14 $mailable->assertHasReplyTo('[email protected]');
15 $mailable->assertHasSubject('Invoice Paid');
16 $mailable->assertHasTag('example-tag');
17 $mailable->assertHasMetadata('key', 'value');
18 
19 $mailable->assertSeeInHtml($user->email);
20 $mailable->assertSeeInHtml('Invoice Paid');
21 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);
22 
23 $mailable->assertSeeInText($user->email);
24 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);
25 
26 $mailable->assertHasAttachment('/path/to/file');
27 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));
28 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);
29 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
30 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);
31}

測試 Mailable 的寄送

我們建議將 Mailable 內容與 Mailable 是否已寄送給特定使用者的測試分開來進行。一般來說,Mailable 的內容通常與要測試的程式碼不相關,因此只需要判斷 Laravel 是否有寄送給定的 Mailable 即可。

可以使用 Mail Facade 的 fake 方法來防止郵件被寄出。呼叫 Mail Facade 的 fake 方法後,就可以判斷 Mailable 是否有被寄送給使用者,並檢查 Mailable 中的資料:

1<?php
2 
3namespace Tests\Feature;
4 
5use App\Mail\OrderShipped;
6use Illuminate\Support\Facades\Mail;
7use Tests\TestCase;
8 
9class ExampleTest extends TestCase
10{
11 public function test_orders_can_be_shipped(): void
12 {
13 Mail::fake();
14 
15 // 進行訂單出貨...
16 
17 // 判斷沒有寄出任何 Mailable...
18 Mail::assertNothingSent();
19 
20 // 判斷 Mailable 是否已寄出...
21 Mail::assertSent(OrderShipped::class);
22 
23 // 判斷 Mailable 是否已寄出兩次...
24 Mail::assertSent(OrderShipped::class, 2);
25 
26 // 判斷 Mailable 是否未寄出...
27 Mail::assertNotSent(AnotherMailable::class);
28 
29 // 判斷是否已寄出總共 3 個 Mailable...
30 Mail::assertSentCount(3);
31 }
32}
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Mail\OrderShipped;
6use Illuminate\Support\Facades\Mail;
7use Tests\TestCase;
8 
9class ExampleTest extends TestCase
10{
11 public function test_orders_can_be_shipped(): void
12 {
13 Mail::fake();
14 
15 // 進行訂單出貨...
16 
17 // 判斷沒有寄出任何 Mailable...
18 Mail::assertNothingSent();
19 
20 // 判斷 Mailable 是否已寄出...
21 Mail::assertSent(OrderShipped::class);
22 
23 // 判斷 Mailable 是否已寄出兩次...
24 Mail::assertSent(OrderShipped::class, 2);
25 
26 // 判斷 Mailable 是否未寄出...
27 Mail::assertNotSent(AnotherMailable::class);
28 
29 // 判斷是否已寄出總共 3 個 Mailable...
30 Mail::assertSentCount(3);
31 }
32}

若要將 Mailable 放在佇列中以在背景寄送,請使用 assertQueued 方法來代替 assertSent 方法:

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

可以傳入一個閉包給 assertSentassertNotSentassertQueuedassertNotQueued 方法來判斷 Mailable 是否通過給定的「真值測試 (Truth Test)」。若至少有一個寄出的 Mailable 通過給定的真值測試,則該 Assertion 會被視為成功:

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

呼叫 Mail Facade 的 Assertion 方法時,所提供的閉包內收到的 Mailable 實體上有一些實用的方法,可用來檢查 Mailable:

1Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) {
2 return $mail->hasTo($user->email) &&
3 $mail->hasCc('...') &&
4 $mail->hasBcc('...') &&
5 $mail->hasReplyTo('...') &&
6 $mail->hasFrom('...') &&
7 $mail->hasSubject('...');
8});
1Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($user) {
2 return $mail->hasTo($user->email) &&
3 $mail->hasCc('...') &&
4 $mail->hasBcc('...') &&
5 $mail->hasReplyTo('...') &&
6 $mail->hasFrom('...') &&
7 $mail->hasSubject('...');
8});

Mailable 實體也包含了多個實用方法,可用來檢查 Mailable 上的附件:

1use Illuminate\Mail\Mailables\Attachment;
2 
3Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
4 return $mail->hasAttachment(
5 Attachment::fromPath('/path/to/file')
6 ->as('name.pdf')
7 ->withMime('application/pdf')
8 );
9});
10 
11Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
12 return $mail->hasAttachment(
13 Attachment::fromStorageDisk('s3', '/path/to/file')
14 );
15});
16 
17Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) {
18 return $mail->hasAttachment(
19 Attachment::fromData(fn () => $pdfData, 'name.pdf')
20 );
21});
1use Illuminate\Mail\Mailables\Attachment;
2 
3Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
4 return $mail->hasAttachment(
5 Attachment::fromPath('/path/to/file')
6 ->as('name.pdf')
7 ->withMime('application/pdf')
8 );
9});
10 
11Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {
12 return $mail->hasAttachment(
13 Attachment::fromStorageDisk('s3', '/path/to/file')
14 );
15});
16 
17Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) {
18 return $mail->hasAttachment(
19 Attachment::fromData(fn () => $pdfData, 'name.pdf')
20 );
21});

讀者可能已經注意到,總共有兩個方法可用來檢查郵件是否未被送出:assertNotSentassertNotQueued。有時候,我們可能會希望判斷沒有任何郵件被寄出,而且 也沒有任何郵件被放入佇列。若要判斷是否沒有郵件被寄出或放入佇列,可使用 assertNothingOutgoingassertNotOutgoing 方法:

1Mail::assertNothingOutgoing();
2 
3Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
4 return $mail->order->id === $order->id;
5});
1Mail::assertNothingOutgoing();
2 
3Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {
4 return $mail->order->id === $order->id;
5});

郵件與本機開發

在開發有寄送郵件的程式時,我們通常都不會想實際將郵件寄到真實的 E-Mail 位址上。Laravel 提供了數種數種方法來在本機上開發時「禁用」郵件的實際傳送。

Log Driver

log 郵件 Driver 不會實際寄送電子郵件,而是將所有電子郵件訊息寫入日誌檔以供檢查。一般來說,Log Driver 只會在開發環境上使用。有關一找不同環境設定專案的方法,請參考設定的說明文件

HELO / Mailtrap / Mailpit

或者,也可以使用如 HELOMailtrap 這類服務搭配 smtp Driver 來將電子郵件寄送到一個「模擬的」收件夾,並像在真的郵件用戶端一樣檢視這些郵件。這種做法的好處就是可以在 Mailtrap 的訊息檢視工具中實際檢視寄出的郵件。

若使用 Laravel Sail,,則可使用 Mailpit 來預覽訊息。當 Sail 有在執行時,可在 http://localhost:8025 上存取 Mailpit 的界面。

使用全域的 to 位址

最後一種方法,就是我們可以叫用 Mail Facade 提供的 alwaysTo 方法指定一個全域的「to」位址。一般來說,應在專案的其中一個 Service Provider 內 boot 方法中呼叫這個方法:

1use Illuminate\Support\Facades\Mail;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 if ($this->app->environment('local')) {
9 Mail::alwaysTo('[email protected]');
10 }
11}
1use Illuminate\Support\Facades\Mail;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 if ($this->app->environment('local')) {
9 Mail::alwaysTo('[email protected]');
10 }
11}

事件

在處理郵件訊息寄送時,Laravel 會觸發兩個事件。MessageSending 事件會在寄出郵件前觸發,而MessageSent 事件則會在訊息寄出後觸發。請記得,這些事件都是在 寄送 郵件的時候出發的,而不是在放入佇列時觸發。可以在 App\Providers\EventServiceProvider Service Provider 上為這些 Event 註冊 Listener:

1use App\Listeners\LogSendingMessage;
2use App\Listeners\LogSentMessage;
3use Illuminate\Mail\Events\MessageSending;
4use Illuminate\Mail\Events\MessageSent;
5 
6/**
7 * The event listener mappings for the application.
8 *
9 * @var array
10 */
11protected $listen = [
12 MessageSending::class => [
13 LogSendingMessage::class,
14 ],
15 
16 MessageSent::class => [
17 LogSentMessage::class,
18 ],
19];
1use App\Listeners\LogSendingMessage;
2use App\Listeners\LogSentMessage;
3use Illuminate\Mail\Events\MessageSending;
4use Illuminate\Mail\Events\MessageSent;
5 
6/**
7 * The event listener mappings for the application.
8 *
9 * @var array
10 */
11protected $listen = [
12 MessageSending::class => [
13 LogSendingMessage::class,
14 ],
15 
16 MessageSent::class => [
17 LogSentMessage::class,
18 ],
19];

自訂 Transport

Laravel 中包含了許多的 Mail Transport。不過,有時候我們可能會需要撰寫自己的 Transport 來使用 Laravel 預設未支援的其他服務來寄送郵件。要開始撰寫 Transport,請先定義一個繼承了Symfony\Component\Mailer\Transport\AbstractTransport 的類別。接著,請在該 Transport 上實作 doSend__toString() 方法:

1use MailchimpTransactional\ApiClient;
2use Symfony\Component\Mailer\SentMessage;
3use Symfony\Component\Mailer\Transport\AbstractTransport;
4use Symfony\Component\Mime\Address;
5use Symfony\Component\Mime\MessageConverter;
6 
7class MailchimpTransport extends AbstractTransport
8{
9 /**
10 * Create a new Mailchimp transport instance.
11 */
12 public function __construct(
13 protected ApiClient $client,
14 ) {
15 parent::__construct();
16 }
17 
18 /**
19 * {@inheritDoc}
20 */
21 protected function doSend(SentMessage $message): void
22 {
23 $email = MessageConverter::toEmail($message->getOriginalMessage());
24 
25 $this->client->messages->send(['message' => [
26 'from_email' => $email->getFrom(),
27 'to' => collect($email->getTo())->map(function (Address $email) {
28 return ['email' => $email->getAddress(), 'type' => 'to'];
29 })->all(),
30 'subject' => $email->getSubject(),
31 'text' => $email->getTextBody(),
32 ]]);
33 }
34 
35 /**
36 * Get the string representation of the transport.
37 */
38 public function __toString(): string
39 {
40 return 'mailchimp';
41 }
42}
1use MailchimpTransactional\ApiClient;
2use Symfony\Component\Mailer\SentMessage;
3use Symfony\Component\Mailer\Transport\AbstractTransport;
4use Symfony\Component\Mime\Address;
5use Symfony\Component\Mime\MessageConverter;
6 
7class MailchimpTransport extends AbstractTransport
8{
9 /**
10 * Create a new Mailchimp transport instance.
11 */
12 public function __construct(
13 protected ApiClient $client,
14 ) {
15 parent::__construct();
16 }
17 
18 /**
19 * {@inheritDoc}
20 */
21 protected function doSend(SentMessage $message): void
22 {
23 $email = MessageConverter::toEmail($message->getOriginalMessage());
24 
25 $this->client->messages->send(['message' => [
26 'from_email' => $email->getFrom(),
27 'to' => collect($email->getTo())->map(function (Address $email) {
28 return ['email' => $email->getAddress(), 'type' => 'to'];
29 })->all(),
30 'subject' => $email->getSubject(),
31 'text' => $email->getTextBody(),
32 ]]);
33 }
34 
35 /**
36 * Get the string representation of the transport.
37 */
38 public function __toString(): string
39 {
40 return 'mailchimp';
41 }
42}

定義好自訂 Transport 後,就可以使用 Mail Facade 的 extend 方法來註冊這個 Transport。一般來說,應在 AppServiceProvider Service Provider 中 boot 方法內註冊這個 Transport。傳給 extend 方法的閉包會收到一個 $config 引數。這個引數中會包含在專案 config/mail.php 內定義給該方法的設定陣列:

1use App\Mail\MailchimpTransport;
2use Illuminate\Support\Facades\Mail;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Mail::extend('mailchimp', function (array $config = []) {
10 return new MailchimpTransport(/* ... */);
11 });
12}
1use App\Mail\MailchimpTransport;
2use Illuminate\Support\Facades\Mail;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 Mail::extend('mailchimp', function (array $config = []) {
10 return new MailchimpTransport(/* ... */);
11 });
12}

定義並註冊好自訂 Transport 後,就可以在專案 config/mail.php 設定檔內建立一個使用這個新 Transport 的 Mailer 定義:

1'mailchimp' => [
2 'transport' => 'mailchimp',
3 // ...
4],
1'mailchimp' => [
2 'transport' => 'mailchimp',
3 // ...
4],

額外的 Symfony Transport

Laravel 支援一些像是 Mailgun 與 Postmark 等現有 Symfony 維護的 Mail Transport。不過,有時候我們可能會需要讓 Laravel 也支援其他由 Symfony 維護的 Transport。若要讓 Laravel 支援這些 Transport,只要使用 Composer 安裝這些 Symfony Mailer,然後再向 Laravel 註冊這個 Transport。舉例來說,我們可以安裝並註冊「Brevo」(前身為「Sendinblue」) Symfony Mailer:

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

安裝好 Brevo Mailer 套件後,就可以在專案的 services 設定檔中加上 Brevo 的 API 認證:

1'brevo' => [
2 'key' => 'your-api-key',
3],
1'brevo' => [
2 'key' => 'your-api-key',
3],

接著,使用 Mail Facade 的 extend 方法來向 Laravel 註冊這個 Transport。一般來說,應在某個 Service Provider 內註冊一個 boot 方法:

1use Illuminate\Support\Facades\Mail;
2use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
3use Symfony\Component\Mailer\Transport\Dsn;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Mail::extend('brevo', function () {
11 return (new BrevoTransportFactory)->create(
12 new Dsn(
13 'brevo+api',
14 'default',
15 config('services.brevo.key')
16 )
17 );
18 });
19}
1use Illuminate\Support\Facades\Mail;
2use Symfony\Component\Mailer\Bridge\Brevo\Transport\BrevoTransportFactory;
3use Symfony\Component\Mailer\Transport\Dsn;
4 
5/**
6 * Bootstrap any application services.
7 */
8public function boot(): void
9{
10 Mail::extend('brevo', function () {
11 return (new BrevoTransportFactory)->create(
12 new Dsn(
13 'brevo+api',
14 'default',
15 config('services.brevo.key')
16 )
17 );
18 });
19}

註冊好 Transport 後,就可以在專案的 config/mail.php 設定檔中建立一個使用這個新 Transport 的 Mailer 定義:

1'brevo' => [
2 'transport' => 'brevo',
3 // ...
4],
1'brevo' => [
2 'transport' => 'brevo',
3 // ...
4],
翻譯進度
100% 已翻譯
更新時間:
2024年6月30日 上午8:27:00 [世界標準時間]
翻譯人員:
  • cornch
幫我們翻譯此頁

留言

尚無留言

“Laravel” is a Trademark of Taylor Otwell.
The source documentation is released under MIT license. See laravel/docs on GitHub for details.
The translated documentations are released under MIT license. See cornch/laravel-docs-l10n on GitHub for details.