说一说 Laravel 邮件发送流程

栏目: PHP · 发布时间: 6年前

内容简介:当我使用 Laravel 的邮件发送功能时,脑子里浮现出这么几个问题:下面就让我们开始徒手扒一扒「邮件发送功能」的实现原理。我们使用阿里云提供的免费邮,和采用「smtp」驱动,作为测试,参考

当我使用 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
复制代码

写个测试流程,还是挺简单的,具体如下:

// 1. 创建测试类
php artisan make:mail TestEmail

// 2. 在 TestEmail 类,载入视图

public function build()
{
    return $this->view('mail.test');
}

// 3. 输出 hello coding01

<p>hello coding01</p>
复制代码

最后写个命令函数:

Artisan::command('test', function () {
    Mail::to('yemeishu@126.com')->send(new \App\Mail\TestEmail());
});
复制代码

执行 php artisan test 看测试是否发送成功:

说一说 Laravel 邮件发送流程

解析 MailServiceProvider

写了不少 Laravel 代码,看

Mail::to('yemeishu@126.com')->send(new \App\Mail\TestEmail());
复制代码

自然而然的想到是不是有一个 MailServiceProvider ,果不其然,在 config/app.php 的数组 providers 就包含了该 ServiceProvider

说一说 Laravel 邮件发送流程

所以我们就开始围绕这个 MailServiceProvider 来解析了

/**
 * Register the service provider.
 *
 * @return void
 */
public function register()
{
    $this->registerSwiftMailer();

    $this->registerIlluminateMailer();

    $this->registerMarkdownRenderer();
}
复制代码

register 函数,一目了然,我们将重点看看这三个方法都是干嘛用的。

registerSwiftMailer

看代码:

/**
 * 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(); 方法:

/**
 * Register the Swift Transport instance.
 *
 * @return void
 */
protected function registerSwiftTransport()
{
    $this->app->singleton('swift.transport', function ($app) {
        return new TransportManager($app);
    });
}
复制代码

看看这个 TransportManager 类是干嘛用的:

<?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 实例对象,主要使用的参数为: hostportencryptionusernamepasswordstream

  • SendmailMail —— 创建 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']` 决定的。

复制代码

通过上文,我们还可以看出在使用 MailgunMandrill 或者 SparkPost 都需要使用插件 guzzle ,这也是为什么官网提示要安装 guzzle 插件的原因了:

说一说 Laravel 邮件发送流程

同时,这些驱动类都是 extends Illuminate\Mail\Transport ,而且抽象类 Transport 是实现 Swift_Transport 接口:

<?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 查看有多少类实现该接口:

说一说 Laravel 邮件发送流程

好了,有了创建驱动的实例,接下来就是创建 Swift_Mailer 对象实例了:

$this->app->singleton('swift.mailer', function ($app) {
    
    ...
     
    return new Swift_Mailer($app['swift.transport']->driver());
});
复制代码

下面借助 $app['swift.transport']->driver() 函数来说一说怎么拿到我们指定的驱动。

TransportManager 的父类 Manager 抽象类找到 driver() 函数:

/**
 * 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()

/**
 * Get the default mail driver name.
 *
 * @return string
 */
public function getDefaultDriver()
{
    return $this->app['config']['mail.driver'];
}
复制代码

这就好理解了,指定的驱动是由 config 自主指定的;当拿到驱动名称后,我们回到 driver() 函数,继续往下看到代码:

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.");
}
复制代码

当然我们的目标就定在这里:

$method = 'create'.Str::studly($driver).'Driver';

if (method_exists($this, $method)) {
    return $this->$method();
}
复制代码

通过拿到的「驱动名称」,拼接成函数名,假如我们的驱动名称为: mailgun ,则函数名: createMailgunDriver ,然后就可以直接执行该方法,拿到对应的驱动对象实例了。

注:推荐看看这个 Str::studly($driver) 函数源码

到此,我们知道了如何利用 config 配置文件,来创建指定的驱动器,最后创建 Swift_Mailer 对象,以供之后执行使用。

registerIlluminateMailer

看代码:

