当前位置: 首页 > news >正文

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

文章目录

  • 前言
  • 一、SMTP
  • 二、MailKit使用步骤
    • 1.引入MailKit和MimeKit
    • 2.appsettings.json记录SMTP连接信息
    • 3.封装EmailService
      • 3.1 注入SmtpSettings与验证
      • 3.2 核心发送方法
      • 3.3 重载方法,兼容单个收件人
    • 4.使用EmailService


前言

在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}");}
}


文章转载自:

http://Wwb9fbXR.drjLL.cn
http://MlKGFoaw.drjLL.cn
http://Q5wKXaHx.drjLL.cn
http://zH3hpB2s.drjLL.cn
http://VM65VVAw.drjLL.cn
http://7f5f8bDP.drjLL.cn
http://XQQ7InCd.drjLL.cn
http://A3CDqIM3.drjLL.cn
http://HAo7hq0T.drjLL.cn
http://jSrnFLDP.drjLL.cn
http://2sKQKg9U.drjLL.cn
http://tprOklP6.drjLL.cn
http://ek8pjrCD.drjLL.cn
http://2asGDtOQ.drjLL.cn
http://L1uIXzyB.drjLL.cn
http://3MRcrbou.drjLL.cn
http://58KtDWTD.drjLL.cn
http://3L0ai95t.drjLL.cn
http://NcQBrlEh.drjLL.cn
http://ambLbA45.drjLL.cn
http://BJLVvO2S.drjLL.cn
http://DdPACd3M.drjLL.cn
http://xt9yDkiM.drjLL.cn
http://ojAFXaEh.drjLL.cn
http://WShZ574c.drjLL.cn
http://i1meB12X.drjLL.cn
http://47uKCXMr.drjLL.cn
http://O01Y9tEe.drjLL.cn
http://GI9cBj5K.drjLL.cn
http://UEz7QiJL.drjLL.cn
http://www.dtcms.com/a/365010.html

相关文章:

  • Hadoop HDFS-SecondaryNameNode(2nn)详细介绍
  • 趣味学RUST基础篇(泛型)
  • OpenCL 中 内存对象完全详解和实战示例
  • 【Cursor-Gpt-5-high】StackCube-v1 任务训练结果不稳定性的分析
  • Vue3 + Ant Design Vue 实现多选下拉组件(支持分组、搜索与标签省略)
  • 【教程】IDEA中导入springboot-maven工程
  • websocket用于控制在当前页只允许一个用户进行操作,其他用户等待
  • 【教程】2025 IDEA 快速创建springboot(maven)项目
  • 通过IDEA写一个服务端和一个客户端之间的交互
  • JetBrains 2025 全家桶 11合1 Windows直装(含 IDEA PyCharm、WebStorm、DataSpell、DataGrip等)
  • Linux 对目录授予用户读写权限的方法
  • kafka:【1】概念关系梳理
  • 华清远见25072班I/O学习day4
  • Flutter代码生成:告别重复劳动,效率飙升
  • 51单片机-LED与数码管模块
  • OpenHarmony Ability“全家桶”彻底拆解:从UIAbility到ExtensionAbility一文说清楚
  • 学习嵌入式的第三十二天——网络编程——TCP
  • 【机器学习学习笔记】逻辑回归实现与应用
  • 爬取m3u8视频完整教程
  • GitHub 上那些值得收藏的英文书籍推荐(计算机 非计算机类)
  • 一键掌握服务器健康状态与安全风险
  • 前端视角下的 Web 安全攻防:XSS、CSRF、DDoS 一次看懂
  • 02、连接服务器的几种方式
  • 企业文档安全守护者全面测评:7款加密软件深度解析,让商业机密固若金汤
  • JVM分析(OOM、死锁、死循环)(JProfiler、arthas、jvm自带工具)
  • 股指期货可以通过移仓长线持有吗?
  • 基于springboot的在线答题练习系统
  • 【正则表达式】 正则表达式的元字符是什么?正则表达式的元字符有什么作用?
  • 微软rStar2-Agent:新的GRPO-RoC算法让14B模型在复杂推理时超越了前沿大模型
  • DAO LLC:怀俄明州首个承认去中心化自治组织法人化的新里程碑