在C#项目中实现邮件发送,本质上是通过SMTP协议与邮件服务器建立会话,并依照RFC标准提交发件人、收件人、正文与附件数据。.NET Framework及早期.NET Core版本提供了System.Net.Mail命名空间下的SmtpClient和MailMessage类型,开发者无需关心底层的Socket通信,即可完成一封符合协议规范的电子邮件构造与投递。下面我们先看一个基础的工具类结构。

一、SMTP邮件发送的核心类说明
MailMessage负责描述一封邮件的所有内容,包括发件人地址、收件人列表、主题、正文以及附件集合。它的From属性接收一个MailAddress实例,To属性是一个MailAddressCollection,可以调用Add方法添加多个收件人。如果业务需要抄送或密送,则分别对应CC和Bcc属性。正文部分可通过Body属性设置纯文本,或将IsBodyHtml设为true来发送HTML格式的邮件。
SmtpClient则是真正执行发送动作的客户端。它需要配置Host(SMTP服务器地址)、Port(常用25、465或587)、Credentials(登录凭据)以及EnableSsl(是否启用加密连接)。在旧版.NET中,调用Send方法会阻塞当前线程,因此工具类通常提供异步的SendMailAsync以避免接口超时。以下代码展示了这两个类的基本关系:
using System;
using System.Net;
using System.Net.Mail;
// 构造邮件内容
MailMessage msg = new MailMessage();
msg.From = new MailAddress("notify@ipipp.com", "系统通知");
msg.To.Add("user@ipipp.com");
msg.Subject = "测试邮件";
msg.Body = "这是一封通过C#发送的测试邮件";
msg.IsBodyHtml = false;
// 配置SMTP客户端
SmtpClient client = new SmtpClient();
client.Host = "smtp.ipipp.com";
client.Port = 587;
client.EnableSsl = true;
client.Credentials = new NetworkCredential("notify@ipipp.com", "your_password");
二、带附件发送的详细实现
附件在MailMessage中以Attachment类表示,最常见的方式是从本地文件路径构造,例如new Attachment("d:/report.pdf")。但在Web或后台服务中,文件可能以流的形式存在,此时应使用接收Stream和MediaTypeNames的构造函数,避免产生临时文件。添加多个附件只需多次调用Attachments.Add方法,系统会自动在邮件体中生成multipart/mixed结构。
需要注意,Attachment默认会根据文件扩展名推断MIME类型,若发送的是自定义格式,应显式指定ContentType。另外,附件名称包含中文时,某些旧邮件服务器会出现乱码,可通过将NameEncoding设为UTF8来规避。下面的工具类方法演示了如何从字节数组添加附件:
public static void AddAttachmentFromBytes(MailMessage msg, string name, byte[] data)
{
using (var stream = new System.IO.MemoryStream(data))
{
Attachment att = new Attachment(stream, name, System.Net.Mime.MediaTypeNames.Application.Octet);
att.NameEncoding = System.Text.Encoding.UTF8;
msg.Attachments.Add(att);
}
}
三、可复用的邮件发送工具类
为了提高代码可维护性,我们将上述逻辑封装为一个静态工具类,支持多收件人、多附件与异步发送。工具类内部统一读取配置项的服务器信息,并在发送失败时抛出包含状态码的异常,方便上层业务记录日志。对于大附件,建议设置SmtpClient.Timeout为较长值,例如30000毫秒,防止网络抖动引发中途断开。
下面的完整示例展示了如何在一个方法中完成收件人拆分、正文渲染与附件绑定。实际项目中可将SMTP账号信息放入appsettings.json,通过IConfiguration注入,但为保持示例简洁,这里采用硬编码演示核心流程。此工具类也演示了如何使用MailPriority设置邮件优先级。
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Threading.Tasks;
public static class EmailHelper
{
private static string SmtpHost = "smtp.ipipp.com";
private static int SmtpPort = 587;
private static string FromAddr = "notify@ipipp.com";
private static string FromPwd = "your_password";
public static async Task SendAsync(List<string> toList, string subject, string body, bool isHtml, List<Tuple<string, byte[]>> attachments)
{
using (MailMessage msg = new MailMessage())
{
msg.From = new MailAddress(FromAddr, "自动通知");
foreach (var item in toList)
{
if (!string.IsNullOrWhiteSpace(item))
msg.To.Add(item.Trim());
}
msg.Subject = subject;
msg.Body = body;
msg.IsBodyHtml = isHtml;
msg.Priority = MailPriority.Normal;
if (attachments != null)
{
foreach (var file in attachments)
{
var stream = new System.IO.MemoryStream(file.Item2);
Attachment att = new Attachment(stream, file.Item1, System.Net.Mime.MediaTypeNames.Application.Octet);
att.NameEncoding = System.Text.Encoding.UTF8;
msg.Attachments.Add(att);
}
}
using (SmtpClient client = new SmtpClient())
{
client.Host = SmtpHost;
client.Port = SmtpPort;
client.EnableSsl = true;
client.Timeout = 30000;
client.Credentials = new NetworkCredential(FromAddr, FromPwd);
await client.SendMailAsync(msg);
}
}
}
}
四、常见错误与避坑建议
第一个常见误区是认为SmtpClient是线程安全的。实际上官方文档明确指出一个SmtpClient实例不应被多个线程同时使用,因此在上述工具类中我们每次发送都new一个新的SmtpClient,并在using块中释放。第二个误区是忽略防火墙或云服务商对25端口的封锁,在云服务器上应优先使用587或465并开启SSL。
另一个容易被忽视的点是Dispose附件流。MailMessage在Dispose时会自动释放Attachment中的流,但如果我们提前关闭了外部传入的流,就会导致发送时读取失败。因此工具类内部采用MemoryStream并在using范围内由MailMessage接管生命周期,是最稳妥的做法。若发送失败,应捕获SmtpException并区分StatusCode,例如MailboxUnavailable代表收件人地址无效,可加入重试白名单逻辑。
五、总结
通过System.Net.Mail下的SmtpClient与MailMessage,C#开发者可以快速搭建起邮件发送能力,配合静态工具类能让业务代码保持整洁。虽然新项目更推荐MailKit以获得更好的协议兼容性与安全性,但理解原生SMTP发送机制仍有助于排查投递故障。只要注意附件编码、超时设置与凭据保护,带附件的邮件发送就能稳定服务于通知、报表推送等场景。