/**
 * 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 能够捕获很多开发者的:two_hearts:,还有一个核心的地方在于:知道开发者想要什么。其中 Markdown 基本就是开发者的必备。用 Markdown 写邮件,是一个不错的方案,下面看看怎么做到的?

为了扒 Markdown 代码,先写个 demo 看怎么使用。

使用命令,带上 --markdown 选项:

php artisan make:mail TestMdEmail --markdown=mail.testmd
复制代码

这样就可以为我们创建了 TestMdEmail

<?php

namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Queue\ShouldQueue;

class TestMdEmail extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->markdown('mail.testmd');
    }
}
复制代码

和视图 testmd.blade.php ,默认视图内容:

@component('mail::message')
# Introduction

The body of your message.

@component('mail::button', ['url' => ''])
Button Text
@endcomponent

Thanks,<br>
{{ config('app.name') }}
@endcomponent
复制代码

写个测试,发送看看运行效果:

Artisan::command('testmd', function () {
    Mail::to('yemeishu@126.com')->send(new \App\Mail\TestMdEmail());
});
复制代码
说一说 Laravel 邮件发送流程

一切使用默认的,就可以很轻易的创建 markdown 格式的邮件内容,并发送。

我们可以看看源码了:

/**
 * Register the Markdown renderer instance.
 *
 * @return void
 */
protected function registerMarkdownRenderer()
{
    if ($this->app->runningInConsole()) {
        $this->publishes([
            __DIR__.'/resources/views' => $this->app->resourcePath('views/vendor/mail'),
        ], 'laravel-mail');
    }

    $this->app->singleton(Markdown::class, function ($app) {
        $config = $app->make('config');

        return new Markdown($app->make('view'), [
            'theme' => $config->get('mail.markdown.theme', 'default'),
            'paths' => $config->get('mail.markdown.paths', []),
        ]);
    });
}
复制代码

目标很简单,就是利用配置信息,创建 Markdown 对象,为后续服务。

我们先看默认的 mail config

/*
   |--------------------------------------------------------------------------
| Markdown Mail Settings
|--------------------------------------------------------------------------
|
| If you are using Markdown based email rendering, you may configure your
| theme and component paths here, allowing you to customize the design
| of the emails. Or, you may simply stick with the Laravel defaults!
|
*/

'markdown' => [
    'theme' => 'default',
    'paths' => [
        resource_path('views/vendor/mail'),
    ],
],
复制代码

默认的 markdown 配置信息都存在 views/vendor/mail 文件夹下,我们可以通过命令:

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

Copied Directory [/vendor/laravel/framework/src/Illuminate/Mail/resources/views] To [/resources/views/vendor/mail]
Publishing complete.
复制代码

所有的默认组件都存在这个文件夹下,还有页面的视图样式主题等:

说一说 Laravel 邮件发送流程

*注:*我们可以自定组件和增加发布邮箱的 css 样式

Maikdown 构造函数:

/**
 * Create a new Markdown renderer instance.
 *
 * @param  \Illuminate\Contracts\View\Factory  $view
 * @param  array  $options
 * @return void
 */
public function __construct(ViewFactory $view, array $options = [])
{
    $this->view = $view;
    $this->theme = $options['theme'] ?? 'default';
    $this->loadComponentsFrom($options['paths'] ?? []);
}
复制代码

主要是传入 View 视图构造器和主题样式,以及各个 markdown 组件。

邮件发送流程

下面我们结合上面的 demo 看看如何构造邮件内容,和发送邮件的,我们看代码:

Mail::to('yemeishu@126.com')->send(new \App\Mail\TestMdEmail());
复制代码

这里的 Mail 就是上面 registerIlluminateMailer 注册的 Illuminate\Mail\Mailer 对象。

我们且看它的 send() 方法:

/**
 * Send a new message using a view.
 *
 * @param  string|array|MailableContract  $view
 * @param  array  $data
 * @param  \Closure|string  $callback
 * @return void
 */
