Skip to content
Bitzsoft.Integrationsbitzsoft.integrations

Concept

EnterpriseMail 企业邮箱

企业邮箱域完整手册——IEnterpriseMailProvider 11 方法三组能力、5 家供应商接入方式差异、网易 MailKit 长连接单例与信号量串行化的特殊生命周期。

Last updated

EnterpriseMail 域统一了 5 家企业邮箱供应商的邮件发送、读取、文件夹管理和附件操作能力。它遵循标准三层包模式,但有一个显著的结构性差异:网易企业邮箱基于 MailKit 的 IMAP/SMTP 长连接,注册为 Singleton 并用信号量串行化访问,和其他 4 家走 Typed HttpClient 的无状态调用完全不同。

域结构

Bitzsoft.Integrations.EnterpriseMail ← 抽象层
├── Bitzsoft.Integrations.EnterpriseMail.Alibaba ← 阿里邮箱(Typed HttpClient)
├── Bitzsoft.Integrations.EnterpriseMail.Feishu ← 飞书邮箱(Typed HttpClient)
├── Bitzsoft.Integrations.EnterpriseMail.Microsoft365 ← 微软 365(Microsoft Graph)
├── Bitzsoft.Integrations.EnterpriseMail.NetEase ← 网易企业邮箱(MailKit,net8+)
├── Bitzsoft.Integrations.EnterpriseMail.Tencent ← 腾讯企业邮箱(Typed HttpClient)
└── Bitzsoft.Integrations.EnterpriseMail.All ← 聚合包

统一接口(11 方法,三组)

IEnterpriseMailProvider 把邮件能力分成三组:邮件操作、文件夹操作、附件操作。

public interface IEnterpriseMailProvider
{
string ProviderName { get; }
// ① 邮件操作(6)
Task<SendMailResult> SendAsync(SendMailRequest request, CancellationToken ct = default);
Task<MailListResult> ListMessagesAsync(string folderId, int pageIndex = 1, int pageSize = 20, CancellationToken ct = default);
Task<MailMessage> GetMessageAsync(string messageId, CancellationToken ct = default);
Task<MailListResult> SearchAsync(MailSearchRequest request, CancellationToken ct = default);
Task DeleteMessageAsync(string messageId, CancellationToken ct = default);
Task MoveMessageAsync(string messageId, string targetFolderId, CancellationToken ct = default);
// ② 文件夹操作(3)
Task<IReadOnlyList<MailFolder>> ListFoldersAsync(CancellationToken ct = default);
Task<MailFolder> CreateFolderAsync(string name, string? parentFolderId = null, CancellationToken ct = default);
Task DeleteFolderAsync(string folderId, CancellationToken ct = default);
// ③ 附件操作(2)
Task<IReadOnlyList<MailAttachment>> ListAttachmentsAsync(string messageId, CancellationToken ct = default);
Task<Stream> DownloadAttachmentAsync(string messageId, string attachmentId, CancellationToken ct = default);
}

接口 XML 文档明确说明:受限于供应商 API 能力,部分操作可能抛 NotSupportedException,调用方应在使用前确认供应商支持所需操作。

错误处理:无 Result 包装

EnterpriseMail 域没有 Result 包装——接口直接返回模型对象(SendMailResult / MailListResult / MailMessage),失败时抛 EnterpriseMailExceptionSendMailResult 用工厂方法 Ok(messageId) / Fail(errorMessage) 构造,但它只表示发送层面的成功与否,不是通用结果包装。

public sealed class EnterpriseMailException : IntegrationException
{
public string ProviderName => Provider ?? string.Empty;
public new string? ErrorCode => base.ErrorCode;
public new string? ProviderMessage => base.ProviderMessage;
public EnterpriseMailException(string providerName, string message,
string? errorCode = null, string? providerMessage = null, Exception? innerException = null)
: base(domain: "EnterpriseMail",
message: $"[{providerName}] {message}", ...) { }
}

发送邮件

SendMailRequest 支持多收件人、抄送、密送、纯文本/HTML 双正文、附件和优先级:

public sealed class SendMailRequest
{
public MailAddress? From { get; init; } // 可选,部分供应商从配置读默认发件人
public IReadOnlyList<MailAddress> To { get; init; } = [];
public IReadOnlyList<MailAddress> Cc { get; init; } = [];
public IReadOnlyList<MailAddress> Bcc { get; init; } = [];
public string Subject { get; init; } = string.Empty;
public string? TextBody { get; init; } // 纯文本正文
public string? HtmlBody { get; init; } // HTML 正文
public IReadOnlyList<MailAttachmentContent> Attachments { get; init; } = [];
public MailPriority Priority { get; init; } = MailPriority.Normal;
}

