【ASP.NET Core】基于MailKit(SMTP 协议)实现邮件发送

发布于:2025-09-06 ⋅ 阅读:(19) ⋅ 点赞:(0)


前言

在ASP.NET Core中,实现邮件发送的功能我们可以依赖MailKit这个邮件处理库来处理邮件的构建和传输。 本文将先介绍下SMTP协议,然后通过MailKit这个库,使用基于SMTP协议实现后端发送邮件的功能。


一、SMTP

SMTP全长Simple Mail Transfer Protocol,直译过来就是简单邮件传输协议。主要负责将邮件从发件人服务器传输到收件人服务器

SMTP仅负责发送邮件,接收邮件一般采用POP3(邮局协议)或 IMAP(互联网消息访问协议)
POP3和IMAP都是从远程邮件服务器读取或同步邮件到本地。IMAP支持多设备同步,对服务器性能要求较高。

SMTP在工作中一般分为四步,建立连接,身份验证,传递邮件信息和确认与断开连接。

发件人服务器通过SMTP端口与收件人服务器建立TCP连接,需要验证发件人服务器自身的身份后向接收方服务器传递邮件的关键信息,比如发件人地址、收件人地址、邮件主题和内容这类基本信息。最后接收方服务器确认接收邮件后,双方断开连接。

二、MailKit使用步骤

1.引入MailKit和MimeKit

dotnet add package MailKit --version 4.13.0
dotnet add package MimeKit --version 4.13.0

MailKit封装了SMTP协议,能帮助我们 在.NET 平台上实现邮件的发送。其本身还封装了POP3、IMAP等协议的底层实现,也能实现邮件的接收。

MimeKit是用来负责处理邮件的内容构建,像是邮件本身的内容文本,还有包含诸如附件之类的邮件结构

2.appsettings.json记录SMTP连接信息

这里将SMTP的连接信息保存到appsettings.json里,不同邮件服务商的SMTP服务器地址和端口存在差异,这里拿126邮箱举例。

QQ 邮箱 smtp.qq.com TLS:587
163 邮箱 smtp.163.com TLS:587
126 邮箱 smtp.126.com TLS:587

"Smtp": {
  "Host": "smtp.126.com",
  "Port": "587",
  "UseSsl": "true",
  "Username": "你的邮箱@126.com",
  "Password": "你的授权密码",
  "DisplayName": "ToDoBetter"
},

SMTP中的Username就是你的邮箱名称,Password一般是一个有期限的授权码。126邮箱中在设置页面开启SMTP服务,并且生成一个授权码作为Password。

这样SMTP的连接信息相关的工作我们就完成了。
在这里插入图片描述

3.封装EmailService

3.1 注入SmtpSettings与验证

SmtpSettings类

/// <summary>
/// SMTP设置
/// </summary>
public class SmtpSettings
{
    public string Host { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
    public string DisplayName { get; set; }
    public bool UseSsl { get; set; }
}

Program.cs 向Configure里注册SmtpSettings配置

builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));

EmailService里检查SmtpSettings是否合规

 public class EmailService
 {
     private readonly SmtpSettings _smtpSettings;
     private readonly ILogger<EmailService> _logger;
     public EmailService(IOptions<SmtpSettings> smtpSettings, ILogger<EmailService> logger)
     {
         _smtpSettings = smtpSettings.Value ?? throw new ArgumentNullException(nameof(smtpSettings));
         _logger = logger;
         ValidateSmtpSettings();
     }
}

验证SMTP设置

/// <summary>
/// 验证SMTP设置
/// </summary>
private void ValidateSmtpSettings()
{
    var missingSettings = new List<string>();
    if (string.IsNullOrWhiteSpace(_smtpSettings.Host))
    {
        missingSettings.Add(nameof(_smtpSettings.Host));
    }
    if (string.IsNullOrWhiteSpace(_smtpSettings.Username))
    {
        missingSettings.Add(nameof(_smtpSettings.Username));
    }
    if (string.IsNullOrWhiteSpace(_smtpSettings.Password))
    {
        missingSettings.Add(nameof(_smtpSettings.Password));
    }
    if (missingSettings.Any())
    {
        var errorMessage = $"SMTP配置不完整,缺少以下设置:{string.Join(", ", missingSettings)}";
        _logger.LogError(errorMessage);
        throw new InvalidOperationException(errorMessage);
    }
}