public function send($view, array $data = [], $callback = null)
{
    if ($view instanceof MailableContract) {
        return $this->sendMailable($view);
    }

    // First we need to parse the view, which could either be a string or an array
    // containing both an HTML and plain text versions of the view which should
    // be used when sending an e-mail. We will extract both of them out here.
    list($view, $plain, $raw) = $this->parseView($view);

    $data['message'] = $message = $this->createMessage();

    // Once we have retrieved the view content for the e-mail we will set the body
    // of this message using the HTML type, which will provide a simple wrapper
    // to creating view based emails that are able to receive arrays of data.
    call_user_func($callback, $message);

    $this->addContent($message, $view, $plain, $raw, $data);

    // If a global "to" address has been set, we will set that address on the mail
    // message. This is primarily useful during local development in which each
    // message should be delivered into a single mail address for inspection.
    if (isset($this->to['address'])) {
        $this->setGlobalTo($message);
    }

    // Next we will determine if the message should be sent. We give the developer
    // one final chance to stop this message and then we will send it to all of
    // its recipients. We will then fire the sent event for the sent message.
    $swiftMessage = $message->getSwiftMessage();

    if ($this->shouldSendMessage($swiftMessage, $data)) {
        $this->sendSwiftMessage($swiftMessage);

        $this->dispatchSentEvent($message, $data);
    }
}
复制代码

我们看第一步:

if ($view instanceof MailableContract) {
    return $this->sendMailable($view);
}
复制代码

执行的 $this->sendMailable($view)

/**
 * Send the message using the given mailer.
 *
 * @param  \Illuminate\Contracts\Mail\Mailer  $mailer
 * @return void
 */
public function send(MailerContract $mailer)
{
    $translator = Container::getInstance()->make(Translator::class);

    $this->withLocale($this->locale, $translator, function () use ($mailer) {
        Container::getInstance()->call([$this, 'build']);

        $mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
            $this->buildFrom($message)
                 ->buildRecipients($message)
                 ->buildSubject($message)
                 ->runCallbacks($message)
                 ->buildAttachments($message);
        });
    });
}
复制代码

核心的在于先执行我们默认 build 方法:

/**
 * Build the message.
 *
 * @return $this
 */
public function build()
{
    return $this->markdown('mail.testmd');
}
复制代码

这就是为什么在命令创建发送邮件模板类时,都会默认创建该 build 方法了,然后在该方法里,载入我们的构建内容和逻辑;在 markdown 视图中,默认的是运行 $this->markdown('mail.testmd') :

/**
 * Set the Markdown template for the message.
 *
 * @param  string  $view
 * @param  array  $data
 * @return $this
 */
public function markdown($view, array $data = [])
{
    $this->markdown = $view;
    $this->viewData = array_merge($this->viewData, $data);

    return $this;
}
复制代码

将视图和视图内容载入对象中。

然后我们继续回到上个 send 方法中:

$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
    $this->buildFrom($message)
         ->buildRecipients($message)
         ->buildSubject($message)
         ->runCallbacks($message)
         ->buildAttachments($message);
});
复制代码

我们一个个方法来解析:

$this->buildView()

/**
 * Build the view for the message.
 *
 * @return array|string
 */
protected function buildView()
{
    if (isset($this->html)) {
        return array_filter([
            'html' => new HtmlString($this->html),
            'text' => isset($this->textView) ? $this->textView : null,
        ]);
    }

    if (isset($this->markdown)) {
        return $this->buildMarkdownView();
    }

    if (isset($this->view, $this->textView)) {
        return [$this->view, $this->textView];
    } elseif (isset($this->textView)) {
        return ['text' => $this->textView];
    }

    return $this->view;
}
复制代码

很显然,执行 $this->buildMarkdownView()

/**
 * Build the Markdown view for the message.
 *
 * @return array
 */
protected function buildMarkdownView()
{
    $markdown = Container::getInstance()->make(Markdown::class);

    if (isset($this->theme)) {
        $markdown->theme($this->theme);
    }

    $data = $this->buildViewData();

    return [
        'html' => $markdown->render($this->markdown, $data),
        'text' => $this->buildMarkdownText($markdown, $data),
    ];
}
复制代码

这时候, Markdown 对象就派上用场了,目标该放在这两个方法上了:

return [
    'html' => $markdown->render($this->markdown, $data),
    'text' => $this->buildMarkdownText($markdown, $data),
];
复制代码

$markdown->render() 方法:

/**
 * Render the Markdown template into HTML.
 *
 * @param  string  $view
 * @param  array  $data
 * @param  \TijsVerkoyen\CssToInlineStyles\CssToInlineStyles|null  $inliner
 * @return \Illuminate\Support\HtmlString
 */
