wordpress制作视频站,html网页留言板代码,广告类的网站,外贸门户网站建设发送邮件几乎是软件系统中必不可少的功能#xff0c;在Asp.Net Core 中我们可以使用MailKit发送邮件#xff0c;MailKit发送邮件比较简单#xff0c;网上有许多可以参考的文章#xff0c;但是应该注意附件名长度#xff0c;和附件名不能出现中文的问题#xff0c;如果你遇… 发送邮件几乎是软件系统中必不可少的功能在Asp.Net Core 中我们可以使用MailKit发送邮件MailKit发送邮件比较简单网上有许多可以参考的文章但是应该注意附件名长度和附件名不能出现中文的问题如果你遇到了这样的问题可以参考我之前写的这篇博客Asp.Net Core MailKit 完美附件中文名、长文件名。在我们简单搜索网络并成功解决了附件的问题之后我们已经能够发送邮件啦不过另一个问题显现出来——发送邮件太慢了没错在我使用QQ邮箱发送时单封邮件发送大概要用1.5秒左右用户可能难以忍受请求发生1.5秒的延迟。所以我们必须解决这个问题我们的解决办法就是使用邮件队列来发送邮件设计邮件队列Ok 第一步就是规划我们的邮件队列有什么EmailOptions我们得有一个邮件Options类来存储邮件相关的选项/// summary/// 邮件选项/// /summarypublic 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 IEnumerableIAttachment Attachments { get; set; } public string Body { get; set; } public IEnumerablestring Cc { get; set; } public bool IsHtml { get; set; } public string Subject { get; set; } public IEnumerablestring To { get; set; }...这里面没什么特殊的大家一看便能理解除了IEnumerableIAttachment 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)是创建的Contentfs是StreamMimeContent的构造函数如下public MimeContent(Stream stream, ContentEncoding encoding ContentEncoding.Default)所以我们的设计的附件是基于Stream的。一般情况附件是磁盘上的文件或者内存流MemoryStream或者 byte[]数据。附件需要实际的文件的流Stream和一个附件名所以附件接口设计如下public interface IAttachment : IDisposable{ Stream GetFileStream(); string GetName();那么我们默认实现了两中附件类型 物理文件附件和内存文件附件byte[]数据可以轻松的转换成 内存流所以没有写这种MemoryStreamAttechmentpublic 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;内存流附件实现要求在创建时传递一个 MemoryStream和附件名称比较简单物理文件附件public class PhysicalFileAttachment : IAttachment{ public PhysicalFileAttachment(string absolutePath) { if (!File.Exists(absolutePath)){ throw new FileNotFoundException(文件未找到, absolutePath);}AbsolutePath absolutePath;} private FileStream _stream; public string AbsolutePath { get; } public void Dispose() {_stream.Dispose();} public Stream GetFileStream() { if (_stream null){_stream new FileStream(AbsolutePath, FileMode.Open);} return _stream;} public string GetName() { return System.IO.Path.GetFileName(AbsolutePath);... 这里我们要注意的是创建FileStream的时机是在请求GetFileStream方法时而不是构造函数中因为创建FileStreamFileStream会占用文件如果我们发两封邮件使用了同一个附件那么会抛出异常。而写在GetFileStream方法中相对比较安全除非发送器是并行的实现邮件队列在我们这篇文章中我们实现的队列提供器是基于内存的日后呢我们还可以实现其它的基于其它存储模式的比如数据库外部持久性队列等等另外基于内存的实现不是持久的一旦程序崩溃。未发出的邮件就会boom然后消失 XD...邮件队列提供器IMailQueueProvider实现代码如下public class MailQueueProvider : IMailQueueProvider{ private static readonly ConcurrentQueueMailBox _mailQueue new ConcurrentQueueMailBox(); public int Count _mailQueue.Count; public bool IsEmpty _mailQueue.IsEmpty; public void Enqueue(MailBox mailBox) {_mailQueue.Enqueue(mailBox);} public bool TryDequeue(out MailBox mailBox) { return _mailQueue.TryDequeue(out mailBox);}本文的实现是一个 ConcurrentQueue邮件服务IMailQueueService实现代码如下public class MailQueueService : IMailQueueService{ private readonly IMailQueueProvider _provider; /// summary/// 初始化实例/// /summary/// param nameprovider/parampublic MailQueueService(IMailQueueProvider provider) {_provider provider;} /// summary/// 入队/// /summary/// param namebox/parampublic void Enqueue(MailBox box) {_provider.Enqueue(box);} 这里我们的服务依赖于IMailQueueProvider使用了其入队功能邮件发送机IMailQueueManager实现这个相对比较复杂我们先看下完整的类再逐步解释public class MailQueueManager : IMailQueueManager{ private readonly SmtpClient _client; private readonly IMailQueueProvider _provider; private readonly ILoggerMailQueueManager _logger; private readonly EmailOptions _options; private bool _isRunning false; private bool _tryStop false; private Thread _thread; /// summary/// 初始化实例/// /summary/// param nameprovider/param/// param nameoptions/param/// param namelogger/parampublic MailQueueManager(IMailQueueProvider provider, IOptionsEmailOptions options, ILoggerMailQueueManager logger) {_options options.Value;_client new SmtpClient{ // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)ServerCertificateValidationCallback (s, c, h, e) true}; // Note: since we dont have an OAuth2 token, disable// the XOAUTH2 authentication mechanism.if (_options.DisableOAuth){_client.AuthenticationMechanisms.Remove(XOAUTH2);}_provider provider;_logger logger;} /// summary/// 正在运行/// /summarypublic bool IsRunning _isRunning; /// summary/// 计数/// /summarypublic int Count _provider.Count; /// summary/// 启动队列/// /summarypublic void Run() { if (_isRunning || (_thread ! null _thread.IsAlive)){_logger.LogWarning(已经运行又被启动了,新线程启动已经取消); return;}_isRunning true;_thread new Thread(StartSendMail){Name PmpEmailQueue,IsBackground true,};_logger.LogInformation(线程即将启动);_thread.Start();_logger.LogInformation(线程已经启动线程Id是{0}, _thread.ManagedThreadId);} /// summary/// 停止队列/// /summarypublic void Stop() { if (_tryStop){ return;}_tryStop true;} private void StartSendMail() { var sw new Stopwatch(); try{ while (true){ if (_tryStop){ break;} if (_provider.IsEmpty){_logger.LogTrace(队列是空开始睡眠);Thread.Sleep(_options.SleepInterval); continue;} if (_provider.TryDequeue(out MailBox box)){_logger.LogInformation(开始发送邮件 标题{0},收件人 {1}, box.Subject, box.To.First());sw.Restart();SendMail(box);sw.Stop();_logger.LogInformation(发送邮件结束标题{0},收件人 {1},耗时{2}, box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);}}} catch (Exception ex){_logger.LogError(ex, 循环中出错,线程即将结束);_isRunning false;}_logger.LogInformation(邮件发送线程即将停止人为跳出循环没有异常发生);_tryStop false;_isRunning false;} private void SendMail(MailBox box) { if (box null){ throw new ArgumentNullException(nameof(box));} try{MimeMessage message ConvertToMimeMessage(box);SendMail(message);} catch (Exception exception){_logger.LogError(exception, 发送邮件发生异常主题{0},收件人{1}, box.Subject, box.To.First());} finally{ if (box.Attachments ! null box.Attachments.Any()){ foreach (var item in box.Attachments){item.Dispose();}}}} private MimeMessage ConvertToMimeMessage(MailBox box) { var message new MimeMessage(); var from InternetAddress.Parse(_options.UserName); from.Name _options.DisplayName;message.From.Add(from); if (!box.To.Any()){ throw new ArgumentNullException(to必须含有值);}message.To.AddRange(box.To.Convert()); if (box.Cc ! null box.Cc.Any()){message.Cc.AddRange(box.Cc.Convert());}message.Subject box.Subject; var builder new BodyBuilder(); if (box.IsHtml){builder.HtmlBody box.Body;} else{builder.TextBody box.Body;} if (box.Attachments ! null box.Attachments.Any()){ foreach (var item in GetAttechments(box.Attachments)) {builder.Attachments.Add(item);}}message.Body builder.ToMessageBody(); return message;} private void SendMail(MimeMessage message) { if (message null){ throw new ArgumentNullException(nameof(message));} try{_client.Connect(_options.Host, _options.Port, false); // Note: only needed if the SMTP server requires authenticationif (!_client.IsAuthenticated){_client.Authenticate(_options.UserName, _options.Password);}_client.Send(message);} finally{_client.Disconnect(false);}} private AttachmentCollection GetAttechments(IEnumerableIAttachment attachments) { if (attachments null){ throw new ArgumentNullException(nameof(attachments));}AttachmentCollection collection new AttachmentCollection();ListStream list new ListStream(attachments.Count()); foreach (var item in attachments){ var fileName item.GetName(); var fileType MimeTypes.GetMimeType(fileName); var contentTypeArr fileType.Split(/); var contentType new ContentType(contentTypeArr[0], contentTypeArr[1]);MimePart attachment null;Stream fs null; try{fs item.GetFileStream();list.Add(fs);} catch (Exception ex){_logger.LogError(ex, 读取文件流发生异常);fs?.Dispose(); continue;}attachment new MimePart(contentType){Content new MimeContent(fs),ContentDisposition new ContentDisposition(ContentDisposition.Attachment),ContentTransferEncoding ContentEncoding.Base64,}; var charset UTF-8;attachment.ContentType.Parameters.Add(charset, name, fileName);attachment.ContentDisposition.Parameters.Add(charset, filename, fileName); foreach (var param in attachment.ContentDisposition.Parameters){param.EncodingMethod ParameterEncodingMethod.Rfc2047;} foreach (var param in attachment.ContentType.Parameters){param.EncodingMethod ParameterEncodingMethod.Rfc2047;}collection.Add(attachment);} return collection;}
}在构造函数中请求了另外三个服务并且初始化了SmtpClient这是MailKit中的 public MailQueueManager(IMailQueueProvider provider, IOptionsEmailOptions options, ILoggerMailQueueManager logger){_options options.Value;_client new SmtpClient{ // For demo-purposes, accept all SSL certificates (in case the server supports STARTTLS)ServerCertificateValidationCallback (s, c, h, e) true}; // Note: since we dont have an OAuth2 token, disable// the XOAUTH2 authentication mechanism.if (_options.DisableOAuth){_client.AuthenticationMechanisms.Remove(XOAUTH2);}_provider provider;_logger logger;}启动队列时创建了新的线程并且将线程句柄保存起来 public void Run() { if (_isRunning || (_thread ! null _thread.IsAlive)){_logger.LogWarning(已经运行又被启动了,新线程启动已经取消); return;}_isRunning true;_thread new Thread(StartSendMail){Name PmpEmailQueue,IsBackground true,};_logger.LogInformation(线程即将启动);_thread.Start();_logger.LogInformation(线程已经启动线程Id是{0}, _thread.ManagedThreadId);}线程启动时运行了方法StartSendMail private void StartSendMail() { var sw new Stopwatch(); try{ while (true){ if (_tryStop){ break;} if (_provider.IsEmpty){_logger.LogTrace(队列是空开始睡眠);Thread.Sleep(_options.SleepInterval); continue;} if (_provider.TryDequeue(out MailBox box)){_logger.LogInformation(开始发送邮件 标题{0},收件人 {1}, box.Subject, box.To.First());sw.Restart();SendMail(box);sw.Stop();_logger.LogInformation(发送邮件结束标题{0},收件人 {1},耗时{2}, box.Subject, box.To.First(), sw.Elapsed.TotalSeconds);}}} catch (Exception ex){_logger.LogError(ex, 循环中出错,线程即将结束);_isRunning false;}_logger.LogInformation(邮件发送线程即将停止人为跳出循环没有异常发生);_tryStop false;_isRunning false;} 这个方法不断的从队列读取邮件并发送当 遇到异常或者_tryStop为true时跳出循环此时线程结束注意我们会让线程睡眠在适当的时候。接下来就是方法SendMail了 private void SendMail(MailBox box) { if (box null){ throw new ArgumentNullException(nameof(box));} try{MimeMessage message ConvertToMimeMessage(box);SendMail(message);} catch (Exception exception){_logger.LogError(exception, 发送邮件发生异常主题{0},收件人{1}, box.Subject, box.To.First());} finally{ if (box.Attachments ! null box.Attachments.Any()){ foreach (var item in box.Attachments){item.Dispose();... 这里有一个特别要注意的就是在发送之后释放附件(非托管资源)foreach (var item in box.Attachments)
{item.Dispose();...发送邮件的核心代码只有两行MimeMessage message ConvertToMimeMessage(box);SendMail(message);第一行将mailbox转换成 MailKit使用的MimeMessage实体第二步切实的发送邮件为什么我们的接口中没有直接使用MimeMessage而是使用MailBox因为MimeMessage比较繁杂而且附件的问题不易处理所以我们设计接口时单独封装MailBox简化了编程接口转换一共两步1是主体转换比较简单。二是附件的处理这里涉及到附件名中文编码的问题。 private MimeMessage ConvertToMimeMessage(MailBox box) { var message new MimeMessage(); var from InternetAddress.Parse(_options.UserName); from.Name _options.DisplayName;message.From.Add(from); if (!box.To.Any()){ throw new ArgumentNullException(to必须含有值);}message.To.AddRange(box.To.Convert()); if (box.Cc ! null box.Cc.Any()){message.Cc.AddRange(box.Cc.Convert());}message.Subject box.Subject; var builder new BodyBuilder(); if (box.IsHtml){builder.HtmlBody box.Body;} else{builder.TextBody box.Body;} if (box.Attachments ! null box.Attachments.Any()){ foreach (var item in GetAttechments(box.Attachments)) {builder.Attachments.Add(item);}}message.Body builder.ToMessageBody(); return message;} private AttachmentCollection GetAttechments(IEnumerableIAttachment attachments) { if (attachments null){ throw new ArgumentNullException(nameof(attachments));}AttachmentCollection collection new AttachmentCollection();ListStream list new ListStream(attachments.Count()); foreach (var item in attachments){ var fileName item.GetName(); var fileType MimeTypes.GetMimeType(fileName); var contentTypeArr fileType.Split(/); var contentType new ContentType(contentTypeArr[0], contentTypeArr[1]);MimePart attachment null;Stream fs null; try{fs item.GetFileStream();list.Add(fs);} catch (Exception ex){_logger.LogError(ex, 读取文件流发生异常);fs?.Dispose(); continue;}attachment new MimePart(contentType){Content new MimeContent(fs),ContentDisposition new ContentDisposition(ContentDisposition.Attachment),ContentTransferEncoding ContentEncoding.Base64,}; var charset UTF-8;attachment.ContentType.Parameters.Add(charset, name, fileName);attachment.ContentDisposition.Parameters.Add(charset, filename, fileName); foreach (var param in attachment.ContentDisposition.Parameters){param.EncodingMethod ParameterEncodingMethod.Rfc2047;} foreach (var param in attachment.ContentType.Parameters){param.EncodingMethod ParameterEncodingMethod.Rfc2047;}collection.Add(attachment);} return collection;}在转化附件时下面的代码用来处理附件名编码问题var charset UTF-8;
attachment.ContentType.Parameters.Add(charset, name, fileName);
attachment.ContentDisposition.Parameters.Add(charset, filename, fileName);foreach (var param in attachment.ContentDisposition.Parameters)
{param.EncodingMethod ParameterEncodingMethod.Rfc2047;
}foreach (var param in attachment.ContentType.Parameters)
{param.EncodingMethod ParameterEncodingMethod.Rfc2047;
}到这了我们的邮件队列就基本完成了接下来就是在程序启动后启动队列找到 Program.cs文件并稍作改写如下var host BuildWebHost(args);var provider host.Services;
provider.GetRequiredServiceIMailQueueManager().Run();
host.Run();这里在host.Run()主机启动之前我们获取了IMailQueueManager并启动队列别忘了注册服务。运行程序我们会看到控制台每隔3秒就会打出日志info: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[0]User profile is available. Using C:\Users\Administrator\AppData\Local\ASP.NET\DataProtection-Keys as key repository and Windows DPAPI to encrypt keys at rest.info: MailQueueManager[0]线程即将启动info: MailQueueManager[0]线程已经启动线程Id是9trce: MailQueueManager[0]队列是空开始睡眠
Hosting environment: Development
Content root path: D:\publish
Now listening on: http://[::]:5000Application started. Press CtrlC to shut down.trce: MailQueueManager[0]队列是空开始睡眠trce: MailQueueManager[0]队列是空开始睡眠到此我们的邮件队列就完成了 D原文地址http://www.cnblogs.com/rocketRobin/p/9294845.html.NET社区新闻深度好文欢迎访问公众号文章汇总 http://www.csharpkit.com