1.添加MailKit依赖
dotnet add package MailKit
2.发送邮件服务
using System.Text;
using dotnet_start.Model;
using MailKit.Net.Smtp;
using MimeKit;
using MimeKit.Text;
using MimeKit.Utils;
using Path = System.IO.Path;
namespace dotnet_start.Services;
public class EmailSendService
{
public readonly IWebHostEnvironment Env;
public readonly ILogger<EmailSendService> Logger;
public EmailSendService(IWebHostEnvironment env, ILogger<EmailSendService> logger)
{
Env = env;
Logger = logger;
}
public async Task SendEmailByTextPartAsync(string fromEmail, string toEmail, string subject, string message)
{
var mineMessage = new MimeMessage();
var fromAddress = new MailboxAddress("这是来ASP.NET Core——文本方式邮件", fromEmail);
mineMessage.From.Add(fromAddress);
var toAddress = new MailboxAddress("菜哥", toEmail);
mineMessage.To.Add(toAddress);
mineMessage.Subject = subject;
mineMessage.Body = new TextPart(TextFormat.Html)
{
Text = message
};
await SendBySmtp(fromEmail, mineMessage);
}
public async Task SendEmailByHtmlAsync(string fromEmail, string toEmail, string subject, string message)
{
var mineMessage = new MimeMessage();
mineMessage.Subject = subject;
var fromAddress = new MailboxAddress("这是来ASP.NET Core——HTML方式的邮件", fromEmail);
mineMessage.From.Add(fromAddress);
var toAddress = new MailboxAddress("菜哥", toEmail);
mineMessage.To.Add(toAddress);
var bodyBuilder = new BodyBuilder();
var imagePath = Path.Combine(Env.ContentRootPath, "Templates", "img", "vue-js.png");
if (File.Exists(imagePath))
{
// 添加附件
var mimeEntity = await bodyBuilder.LinkedResources.AddAsync(imagePath);
mimeEntity.ContentId = MimeUtils.GenerateMessageId();
bodyBuilder.HtmlBody = $"<p>{message}</p>" +
$"<p><img src=\"cid:{mimeEntity.ContentId}\" alt=\"\" width=\"500\" height=\"300\"/></p>";
}
mineMessage.Body = bodyBuilder.ToMessageBody();
await SendBySmtp(fromEmail, mineMessage);
}
public async Task SendEmailByTemplateAsync(string fromEmail, string toEmail, string subject,
string tempName,
Dictionary<string, string> variables,
List<string>? attachments = null,
List<string>? inlineImages = null)
{
var mineMessage = new MimeMessage();
mineMessage.Subject = subject;
var fromAddress = new MailboxAddress("ASP.NET Core 模板邮件", fromEmail);
mineMessage.From.Add(fromAddress);
var toAddress = new MailboxAddress("菜哥", toEmail);
mineMessage.To.Add(toAddress);
var bodyBuilder = new BodyBuilder();
// 读取模板
var templatePath = Path.Combine(Env.ContentRootPath, "Templates", tempName);
if (!File.Exists(templatePath))
{
throw new FileNotFoundException($"邮件模板不存在: {templatePath}");
}
var templateContent = await File.ReadAllTextAsync(templatePath);
// 替换变量 {{Key}}
templateContent = variables.Aggregate(templateContent, (current, kv) => current.Replace($"{{{{{kv.Key}}}}}", kv.Value));
// 内联图片
if (inlineImages != null)
{
foreach (var imgPath in inlineImages.Where(File.Exists))
{
var img = await bodyBuilder.LinkedResources.AddAsync(imgPath);
var contentId = Path.GetFileNameWithoutExtension(imgPath);
img.ContentId = contentId;
}
}
// 附件
if (attachments != null)
{
foreach (var filePath in attachments.Where(File.Exists))
{
await bodyBuilder.Attachments.AddAsync(filePath);
}
}
bodyBuilder.HtmlBody = templateContent;
mineMessage.Body = bodyBuilder.ToMessageBody();
await SendBySmtp(fromEmail, mineMessage);
}
public async Task SendEmailByTemplateFormAsync(string fromEmail, string toEmail, string subject, string tempName,
Dictionary<string, string> variables,
List<IFormFile>? attachments = null,
List<IFormFile>? inlineImages = null)
{
var mimeMessage = new MimeMessage();
mimeMessage.Subject = subject;
mimeMessage.From.Add(new MailboxAddress("ASP.NET Core 模板邮件", fromEmail));
mimeMessage.To.Add(new MailboxAddress("收件人", toEmail));
var bodyBuilder = new BodyBuilder();
try
{
// 读取模板
var templatePath = Path.Combine(Env.ContentRootPath, "Templates", tempName);
if (!File.Exists(templatePath))
throw new FileNotFoundException($"邮件模板不存在: {templatePath}");
var templateContent = await File.ReadAllTextAsync(templatePath);
// 替换普通变量
templateContent = variables.Aggregate(templateContent, (current, kv) =>
current.Replace($"{{{{{kv.Key}}}}}", kv.Value));
using var tempDir = new TempDirectory(Logger);
// 内联图片
if (inlineImages is { Count: > 0 })
{
var sb = new StringBuilder();
foreach (var file in inlineImages.Where(file => file.Length > 0))
{
var fileName = file.FileName;
// 读取文件内容到字节数组
byte[] bytes;
await using (var stream = file.OpenReadStream())
{
bytes = new byte[stream.Length];
var readAsync = await stream.ReadAsync(bytes);
if (readAsync == file.Length)
{
Logger.LogInformation("内联文件==={fileName},大小==={readAsync} 读取完成", fileName, readAsync);
}
}
await HandlerAttachment(file, bodyBuilder, tempDir, bytes);
// 转 Base64
var base64 = Convert.ToBase64String(bytes);
var ext = Path.GetExtension(fileName).TrimStart('.').ToLower();
// 生成对应 img 标签
sb.AppendLine($"<img src=\"data:image/{ext};base64,{base64}\" alt=\"{fileName}\" />");
}
// 替换模板占位符
templateContent = templateContent.Replace("{{InlineImages}}", sb.ToString());
}
// 处理附件
if (attachments is { Count: > 0 })
{
foreach (var file in attachments.Where(file => file.Length > 0))
{
await HandlerAttachment(file, bodyBuilder, tempDir);
}
}
bodyBuilder.HtmlBody = templateContent;
mimeMessage.Body = bodyBuilder.ToMessageBody();
await SendBySmtp(fromEmail, mimeMessage);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// 处理附件
/// </summary>
/// <param name="file">附件文件</param>
/// <param name="bodyBuilder">邮件内容构造器</param>
/// <param name="tempDir">临时目录</param>
/// <param name="bytes">文件字节</param>
private async Task HandlerAttachment(IFormFile file, BodyBuilder bodyBuilder, TempDirectory tempDir, byte[]? bytes = null)
{
var fileName = file.FileName;
var fileLength = file.Length;
var filContentType = file.ContentType;
var contentType = !string.IsNullOrWhiteSpace(filContentType)
? ContentType.Parse(filContentType)
: new ContentType("application", "octet-stream");
// 附件小于5M的直接使用内存流,
if (file.Length < 5 << 20)
{
Logger.LogInformation("附件名称: {Name},大小==={Length}小于5MB使用内存流", fileName, fileLength);
if (bytes is { Length: > 0 })
{
await bodyBuilder.Attachments.AddAsync(fileName, new MemoryStream(bytes), contentType);
}
else
{
var stream = new MemoryStream();
await file.CopyToAsync(stream);
// 重置流位置
stream.Position = 0;
await bodyBuilder.Attachments.AddAsync(fileName, stream, contentType);
}
}
else
{
using var tempFilePath = new TempFile(tempDir.Path, Logger);
Logger.LogInformation("附件名称: {Name},大小==={Length}大于5MB使用临时文件==={@tempFilePath}", fileName, fileLength, tempFilePath.Path);
await using var stream = new FileStream(tempFilePath.Path, FileMode.Create);
await file.CopyToAsync(stream);
var attachment = await bodyBuilder.Attachments.AddAsync(tempFilePath.Path, contentType);
// 设置附件的文件名为原始文件名
attachment.ContentDisposition.FileName = fileName;
}
}
private static async Task SendBySmtp(string fromEmail, MimeMessage mineMessage)
{
using var smtp = new SmtpClient();
await smtp.ConnectAsync("smtp.qq.com", 465, true);
// TODO 设置认证信息——授权码(不是密码),我这里是以QQ邮箱为例的,QQ邮箱的SMTP服务怎么开桶及授权码,请参照QQ邮箱安全设置相关
await smtp.AuthenticateAsync(fromEmail, "授权码,请自行参照网上教程");
await smtp.SendAsync(mineMessage);
await smtp.DisconnectAsync(true);
}
}
3.邮件发送控制器
using dotnet_start.Model.Response;
using dotnet_start.Services;
using Microsoft.AspNetCore.Mvc;
using Swashbuckle.AspNetCore.Annotations;
using Path = System.IO.Path;
namespace dotnet_start.Controllers;
/// <summary>
/// 邮件发送控制器
/// </summary>
[SwaggerTag("邮件发送控制器")]
[ApiController]
[Route("email/send")]
public class EmailSendController(EmailSendService service) : ControllerBase
{
/// <summary>
/// 发送邮件
/// </summary>
/// <param name="from">发件人</param>
/// <param name="to">收件人</param>
/// <param name="subject">主题</param>
/// <param name="message">信息</param>
/// <returns>发送结果</returns>
[HttpPost("text")]
[ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendByTextPart([FromForm] string from, [FromForm] string to,
[FromForm] string subject, [FromForm] string message)
{
await service.SendEmailByTextPartAsync(from, to, subject, message);
return Ok(CommonResult<string>.Success(200, "邮件发送成功"));
}
/// <summary>
/// 发送邮件-html
/// </summary>
/// <param name="from">发件人</param>
/// <param name="to">收件人</param>
/// <param name="subject">主题</param>
/// <param name="message">信息</param>
/// <returns>发送结果</returns>
[HttpPost("html")]
[ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendByHtml([FromForm] string from, [FromForm] string to,
[FromForm] string subject, [FromForm] string message)
{
await service.SendEmailByHtmlAsync(from, to, subject, message);
return Ok(CommonResult<string>.Success(200, "邮件发送成功"));
}
/// <summary>
/// 发送邮件-template
/// </summary>
/// <param name="from">发件人</param>
/// <param name="to">收件人</param>
/// <param name="subject">主题</param>
/// <param name="message">信息</param>
/// <returns>邮件发送结果</returns>
[HttpPost("template")]
[ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendByHtmlTemplate([FromForm] string from, [FromForm] string to,
[FromForm] string subject)
{
var dictionary = new Dictionary<string, string>
{
{ "UserName", "菜哥" },
{ "Message", "这里是带附件的邮件 📎" }
};
var attachments = new List<string>
{
// 这里表示项目根目录下的Templates目录中的files目录中的xxxx.pdf
Path.Combine(service.Env.ContentRootPath, "Templates", "files", "xxxx.pdf"),
"/xxxx/xxxx/xxx.doc" // 绝对路径,请自行更换为自己系统上文件的路径
};
var inlineImages = new List<string>
{
Path.Combine(service.Env.ContentRootPath, "Templates", "img", "vue-js.png")
};
await service.SendEmailByTemplateAsync(from, to, subject, "mail_template.html", dictionary, attachments, inlineImages);
return Ok(CommonResult<string>.Success(200, "邮件发送成功"));
}
/// <summary>
/// 发送邮件——表单上传文件作为内联文件或者附件文件
/// </summary>
/// <param name="from">发件人</param>
/// <param name="to">收件人</param>
/// <param name="username">用户名</param>
/// <param name="message">邮件信息</param>
/// <param name="subject">主题</param>
/// <param name="attachments">附件文件</param>
/// <param name="inlineImages">内联图片</param>
/// <returns>结果</returns>
[HttpPost("form")]
[ProducesResponseType(typeof(CommonResult<string>), StatusCodes.Status200OK)]
public async Task<IActionResult> SendByHtmlTemplateByForm(
[FromForm] string from, [FromForm] string to, [FromForm] string username, [FromForm] string message,
[FromForm] string subject,
[FromForm] List<IFormFile> attachments,
[FromForm] List<IFormFile> inlineImages
)
{
var dictionary = new Dictionary<string, string>
{
{ "UserName", username },
{ "Message", message }
};
await service.SendEmailByTemplateFormAsync(
from,
to,
subject,
"mail_form.html",
dictionary,
attachments,
inlineImages
);
return Ok(CommonResult<string>.Success(200, "邮件发送成功"));
}
}
4.自定义的临时目录与临时文件
临时目录
namespace dotnet_start.Model;
/// <summary>
/// 自动删除的临时目录
/// </summary>
public class TempDirectory : IDisposable
{
private readonly ILogger _logger;
public string Path { get; }
public TempDirectory(ILogger logger)
{
_logger = logger;
Path = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName());
Directory.CreateDirectory(Path);
}
public void Dispose()
{
try
{
if (!Directory.Exists(Path)) return;
Directory.Delete(Path, true);
_logger.LogDebug("删除临时目录==={Path}", Path);
}
catch(Exception ex)
{
// 忽略删除异常就直接使用 catch { }
_logger.LogDebug(ex,"删除临时目录==={}出错了", Path);
}
}
}
临时文件
namespace dotnet_start.Model;
/// <summary>
/// 自动删除的临时文件
/// </summary>
public class TempFile : IDisposable
{
private readonly ILogger _logger;
public string Path { get; }
public TempFile(string directory, ILogger logger)
{
Path = System.IO.Path.Combine(directory, System.IO.Path.GetRandomFileName());
_logger = logger;
}
public void Dispose()
{
try
{
if (!File.Exists(Path)) return;
File.Delete(Path);
_logger.LogDebug("删除临时文件==={Path}",Path);
}
catch(Exception ex)
{
// 忽略删除异常就直接使用 catch { }
_logger.LogDebug(ex,"删除临时文件==={}出错了", Path);
}
}
}
这样使用using来定义TempDirectory或者TempFile的时候,就会自动调用Dispose方法执行删除的操作了,不需要额外手动删除了。
5.邮件发送html模板
mail_form.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>邮件发送模板</title>
<style>
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #f9f9f9;
margin: 0;
padding: 20px;
}
.container {
margin: 0 auto;
background: #fff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
h1 {
color: #333;
font-size: 22px;
}
p {
color: #555;
line-height: 1.6;
}
.footer {
margin-top: 30px;
font-size: 12px;
color: #999;
text-align: center;
}
.logo {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 10px;
margin-bottom: 20px;
}
.logo img {
cursor: pointer;
max-width: 300px;
max-height: 200px;
width: auto;
height: auto;
object-fit: contain;
border-radius: 8px;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.logo img:hover {
transform: scale(1.05);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
{{InlineImages}}
</div>
<h1>您好,{{UserName}} 👋</h1>
<p>{{Message}}</p>
<div class="footer">
<p>这是一封系统自动发送的邮件,请勿直接回复。</p>
</div>
</div>
</body>
</html>
mail_template.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>邮件发送模板</title>
<style>
body {
font-family: "Microsoft YaHei", Arial, sans-serif;
background-color: #f9f9f9;
margin: 0;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #fff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
h1 {
color: #333;
font-size: 22px;
}
p {
color: #555;
line-height: 1.6;
}
.footer {
margin-top: 30px;
font-size: 12px;
color: #999;
text-align: center;
}
.logo {
text-align: center;
margin-bottom: 20px;
}
.logo img {
max-width: 300px;
max-height: 200px;
}
</style>
</head>
<body>
<div class="container">
<div class="logo">
<img src="cid:vue-js" alt="vue"/>
</div>
<h1>您好,{{UserName}} 👋</h1>
<p>{{Message}}</p>
<div class="footer">
<p>这是一封系统自动发送的邮件,请勿直接回复。</p>
</div>
</div>
</body>
</html>
6.QQ邮箱开启SMTP页面
7.使用apifox测试邮件发送
到此,邮件发送功能完成,大家可以拷贝粘贴代码自行尝试即可。