3.2 核心发送方法

通过var emailMessage = new MimeMessage();来启用MimeKit。通过MimeMessage的From,To和Subject这些方法来配置邮件的发送方,接收方和邮件主题。接收方支持多收件人时,使用emailMessage.To.AddRange()方法来分别添加。

var emailMessage = new MimeMessage();
try
{
    //发送方
    emailMessage.From.Add(new MailboxAddress(_smtpSettings.DisplayName, _smtpSettings.Username));
    //接收方
    emailMessage.To.AddRange(toEmails.Select(email => new MailboxAddress("", email)));
    //邮件主题
    emailMessage.Subject = subject;
}

使用BodyBuilder构建邮件内容的时候可以通过bodyBuilder.Attachments()方法添加附件。附件内容通过byte[]挂载在bodyBuilder里。
但是必须要指定附件的MIME类型来生成MimeKit.ContentType,这样才能正确的给bodyBuilder来添加附件。MimeKit生成ContentType要求MIME类型格式分为mainType和subType。

byte[] fileContent;
using (var fileStream = new FileStream(attachment.FilePath, FileMode.Open, FileAccess.Read))
using (var memoryStream = new MemoryStream())
{
    await  fileStream.CopyToAsync(memoryStream,cancellationToken);
    fileContent = memoryStream.ToArray();
}

//解析MIME类型
var (mainType, subType) = ParseMimeType(attachment.MimeType);

// 创建正确的ContentType(使用两个参数的构造函数)
var contentType = new MimeKit.ContentType(mainType, subType);
bodyBuilder.Attachments.Add(attachment.FileName, fileContent, contentType);

附件类

 /// <summary>
 /// 附件
 /// </summary>
 public class Attachment
 {
     /// <summary>
     /// 本地文件路径
     /// </summary>
     public string FilePath { get; set; }

     /// <summary>
     /// 附件显示名称
     /// </summary>
     public string FileName { get; set; }
     /// <summary>
     /// MIME类型 (如 "image/jpeg", "application/pdf")
     /// </summary>
     public string MimeType { get; set; }
 }

以下为完整代码。

/// <summary>
/// 发送邮件【多收件人】
/// </summary>
/// <param name="toEmails"></param>
/// <param name="subject"></param>
/// <param name="message"></param>
/// <param name="attachments"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
/// <exception cref="EmailServiceException"></exception>
public async Task SendEmailAsync(IEnumerable<string> toEmails, string subject, string message,
    IEnumerable<Attachment> attachments = null, CancellationToken cancellationToken = default)
{
    if (toEmails == null || !toEmails.Any())
    {
        throw new ArgumentNullException("至少需要一个收件人", nameof(toEmails));
    }
    if (string.IsNullOrEmpty(subject))
    {
        throw new ArgumentNullException("邮件主题不能为空", nameof(subject));
    }
    var emailMessage = new MimeMessage();
    try
    {
        //发送方
        emailMessage.From.Add(new MailboxAddress(_smtpSettings.DisplayName, _smtpSettings.Username));
        //接收方
        emailMessage.To.AddRange(toEmails.Select(email => new MailboxAddress("", email)));
        //邮件主题
        emailMessage.Subject = subject;
        //构建邮件内容(内容文本+附件)
        var bodyBuilder = new BodyBuilder { HtmlBody = message };
        if (attachments != null)
        {
            foreach (var attachment in attachments)
            {
                byte[] fileContent;
                using (var fileStream = new FileStream(attachment.FilePath, FileMode.Open, FileAccess.Read))
                using (var memoryStream = new MemoryStream())
                {
                    await  fileStream.CopyToAsync(memoryStream,cancellationToken);
                    fileContent = memoryStream.ToArray();
                }

                //解析MIME类型
                var (mainType, subType) = ParseMimeType(attachment.MimeType);

                // 创建正确的ContentType(使用两个参数的构造函数)
                var contentType = new MimeKit.ContentType(mainType, subType);
                bodyBuilder.Attachments.Add(attachment.FileName, fileContent, contentType);
            }
        }
        emailMessage.Body = bodyBuilder.ToMessageBody();
        using (var smtpClient = new SmtpClient())
        {
            _logger.LogInformation("正在连接到SMTP服务器: {Host}:{Port}",
            _smtpSettings.Host, _smtpSettings.Port);
            await smtpClient.ConnectAsync(_smtpSettings.Host, _smtpSettings.Port, _smtpSettings.UseSsl, cancellationToken);

            _logger.LogInformation("正在进行SMTP认证: {Username}", _smtpSettings.Username);
            await smtpClient.AuthenticateAsync(_smtpSettings.Username, _smtpSettings.Password, cancellationToken);

            _logger.LogInformation("正在发送邮件至: {Recipients}", string.Join(",", toEmails));
            await smtpClient.SendAsync(emailMessage, cancellationToken);

            _logger.LogInformation("邮件发送成功,正在断开连接");
            await smtpClient.DisconnectAsync(true, cancellationToken);
        }
    }
    catch (System.Net.Mail.SmtpException ex)
    {
        _logger.LogError(ex, "SMTP错误: 发送邮件失败 - {Message}", ex.Message);
        throw new EmailServiceException("发送邮件失败: " + ex.Message, ex);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "发送邮件失败 - {Message}", ex.Message);
        throw new EmailServiceException("发送邮件失败: " + ex.Message, ex);
    }
}