TextBodyHtmlBody 至少传一个,部分供应商同时支持时以 HTML 优先、纯文本作降级。

供应商清单

供应商Provider ID接入方式生命周期DI 方法
阿里邮箱AlibabaTyped HttpClientTransientAddBitzsoftAlibabaMail()
飞书邮箱FeishuTyped HttpClientTransientAddBitzsoftFeishuMail()
微软 365Microsoft365Microsoft GraphTransientAddBitzsoftMicrosoft365Mail()
网易企业邮箱NetEaseMailKit(IMAP + SMTP)SingletonAddBitzsoftNetEaseMail()
腾讯企业邮箱TencentTyped HttpClientTransientAddBitzsoftTencentMail()

4 家 HTTP 类供应商每次调用无状态,Typed HttpClient 管理连接池。网易是唯一的例外。

网易长连接单例

其他 4 家供应商通过 HTTP 发请求,每次调用无状态。网易基于 MailKit 的 IMAP/SMTP 协议,需要持有持久连接,因此 NetEaseMailClient 注册为 Singleton,整个进程共享一个连接管理器:

internal sealed class NetEaseMailClient : IDisposable
{
private readonly ImapClient _imapClient = new();
private readonly SmtpClient _smtpClient = new();
// 两把信号量分别串行化 IMAP 和 SMTP 访问
private readonly SemaphoreSlim _imapLock = new(1, 1);
private readonly SemaphoreSlim _smtpLock = new(1, 1);
}
// DI 注册为 Singleton
services.TryAddSingleton<NetEaseMailClient>();

连接管理的设计要点:

  • 信号量串行化_imapLock / _smtpLock 把对共享 IMAP/SMTP 客户端的并发操作串行化,因为单个 MailKit 客户端不是线程安全的。
  • 双重检查连接ConnectAndAuthenticateAsync 进入临界区后做双重检查——等待锁期间可能已有其他线程完成连接,避免重复登录。
  • 异常过滤ex is not OperationCanceledException,确保取消不变成连接失败。
  • 安全释放Dispose 里安全断开连接并释放信号量,忽略断开过程中的异常。

注册

单厂商

// ① 阿里邮箱(Typed HttpClient)
builder.Services.AddBitzsoftAlibabaMail(builder.Configuration.GetSection("EnterpriseMail:Alibaba"));
// ② 微软 365(Microsoft Graph)
builder.Services.AddBitzsoftMicrosoft365Mail(builder.Configuration.GetSection("EnterpriseMail:Microsoft365"));
// ③ 网易企业邮箱(MailKit 长连接,仅 net8+)
builder.Services.AddBitzsoftNetEaseMail(builder.Configuration.GetSection("EnterpriseMail:NetEase"));

全量聚合

// 注意:聚合方法名是 AddBitzsoftEnterpriseMail,不带 All 后缀
builder.Services.AddBitzsoftEnterpriseMail(builder.Configuration, "EnterpriseMail");

聚合包按配置节存在性自动注册,appsettings.json 只需写出启用的供应商:

{
"EnterpriseMail": {
"Alibaba": { "AccessKeyId": "...", "AccessKeySecret": "...", "Account": "..." },
"NetEase": { "ImapHost": "hwimap.qiye.163.com", "Username": "...", "Password": "..." }
}
}

消费

发送邮件

public class MailSender(IEnterpriseMailProvider mail)
{
public async Task SendWelcomeAsync(string to, string name)
{
var result = await mail.SendAsync(new SendMailRequest
{
To = [new MailAddress(to)],
Subject = $"欢迎,{name}",
HtmlBody = $"<p>{name},感谢注册。</p>",
Attachments = [new MailAttachmentContent { FileName = "新手指南.pdf", Content = pdfBytes }],
});
if (!result.Success)
throw new InvalidOperationException($"邮件发送失败: {result.ErrorMessage}");
}
}

读取与文件夹操作

public class MailReader(IEnterpriseMailProvider mail)
{
public async Task<IReadOnlyList<MailFolder>> GetFoldersAsync()
=> await mail.ListFoldersAsync();
public async Task<MailListResult> GetInboxAsync(int page = 1)
=> await mail.ListMessagesAsync("INBOX", pageIndex: page, pageSize: 20);
public async Task<Stream> DownloadAttachmentAsync(string messageId, string attachmentId)
=> await mail.DownloadAttachmentAsync(messageId, attachmentId);
}

多厂商路由

public class MailRouter(IIntegrationProviderResolver<IEnterpriseMailProvider> providers)
{
public async Task<SendMailResult> SendAsync(string vendor, SendMailRequest request)
{
var mail = providers.GetRequired(vendor); // "Alibaba" / "NetEase" / "Microsoft365" ...
return await mail.SendAsync(request);
}
}

相关

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%