郵件
簡介
Sending email doesn't have to be complicated. Laravel provides a clean, simple email API powered by the popular Symfony Mailer component. Laravel and Symfony Mailer provide drivers for sending email via SMTP, Mailgun, Postmark, Amazon SES, and sendmail
, allowing you to quickly get started sending mail through a local or cloud based service of your choice.
設定
可以使用專案的 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
Next, set the default
option in your application's config/mail.php
configuration file to mailgun
and add the following configuration array to your array of mailers
:
1'mailgun' => [2 'transport' => 'mailgun',3 // 'client' => [4 // 'timeout' => 5,5 // ],6],
1'mailgun' => [2 'transport' => 'mailgun',3 // 'client' => [4 // 'timeout' => 5,5 // ],6],
After configuring your application's default mailer, add the following options to your config/services.php
configuration file:
1'mailgun' => [2 'domain' => env('MAILGUN_DOMAIN'),3 'secret' => env('MAILGUN_SECRET'),4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),5 'scheme' => 'https',6],
1'mailgun' => [2 'domain' => env('MAILGUN_DOMAIN'),3 'secret' => env('MAILGUN_SECRET'),4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),5 'scheme' => 'https',6],
若你使用的 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 'scheme' => 'https',6],
1'mailgun' => [2 'domain' => env('MAILGUN_DOMAIN'),3 'secret' => env('MAILGUN_SECRET'),4 'endpoint' => env('MAILGUN_ENDPOINT', 'api.eu.mailgun.net'),5 'scheme' => 'https',6],
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
Next, set the default
option in your application's config/mail.php
configuration file to postmark
. After configuring your application's default mailer, ensure that your config/services.php
configuration file contains the following options:
1'postmark' => [2 'token' => env('POSTMARK_TOKEN'),3],
1'postmark' => [2 'token' => env('POSTMARK_TOKEN'),3],
若想為給定 Mailer 指定 Postmark 訊息串流,請在該 Mailer 的設定陣列中加上 message_stream_id
設定選項。該設定陣列可在 config/mail.php
設定檔中找到:
1'postmark' => [2 'transport' => 'postmark',3 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),4 // 'client' => [5 // 'timeout' => 5,6 // ],7],
1'postmark' => [2 'transport' => 'postmark',3 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),4 // 'client' => [5 // 'timeout' => 5,6 // ],7],
這樣一來,我們就能設定多個 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=mailersend3MAIL_FROM_NAME="App Name"45MAILERSEND_API_KEY=your-api-key
1MAIL_MAILER=mailersend3MAIL_FROM_NAME="App Name"45MAILERSEND_API_KEY=your-api-key
欲瞭解更多有關 MailerSend 的資訊,包含如何使用 Hosted Template (託管的樣板),請參考 MailerSend Driver 的說明文件。
Failover 設定
有時候,我們設定要用來寄送郵件的外部服務可能沒辦法用。因為這種情況,所以最好定義一個或多個備用的郵件寄送設定,以免主要寄送 Driver 無法使用。
To accomplish this, you should define a mailer within your application's mail
configuration file that uses the failover
transport. The configuration array for your application's failover
mailer should contain an array of mailers
that reference the order in which configured mailers should be chosen for delivery:
1'mailers' => [2 'failover' => [3 'transport' => 'failover',4 'mailers' => [5 'postmark',6 'mailgun',7 'sendmail',8 ],9 ],1011 // ...12],
1'mailers' => [2 'failover' => [3 'transport' => 'failover',4 'mailers' => [5 'postmark',6 'mailgun',7 'sendmail',8 ],9 ],1011 // ...12],
定義好 Failover Mailer 後,請將 mail
設定檔中的 default
設定索引鍵設為該 Failover Mailer 的名稱,以將其設為預設 Mailer。
1'default' => env('MAIL_MAILER', 'failover'),
1'default' => env('MAIL_MAILER', 'failover'),
Round Robin Configuration
The roundrobin
transport allows you to distribute your mailing workload across multiple mailers. To get started, define a mailer within your application's mail
configuration file that uses the roundrobin
transport. The configuration array for your application's roundrobin
mailer should contain an array of mailers
that reference which configured mailers should be used for delivery:
1'mailers' => [2 'roundrobin' => [3 'transport' => 'roundrobin',4 'mailers' => [5 'ses',6 'postmark',7 ],8 ],910 // ...11],
1'mailers' => [2 'roundrobin' => [3 'transport' => 'roundrobin',4 'mailers' => [5 'ses',6 'postmark',7 ],8 ],910 // ...11],
Once your round robin mailer has been defined, you should set this mailer as the default mailer used by your application by specifying its name as the value of the default
configuration key within your application's mail
configuration file:
1'default' => env('MAIL_MAILER', 'roundrobin'),
1'default' => env('MAIL_MAILER', 'roundrobin'),
The round robin transport selects a random mailer from the list of configured mailers and then switches to the next available mailer for each subsequent email. In contrast to failover
transport, which helps to achieve high availability, the roundrobin
transport provides load balancing.
產生 Mailable
在撰寫 Laravel 專案時,程式所寄出的所有郵件都以「Mailable」類別的形式呈現。這些類別保存在 app/Mail
目錄中。若沒看到這個目錄,請別擔心。使用 make:mail
Artisan 指令初次建立 Mailable 類別時會自動產生該目錄:
1php artisan make:mail OrderShipped
1php artisan make:mail OrderShipped
撰寫 Mailable
產生 Mailable 類別後,請先開啟該類別,讓我們來看看該類別的內容。Mailable 類別可通過多個方法來進行設定,包含 envelope
、content
、與 attachments
方法。
evelope
方法回傳 Illuminate\Mail\Mailables\Envelope
物件,用來定義標題,而有的時候也會用來定義收件者與訊息。content
方法回傳 Illuminate\Mail\Mailables\Content
物件,該物件定義用來產生訊息內容的 Blade 樣板。
Configuring the Sender
Using the Envelope
首先,我們先來看看如何設定寄件人。或者,換句話說,也就是郵件要「從 (From)」誰那裡寄出。要設定寄件人,有兩種方法。第一種方法,我們可以在訊息的 Evelope 上指定「from」位址:
1use Illuminate\Mail\Mailables\Address;2use Illuminate\Mail\Mailables\Envelope;34/**5 * Get the message envelope.6 */7public function envelope(): Envelope8{9 return new Envelope(11 subject: 'Order Shipped',12 );13}
1use Illuminate\Mail\Mailables\Address;2use Illuminate\Mail\Mailables\Envelope;34/**5 * Get the message envelope.6 */7public function envelope(): Envelope8{9 return new Envelope(11 subject: 'Order Shipped',12 );13}
若有需要的話,可以指定 replyTo
位址:
1return new Envelope(3 replyTo: [5 ],6 subject: 'Order Shipped',7);
1return new Envelope(3 replyTo: [5 ],6 subject: 'Order Shipped',7);
Using a Global from
Address
不過,若你的專案中所有的郵件都使用相同的寄件人位址,在每個產生的 Mailable 類別內都呼叫 from
方法會很麻煩。比起在每個 Mailable 內呼叫 from
方法,我們可以在 config/mail.php
設定檔中指定一個全域的「from」位址。若 Mailable 類別內沒有指定「from」位址,就會使用這個全域的位址:
1'from' => [3 'name' => env('MAIL_FROM_NAME', 'Example'),4],
1'from' => [3 'name' => env('MAIL_FROM_NAME', 'Example'),4],
此外,也可以在 config/mail.php
設定檔中定義一個全域的「reply_to」位址:
Configuring the View
在 Mailable 類別的 content
方法中,可以定義 view
,或者,可以說在 content
方法中指定轉譯郵件內容時要使用哪個樣板。由於一般來說大部分郵件都是使用 [Blade 樣板]來轉譯內容的,因此在建立郵件內容時,我們就可以使用 Blade 樣板引擎的完整功能與便利:
1/**2 * Get the message content definition.3 */4public function content(): Content5{6 return new Content(7 view: 'mail.orders.shipped',8 );9}
1/**2 * Get the message content definition.3 */4public function content(): Content5{6 return new Content(7 view: 'mail.orders.shipped',8 );9}
可以建立一個 resources/views/emails
目錄來放置所有的郵件樣板。不過,不一定要放在這個目錄,可以隨意放在 resources/views
目錄下。
純文字郵件
若想為郵件定義純文字版本,可以在定義訊息的 Content
時使用 text
方法。與 view
參數類似,text
參數應為用來轉譯 E-Mail 內容的樣板名稱。可以同時為訊息定義 HTML 與純文字的版本:
1/**2 * Get the message content definition.3 */4public function content(): Content5{6 return new Content(7 view: 'mail.orders.shipped',8 text: 'mail.orders.shipped-text'9 );10}
1/**2 * Get the message content definition.3 */4public function content(): Content5{6 return new Content(7 view: 'mail.orders.shipped',8 text: 'mail.orders.shipped-text'9 );10}
為了讓程式碼更清除,可以使用 html
參數。這個參數是 view
參數的別名:
1return new Content(2 html: 'mail.orders.shipped',3 text: 'mail.orders.shipped-text'4);
1return new Content(2 html: 'mail.orders.shipped',3 text: 'mail.orders.shipped-text'4);
View 資料
使用公開屬性
一般來說,在轉譯 HTML 版本的郵件時,我們會需要將資料傳入 View 來在其中使用。要將資料傳入 View 有兩種方法。第一種方法,即是在 Mailable 類別裡的公用變數,在 View 裡面可以直接使用。因此,舉例來說,我們可以將資料傳入 Mailable 類別的 Constructor 內,然後將資料設為該類別中定義的公用變數:
1<?php23namespace App\Mail;45use App\Models\Order;6use Illuminate\Bus\Queueable;7use Illuminate\Mail\Mailable;8use Illuminate\Mail\Mailables\Content;9use Illuminate\Queue\SerializesModels;1011class OrderShipped extends Mailable12{13 use Queueable, SerializesModels;1415 /**16 * Create a new message instance.17 */18 public function __construct(19 public Order $order,20 ) {}2122 /**23 * Get the message content definition.24 */25 public function content(): Content26 {27 return new Content(28 view: 'mail.orders.shipped',29 );30 }31}
1<?php23namespace App\Mail;45use App\Models\Order;6use Illuminate\Bus\Queueable;7use Illuminate\Mail\Mailable;8use Illuminate\Mail\Mailables\Content;9use Illuminate\Queue\SerializesModels;1011class OrderShipped extends Mailable12{13 use Queueable, SerializesModels;1415 /**16 * Create a new message instance.17 */18 public function __construct(19 public Order $order,20 ) {}2122 /**23 * Get the message content definition.24 */25 public function content(): Content26 {27 return new Content(28 view: 'mail.orders.shipped',29 );30 }31}
將資料設為公用變數後,在 View 中就自動可以使用該資料。因此在 Blade 樣板中,我們可以像存取其他資料一樣存取這些資料:
1<div>2 Price: {{ $order->price }}3</div>
1<div>2 Price: {{ $order->price }}3</div>
Via the with
Parameter:
若想在資料被傳給樣板前自訂其格式,可使用 Content
定義的 with
參數來手動將資料傳給 View。一般來說,我們還是會使用 Mailable 類別的 Constroctor 來傳入資料。不過,我們可以將該資料設為 protected
或 private
屬性,這樣這些資料才不會被自動暴露到樣板中:
1<?php23namespace App\Mail;45use App\Models\Order;6use Illuminate\Bus\Queueable;7use Illuminate\Mail\Mailable;8use Illuminate\Mail\Mailables\Content;9use Illuminate\Queue\SerializesModels;1011class OrderShipped extends Mailable12{13 use Queueable, SerializesModels;1415 /**16 * Create a new message instance.17 */18 public function __construct(19 protected Order $order,20 ) {}2122 /**23 * Get the message content definition.24 */25 public function content(): Content26 {27 return new Content(28 view: 'mail.orders.shipped',29 with: [30 'orderName' => $this->order->name,31 'orderPrice' => $this->order->price,32 ],33 );34 }35}
1<?php23namespace App\Mail;45use App\Models\Order;6use Illuminate\Bus\Queueable;7use Illuminate\Mail\Mailable;8use Illuminate\Mail\Mailables\Content;9use Illuminate\Queue\SerializesModels;1011class OrderShipped extends Mailable12{13 use Queueable, SerializesModels;1415 /**16 * Create a new message instance.17 */18 public function __construct(19 protected Order $order,20 ) {}2122 /**23 * Get the message content definition.24 */25 public function content(): Content26 {27 return new Content(28 view: 'mail.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;23/**4 * Get the attachments for the message.5 *6 * @return array<int, \Illuminate\Mail\Mailables\Attachment>7 */8public function attachments(): array9{10 return [11 Attachment::fromPath('/path/to/file'),12 ];13}
1use Illuminate\Mail\Mailables\Attachment;23/**4 * Get the attachments for the message.5 *6 * @return array<int, \Illuminate\Mail\Mailables\Attachment>7 */8public function attachments(): array9{10 return [11 Attachment::fromPath('/path/to/file'),12 ];13}
將檔案附加至訊息時,也可以使用 as
與 withMime
方法來指定附件的顯示名稱與/或 MIME 型別:
1/**2 * Get the attachments for the message.3 *4 * @return array<int, \Illuminate\Mail\Mailables\Attachment>5 */6public function attachments(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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(): array7{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:34 <img src="{{ $message->embed($pathToImage) }}">5</body>
1<body>2 Here is an image:34 <img src="{{ $message->embed($pathToImage) }}">5</body>
$message
變數無法在純文字訊息樣板中使用,因為純文字樣板無法使用內嵌的附加檔案。
內嵌原始資料附件
若有欲嵌入到郵件樣板中的原始圖片字串,可呼叫 $message
變數上的 embedData
方法。呼叫 embedData
方法時,請提供一個欲設定給嵌入圖片的檔案名稱:
1<body>2 Here is an image from raw data:34 <img src="{{ $message->embedData($data, 'example-image.jpg') }}">5</body>
1<body>2 Here is an image from raw data:34 <img src="{{ $message->embedData($data, 'example-image.jpg') }}">5</body>
可附加的物件
雖然一般來說,以簡單的字串路徑來將檔案附加到訊息上通常就夠了。但很多情況下,在專案中,可附加的物件都是以類別形式存在的。舉例來說,若要將照片附加到訊息中,則專案內可能有一個用來代表該照片的 Photo
Model。 這時,若可以直接將
PhotoModel 附加到
attach` 方法上不是很方便嗎?使用可附加的物件,就可以輕鬆達成。
若要開始定義可附加物件,請在要被附加到訊息的物件上實作 Illuminate\Contracts\Mail\Attachable
介面。該介面會要求這個類別定義 toMailAttachment
,且該方法應回傳 Illuminate\Mail\Attachment
實體:
1<?php23namespace App\Models;45use Illuminate\Contracts\Mail\Attachable;6use Illuminate\Database\Eloquent\Model;7use Illuminate\Mail\Attachment;89class Photo extends Model implements Attachable10{11 /**12 * Get the attachable representation of the model.13 */14 public function toMailAttachment(): Attachment15 {16 return Attachment::fromPath('/path/to/file');17 }18}
1<?php23namespace App\Models;45use Illuminate\Contracts\Mail\Attachable;6use Illuminate\Database\Eloquent\Model;7use Illuminate\Mail\Attachment;89class Photo extends Model implements Attachable10{11 /**12 * Get the attachable representation of the model.13 */14 public function toMailAttachment(): Attachment15 {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(): array7{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(): array7{8 return [$this->photo];9}
當然,要附加的資料也可能存放在如 Amazon S3 之類的遠端檔案儲存服務上。因此,在 Laravel 中,我們可以從存放在專案檔案系統磁碟上的資料來產生附件實體:
1// Create an attachment from a file on your default disk...2return Attachment::fromStorage($this->path);34// Create an attachment from a file on a specific disk...5return Attachment::fromStorageDisk('backblaze', $this->path);
1// Create an attachment from a file on your default disk...2return Attachment::fromStorage($this->path);34// Create an attachment from a file on a specific disk...5return Attachment::fromStorageDisk('backblaze', $this->path);
此外,也可以使用記憶體中的資料來建立附件實體。若要從記憶體中建立,請傳入一個閉包給 fromData
方法。該閉包應回傳代表該附件的原始資料:
1return Attachment::fromData(fn () => $this->content, 'Photo Name');
1return Attachment::fromData(fn () => $this->content, 'Photo Name');
Laravel 也提供了一些額外的方法,讓我們可以自訂附件。舉例來說,可以使用 as
與 withMime
方法來自訂檔案名稱與 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
實體。該類別接受 messageId
、references
、與 text
參數。當然,我們只需要提供該訊息所需要的參數即可:
1use Illuminate\Mail\Mailables\Headers;23/**4 * Get the message headers.5 */6public function headers(): Headers7{8 return new Headers(11 text: [12 'X-Custom-Header' => 'Custom Value',13 ],14 );15}
1use Illuminate\Mail\Mailables\Headers;23/**4 * Get the message headers.5 */6public function headers(): Headers7{8 return new Headers(11 text: [12 'X-Custom-Header' => 'Custom Value',13 ],14 );15}
Tags and Metadata
有的第三方 E-Mail 提供商,如 Mailgun 或 Postmark 等,支援訊息的「Tag」與「詮釋資料 (Metadata)」,使用 Tag 與詮釋資料,就可以對專案所送出的 E-Mail 進行分組與追蹤。可以通過 Evelope
定義來為 E-Mail 訊息加上 Tag 與詮釋資料:
1use Illuminate\Mail\Mailables\Envelope;23/**4 * Get the message envelope.5 *6 * @return \Illuminate\Mail\Mailables\Envelope7 */8public function envelope(): Envelope9{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;23/**4 * Get the message envelope.5 *6 * @return \Illuminate\Mail\Mailables\Envelope7 */8public function envelope(): Envelope9{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」附加到訊息上。
Customizing the Symfony Message
Laravel 的郵件是使用 Symfony Mailer 驅動的。在 Laravel 中,我們可以註冊一個在寄送訊息前會被呼叫的回呼,該回呼會收到 Symfony Message 實體。這樣,我們就能在郵件被寄送前深度自定訊息。若要註冊這個回呼,可以在 Evelope
實體上定義一個 using
參數:
1use Illuminate\Mail\Mailables\Envelope;2use Symfony\Component\Mime\Email;34/**5 * Get the message envelope.6 */7public function envelope(): Envelope8{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;34/**5 * Get the message envelope.6 */7public function envelope(): Envelope8{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=mail.orders.shipped
1php artisan make:mail OrderShipped --markdown=mail.orders.shipped
接著,在 content
方法中設定 Mailable 的 Content
定義時,請將 view
參數改成 markdown
參數:
1use Illuminate\Mail\Mailables\Content;23/**4 * Get the message content definition.5 */6public function content(): Content7{8 return new Content(9 markdown: 'mail.orders.shipped',10 with: [11 'url' => $this->orderUrl,12 ],13 );14}
1use Illuminate\Mail\Mailables\Content;23/**4 * Get the message content definition.5 */6public function content(): Content7{8 return new Content(9 markdown: 'mail.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 Shipped34Your order has been shipped!56<x-mail::button :url="$url">7View Order8</x-mail::button>910Thanks,<br>11{{ config('app.name') }}12</x-mail::message>
1<x-mail::message>2# Order Shipped34Your order has been shipped!56<x-mail::button :url="$url">7View Order8</x-mail::button>910Thanks,<br>11{{ config('app.name') }}12</x-mail::message>
在撰寫 Markdown 郵件時請不要增加縮排。依據 Markdown 標準,Markdown 解析程式會將縮排的內容轉譯為程式碼區塊。
Button 元件
Button 元件轉譯一個置中的按鈕連結。這個元件接受兩個引數,一個是 url
網址,另一個則是可選的 color
顏色。支援的顏色有 primary
、success
、error
。在訊息中可以加上不限數量的 Button 元件:
1<x-mail::button :url="$url" color="success">2View Order3</x-mail::button>
1<x-mail::button :url="$url" color="success">2View Order3</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>
Customizing the Components
可以將所有的 Markdown 郵件元件匯出到專案內來自訂這些元件。若要匯出元件,請使用 vendor:publish
Artisan 指令來安裝 laravel-mail
素材標籤:
1php artisan vendor:publish --tag=laravel-mail
1php artisan vendor:publish --tag=laravel-mail
這個指令會將 Markdown 郵件元件安裝到 resources/views/vendor/mail
目錄下。mail
目錄會包含 html
與 text
目錄,這些目錄中包含了所有可用元件對應的呈現方式。可以隨意自訂這些元件。
Customizing the 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 在判斷收件人時會自動使用這些物件的
email與
name屬性來判斷。因此,請確認這些物件上是否有這兩個屬性。指定好收件人後,就可傳入 Mailable 類別的實體給
send` 方法:
1<?php23namespace App\Http\Controllers;45use 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;1112class OrderShipmentController extends Controller13{14 /**15 * Ship the given order.16 */17 public function store(Request $request): RedirectResponse18 {19 $order = Order::findOrFail($request->order_id);2021 // Ship the order...2223 Mail::to($request->user())->send(new OrderShipped($order));2425 return redirect('/orders');26 }27}
1<?php23namespace App\Http\Controllers;45use 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;1112class OrderShipmentController extends Controller13{14 /**15 * Ship the given order.16 */17 public function store(Request $request): RedirectResponse18 {19 $order = Order::findOrFail($request->order_id);2021 // Ship the order...2223 Mail::to($request->user())->send(new OrderShipped($order));2425 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 實體:
2 Mail::to($recipient)->send(new OrderShipped($order));3}
2 Mail::to($recipient)->send(new OrderShipped($order));3}
Sending Mail via a Specific 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));
將郵件放入佇列
Queueing a Mail Message
由於傳送郵件訊息可能對程式的 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));
Pushing to Specific Queues
由於所有使用 make:mail
指令產生的 Mailable 類別都使用 Illiminate\Bus\Queuable
Trait,因此我們可以在任何一個 Mailable 類別實體上呼叫 onQueue
與 onConnection
方法,可讓我們指定該訊息要使用的佇列名稱:
1$message = (new OrderShipped($order))2 ->onConnection('sqs')3 ->onQueue('emails');45Mail::to($request->user())6 ->cc($moreUsers)7 ->bcc($evenMoreUsers)8 ->queue($message);
1$message = (new OrderShipped($order))2 ->onConnection('sqs')3 ->onQueue('emails');45Mail::to($request->user())6 ->cc($moreUsers)7 ->bcc($evenMoreUsers)8 ->queue($message);
Queueing by Default
若有想要永遠放入佇列的 Mailable 類別,可在該類別上實作 ShouldQueue
Contract。接著,即使使用 send
方法來寄送郵件,由於該 Mailable 有實作 ShouldQueue
Contract,因此還是會被放入佇列:
1use Illuminate\Contracts\Queue\ShouldQueue;23class OrderShipped extends Mailable implements ShouldQueue4{5 // ...6}
1use Illuminate\Contracts\Queue\ShouldQueue;23class OrderShipped extends Mailable implements ShouldQueue4{5 // ...6}
Queued Mailables and Database Transactions
當佇列 Mailable 是在資料庫 Transaction 內分派的時候,這個 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<?php23namespace App\Mail;45use Illuminate\Bus\Queueable;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Mail\Mailable;8use Illuminate\Queue\SerializesModels;910class OrderShipped extends Mailable implements ShouldQueue11{12 use Queueable, SerializesModels;1314 /**15 * Create a new message instance.16 */17 public function __construct()18 {19 $this->afterCommit();20 }21}
1<?php23namespace App\Mail;45use Illuminate\Bus\Queueable;6use Illuminate\Contracts\Queue\ShouldQueue;7use Illuminate\Mail\Mailable;8use Illuminate\Queue\SerializesModels;910class OrderShipped extends Mailable implements ShouldQueue11{12 use Queueable, SerializesModels;1314 /**15 * Create a new message instance.16 */17 public function __construct()18 {19 $this->afterCommit();20 }21}
要瞭解更多有關這類問題的解決方法,請參考有關佇列任務與資料庫 Transaction 有關的說明文件。
轉譯 Mailable
有時候我們會想在不寄送 Mailable 的情況下截取其 HTML 內容。若要截取其內容,可呼叫 Mailable 的 render
方法。該方法會以字串回傳該 Mailable 的 HTML 取值內容:
1use App\Mail\InvoicePaid;2use App\Models\Invoice;34$invoice = Invoice::find(1);56return (new InvoicePaid($invoice))->render();
1use App\Mail\InvoicePaid;2use App\Models\Invoice;34$invoice = Invoice::find(1);56return (new InvoicePaid($invoice))->render();
Previewing Mailables in the Browser
在設計 Mailable 樣板時,若能像普通的 Blade 樣板一樣在瀏覽器中預覽轉譯後的 Mailable 該有多方便。因為這樣,在 Laravel 中,可以直接在 Route 閉包或 Controller 中回傳任何的 Mailable。若回傳 Mailable,則會轉譯該 Mailable 並顯示在瀏覽器上,讓我們不需將其寄到真實的電子郵件上也能快速檢視其設計:
1Route::get('/mailable', function () {2 $invoice = App\Models\Invoice::find(1);34 return new App\Mail\InvoicePaid($invoice);5});
1Route::get('/mailable', function () {2 $invoice = App\Models\Invoice::find(1);34 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;23class User extends Model implements HasLocalePreference4{5 /**6 * Get the user's preferred locale.7 */8 public function preferredLocale(): string9 {10 return $this->locale;11 }12}
1use Illuminate\Contracts\Translation\HasLocalePreference;23class User extends Model implements HasLocalePreference4{5 /**6 * Get the user's preferred locale.7 */8 public function preferredLocale(): string9 {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;34test('mailable content', function () {5 $user = User::factory()->create();67 $mailable = new InvoicePaid($user);814 $mailable->assertHasSubject('Invoice Paid');15 $mailable->assertHasTag('example-tag');16 $mailable->assertHasMetadata('key', 'value');1718 $mailable->assertSeeInHtml($user->email);19 $mailable->assertSeeInHtml('Invoice Paid');20 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);2122 $mailable->assertSeeInText($user->email);23 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);2425 $mailable->assertHasAttachment('/path/to/file');26 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));27 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);28 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);29 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);30});
1use App\Mail\InvoicePaid;2use App\Models\User;34test('mailable content', function () {5 $user = User::factory()->create();67 $mailable = new InvoicePaid($user);814 $mailable->assertHasSubject('Invoice Paid');15 $mailable->assertHasTag('example-tag');16 $mailable->assertHasMetadata('key', 'value');1718 $mailable->assertSeeInHtml($user->email);19 $mailable->assertSeeInHtml('Invoice Paid');20 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);2122 $mailable->assertSeeInText($user->email);23 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);2425 $mailable->assertHasAttachment('/path/to/file');26 $mailable->assertHasAttachment(Attachment::fromPath('/path/to/file'));27 $mailable->assertHasAttachedData($pdfData, 'name.pdf', ['mime' => 'application/pdf']);28 $mailable->assertHasAttachmentFromStorage('/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);29 $mailable->assertHasAttachmentFromStorageDisk('s3', '/path/to/file', 'name.pdf', ['mime' => 'application/pdf']);30});
1use App\Mail\InvoicePaid;2use App\Models\User;34public function test_mailable_content(): void5{6 $user = User::factory()->create();78 $mailable = new InvoicePaid($user);915 $mailable->assertHasSubject('Invoice Paid');16 $mailable->assertHasTag('example-tag');17 $mailable->assertHasMetadata('key', 'value');1819 $mailable->assertSeeInHtml($user->email);20 $mailable->assertSeeInHtml('Invoice Paid');21 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);2223 $mailable->assertSeeInText($user->email);24 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);2526 $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;34public function test_mailable_content(): void5{6 $user = User::factory()->create();78 $mailable = new InvoicePaid($user);915 $mailable->assertHasSubject('Invoice Paid');16 $mailable->assertHasTag('example-tag');17 $mailable->assertHasMetadata('key', 'value');1819 $mailable->assertSeeInHtml($user->email);20 $mailable->assertSeeInHtml('Invoice Paid');21 $mailable->assertSeeInOrderInHtml(['Invoice Paid', 'Thanks']);2223 $mailable->assertSeeInText($user->email);24 $mailable->assertSeeInOrderInText(['Invoice Paid', 'Thanks']);2526 $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<?php23use App\Mail\OrderShipped;4use Illuminate\Support\Facades\Mail;56test('orders can be shipped', function () {7 Mail::fake();89 // Perform order shipping...1011 // Assert that no mailables were sent...12 Mail::assertNothingSent();1314 // Assert that a mailable was sent...15 Mail::assertSent(OrderShipped::class);1617 // Assert a mailable was sent twice...18 Mail::assertSent(OrderShipped::class, 2);1920 // Assert a mailable was not sent...21 Mail::assertNotSent(AnotherMailable::class);2223 // Assert 3 total mailables were sent...24 Mail::assertSentCount(3);25});
1<?php23use App\Mail\OrderShipped;4use Illuminate\Support\Facades\Mail;56test('orders can be shipped', function () {7 Mail::fake();89 // Perform order shipping...1011 // Assert that no mailables were sent...12 Mail::assertNothingSent();1314 // Assert that a mailable was sent...15 Mail::assertSent(OrderShipped::class);1617 // Assert a mailable was sent twice...18 Mail::assertSent(OrderShipped::class, 2);1920 // Assert a mailable was not sent...21 Mail::assertNotSent(AnotherMailable::class);2223 // Assert 3 total mailables were sent...24 Mail::assertSentCount(3);25});
1<?php23namespace Tests\Feature;45use App\Mail\OrderShipped;6use Illuminate\Support\Facades\Mail;7use Tests\TestCase;89class ExampleTest extends TestCase10{11 public function test_orders_can_be_shipped(): void12 {13 Mail::fake();1415 // Perform order shipping...1617 // Assert that no mailables were sent...18 Mail::assertNothingSent();1920 // Assert that a mailable was sent...21 Mail::assertSent(OrderShipped::class);2223 // Assert a mailable was sent twice...24 Mail::assertSent(OrderShipped::class, 2);2526 // Assert a mailable was not sent...27 Mail::assertNotSent(AnotherMailable::class);2829 // Assert 3 total mailables were sent...30 Mail::assertSentCount(3);31 }32}
1<?php23namespace Tests\Feature;45use App\Mail\OrderShipped;6use Illuminate\Support\Facades\Mail;7use Tests\TestCase;89class ExampleTest extends TestCase10{11 public function test_orders_can_be_shipped(): void12 {13 Mail::fake();1415 // Perform order shipping...1617 // Assert that no mailables were sent...18 Mail::assertNothingSent();1920 // Assert that a mailable was sent...21 Mail::assertSent(OrderShipped::class);2223 // Assert a mailable was sent twice...24 Mail::assertSent(OrderShipped::class, 2);2526 // Assert a mailable was not sent...27 Mail::assertNotSent(AnotherMailable::class);2829 // Assert 3 total mailables were sent...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);
可以傳入一個閉包給 assertSent
、assertNotSent
、assertQueued
、assertNotQueued
方法來判斷 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;23Mail::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});1011Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {12 return $mail->hasAttachment(13 Attachment::fromStorageDisk('s3', '/path/to/file')14 );15});1617Mail::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;23Mail::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});1011Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) {12 return $mail->hasAttachment(13 Attachment::fromStorageDisk('s3', '/path/to/file')14 );15});1617Mail::assertSent(OrderShipped::class, function (OrderShipped $mail) use ($pdfData) {18 return $mail->hasAttachment(19 Attachment::fromData(fn () => $pdfData, 'name.pdf')20 );21});
讀者可能已經注意到,總共有兩個方法可用來檢查郵件是否未被送出:assertNotSent
、assertNotQueued
。有時候,我們可能會希望判斷沒有任何郵件被寄出,而且 也沒有任何郵件被放入佇列。若要判斷是否沒有郵件被寄出或放入佇列,可使用 assertNothingOutgoing
與 assertNotOutgoing
方法:
1Mail::assertNothingOutgoing();23Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {4 return $mail->order->id === $order->id;5});
1Mail::assertNothingOutgoing();23Mail::assertNotOutgoing(function (OrderShipped $mail) use ($order) {4 return $mail->order->id === $order->id;5});
Mail and Local Development
在開發有寄送郵件的程式時,我們通常都不會想實際將郵件寄到真實的 E-Mail 位址上。Laravel 提供了數種數種方法來在本機上開發時「禁用」郵件的實際傳送。
Log Driver
log
郵件 Driver 不會實際寄送電子郵件,而是將所有電子郵件訊息寫入日誌檔以供檢查。一般來說,Log Driver 只會在開發環境上使用。有關一找不同環境設定專案的方法,請參考設定的說明文件。
HELO / Mailtrap / Mailpit
或者,也可以使用如 HELO 或 Mailtrap 這類服務搭配 smtp
Driver 來將電子郵件寄送到一個「模擬的」收件夾,並像在真的郵件用戶端一樣檢視這些郵件。這種做法的好處就是可以在 Mailtrap 的訊息檢視工具中實際檢視寄出的郵件。
若使用 Laravel Sail,,則可使用 Mailpit 來預覽訊息。當 Sail 有在執行時,可在 http://localhost:8025
上存取 Mailpit 的界面。
Using a Global to
Address
最後一種方法,就是我們可以叫用 Mail
Facade 提供的 alwaysTo
方法指定一個全域的「to」位址。一般來說,應在專案的其中一個 Service Provider 內 boot
方法中呼叫這個方法:
1use Illuminate\Support\Facades\Mail;23/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 if ($this->app->environment('local')) {10 }11}
1use Illuminate\Support\Facades\Mail;23/**4 * Bootstrap any application services.5 */6public function boot(): void7{8 if ($this->app->environment('local')) {10 }11}
事件
Laravel dispatches two events while sending mail messages. The MessageSending
event is dispatched prior to a message being sent, while the MessageSent
event is dispatched after a message has been sent. Remember, these events are dispatched when the mail is being sent, not when it is queued. You may create event listeners for these events within your application:
1use Illuminate\Mail\Events\MessageSending;2// use Illuminate\Mail\Events\MessageSent;34class LogMessage5{6 /**7 * Handle the given event.8 */9 public function handle(MessageSending $event): void10 {11 // ...12 }13}
1use Illuminate\Mail\Events\MessageSending;2// use Illuminate\Mail\Events\MessageSent;34class LogMessage5{6 /**7 * Handle the given event.8 */9 public function handle(MessageSending $event): void10 {11 // ...12 }13}
自訂 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;67class MailchimpTransport extends AbstractTransport8{9 /**10 * Create a new Mailchimp transport instance.11 */12 public function __construct(13 protected ApiClient $client,14 ) {15 parent::__construct();16 }1718 /**19 * {@inheritDoc}20 */21 protected function doSend(SentMessage $message): void22 {23 $email = MessageConverter::toEmail($message->getOriginalMessage());2425 $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 }3435 /**36 * Get the string representation of the transport.37 */38 public function __toString(): string39 {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;67class MailchimpTransport extends AbstractTransport8{9 /**10 * Create a new Mailchimp transport instance.11 */12 public function __construct(13 protected ApiClient $client,14 ) {15 parent::__construct();16 }1718 /**19 * {@inheritDoc}20 */21 protected function doSend(SentMessage $message): void22 {23 $email = MessageConverter::toEmail($message->getOriginalMessage());2425 $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 }3435 /**36 * Get the string representation of the transport.37 */38 public function __toString(): string39 {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;34/**5 * Bootstrap any application services.6 */7public function boot(): void8{9 Mail::extend('mailchimp', function (array $config = []) {10 return new MailchimpTransport(/* ... */);11 });12}
1use App\Mail\MailchimpTransport;2use Illuminate\Support\Facades\Mail;34/**5 * Bootstrap any application services.6 */7public function boot(): void8{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;45/**6 * Bootstrap any application services.7 */8public function boot(): void9{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;45/**6 * Bootstrap any application services.7 */8public function boot(): void9{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],