发送邮件几乎是软件系统中必不可少的功能,在Asp.Net Core 中我们可以使用MailKit发送邮件,MailKit发送邮件比较简单,网上有许多可以参考的文章,但是应该注意附件名长度,和附件名不能出现中文的问题,如果你遇到了这样的问题可以参考我之前写的这篇博客Asp.Net Core MailKit 完美附件(中文名、长文件名)。
在我们简单搜索网络,并成功解决了附件的问题之后,我们已经能够发送邮件啦!不过另一个问题显现出来——发送邮件太慢了,没错,在我使用QQ邮箱发送时,单封邮件发送大概要用1.5秒左右,用户可能难以忍受请求发生1.5秒的延迟。
所以,我们必须解决这个问题,我们的解决办法就是使用邮件队列来发送邮件
设计邮件队列Ok, 第一步就是规划我们的邮件队列有什么
EmailOptions我们得有一个邮件Options类,来存储邮件相关的选项
/// <summary> /// 邮件选项 /// </summary> public class EmailOptions { public bool DisableOAuth { get; set; } public string DisplayName { get; set; } public string Host { get; set; } // 邮件主机地址 public string Password { get; set; } public int Port { get; set; } public string UserName { get; set; } public int SleepInterval { get; set; } = 3000; ...SleepInterval 是睡眠间隔,因为目前我们实现的队列是进程内的独立线程,发送器会循环读取队列,当队列是空的时候,我们应该让线程休息一会,不然无限循环会消耗大量CPU资源
然后我们还需要的就是 一个用于存储邮件的队列,或者叫队列提供器,总之我们要将邮件存储起来。以及一个发送器,发送器不断的从队列中读取邮件并发送。还需要一个邮件写入工具,想要发送邮件的代码使用写入工具将邮件转储到队列中。
那么我们设计的邮件队列事实上就有了三个部分:
队列存储提供器(邮件的事实存储)
邮件发送机 (不断读取队列中的邮件,并发送)
邮件服务 (想法送邮件时,调用邮件服务,邮件服务会将邮件写入队列)
队列存储提供器设计那么我们设计的邮件队列提供器接口如下:
public interface IMailQueueProvider { void Enqueue(MailBox mailBox); bool TryDequeue(out MailBox mailBox); int Count { get; } bool IsEmpty { get; } ...四个方法,入队、出队、队列剩余邮件数量、队列是否是空,我们对队列的基本需求就是这样。
MailBox是对邮件的封装,并不复杂,稍后会介绍到
邮件服务设计 public interface IMailQueueService { void Enqueue(MailBox box);对于想要发送邮件的组件或者代码部分来讲,只需要将邮件入队,这就足够了
邮件发送机(兼邮件队列管理器)设计 public interface IMailQueueManager { void Run(); void Stop(); bool IsRunning { get; } int Count { get; }启动队列,停止队列,队列运行中状态,邮件计数
现在,三个主要部分就设计好了,我们先看下MailBox,接下来就去实现这三个接口
MailBoxMailBox 如下:
public class MailBox { public IEnumerable<IAttachment> Attachments { get; set; } public string Body { get; set; } public IEnumerable<string> Cc { get; set; } public bool IsHtml { get; set; } public string Subject { get; set; } public IEnumerable<string> To { get; set; } ...这里面没什么特殊的,大家一看便能理解,除了IEnumerable<IAttachment> Attachments { get; set; }。
附件的处理在发送邮件中最复杂的就是附件了,因为附件体积大,往往还涉及非托管资源(例如:文件),所以附件处理一定要小心,避免留下漏洞和bug。
在MailKit中附件实际上是流Stream,例如下面的代码:
attachment = new MimePart(contentType) { Content = new MimeContent(fs), ContentDisposition = new ContentDisposition(ContentDisposition.Attachment), ContentTransferEncoding = ContentEncoding.Base64, };其中new MimeContent(fs)是创建的Content,fs是Stream,MimeContent的构造函数如下:
public MimeContent(Stream stream, ContentEncoding encoding = ContentEncoding.Default)所以我们的设计的附件是基于Stream的。
一般情况附件是磁盘上的文件,或者内存流MemoryStream或者 byte[]数据。附件需要实际的文件的流Stream和一个附件名,所以附件接口设计如下:
public interface IAttachment : IDisposable { Stream GetFileStream(); string GetName();那么我们默认实现了两中附件类型 物理文件附件和内存文件附件,byte[]数据可以轻松的转换成 内存流,所以没有写这种
MemoryStreamAttechment public class MemoryStreamAttechment : IAttachment { private readonly MemoryStream _stream; private readonly string _fileName; public MemoryStreamAttechment(MemoryStream stream, string fileName) { _stream = stream; _fileName = fileName; } public void Dispose() => _stream.Dispose(); public Stream GetFileStream() => _stream; public string GetName() => _fileName;