一、簡介

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 類圖:

classDiagram
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 類圖關係如下。

classDiagram
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 中獲取具體的配置資訊。

sequenceDiagram
傳送郵件 ->> 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分