[Abp vNext 源码分析] - 20. 电子邮件与短信支持
一、简介
ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSender
或 IEmailSender
即可实现短信发送和邮件发送。
二、源码分析
2.1 启动模块
短信发送的抽象层比较简单,AbpSmsModule
模块内部并无任何操作,仅作为空模块进行定义。
电子邮件的 AbpEmailingModule
模块内,主要添加了一些本地化资源支持。另一个动作就是添加了一个 BackgroundEmailSendingJob
后台作业,这个后台作业主要是用于后续发送电子邮件使用。因为邮件发送这个动作实时性要求并不高,在实际的业务实践当中,我们基本会将其加入到一个后台队列慢慢发送,所以这里 ABP 为我们实现了 BackgroundEmailSendingJob
。
BackgroundEmailSendingJob.cs:
public class BackgroundEmailSendingJob : AsyncBackgroundJob<BackgroundEmailSendingJobArgs>, ITransientDependency
{
protected IEmailSender EmailSender { get; }
public BackgroundEmailSendingJob(IEmailSender emailSender)
{
EmailSender = emailSender;
}
public override async Task ExecuteAsync(BackgroundEmailSendingJobArgs args)
{
if (args.From.IsNullOrWhiteSpace())
{
await EmailSender.SendAsync(args.To, args.Subject, args.Body, args.IsBodyHtml);
}
else
{
await EmailSender.SendAsync(args.From, args.To, args.Subject, args.Body, args.IsBodyHtml);
}
}
}
这个后台任务的逻辑也不复杂,就使用 IEmailSender
发送邮件,我们在任何地方需要后台发送邮件的时,只需要注入 IBackgroundJobManager
,使用 BackgroundEmailSendingJobArgs
作为参数添加入队一个后台作业即可。
使用 IBackgroundJobManager
添加一个新的邮件发送欢迎邮件:
public class DemoClass
{
private readonly IBackgroundJobManager _backgroundJobManager;
private readonly IUserInfoRepository _userRep;
public DemoClass(IBackgroundJobManager backgroundJobManager,
IUserInfoRepository userRep)
{
_backgroundJobManager = backgroundJobManager;
_userRep = userRep;
}
public async Task SendWelcomeEmailAsync(Guid userId)
{
var userInfo = await _userRep.GetByIdAsync(userId);
await _backgroundJobManager.EnqueueAsync(new BackgroundEmailSendingJobArgs
{
To = userInfo.EmailAddress,
Subject = "Welcome",
Body = "Welcome, Hello World!",
IsBodyHtml = false;
});
}
}
注意
目前
BackgroundEmailSendingJobArgs
参数不支持发送附件,ABP 可能在以后的版本会进行实现。
2.2 Email 的核心组件
ABP 定义了一个 IEmailSender
接口,定义了多个 SendAsync()
方法重载,用于直接发送电子邮件。同时也提供了 QueueAsync()
方法,通过后台任务队列来发送邮件。
public interface IEmailSender
{
Task SendAsync(
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task SendAsync(
string from,
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task SendAsync(
MailMessage mail,
bool normalize = true
);
Task QueueAsync(
string to,
string subject,
string body,
bool isBodyHtml = true
);
Task QueueAsync(
string from,
string to,
string subject,
string body,
bool isBodyHtml = true
);
//TODO: 准备添加的 QueueAsync 方法。目前存在的问题: MailMessage 不能够被序列化,所以不能加入到后台任务队列当中。
}
ABP 实际拥有两种 Email Sender 实现,分别是 SmtpEmailSender
和 MailkitEmailSender
,各个类型的关系如下。
UML 类图:
class IEmailSender{
<<Interface>>
+SendAsync(string,string,string,bool=true) Task
+SendAsync(string,string,string,string,bool=true) Task
+SendAsync(MailMessage,bool=true) Task
+QueueAsync(string,string,string,bool=true) Task
+QueueAsync(string,string,string,string,bool=true) Task
}
class ISmtpEmailSender{
<<Interface>>
......
+BuildClientAsync() Task~SmtpClient~
}
class IMailKitSmtpEmailSemder{
<<Interface>>
......
+BuildClientAsync() Task~SmtpClient~
}
class EmailSenderBase{
<<Abstract>>
......
}
class SmtpEmailSender{
......
}
class MailKitSmtpEmailSender{
......
}
class NullEmailSender{
......
}
ISmtpEmailSender --|> IEmailSender: 继承
IMailKitSmtpEmailSemder --|> IEmailSender: 继承
EmailSenderBase ..|> IEmailSender: 实现
SmtpEmailSender ..|> ISmtpEmailSender: 实现
SmtpEmailSender --|> EmailSenderBase: 继承
NullEmailSender --|> EmailSenderBase: 继承
MailKitSmtpEmailSender ..|> IMailKitSmtpEmailSemder: 实现
MailKitSmtpEmailSender --|> EmailSenderBase: 继承
可以从 UML 类图看出,每个 EmailSender 实现都与一个 IXXXConfiguration
对应,这个配置类存储了基于 Smtp 发件的必须配置。因为 MailKit 本身也是基于 Smtp 发送邮件的,所以没有重新定义新的配置类,而是直接复用的 ISmtpEmailSenderConfiguration
接口与实现。
在 EmailSenderBase
基类当中,基本实现了 IEmailSender
接口的所有方法的逻辑,只留下了 SendEmailAsync(MailMessage mail)
作为一个抽象方法等待子类实现。也就是说其他的方法最终都是使用该方法来最终发送邮件。
public abstract class EmailSenderBase : IEmailSender
{
protected IEmailSenderConfiguration Configuration { get; }
protected IBackgroundJobManager BackgroundJobManager { get; }
protected EmailSenderBase(IEmailSenderConfiguration configuration, IBackgroundJobManager backgroundJobManager)
{
Configuration = configuration;
BackgroundJobManager = backgroundJobManager;
}
// ... 实现的接口方法
protected abstract Task SendEmailAsync(MailMessage mail);
// 使用 Configuration 里面的参数,统一处理邮件数据。
protected virtual async Task NormalizeMailAsync(MailMessage mail)
{
if (mail.From == null || mail.From.Address.IsNullOrEmpty())
{
mail.From = new MailAddress(
await Configuration.GetDefaultFromAddressAsync(),
await Configuration.GetDefaultFromDisplayNameAsync(),
Encoding.UTF8
);
}
if (mail.HeadersEncoding == null)
{
mail.HeadersEncoding = Encoding.UTF8;
}
if (mail.SubjectEncoding == null)
{
mail.SubjectEncoding = Encoding.UTF8;
}
if (mail.BodyEncoding == null)
{
mail.BodyEncoding = Encoding.UTF8;
}
}
}
ABP 默认可用的邮件发送组件是 SmtpEmailSender
,它使用的是 .NET 自带的邮件发送组件,本质上就是构建了一个 SmtpClient
客户端,然后调用它的发件方法进行邮件发送。
public class SmtpEmailSender : EmailSenderBase, ISmtpEmailSender, ITransientDependency
{
// ... 省略的代码。
public async Task<SmtpClient> BuildClientAsync()
{
var host = await SmtpConfiguration.GetHostAsync();
var port = await SmtpConfiguration.GetPortAsync();
var smtpClient = new SmtpClient(host, port);
// 从 SettingProvider 中获取各个配置参数,构建 Client 进行发送。
try
{
if (await SmtpConfiguration.GetEnableSslAsync())
{
smtpClient.EnableSsl = true;
}
if (await SmtpConfiguration.GetUseDefaultCredentialsAsync())
{
smtpClient.UseDefaultCredentials = true;
}
else
{
smtpClient.UseDefaultCredentials = false;
var userName = await SmtpConfiguration.GetUserNameAsync();
if (!userName.IsNullOrEmpty())
{
var password = await SmtpConfiguration.GetPasswordAsync();
var domain = await SmtpConfiguration.GetDomainAsync();
smtpClient.Credentials = !domain.IsNullOrEmpty()
? new NetworkCredential(userName, password, domain)
: new NetworkCredential(userName, password);
}
}
return smtpClient;
}
catch
{
smtpClient.Dispose();
throw;
}
}
protected override async Task SendEmailAsync(MailMessage mail)
{
// 调用构建方法,构建 Client,用于发送 mail 数据。
using (var smtpClient = await BuildClientAsync())
{
await smtpClient.SendMailAsync(mail);
}
}
}
针对属性注入失败的情况,ABP 提供了 NullEmailSender
作为默认实现,在发送邮件的时候会使用 Logger 打印具体的信息。
public class NullEmailSender : EmailSenderBase
{
public ILogger<NullEmailSender> Logger { get; set; }
public NullEmailSender(IEmailSenderConfiguration configuration, IBackgroundJobManager backgroundJobManager)
: base(configuration, backgroundJobManager)
{
Logger = NullLogger<NullEmailSender>.Instance;
}
protected override Task SendEmailAsync(MailMessage mail)
{
Logger.LogWarning("USING NullEmailSender!");
Logger.LogDebug("SendEmailAsync:");
LogEmail(mail);
return Task.FromResult(0);
}
// ... 其他方法。
}
2.3 Email 的配置存储
从 EmailSenderBase
里面可以看到,它从 IEmailSenderConfiguration
当中获取发件人的邮箱地址和展示名称,它的 UML 类图关系如下。
class IEmailSenderConfiguration{
<<Interface>>
+GetDefaultFromAddressAsync() Task~string~
+GetDefaultFromDisplayNameAsync() Task~string~
}
class ISmtpEmailSenderConfiguration{
<<Interface>>
+GetHostAsync() Task~string~
+GetPortAsync() Task~int~
+GetUserNameAsync() Task~string~
+GetPasswordAsync() Task~string~
+GetDomainAsync() Task~string~
+GetEnableSslAsync() Task~bool~
+GetUseDefaultCredentialsAsync() Task~bool~
}
class EmailSenderConfiguration{
#GetNotEmptySettingValueAsync(string name) Task~string~
}
class SmtpEmailSenderConfiguration{
}
class ISettingProvider{
<<Interface>>
+GetOrNullAsync(string name) Task~string~
}
ISmtpEmailSenderConfiguration --|> IEmailSenderConfiguration: 继承
EmailSenderConfiguration ..|> IEmailSenderConfiguration: 实现
EmailSenderConfiguration ..> ISettingProvider: 依赖
SmtpEmailSenderConfiguration --|> EmailSenderConfiguration: 继承
SmtpEmailSenderConfiguration ..|> ISmtpEmailSenderConfiguration: 实现
可以看到配置文件时通过 ISettingProvider
获取的,这样就可以保证从不同租户甚至是用户来获取发件人的配置信息。这里值得注意的是在 EmailSenderConfiguration
中,实现了一个 GetNotEmptySettingValueAsync(string name)
方法,该方法主要是封装了获取逻辑,当值不存在的时候抛出 AbpException
异常。
protected async Task<string> GetNotEmptySettingValueAsync(string name)
{
var value = await SettingProvider.GetOrNullAsync(name);
if (value.IsNullOrEmpty())
{
throw new AbpException($"Setting value for '{name}' is null or empty!");
}
return value;
}
至于 SmtpEmailSenderConfiguration
,只是提供了其他的属性获取(密码、端口等)而已,本质上还是调用的 GetNotEmptySettingValueAsync()
方法从 SettingProvider
中获取具体的配置信息。
发送邮件 ->> Smtp 配置类: 1.GetHostAsync()
Smtp 配置类 ->> Email 配置类: 2.GetNotEmptySettingValueAsync("HotsItem")
Email 配置类 ->> Setting Provider: 3.GetOrNullAsync("HotsItem")
Setting Provider -->> 发送邮件: 4.获得主机数据。
关于配置名称的常量,都在 EmailSettingNames
里面进行定义,并使用 EmailSettingProvider
将其注册到 ABP 的配置模块当中:
EmailSettingNames.cs
namespace Volo.Abp.Emailing
{
public static class EmailSettingNames
{
public const string DefaultFromAddress = "Abp.Mailing.DefaultFromAddress";
public const string DefaultFromDisplayName = "Abp.Mailing.DefaultFromDisplayName";
public static class Smtp
{
public const string Host = "Abp.Mailing.Smtp.Host";
public const string Port = "Abp.Mailing.Smtp.Port";
// ... 其他常量定义。
}
}
}
EmailSettingProvider.cs
internal class EmailSettingProvider : SettingDefinitionProvider
{
public override void Define(ISettingDefinitionContext context)
{
context.Add(
new SettingDefinition(
EmailSettingNames.Smtp.Host,
"127.0.0.1",
L("DisplayName:Abp.Mailing.Smtp.Host"),
L("Description:Abp.Mailing.Smtp.Host")),
new SettingDefinition(EmailSettingNames.Smtp.Port,
"25",
L("DisplayName:Abp.Mailing.Smtp.Port"),
L("Description:Abp.Mailing.Smtp.Port")),
// ... 其他配置参数。
);
}
private static LocalizableString L(string name)
{
return LocalizableString.Create<EmailingResource>(name);
}
}
2.4 邮件模板
文字模板是 ABP 后续提供的一个新的模块,它可以让开发人员预先定义文本模板,然后使用时根据对象数据替换模板中的内容,并且 ABP 提供的文本模板还支持本地化。关于文本模板的功能,我们后续单独会写一篇文章进行说明,在这里只是大概 Mail 是如何使用的。
在项目当中,ABP 仅定义了两个 *.tpl 的模板文件,分别是控制布局的 Layout.tpl,还有渲染具体消息的 Message.tpl。同权限、Setting 一样,模板也会使用一个 StandardEmailTemplates
类型定义模板的编码常量,并且实现一个 XXXDefinitionProvider
类型将其注入到 ABP 框架当中。
StandardEmailTemplates.cs
public static class StandardEmailTemplates
{
public const string Layout = "Abp.StandardEmailTemplates.Layout";
public const string Message = "Abp.StandardEmailTemplates.Message";
}
StandardEmailTemplateDefinitionProvider.cs
public class StandardEmailTemplateDefinitionProvider : TemplateDefinitionProvider
{
public override void Define(ITemplateDefinitionContext context)
{
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Layout,
displayName: LocalizableString.Create<EmailingResource>("TextTemplate:StandardEmailTemplates.Layout"),
isLayout: true
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Layout.tpl", true)
);
context.Add(
new TemplateDefinition(
StandardEmailTemplates.Message,
displayName: LocalizableString.Create<EmailingResource>("TextTemplate:StandardEmailTemplates.Message"),
layout: StandardEmailTemplates.Layout
).WithVirtualFilePath("/Volo/Abp/Emailing/Templates/Message.tpl", true)
);
}
}
2.5 MailKit 集成
MailKit 是一个优秀跨平台的 .NET 邮件操作库,它的官方 GitHub 地址为 https://github.com/jstedfast/MailKit ,支持很多高级特性,这里我就不再详细介绍 MailKit 的其他特性,只是讲解一下 MailKit 同 ABP 自带的邮件模块是如何集成的。
官方的 Volo.Abp.MailKit 包仅包含 4 个文件,它们分别是 AbpMailKitModule.cs (空模块,占位)、AbpMailKitOptions.cs (MailKit 的特殊配置)、IMailKitSmtpEmailSender.cs (实现了 IEmailSender
基类的一个接口)、MailKitSmtpEmailSender.cs (具体的发送逻辑实现)。
需要注意一下,这里针对 MailKit 的特殊配置是使用的 IConfiguration
里面的数据(通常是 appsetting.json),而不是从 Abp.Settings 里面获取的。
MailKitSmtpEmailSender.cs
[Dependency(ServiceLifetime.Transient, ReplaceServices = true)]
public class MailKitSmtpEmailSender : EmailSenderBase, IMailKitSmtpEmailSender
{
protected AbpMailKitOptions AbpMailKitOptions { get; }
protected ISmtpEmailSenderConfiguration SmtpConfiguration { get; }
// ... 构造函数。
protected override async Task SendEmailAsync(MailMessage mail)
{
using (var client = await BuildClientAsync())
{
// 使用了 mail 参数来构造 MailKit 的对象。
var message = MimeMessage.CreateFromMailMessage(mail);
await client.SendAsync(message);
await client.DisconnectAsync(true);
}
}
// 构造 MailKit 所需要的 Client 对象。
public async Task<SmtpClient> BuildClientAsync()
{
var client = new SmtpClient();
try
{
await ConfigureClient(client);
return client;
}
catch
{
client.Dispose();
throw;
}
}
// 进行一些基本配置,比如服务器信息和密码信息等。
protected virtual async Task ConfigureClient(SmtpClient client)
{
await client.ConnectAsync(
await SmtpConfiguration.GetHostAsync(),
await SmtpConfiguration.GetPortAsync(),
await GetSecureSocketOption()
);
if (await SmtpConfiguration.GetUseDefaultCredentialsAsync())
{
return;
}
await client.AuthenticateAsync(
await SmtpConfiguration.GetUserNameAsync(),
await SmtpConfiguration.GetPasswordAsync()
);
}
// 根据 Option 的值获取一些安全配置。
protected virtual async Task<SecureSocketOptions> GetSecureSocketOption()
{
if (AbpMailKitOptions.SecureSocketOption.HasValue)
{
return AbpMailKitOptions.SecureSocketOption.Value;
}
return await SmtpConfiguration.GetEnableSslAsync()
? SecureSocketOptions.SslOnConnect
: SecureSocketOptions.StartTlsWhenAvailable;
}
}
三、总结
ABP 将 Email 这块功能封装成了单独的模块,便于开发人员进行邮件发送。并且官方也提供了 MailKit 的支持,我们可以根据自己的需求来替换不同的实现。只不过针对于一些异步邮件发送的场景,目前还不能很好的支持(主要是使用了 MailMessage
无法序列化)。
我觉得 ABP 应该自己定义一个 Context 类型,反转依赖,在具体的实现当中确定邮件发送的对象类型。或者是将默认的 Smtp 发送者独立出来一个模块,就跟 MailKit 一样,使用 ABP 的 Context 类型来构造 MailMessage
对象。
四、总目录
欢迎翻阅作者的其他文章,请 点击我 进行跳转,如果你觉得本篇文章对你有帮助,请点击文章末尾的 推荐按钮。
最后更新时间: 2021年6月27日 23点31分
[Abp vNext 源码分析] - 20. 电子邮件与短信支持的更多相关文章
- [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)
一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...
- [Abp vNext 源码分析] - 文章目录
一.简要介绍 ABP vNext 是 ABP 框架作者所发起的新项目,截止目前 (2019 年 2 月 18 日) 已经拥有 1400 多个 Star,最新版本号为 v 0.16.0 ,但还属于预览版 ...
- [Abp vNext 源码分析] - 3. 依赖注入与拦截器
一.简要说明 ABP vNext 框架在使用依赖注入服务的时候,是直接使用的微软提供的 Microsoft.Extensions.DependencyInjection 包.这里与原来的 ABP 框架 ...
- [Abp vNext 源码分析] - 2. 模块系统的变化
一.简要说明 本篇文章主要分析 Abp vNext 当中的模块系统,从类型构造层面上来看,Abp vNext 当中不再只是单纯的通过 AbpModuleManager 来管理其他的模块,它现在则是 I ...
- [Abp vNext 源码分析] - 1. 框架启动流程分析
一.简要说明 本篇文章主要剖析与讲解 Abp vNext 在 Web API 项目下的启动流程,让大家了解整个 Abp vNext 框架是如何运作的.总的来说 ,Abp vNext 比起 ABP 框架 ...
- [Abp vNext 源码分析] - 4. 工作单元
一.简要说明 统一工作单元是一个比较重要的基础设施组件,它负责管理整个业务流程当中涉及到的数据库事务,一旦某个环节出现异常自动进行回滚处理. 在 ABP vNext 框架当中,工作单元被独立出来作为一 ...
- [Abp vNext 源码分析] - 6. DDD 的应用层支持 (应用服务)
一.简要介绍 ABP vNext 针对于应用服务层,为我们单独设计了一个模块进行实现,即 Volo.Abp.Ddd.Application 模块. PS:最近博主也是在恶补 DDD 相关的知识,这里推 ...
- [Abp vNext 源码分析] - 7. 权限与验证
一.简要说明 在上篇文章里面,我们在 ApplicationService 当中看到了权限检测代码,通过注入 IAuthorizationService 就可以实现权限检测.不过跳转到源码才发现,这个 ...
- [Abp vNext 源码分析] - 9. 接口参数的验证
一.简要说明 ABP vNext 当中的审计模块早在 依赖注入与拦截器一文中有所提及,但没有详细的对其进行分析. 审计模块是 ABP vNext 框架的一个基本组件,它能够提供一些实用日志记录.不过这 ...
随机推荐
- windows性能计数器搜集方法
windows性能计数器搜集方法 PerfMon-Windows性能监视器是个好东西,可以辅助我们分析发生问题时间段服务器资源占用情况,但是部署性能计数器确实一个相当麻烦的事情,往往这种枯燥的事别人还 ...
- Django(21)migrate报错的解决方案
前言 在讲解如何解决migrate报错原因前,我们先要了解migrate做了什么事情,migrate:将新生成的迁移脚本.映射到数据库中.创建新的表或者修改表的结构. 问题1:migrate怎么判断哪 ...
- java面试一日一题:java中一个对象实例的结构是什么样子的
问题:请讲下在java程序运行时一个对象实例的数据结构是什么样子的 分析:该问题主要考察对java中对象的理解,在程序运行过程中一个对象实例是以什么样的形式存在的 回答要点: 主要从以下几点去考虑, ...
- 通俗易懂的JS之Proxy
与掘金文章同步,地址:https://juejin.cn/post/6964398933229436935 什么是代理模式 引入一个现实生活中的案例 我们作为用户需要去如何评估一个房子的好坏.如何办理 ...
- linux-TCP多线程的并发服务器- 以言责人甚易,以义持己实难!!!
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <ti ...
- mysql登录框注入绕过单引号匹配
0x00 原理 网站使用正则匹配对用户名一栏传到服务器的参数进行了匹配,如果匹配到了单引号则报错 0x01 简单例子 当我们输入admin'时,网站直接报错,很有可能就是用了正则,这样我们也不 ...
- ES6对象的新增方法的使用
Object.assign Object Object.assign(target, ...sources) 将所有可枚举属性的值从一个或多个源对象复制到目标对象 参数: target 目标对象 so ...
- MySQL优化|一分钟带你了解单表优化
在开始前,分享给大家我看过觉得讲数据库讲的算是很不错的,也在B站拥有百万播放量的教程. 这个MySQL视频是动力节点的老杜讲解,个人也很喜欢老杜的教学风格,老杜真的是从MySQL基础一点点带我入门,基 ...
- 【python】读取和输出到txt
读取txt的数据和把数据保存到txt中是经常要用到的,下面我就总结一下. 读txt文件python常用的读取文件函数有三种read().readline().readlines() 以读取上述txt为 ...
- 马哥Linux SysAdmin学习笔记(四)
sed:编辑器 sed:Stream EDitor,行编辑器 用法: sed [option]... 'script' inputfile... script: '地址命令' 常用选项: -n:不输出 ...