137. 邮件系统
在一个Web系统中,向用户发送的邮件绝大部分都是有固定形式的,换句话说绝大多数时候邮件需要一个固定模板,在模板里面编辑好邮件内容,动态内容使用占位符代替,这些占位符在实际发送邮件时会依据上下文动态替换,不同类型的邮件对应不同的模板,如注册欢迎邮件、重置密码邮件等等;这里出现了几个关键词:“类型”、“模板”、“占位符”,这是设计邮件系统的核心,Drupal的实现不但满足模板式邮件还满足非模板式邮件发送。
Drupal邮件系统概述:
在Drupal中,如果模块需要发送邮件,最佳实践是为该类型邮件定义一个类型,并指定类型标识符,然后调用邮件管理器去发送,邮件管理器以邮件类型为核心来管理系统中所有的邮件,在发送时派发邮件钩子(mail)及其修改钩子,定义该邮件类型的模块应通过钩子去修改邮件标题、内容等,这需要模块自行去取回模板并实现占位符替换,通常占位符替换需要用到系统token服务(详见本系列token服务主题)。
以上发送方式允许其他模块干预或修改邮件,如果发起模块不希望其他模块干预,可直接调用邮件发送器发送即可。
邮件发送器:
邮件发送器用于实际执行准备好的邮件的发送工作,是比较底层的操作,并不涉及钩子派发、模板处理、内容准备等系统层面的事情,以插件方式提供,须实现以下接口:
Drupal\Core\Mail\MailInterface
接口方法说明如下:
public function format(array $message);
在发送前格式化邮件消息,如将html转化为原始文本,或将渲染数组转化为字符串等,接收邮件消息数组(该数组见下),返回修改过的邮件消息数组
public function mail(array $message);
实际执行邮件发送任务,接收邮件消息数组(见下),返回布尔值,指示邮件是否成功投递(成功投递不意味着用户成功收到)
邮件管理器:
邮件管理器综合管理邮件发送,同时也是一个插件管理器,每个插件都是一个邮件发送器,用于实际执行发送任务,邮件管理器定义如下:
服务id:plugin.manager.mail
类:Drupal\Core\Mail\MailManager
插件目录:Plugin/Mail
插件定义修改钩子:mail_backend_info
插件(邮件发送器)须实现的接口:Drupal\Core\Mail\MailInterface
邮件发送:
模块需要发送某类型邮件时,须调用邮件管理器的以下方法并实现邮件钩子:
public function mail($module, $key, $to, $langcode, $params = [], $reply = NULL, $send = TRUE);
参数含义如下:
$module:
字符串值,发送邮件的模块的名字,如user
$key:
字符串值,模块定义的邮件类型标识符
$to:
字符串值,邮件接收地址,可多个(通常用“, ”分隔多个地址),该参数有效格式见
http://php.net/manual/filter.filters.validate.php
$langcode:
邮件所用的语言代码
$params:
构建邮件所用的参数数组,不同类型邮件所需参数不同,是可选的,其中参数项(键名)“_error_message”是一个通用参数,用于邮件发送出现错误时,显示给用户的错误消息,是一个翻译对象(默认错误消息为“不能发送邮件,如果问题依旧请联系站点管理员”),如果设置为false将禁止显示错误消息
$reply:
用户回复邮件时所用的邮件地址(即回复的消息将发送到该地址)可选,默认NULL
$send:
布尔值,指示是否真的发送邮件,如果为false那么邮件一定不会被真实发送,默认为TRUE,表示会执行发送,但未必真实发送,因为此时模块可以通过邮件钩子取消(见后)
返回邮件消息数组:
这个数组包含了被发送邮件的所有信息,称为邮件消息数组,其中键名“result”的值是一个布尔值或NULL,指示邮件是否成功投递:
true表示投递成功,但注意成功投递并不意味着用户成功收到,有很多因素都可能导致用户实际上没有收到邮件,如收、发件服务器故障,或对方邮件的垃圾管控机制等;
NULL或不存在键名“result”表示没有执行邮件发送(可能原因是钩子取消发送或$send参数为false);
false意味着投递失败,用户必定收不到邮件,此时该方法将记录邮件错误日志,并向用户显示错误消息;
邮件消息数组贯彻整个邮件系统,结构见下。
发送过程:
系统首先构建邮件消息数组,该数组结构类似如下:
$message = [
'id' => $module . '_' . $key,
'module' => $module, //发送邮件的模块
'key' => $key, //邮件类型
'to' => $to, //收件地址,可多个,此时用英文逗号分隔
'from' => $site_mail, //站点邮件地址,如无将从php配置中取
'reply-to' => $reply, //回复邮件地址
'langcode' => $langcode, //邮件所用语言的语言代码
'params' => $params, //构造邮件所需的参数
'send' => TRUE, //是否发送邮件,模块通过钩子修改此值可阻止邮件发送
'subject' => '', //邮件标题
'body' => [], //邮件体,数组,模块以元素方式添加数据,在发送器中转变为字符串
'headers' => [ //邮件发送头
'MIME-Version' => '1.0',
'Content-Type' => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
'Content-Transfer-Encoding' => '8Bit',
'X-Mailer' => 'Drupal',
'Sender' => $site_mail,
'Return-Path' => $site_mail,
'From' => '站点名<站点邮件地址>',
'Reply-to' => $reply,
],
];
该数组详见以下方法:
\Drupal\Core\Mail\MailManager::doMail
构建邮件消息数组后执行模块的邮件钩子“mail”,如:
user_mail($key, &$message, $params)
注意:仅发送邮件的模块的该钩子被执行,在该钩子中,发送模块应至少填充邮件消息数组中的邮件标题和邮件体
紧接着系统派发邮件修改钩子“mail”,其他模块可以通过该修改钩子调整参数或中断邮件发送
随后系统初始化邮件发送器插件,选择插件的逻辑依据配置对象“system.mail”而定,详见:
\Drupal\Core\Mail\MailManager::getInstance
在该配置对象中我们可以精确控制某类型或某模块的邮件使用哪个发送器发送,如果没有配置数据将用默认发送器发送
初始化发送器后会首先调用其格式化方法(此时在邮件消息数组中,邮件内容应已构建完成),然后在允许发送的情况下调用发送方法进行发送
默认发送器:
系统默认提供了两个邮件发送器:
插件id:php_mail:
类:Drupal\Core\Mail\Plugin\Mail\PhpMail
以纯文本方式发送邮件,发送采用php原生的mail()函数
插件id:test_mail_collector:
类:Drupal\Core\Mail\Plugin\Mail\TestMailCollector
用于开发测试,该发送器模拟邮件发送,并没有真的发送,在内部将邮件消息数组存放到状态系统中以供查阅调试
自定义发送器:
如果php的mail函数不能被使用,就需要自定义发送器了,定义一个插件类,实现以下接口:
\Drupal\Core\Mail\MailInterface
详见插件定义,定义好后还需要修改配置对象“system.mail”,以让某类型邮件或全部默认使用自定义的发送器。通常我们会采用SMTP进行发送,这样的贡献模块在官网有很多
用户user模块邮件发送:
在核心模块中用户模块需要用到邮件发送,这里也将其作为自定义模块发送邮件的示例来讲解
该模块定义了以下函数进行各种类型的邮件统一发送调用:
function _user_mail_notify($op, $account, $langcode = NULL)
参数$op为操作类型,对应邮件类型标识符
参数$account为用户实体对象,将作为参数传递,
参数$langcode为邮件采用的语言,
该函数将依据用户模块自身的配置决定邮件是否发送,返回布尔值或NULL以示邮件是否投递成功
如发送账户密码重置请求邮件如下:
$user = \Drupal::entityTypeManager()->getStorage("user")->load(75);
$isSend = _user_mail_notify('password_reset', $user);
echo $isSend ? "is send" : "no send";
用户通过收到邮件中的链接就可以重置密码了
用户模块实际处理邮件的接口函数为(mail钩子):
user_mail($key, &$message, $params)
在该钩子函数中向邮件消息数组填充了邮件标题和邮件体,这里填充时都采用了以追加方式进行,实际上其他模块的邮件钩子并不执行,所以仅在修改钩子中才有此必要,邮件模板来自配置系统,并采用了token子系统进行变量替换(详见本系列token主题)
通常模块需发送邮件时,首先确定一个邮件类型标识符,然后在邮件钩子中填充邮件标题和邮件体即可,如果需要特定的发送器,那么同时还要在配置对象“system.mail”中添加配置
用邮件系统发送手机短信:
通常我们不应该这么干,最佳实践应是去定义手机短信发送服务,但在系统架构上确实能够很方便的通过邮件系统去发送手机短信,仅需要定义一个专门处理短信发送的邮件发送器即可,在需要发送短信的地方配置为使用该发送器去发送。
这里云客在思考:通知用户的方式有很多,除了邮件、短信外还有、微信、微博、QQ等,如何进一步抽象将她们统统定义为用户通知接口,如何添加管理通知类型,如何处理消息差异,以实现各类消息的统一处理,读者可以尝试一下
大量邮件发送:
单个邮件的处理还是较为耗费资源的,主要体现在和邮件服务器的交互上,网络延时会浪费很多时间,网速慢时尤其严重,如果系统要发送大量邮件,比如给几百万会员发一封通知邮件,那怎么处理呢?此时不可能在一个请求中完成了,此时如果要求邮件立即发送,那么可以结合批处理系统,如果不要求立即处理可以采用队列系统在系统闲时静默在后台发送,关于批处理和列队的使用请见本系列相关主题。
邮件验证服务:
用于验证一个email地址的有效性,定义如下:
服务id:email.validator
类:Drupal\Component\Utility\EmailValidator
获取方法:\Drupal::service('email.validator')
调用验证:
\Drupal::service('email.validator')->isValid($email)
该方法接收一个字符串email地址,返回一个布尔值,如果为true则说明地址有效,否则地址无效
邮件辅助类:
Drupal\Component\Utility\Mail
用于确保邮件中的显示名和RFC-822、RFC-2822是兼容的
\Drupal\Core\Mail\MailFormatHelper
辅助转换换行符、将html转化到原始文本等
补充:
1、邮件发送失败时日志中并没有记录邮件类型标识符,如果能记录会更好,排除故障时有帮助
2、在php中最著名的邮件发送库是PHPMailer,她可采用SMTP进行邮件发送,目前官网有很多模块是基于此开发的邮件发送器,其中用户量最多的贡献模块是“smtp”,但测试发现该模块在没有ssl支持时会出错,开启调式也会出错,已反馈