3

[Abp vNext 源码分析] - 20. 电子邮件与短信支持

 2 years ago
source link: https://www.cnblogs.com/myzony/p/abp-vnext-email-and-sms-source-analyzsis.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

ABP vNext 使用 Volo.Abp.Sms 包和 Volo.Abp.Emailing 包将短信和电子邮件作为基础设施进行了抽象,开发人员仅需要在使用的时候注入 ISmsSenderIEmailSender 即可实现短信发送和邮件发送。

二、源码分析

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 实现,分别是 SmtpEmailSenderMailkitEmailSender,各个类型的关系如下。

UML 类图:

«Interface»IEmailSender+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«Interface»ISmtpEmailSender......+BuildClientAsync() : Task<SmtpClient>«Interface»IMailKitSmtpEmailSemder......+BuildClientAsync() : Task<SmtpClient>«Abstract»EmailSenderBase......SmtpEmailSender......MailKitSmtpEmailSender......NullEmailSender......继承继承实现实现继承继承实现继承

可以从 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 类图关系如下。

«Interface»IEmailSenderConfiguration+GetDefaultFromAddressAsync() : Task<string>+GetDefaultFromDisplayNameAsync() : Task<string>«Interface»ISmtpEmailSenderConfiguration+GetHostAsync() : Task<string>+GetPortAsync() : Task<int>+GetUserNameAsync() : Task<string>+GetPasswordAsync() : Task<string>+GetDomainAsync() : Task<string>+GetEnableSslAsync() : Task<bool>+GetUseDefaultCredentialsAsync() : Task<bool>EmailSenderConfiguration#GetNotEmptySettingValueAsync(string name) : Task<string>SmtpEmailSenderConfiguration«Interface»ISettingProvider+GetOrNullAsync(string name) : Task<string>继承实现依赖继承实现

可以看到配置文件时通过 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 配置类Email 配置类Setting Provider1.GetHostAsync()2.GetNotEmptySettingValueAsync("HotsItem")3.GetOrNullAsync("HotsItem")4.获得主机数据。发送邮件Smtp 配置类Email 配置类Setting Provider

关于配置名称的常量,都在 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;
    }
}

2.6 短信发送的核心组件

短信发送仅提供了一个 ISmsSender 接口,该接口有提供一个发送方法,ABP 官方提供了 Aliyun 的短信发送功能(Volo.Abp.Sms.Aliyun)。

UML 图:

«Interface»ISmsSenderSendAsync(SmsMessage smsMessage) : TaskNullSmsSenderSmsMessage+string PhoneNumber+string Text+IDictionary<string, object> Properties实现依赖

功能比较简单,重点是 SmsMessage 里面的参数,第一个是发送的号码,第二个是发送的内容。仅凭上述参数肯定不够,所以 ABP 提供了一个属性字典,便于我们传入一些特定的参数。

ABP 将 Email 这块功能封装成了单独的模块,便于开发人员进行邮件发送。并且官方也提供了 MailKit 的支持,我们可以根据自己的需求来替换不同的实现。只不过针对于一些异步邮件发送的场景,目前还不能很好的支持(主要是使用了 MailMessage 无法序列化)。

我觉得 ABP 应该自己定义一个 Context 类型,反转依赖,在具体的实现当中确定邮件发送的对象类型。或者是将默认的 Smtp 发送者独立出来一个模块,就跟 MailKit 一样,使用 ABP 的 Context 类型来构造 MailMessage 对象。

四、总目录

欢迎翻阅作者的其他文章,请 点击我 进行跳转,如果你觉得本篇文章对你有帮助,请点击文章末尾的 推荐按钮

最后更新时间: 2021年6月27日 23点31分


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK