月色真美

月色真美

.Net使用MimeKit发送邮件

13
2022-03-28

网站需要用到邮件功能给用户发送验证码,查了一些资料,测试了各种发送邮件的第三方依赖,发现MimeKit是最好用的,于是封装了一个依赖注入的邮件服务。测试的过程中,发现了MimeKit在添加中文名称的邮箱附件时,有乱码问题,经过各种咨询和测试,发现属于编码问题,mark。

Nuget下载此依赖。

Install-Package MailKit

1.接口

/// <summary>
/// 邮箱服务
/// </summary>
public interface IEMailService
{
    /// <summary>
    /// 发送邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="name">目标用户名</param>
    /// <param name="address">目标邮箱地址</param>
    /// <returns></returns>
    bool Send(string title, string body, string name, string address);

    /// <summary>
    /// 发送邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="name">目标用户名</param>
    /// <param name="address">目标邮箱地址</param>
    /// <returns></returns>
    Task<bool> SendAsync(string title, string body, string name, string address);

    /// <summary>
    /// 发送邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="receives">接收目标</param>
    /// <returns></returns>
    bool Send(string title, string body, params (string name, string address)[] receives);

    /// <summary>
    /// 发送邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="receives">接收目标</param>
    /// <returns></returns>
    Task<bool> SendAsync(string title, string body, params (string name, string address)[] receives);

    /// <summary>
    /// 发送携带附件的邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="name">目标用户名</param>
    /// <param name="address">目标邮箱地址</param>
    /// <param name="attachments">附件</param>
    /// <returns></returns>
    bool SendWithAttachment(string title, string body, string name, string address, params string[] attachments);

    /// <summary>
    /// 发送携带附件的邮件
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="body">内容</param>
    /// <param name="name">目标用户名</param>
    /// <param name="address">目标邮箱地址</param>
    /// <param name="attachments">附件</param>
    /// <returns></returns>
    Task<bool> SendWithAttachmentAsync(string title, string body, string name, string address, params string[] attachments);
}

2.实现

配置服务可以换成你的配置项,代码里用到的其他封装,可以自行实现,并无其他依赖。

/// <summary>
/// 邮箱服务
/// </summary>
[Component(AutofacScope = AutofacScope.SingleInstance)]
public class EMailService : IEMailService
{
    /// <summary>
    /// 配置服务
    /// </summary>
    [Autowired]
    private IConfigService configService { get; set; }

    /// <summary>
    /// 获取发送方邮箱地址
    /// </summary>
    private MailboxAddress FromMailboxAddress => new MailboxAddress(Encoding.UTF8,
        configService.GetCacheValue<string>("Config:System:EMail:Name"),
        configService.GetCacheValue<string>("Config:System:EMail:Address"));

    /// <summary>
    /// 邮箱地址
    /// </summary>
    private string Address => configService.GetCacheValue<string>("Config:System:EMail:Address");

    /// <summary>
    /// 服务器
    /// </summary>
    private string Host => configService.GetCacheValue<string>("Config:System:EMail:Host");

    /// <summary>
    /// 端口
    /// </summary>
    private int Port => configService.GetCacheValue<int>("Config:System:EMail:Port");

    /// <summary>
    /// 密码
    /// </summary>
    private string Password => configService.GetCacheValue<string>("Config:System:EMail:Password");

    /// <inheritdoc/>
    public bool Send(string title, string body, string name, string address) => Send(title, body, (name, address));

    /// <inheritdoc/>
    public bool Send(string title, string body, params (string name, string address)[] receives)
    {
        if (receives == null || receives.Length < 1)
            return false;

        //邮箱信息
        using var mimeMessage = new MimeMessage();
        //邮件标题
        mimeMessage.Subject = title;
        //发送方
        mimeMessage.From.Add(FromMailboxAddress);
        //接收方
        foreach (var receive in receives)
            mimeMessage.To.Add(new MailboxAddress(receive.name, receive.address));
        //优先级
        mimeMessage.Importance = MessageImportance.High;
        //邮件内容
        mimeMessage.Body = new TextPart(TextFormat.Html)
        {
            Text = body
        };

        try
        {
            using var smtp = new SmtpClient();
            smtp.MessageSent += (sender, args) =>
            {
                //响应事件,方便记录,args.Response
            };
            smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;

            //outlook.com需要设置为SecureSocketOptions.StartTls
            if (mimeMessage.From.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")) || mimeMessage.To.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")))
                smtp.Connect(Host, Port, SecureSocketOptions.StartTls);
            else
                smtp.Connect(Host, Port, SecureSocketOptions.Auto);

            smtp.Authenticate(Address, Password);
            smtp.Send(mimeMessage);
            smtp.Disconnect(true);
            return true;
        }
        catch (Exception ex)
        {
            LogHelper.Error($"发送EMail给[{string.Join("、", receives.Select(_ => $"{_.name}({_.address})"))}]失败,{ex.Message},{ex.StackTrace}");
            return false;
        }
    }

    /// <inheritdoc/>
    public async Task<bool> SendAsync(string title, string body, string name, string address) => await SendAsync(title, body, (name, address));

    /// <inheritdoc/>
    public async Task<bool> SendAsync(string title, string body, params (string name, string address)[] receives)
    {
        if (receives == null || receives.Length < 1)
            return false;

        //邮箱信息
        using var mimeMessage = new MimeMessage();
        //邮件标题
        mimeMessage.Subject = title;
        //发送方
        mimeMessage.From.Add(FromMailboxAddress);
        //接收方
        foreach (var receive in receives)
            mimeMessage.To.Add(new MailboxAddress(receive.name, receive.address));
        //优先级
        mimeMessage.Importance = MessageImportance.High;
        //邮件内容
        mimeMessage.Body = new TextPart(TextFormat.Html)
        {
            Text = body
        };

        try
        {
            using var smtp = new SmtpClient();
            smtp.MessageSent += (sender, args) =>
            {
                //响应事件,方便记录,args.Response
            };
            smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;

            //outlook.com需要设置为SecureSocketOptions.StartTls
            if (mimeMessage.From.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")) || mimeMessage.To.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")))
                smtp.Connect(Host, Port, SecureSocketOptions.StartTls);
            else
                smtp.Connect(Host, Port, SecureSocketOptions.Auto);

            smtp.Authenticate(Address, Password);
            await smtp.SendAsync(mimeMessage);
            await smtp.DisconnectAsync(true);
            return true;
        }
        catch (Exception ex)
        {
            LogHelper.Error($"发送EMail给[{string.Join("、", receives.Select(_ => $"{_.name}({_.address})"))}]失败,{ex.Message},{ex.StackTrace}");
            return false;
        }
    }

    /// <inheritdoc/>
    public bool SendWithAttachment(string title, string body, string name, string address, params string[] attachments)
    {
        //邮箱信息
        using var mimeMessage = new MimeMessage();
        //邮件标题
        mimeMessage.Subject = title;
        //发送方
        mimeMessage.From.Add(FromMailboxAddress);
        //接收方
        mimeMessage.To.Add(new MailboxAddress(name, address));
        //优先级
        mimeMessage.Importance = MessageImportance.High;
        //邮件内容
        var multipart = new Multipart("mixed")
        {
            new TextPart(TextFormat.Html)
            {
                Text = body
            }
        };

        var attachmentFiles = new List<(bool isTemp, string filePath, FileStream file)>();
        try
        {
            //添加邮件附件
            foreach (var path in attachments)
            {
                var filePath = path;
                var isTemp = false;
                var fileName = string.Empty;
                if (File.Exists(path))
                {
                    filePath = path;
                    fileName = Path.GetFileName(filePath);
                }
                else if (path.StartsWith("http"))
                {
                    var uri = new Uri(path);
                    var query = HttpUtility.ParseQueryString(uri.Query);
                    if (query != null && query.HasKeys())
                    {
                        fileName = query.Get("name");
                        if (!string.IsNullOrWhiteSpace(fileName))
                            fileName = $"{fileName}{Path.GetExtension(uri.AbsolutePath)}";
                    }

                    if (string.IsNullOrWhiteSpace(fileName))
                        fileName = Path.GetFileName(path);

                    var tempFolderPath = Path.Combine(AppContext.BaseDirectory, "temp");
                    if (!Directory.Exists(tempFolderPath))
                        Directory.CreateDirectory(tempFolderPath);

                    filePath = Path.Combine(tempFolderPath, $"{Guid.NewGuid():N}{Path.GetExtension(uri.AbsolutePath)}");
                    if (!HttpHelper.Download(filePath, uri.AbsoluteUri, null, null))
                    {
                        LogHelper.Error($"发送EMail失败,附件地址[{path}]无法下载");
                        return false;
                    }

                    isTemp = true;
                }
                else
                {
                    LogHelper.Error($"发送EMail失败,附件地址[{path}]异常");
                    return false;
                }

                var fileType = MimeTypes.GetMimeType(filePath);
                var contentTypeArr = fileType.Split('/');
                var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);

                MimePart attachment = null;
                var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
                attachmentFiles.Add((isTemp, filePath, fs));
                attachment = new MimePart(contentType)
                {
                    Content = new MimeContent(fs),
                    ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
                    ContentTransferEncoding = ContentEncoding.Base64
                };

                //编码处理,否则中文附件会错误
                var charset = "GB18030";
                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;

                multipart.Add(attachment);
            }

            //邮件实体
            mimeMessage.Body = multipart;

            using var smtp = new SmtpClient();
            smtp.MessageSent += (sender, args) =>
            {
                //响应事件,方便记录,args.Response
            };
            smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;

            //outlook.com需要设置为SecureSocketOptions.StartTls
            if (mimeMessage.From.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")) || mimeMessage.To.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")))
                smtp.Connect(Host, Port, SecureSocketOptions.StartTls);
            else
                smtp.Connect(Host, Port, SecureSocketOptions.Auto);

            smtp.Authenticate(Address, Password);
            smtp.Send(mimeMessage);
            smtp.Disconnect(true);

            //文件流释放和临时文件清理
            foreach (var fs in attachmentFiles)
            {
                fs.file.Dispose();
                fs.file.Close();

                if (fs.isTemp)
                {
                    if (File.Exists(fs.filePath))
                        File.Delete(fs.filePath);
                }
            }

            return true;
        }
        catch (Exception ex)
        {
            LogHelper.Error($"发送EMail给[{$"{name}({address})"}]失败,{ex.Message},{ex.StackTrace}");
            return false;
        }
    }

    /// <inheritdoc/>
    public async Task<bool> SendWithAttachmentAsync(string title, string body, string name, string address, params string[] attachments)
    {
        //邮箱信息
        using var mimeMessage = new MimeMessage();
        //邮件标题
        mimeMessage.Subject = title;
        //发送方
        mimeMessage.From.Add(FromMailboxAddress);
        //接收方
        mimeMessage.To.Add(new MailboxAddress(name, address));
        //优先级
        mimeMessage.Importance = MessageImportance.High;
        //邮件内容
        var multipart = new Multipart("mixed")
        {
            new TextPart(TextFormat.Html)
            {
                Text = body
            }
        };

        var attachmentFiles = new List<(bool isTemp, string filePath, FileStream file)>();
        try
        {
            //添加邮件附件
            foreach (var path in attachments)
            {
                var filePath = path;
                var isTemp = false;
                var fileName = string.Empty;
                if (File.Exists(path))
                {
                    filePath = path;
                    fileName = Path.GetFileName(filePath);
                }
                else if (path.StartsWith("http"))
                {
                    var uri = new Uri(path);
                    var query = HttpUtility.ParseQueryString(uri.Query);
                    if (query != null && query.HasKeys())
                    {
                        fileName = query.Get("name");
                        if (!string.IsNullOrWhiteSpace(fileName))
                            fileName = $"{fileName}{Path.GetExtension(uri.AbsolutePath)}";
                    }

                    if (string.IsNullOrWhiteSpace(fileName))
                        fileName = Path.GetFileName(path);

                    var tempFolderPath = Path.Combine(AppContext.BaseDirectory, "temp");
                    if (!Directory.Exists(tempFolderPath))
                        Directory.CreateDirectory(tempFolderPath);

                    filePath = Path.Combine(tempFolderPath, $"{Guid.NewGuid():N}{Path.GetExtension(uri.AbsolutePath)}");
                    if (!HttpHelper.Download(filePath, uri.AbsoluteUri, null, null))
                    {
                        LogHelper.Error($"发送EMail失败,附件地址[{path}]无法下载");
                        return false;
                    }

                    isTemp = true;
                }
                else
                {
                    LogHelper.Error($"发送EMail失败,附件地址[{path}]异常");
                    return false;
                }

                var fileType = MimeTypes.GetMimeType(filePath);
                var contentTypeArr = fileType.Split('/');
                var contentType = new ContentType(contentTypeArr[0], contentTypeArr[1]);

                MimePart attachment = null;
                var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
                attachmentFiles.Add((isTemp, filePath, fs));
                attachment = new MimePart(contentType)
                {
                    Content = new MimeContent(fs),
                    ContentDisposition = new ContentDisposition(ContentDisposition.Attachment),
                    ContentTransferEncoding = ContentEncoding.Base64
                };

                //编码处理,否则中文附件会错误
                var charset = "GB18030";
                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;

                multipart.Add(attachment);
            }

            //邮件实体
            mimeMessage.Body = multipart;

            using var smtp = new SmtpClient();
            smtp.MessageSent += (sender, args) =>
            {
                //响应事件,方便记录,args.Response
            };
            smtp.ServerCertificateValidationCallback = (s, c, h, e) => true;

            //outlook.com需要设置为SecureSocketOptions.StartTls
            if (mimeMessage.From.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")) || mimeMessage.To.Any(a => ((MailboxAddress)a).Address.Contains("outlook.com")))
                smtp.Connect(Host, Port, SecureSocketOptions.StartTls);
            else
                smtp.Connect(Host, Port, SecureSocketOptions.Auto);

            smtp.Authenticate(Address, Password);
            await smtp.SendAsync(mimeMessage);
            await smtp.DisconnectAsync(true);

            //文件流释放和临时文件清理
            foreach (var fs in attachmentFiles)
            {
                fs.file.Dispose();
                fs.file.Close();

                if (fs.isTemp)
                {
                    if (File.Exists(fs.filePath))
                        File.Delete(fs.filePath);
                }
            }

            return true;
        }
        catch (Exception ex)
        {
            LogHelper.Error($"发送EMail给[{$"{name}({address})"}]失败,{ex.Message},{ex.StackTrace}");
            return false;
        }
    }
}