public function render($view, array $data = [], $inliner = null)
{
    $this->view->flushFinderCache();

    $contents = $this->view->replaceNamespace(
        'mail', $this->htmlComponentPaths()
    )->make($view, $data)->render();

    return new HtmlString(($inliner ?: new CssToInlineStyles)->convert(
        $contents, $this->view->make('mail::themes.'.$this->theme)->render()
    ));
}
复制代码

$markdown->renderText() 方法:

/**
 * Render the Markdown template into HTML.
 *
 * @param  string  $view
 * @param  array  $data
 * @return \Illuminate\Support\HtmlString
 */
public function renderText($view, array $data = [])
{
    $this->view->flushFinderCache();

    $contents = $this->view->replaceNamespace(
        'mail', $this->markdownComponentPaths()
    )->make($view, $data)->render();

    return new HtmlString(
        html_entity_decode(preg_replace("/[\r\n]{2,}/", "\n\n", $contents), ENT_QUOTES, 'UTF-8')
    );
}
复制代码

主要的逻辑,就是将 markdown 格式转变成 html 格式,以及构成数组 ['html', 'data'] 输出,最后再次执行 send 方法,并传入闭包函数,供构建 message 服务:

$mailer->send($this->buildView(), $this->buildViewData(), function ($message) {
    $this->buildFrom($message)
         ->buildRecipients($message)
         ->buildSubject($message)
         ->runCallbacks($message)
         ->buildAttachments($message);
});
复制代码

我们回头再看 send 方法,未解析的代码:

// First we need to parse the view, which could either be a string or an array
    // containing both an HTML and plain text versions of the view which should
    // be used when sending an e-mail. We will extract both of them out here.
    list($view, $plain, $raw) = $this->parseView($view);

    $data['message'] = $message = $this->createMessage();

    // Once we have retrieved the view content for the e-mail we will set the body
    // of this message using the HTML type, which will provide a simple wrapper
    // to creating view based emails that are able to receive arrays of data.
    call_user_func($callback, $message);

    $this->addContent($message, $view, $plain, $raw, $data);

    // If a global "to" address has been set, we will set that address on the mail
    // message. This is primarily useful during local development in which each
    // message should be delivered into a single mail address for inspection.
    if (isset($this->to['address'])) {
        $this->setGlobalTo($message);
    }

    // Next we will determine if the message should be sent. We give the developer
    // one final chance to stop this message and then we will send it to all of
    // its recipients. We will then fire the sent event for the sent message.
    $swiftMessage = $message->getSwiftMessage();

    if ($this->shouldSendMessage($swiftMessage, $data)) {
        $this->sendSwiftMessage($swiftMessage);

        $this->dispatchSentEvent($message, $data);
    }
复制代码

第一步无非就是将上面的数组遍历出来,然后再创建 Message 对象:

$data['message'] = $message = $this->createMessage();

/**
 * Create a new message instance.
 *
 * @return \Illuminate\Mail\Message
 */
protected function createMessage()
{
    $message = new Message($this->swift->createMessage('message'));

    // If a global from address has been specified we will set it on every message
    // instance so the developer does not have to repeat themselves every time
    // they create a new message. We'll just go ahead and push this address.
    if (! empty($this->from['address'])) {
        $message->from($this->from['address'], $this->from['name']);
    }

    // When a global reply address was specified we will set this on every message
    // instance so the developer does not have to repeat themselves every time
    // they create a new message. We will just go ahead and push this address.
    if (! empty($this->replyTo['address'])) {
        $message->replyTo($this->replyTo['address'], $this->replyTo['name']);
    }

    return $message;
}
复制代码

这个 Message 构造函数传入的 swift 服务对象,以后通过 message 传入的数据,都是传给 swift 服务对象。

$message = new Message($this->swift->createMessage('message'));

...

/**
 * Create a new class instance of one of the message services.
 *
 * For example 'mimepart' would create a 'message.mimepart' instance
 *
 * @param string $service
 *
 * @return object
 */
public function createMessage($service = 'message')
{
    return Swift_DependencyContainer::getInstance()
        ->lookup('message.'.$service);
}
复制代码