/// <summary>
/// 解析MIME类型为主要类型和子类型
/// </summary>
private (string mainType, string subType) ParseMimeType(string mimeType)
{
    if (string.IsNullOrWhiteSpace(mimeType))
        return ("application", "octet-stream");

    var parts = mimeType.Split('/', 2);
    if (parts.Length != 2)
        return ("application", "octet-stream");

    return (parts[0], parts[1]);
}

3.3 重载方法,兼容单个收件人

/// <summary>
/// 发送邮件
/// </summary>
/// <param name="toEmail"></param>
/// <param name="subject"></param>
/// <param name="message"></param>
/// <param name="attachments"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public async Task SendEmailAsync(string toEmail, string subject, string message, IEnumerable<Attachment> attachments = null, CancellationToken cancellationToken = default)
{
    if (string.IsNullOrEmpty(toEmail))
    {
        throw new ArgumentNullException("收件人邮箱不能为空", nameof(toEmail));
    }
    await SendEmailAsync(new List<string> { toEmail }, subject, message, attachments, cancellationToken);
}

4.使用EmailService

注册服务

builder.Services.AddScoped<EmailService>();

注入服务并使用发送邮件

private readonly EmailService _emailService;
private readonly IWebHostEnvironment _env; // 新增:用于获取项目路径


// 注入邮件服务
public EmailController(EmailService emailService, IWebHostEnvironment env)
{
    _emailService = emailService;
    _env = env;
}

[HttpGet("SendReportWithAttachments")]
public async Task<ActionResult> SendReportWithAttachments()
{
    try
    {
        //1. 准备收件人、主题和内容
        var toEmails = "xxxx@163.com";
        var subject = "测试邮件";
        var htmlMessage = "<h1>这是一封测试邮件</h1><p>使用MailKit发送</p>";

         // 获取静态文件根目录(根据项目实际目录决定
		 var rootPath = _env.ContentRootPath + ".Static";
		 // 拼接相对路径(根目录 + 相对路径)
		 var filePath = Path.Combine(rootPath, "img", "3f5d3aaa8edefe361918fe1cce595d9f.png");
		 //2. 准备附件(可以添加多个)
		 var attachments = new List<Attachment>{
		     new Attachment
		     {
		         FilePath = filePath,
		         FileName = "附件图.png",
		         MimeType = "image/png"  // PNG图片的MIME类型
		     }
		 };
		 //3. 发送邮件
		 //传递 cancellationToken 用于取消操作
		 await _emailService.SendEmailAsync(toEmails, subject, htmlMessage, attachments, HttpContext.RequestAborted);
		
		 return Content("邮件发送成功!");
    }
    catch (EmailServiceException ex)
    {
        return Content($"发送失败:{ex.Message}");
        // 记录详细错误日志
    }
    catch (ArgumentNullException ex)
    {
        return Content($"参数错误:{ex.Message}");
    }
}


网站公告

今日签到

点亮在社区的每一天
去签到