内存流附件实现要求在创建时传递一个 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 ConcurrentQueue<MailBox> _mailQueue = new ConcurrentQueue<MailBox>(); 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></param> public MailQueueService(IMailQueueProvider provider) { _provider = provider; } /// <summary> /// 入队 /// </summary> /// <param></param> public void Enqueue(MailBox box) { _provider.Enqueue(box); }这里,我们的服务依赖于IMailQueueProvider,使用了其入队功能
邮件发送机IMailQueueManager实现这个相对比较复杂,我们先看下完整的类,再逐步解释:
public class MailQueueManager : IMailQueueManager { private readonly SmtpClient _client; private readonly IMailQueueProvider _provider; private readonly ILogger<MailQueueManager> _logger; private readonly EmailOptions _options; private bool _isRunning = false; private bool _tryStop = false; private Thread _thread; /// <summary> /// 初始化实例 /// </summary> /// <param></param> /// <param></param> /// <param></param> public MailQueueManager(IMailQueueProvider provider, IOptions<EmailOptions> options, ILogger<MailQueueManager> 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 don't have an OAuth2 token, disable // the XOAUTH2 authentication mechanism. if (_options.DisableOAuth) { _client.AuthenticationMechanisms.Remove("XOAUTH2"); } _provider = provider; _logger = logger; } /// <summary> /// 正在运行 /// </summary> public bool IsRunning => _isRunning; /// <summary> /// 计数 /// </summary> public int Count => _provider.Count; /// <summary> /// 启动队列 /// </summary> 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); } /// <summary> /// 停止队列 /// </summary> public 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 authentication if (!_client.IsAuthenticated) { _client.Authenticate(_options.UserName, _options.Password); } _client.Send(message); } finally { _client.Disconnect(false); } } private AttachmentCollection GetAttechments(IEnumerable<IAttachment> attachments) { if (attachments == null) { throw new ArgumentNullException(nameof(attachments)); } AttachmentCollection collection = new AttachmentCollection(); List<Stream> list = new List<Stream>(attachments.Count()); foreach (var item in attachments) { var fileName = item.GetName(); var fileType = MimeTypes.GetMimeType(fileName); var contentTypeArr = fileType.Split('http://www.likecs.com/'); 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; } }