如:

/**
 * Add a "from" address to the message.
 *
 * @param  string|array  $address
 * @param  string|null  $name
 * @return $this
 */
public function from($address, $name = null)
{
    $this->swift->setFrom($address, $name);

    return $this;
}

/**
 * Set the "sender" of the message.
 *
 * @param  string|array  $address
 * @param  string|null  $name
 * @return $this
 */
public function sender($address, $name = null)
{
    $this->swift->setSender($address, $name);

    return $this;
}
复制代码

这样,我们就开始使用 MailServiceProvider 中创建的 Swift_Mailer 对象了。

好了,终于到最后一个步骤了:

// Next we will determine if the message should be sent. We give the developer
    // one final chance to stop this message and then we will send it to all of
    // its recipients. We will then fire the sent event for the sent message.
    $swiftMessage = $message->getSwiftMessage();

    if ($this->shouldSendMessage($swiftMessage, $data)) {
        $this->sendSwiftMessage($swiftMessage);

        $this->dispatchSentEvent($message, $data);
    }
复制代码

获取 swift 服务对象,然后开始执行发送逻辑,和分发发送邮件事件了。

/**
 * Send a Swift Message instance.
 *
 * @param  \Swift_Message  $message
 * @return void
 */
protected function sendSwiftMessage($message)
{
    try {
        return $this->swift->send($message, $this->failedRecipients);
    } finally {
        $this->forceReconnection();
    }
}

...

/**
 * Dispatch the message sent event.
 *
 * @param  \Illuminate\Mail\Message  $message
 * @param  array  $data
 * @return void
 */
protected function dispatchSentEvent($message, $data = [])
{
    if ($this->events) {
        $this->events->dispatch(
            new Events\MessageSent($message->getSwiftMessage(), $data)
        );
    }
}
复制代码

继续看如何利用 swift 对象发送邮件。

/**
 * Send a Swift Message instance.
 *
 * @param  \Swift_Message  $message
 * @return void
 */
protected function sendSwiftMessage($message)
{
    try {
        return $this->swift->send($message, $this->failedRecipients);
    } finally {
        $this->forceReconnection();
    }
}
复制代码

$this->swift->send() 方法:

/**
 * Send the given Message like it would be sent in a mail client.
 *
 * All recipients (with the exception of Bcc) will be able to see the other
 * recipients this message was sent to.
 *
 * Recipient/sender data will be retrieved from the Message object.
 *
 * The return value is the number of recipients who were accepted for
 * delivery.
 *
 * @param Swift_Mime_SimpleMessage $message
 * @param array                    $failedRecipients An array of failures by-reference
 *
 * @return int The number of successful recipients. Can be 0 which indicates failure
 */
public function send(Swift_Mime_SimpleMessage $message, &$failedRecipients = null)
{
    $failedRecipients = (array) $failedRecipients;

    if (!$this->transport->isStarted()) {
        $this->transport->start();
    }

    $sent = 0;

    try {
        $sent = $this->transport->send($message, $failedRecipients);
    } catch (Swift_RfcComplianceException $e) {
        foreach ($message->getTo() as $address => $name) {
            $failedRecipients[] = $address;
        }
    }

    return $sent;
}
复制代码

还记得一开始对每个发送驱动做封装了吧, send 动作,最终还是交给我们的邮件发送驱动去执行,默认的是使用 SmtpTransport ,即 Swift_SmtpTransport 发送。

$sent = $this->transport->send($message, $failedRecipients);
复制代码

总结

过了一遍代码,粗略了解下怎么封装各个驱动器,将 markdown 格式转成 html 格式,然后再封装成 Message 对象,交给驱动器去发送邮件。

下一步说一说 Swift_SmtpTransport 实现原理,和我们自己怎么制作一个驱动器,最后再说一说这过程用到了哪些设计模式?

未完待续


以上所述就是小编给大家介绍的《说一说 Laravel 邮件发送流程》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Learn Python the Hard Way

Learn Python the Hard Way

Zed Shaw / Example Product Manufacturer / 2011

This is a very beginner book for people who want to learn to code. If you can already code then the book will probably drive you insane. It's intended for people who have no coding chops to build up t......一起来看看 《Learn Python the Hard Way》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具