必须使用 MailMessage 实例并调用 Attachments.Add() 添加附件,SmtpClient.Send() 字符串重载已废弃且不支持附件;需手动管理 Attachment 和 MailMessage 的释放,注意流状态与超时设置。

SmtpClient 发送带附件的邮件必须手动创建 MailMessage 并添加 Attachment
直接调用 SmtpClient.Send() 传字符串发不了附件——它只接受 MailMessage 实例。附件不是“附在字符串后面”,而是作为独立对象挂载到 MailMessage.Attachments 集合里。
常见错误现象:SmtpClient.Send("to@example.com", "from@example.com", "subject", "body") 这种重载早已被标记为 Obsolete,.NET 6+ 编译直接报错;即便低版本能跑,也完全不支持附件。
- 必须 new 一个
MailMessage,设置To、From、Subject、Body - 每个附件需 new
Attachment(支持string路径或Stream),再用message.Attachments.Add() - 附件路径若含中文或空格,
Attachment构造函数内部会自动处理,不用额外编码 - 别忘了在
using块里释放Attachment和MailMessage,否则文件句柄可能被锁住
附件流没关闭导致发送失败或文件损坏
Attachment 接收 Stream 时,SmtpClient 不负责关闭它——你关,它才关。很多代码把 FileStream 直接传进去就完事,结果发送中途抛 ObjectDisposedException 或收到的附件打不开。
使用场景:从数据库读取二进制内容、内存中生成 PDF、动态压缩多个文件后发 ZIP。
- 用
new Attachment(stream, contentType)后,确保该stream在SmtpClient.Send()返回前仍处于打开状态 - 更稳妥做法:用
using var stream = File.OpenRead(path);包裹整个发送逻辑,而不是只包Attachment创建那一行 - 如果传的是
MemoryStream,记得.Position = 0,否则附件内容为空
.NET 6+ 中 SmtpClient 已过时,但还没删,替代方案不是简单换类名
官方标记 SmtpClient 为 Obsolete 是因为它的设计无法很好支持现代认证(如 OAuth2)、扩展性差、API 同步阻塞且难测试——但目前没有内置的“官方替代品”。别急着搜 “.NET 6 SMTP 替代方案” 就去装第三方包。
性能 / 兼容性影响:旧代码在 .NET 6/7/8 仍可运行,只是编译警告;但若用到 EnableSsl = true + 端口 587,部分邮箱(如 Outlook)已拒绝连接,因 TLS 协商方式不兼容。
- 短期维持:保留
SmtpClient,但显式指定DeliveryMethod.Network,禁用UseDefaultCredentials,改用Credentials赋值NetworkCredential - 长期迁移:优先考虑
MailKit(SmtpClient类名一样但完全重写),它支持 OAuth2、异步 API、更细粒度错误反馈 - 别踩坑:不要试图用
HttpClient自己拼 SMTP 协议——没意义,也做不到
附件过大或数量多时,SmtpClient.Timeout 默认值根本不够用
默认 Timeout 是 100000 毫秒(100 秒),看似很长。但上传 10MB 附件走家庭宽带,加上服务器响应延迟,很容易超时。此时抛出的异常是 SmtpException,InnerException 是 IOException,错误信息里带 "The operation has timed out",容易误判为网络问题。
- 务必在
SmtpClient实例化后立即设置client.Timeout = 5 * 60 * 1000;(5 分钟),尤其当附件总大小 > 2MB - 不要依赖
App.config或web.config的system.net/mailSettings,它不控制Timeout - 如果发多个邮件带附件,建议每个
SmtpClient实例只发一次,然后丢弃——复用实例在并发下可能状态混乱
事情说清了就结束。真正麻烦的从来不是“怎么加附件”,而是附件来源多样、网络不可控、目标邮箱策略各异——这些没法靠一个 Attach() 调用解决。










