当我使用 Laravel 的邮件发送功能时,脑子里浮现出这么几个问题:
> 1. Laravel 集成了 SMTP 、Mailgun 、SparkPost 、 Amazon SES 等驱动,是怎么做到的?
>
> 2. Laravel 提供全文本格式、网页格式和 Markdown 格式,是怎么实现的?
>
> 3. 整个邮件发送流程是什么样的?
>
下面就让我们开始徒手扒一扒「邮件发送功能」的实现原理。
## 写个 demo
我们使用阿里云提供的免费邮,和采用「smtp」驱动,作为测试,参考 `.env` 配置:
```
MAIL_DRIVER=smtp
MAIL_HOST=smtp.mxhichina.com
MAIL_PORT=25
MAIL_USERNAME=***@coding01.cn
MAIL_PASSWORD=****
MAIL_ENCRYPTION=tls
MAIL_FROM=***@coding01.cn
MAIL_NAME=coding01
```
写个测试流程,还是挺简单的,具体如下:
```bash
// 1. 创建测试类
php artisan make:mail TestEmail
// 2. 在 TestEmail 类,载入视图
public function build()
{
return $this->view('mail.test');
}
// 3. 输出 hello coding01
<p>hello coding01</p>
```
最后写个命令函数:
```php
Artisan::command('test', function () {
Mail::to('[email protected]')->send(new \App\Mail\TestEmail());
});
```
执行 `php artisan test` 看测试是否发送成功:
![](https://user-gold-cdn.xitu.io/2018/8/26/165758aba593d21d?w=326&h=186&f=jpeg&s=28649)
## 解析 MailServiceProvider
写了不少 Laravel 代码,看
```php
Mail::to('[email protected]')->send(new \App\Mail\TestEmail());
```
自然而然的想到是不是有一个 `MailServiceProvider`,果不其然,在 `config/app.php` 的数组 `providers` 就包含了该 `ServiceProvider`
![](https://user-gold-cdn.xitu.io/2018/8/26/165758aba6f594bb?w=774&h=212&f=jpeg&s=99723)
所以我们就开始围绕这个 `MailServiceProvider` 来解析了
```php
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerSwiftMailer();
$this->registerIlluminateMailer();
$this->registerMarkdownRenderer();
}
```
看 `register` 函数,一目了然,我们将重点看看这三个方法都是干嘛用的。
### registerSwiftMailer
看代码:
```php
/**
* Register the Swift Mailer instance.
*
* @return void
*/
public function registerSwiftMailer()
{
$this->registerSwiftTransport();
// Once we have the transporter registered, we will register the actual Swift
// mailer instance, passing in the transport instances, which allows us to
// override this transporter instances during app start-up if necessary.
$this->app->singleton('swift.mailer', function ($app) {
if ($domain = $app->make('config')->get('mail.domain')) {
Swift_DependencyContainer::getInstance()
->register('mime.idgenerator.idright')
->asValue($domain);
}
return new Swift_Mailer($app['swift.transport']->driver());
});
}
```
很好理解,就是注册 `Swift Mailer` 实例。在创建实例之前,执行 `$this->registerSwiftTransport();`方法:
```php
/**
* Register the Swift Transport instance.
*
* @return void
*/
protected function registerSwiftTransport()
{
$this->app->singleton('swift.transport', function ($app) {
return new TransportManager($app);
});
}
```
看看这个 `TransportManager` 类是干嘛用的:
```php
<?php
namespace Illuminate\Mail;
use Aws\Ses\SesClient;
use Illuminate\Support\Arr;
use Psr\Log\LoggerInterface;
use Illuminate\Support\Manager;
use GuzzleHttp\Client as HttpClient;
use Swift_SmtpTransport as SmtpTransport;
use Illuminate\Mail\Transport\LogTransport;
use Illuminate\Mail\Transport\SesTransport;
use Illuminate\Mail\Transport\ArrayTransport;
use Swift_SendmailTransport as MailTransport;
use Illuminate\Mail\Transport\MailgunTransport;
use Illuminate\Mail\Transport\MandrillTransport;
use Illuminate\Mail\Transport\SparkPostTransport;
use Swift_SendmailTransport as SendmailTransport;
class TransportManager extends Manager
{
/**
* Create an instance of the SMTP Swift Transport driver.
*
* @return \Swift_SmtpTransport
*/
protected function createSmtpDriver()
{
$config = $this->app->make('config')->get('mail');
// The Swift SMTP transport instance will allow us to use any SMTP backend
// for delivering mail such as Sendgrid, Amazon SES, or a custom server
// a developer has available. We will just pass this configured host.
$transport = new SmtpTransport($config['host'], $config['port']);
if (isset($config['encryption'])) {
$transport->setEncryption($config['encryption']);
}
// Once we have the transport we will check for the presence of a username
// and password. If we have it we will set the credentials on the Swift
// transporter instance so that we'll properly authenticate delivery.
if (isset($config['username'])) {
$transport->setUsername($config['username']);
$transport->setPassword($config['password']);
}
// Next we will set any stream context options specified for the transport
// and then return it. The option is not required any may not be inside
// the configuration array at all so we'll verify that before adding.
if (isset($config['stream'])) {
$transport->setStreamOptions($config['stream']);
}
return $transport;
}
/**
* Create an instance of the Sendmail Swift Transport driver.
*
* @return \Swift_SendmailTransport
*/
protected function createSendmailDriver()
{
return new SendmailTransport($this->app['config']['mail']['sendmail']);
}
/**
* Create an instance of the Amazon SES Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\SesTransport
*/
protected function createSesDriver()
{
$config = array_merge($this->app['config']->get('services.ses', []), [
'version' => 'latest', 'service' => 'email',
]);
return new SesTransport(new SesClient(
$this->addSesCredentials($config)
));
}
/**
* Add the SES credentials to the configuration array.
*
* @param array $config
* @return array
*/
protected function addSesCredentials(array $config)
{
if ($config['key'] && $config['secret']) {
$config['credentials'] = Arr::only($config, ['key', 'secret']);
}
return $config;
}
/**
* Create an instance of the Mail Swift Transport driver.
*
* @return \Swift_SendmailTransport
*/
protected function createMailDriver()
{
return new MailTransport;
}
/**
* Create an instance of the Mailgun Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\MailgunTransport
*/
protected function createMailgunDriver()
{
$config = $this->app['config']->get('services.mailgun', []);
return new MailgunTransport(
$this->guzzle($config),
$config['secret'], $config['domain']
);
}
/**
* Create an instance of the Mandrill Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\MandrillTransport
*/
protected function createMandrillDriver()
{
$config = $this->app['config']->get('services.mandrill', []);
return new MandrillTransport(
$this->guzzle($config), $config['secret']
);
}
/**
* Create an instance of the SparkPost Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\SparkPostTransport
*/
protected function createSparkPostDriver()
{
$config = $this->app['config']->get('services.sparkpost', []);
return new SparkPostTransport(
$this->guzzle($config), $config['secret'], $config['options'] ?? []
);
}
/**
* Create an instance of the Log Swift Transport driver.
*
* @return \Illuminate\Mail\Transport\LogTransport
*/
protected function createLogDriver()
{
return new LogTransport($this->app->make(LoggerInterface::class));
}
/**
* Create an instance of the Array Swift Transport Driver.
*
* @return \Illuminate\Mail\Transport\ArrayTransport
*/
protected function createArrayDriver()
{
return new ArrayTransport;
}
/**
* Get a fresh Guzzle HTTP client instance.
*
* @param array $config
* @return \GuzzleHttp\Client
*/
protected function guzzle($config)
{
return new HttpClient(Arr::add(
$config['guzzle'] ?? [], 'connect_timeout', 60
));
}
/**
* Get the default mail driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['mail.driver'];
}
/**
* Set the default mail driver name.
*
* @param string $name
* @return void
*/
public function setDefaultDriver($name)
{
$this->app['config']['mail.driver'] = $name;
}
}
```
通过观察,可以看出,`TransportManager` 主要是为了创建各种驱动:
> - `Smtp` —— 创建 `Swift_SmtpTransport` 实例对象,主要使用的参数为:`host`、`port`、`encryption`、`username`、`password`、`stream`;
>
> - `Sendmail`、`Mail` —— 创建 `Swift_SendmailTransport` 实例对象,使用的参数为:`sendmail`;
>
> - `Ses` —— 创建 `SesTransport` 实例对象,使用的参数为 `config/services` 下对应的值:
>
> ```
> 'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
> ```
>
> - `Mailgun` —— 创建 `MailgunTransport` 实例对象,使用的参数为 `config/services` 下对应的值:
>
> ```
> 'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
],
> ```
>
> - `Mandrill` —— 创建 `MandrillTransport` 实例对象,使用的参数为 `config/services` 下对应的值:「暂无」,可以自行添加
>
> - `SparkPost` —— 创建 `SparkPostTransport` 实例对象,使用的参数为 `config/services` 下对应的值:
>
> ```
> 'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],
> ```
>
> 此外,就是创建 `Log` 驱动,和设置默认的驱动,由 `app['config']['mail.driver']` 决定的。
>
通过上文,我们还可以看出在使用 `Mailgun`、`Mandrill` 或者 `SparkPost` 都需要使用插件 `guzzle`,这也是为什么官网提示要安装 `guzzle` 插件的原因了:
![](https://user-gold-cdn.xitu.io/2018/8/26/165758aba50f58ba?w=812&h=402&f=jpeg&s=77301)
同时,这些驱动类都是 `extends Illuminate\Mail\Transport`,而且抽象类 `Transport` 是实现 `Swift_Transport` 接口:
```php
<?php
/*
* This file is part of SwiftMailer.
* (c) 2004-2009 Chris Corbyn
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Sends Messages via an abstract Transport subsystem.
*
* @author Chris Corbyn
*/
interface Swift_Transport
{
/**
* Test if this Transport mechanism has started.
*
* @return bool
*/
public function isStarted();
/**
* Start this Transport mechanism.
*/
public function start();
/**
* Stop this Transport mechanism.
*/
public function stop();
/**
* Check if this Transport mechanism is alive.
*
* If a Transport mechanism session is no longer functional, the method
* returns FALSE. It is the responsibility of the developer to handle this
* case and restart the Transport mechanism manually.
*
* @example
*
* if (!$transport->ping()) {
* $transport->stop();
* $transport->start();
* }
*
* The Transport mechanism will be started, if it is not already.
*
* It is undefined if the Transport mechanism attempts to restart as long as
* the return value reflects whether the mechanism is now functional.
*
* @return bool TRUE if the transport is alive
*/
public function ping();
/**
* Send the given Message.
*
* Recipient/sender data will be retrieved from the Message API.
* The return value is the number of recipients who were accepted for delivery.
*
* @param Swift_Mime_SimpleMessage $message
* @param string[] $failedRecipients An array of failures by-reference
*
* @return int
*/
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null);
/**
* Register a plugin in the Transport.
*
* @param Swift_Events_EventListener $plugin
*/
public function registerPlugin(Swift_Events_EventListener $plugin);
}
```
我们利用 `PhpStorm` 查看有多少类实现该接口:
![](https://user-gold-cdn.xitu.io/2018/8/26/165758aba6e5c90f?w=668&h=425&f=jpeg&s=155689)
好了,有了创建驱动的实例,接下来就是创建 `Swift_Mailer` 对象实例了:
```php
$this->app->singleton('swift.mailer', function ($app) {
...
return new Swift_Mailer($app['swift.transport']->driver());
});
```
下面借助 `$app['swift.transport']->driver()` 函数来说一说怎么拿到我们指定的驱动。
从 `TransportManager` 的父类 `Manager` 抽象类找到`driver()` 函数:
```php
/**
* Get the default driver name.
*
* @return string
*/
abstract public function getDefaultDriver();
/**
* Get a driver instance.
*
* @param string $driver
* @return mixed
*/
public function driver($driver = null)
{
$driver = $driver ?: $this->getDefaultDriver();
if (is_null($driver)) {
throw new InvalidArgumentException(sprintf(
'Unable to resolve NULL driver for [%s].', static::class
));
}
// If the given driver has not been created before, we will create the instances
// here and cache it so we can return it next time very quickly. If there is
// already a driver created by this name, we'll just return that instance.
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
return $this->drivers[$driver];
}
```
主要的使用各个继承类 (`TransportManager`) 实现的 `$this->getDefaultDriver()`
```php
/**
* Get the default mail driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['mail.driver'];
}
```
这就好理解了,指定的驱动是由 `config` 自主指定的;当拿到驱动名称后,我们回到 `driver()` 函数,继续往下看到代码:
```php
if (! isset($this->drivers[$driver])) {
$this->drivers[$driver] = $this->createDriver($driver);
}
// 注:$this->createDriver($driver) 这才是真正创建指定驱动的方法
/**
* Create a new driver instance.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
// We'll check to see if a creator method exists for the given driver. If not we
// will check for a custom driver creator, which allows developers to create
// drivers using their own customized driver creator Closure to create it.
if (isset($this->customCreators[$driver])) {
return $this->callCustomCreator($driver);
} else {
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
}
throw new InvalidArgumentException("Driver [$driver] not supported.");
}
```
当然我们的目标就定在这里:
```php
$method = 'create'.Str::studly($driver).'Driver';
if (method_exists($this, $method)) {
return $this->$method();
}
```
通过拿到的「驱动名称」,拼接成函数名,假如我们的驱动名称为:`mailgun`,则函数名:`createMailgunDriver`,然后就可以直接执行该方法,拿到对应的驱动对象实例了。
> 注:推荐看看这个 `Str::studly($driver)` 函数源码
到此,我们知道了如何利用 `config` 配置文件,来创建指定的驱动器,最后创建 `Swift_Mailer` 对象,以供之后执行使用。
### registerIlluminateMailer
看代码:
```php
/**
* Register the Illuminate mailer instance.
*
* @return void
*/
protected function registerIlluminateMailer()
{
$this->app->singleton('mailer', function ($app) {
$config = $app->make('config')->get('mail');
// Once we have create the mailer instance, we will set a container instance
// on the mailer. This allows us to resolve mailer classes via containers
// for maximum testability on said classes instead of passing Closures.
$mailer = new Mailer(
$app['view'], $app['swift.mailer'], $app['events']
);
if ($app->bound('queue')) {
$mailer->setQueue($app['queue']);
}
// Next we will set all of the global addresses on this mailer, which allows
// for easy unification of all "from" addresses as well as easy debugging
// of sent messages since they get be sent into a single email address.
foreach (['from', 'reply_to', 'to'] as $type) {
$this->setGlobalAddress($mailer, $config, $type);
}
return $mailer;
});
}
```
光看这个,比较简单,就是传入 `view`、第一步创建好的邮件发送器`Swift_Mailer` 对象,和 `events` 事件分发器,如果有队列,传入队列,创建 `Illuminate mailer` 对象,供我们真正场景使用;最后就是配置全局参数了。
### registerMarkdownRenderer
Laravel 能够捕获很多开发者的