.NET 內建 System.Net.Mail.SmtpClient 雖然方便,但在某些舊版框架、特殊伺服器需求、或需深度控制 MIME 結構(如自定義 Boundary、優先序、多重替代內容)時,自行實作 SMTP 協定能獲得最大彈性。本文提供一個 零依賴、同步阻塞、支援 ESMTP AUTH LOGIN、多附件、HTML/Plain Text 替代內容 的 MailSender 元件完整原始碼,並解析關鍵協定細節。
namespace JcPersonal.Utility
{
public class MailSender
{
// 屬性:Server, Port, UserName, PassWord, From, To, FromName, ToName,
// Subject, Body, HtmlBody, IsHtml, LanguageEncoding, MailEncoding, Priority
// 集合:Attachments (IList)
public void SendMail() { /* 核心流程 */ }
// 內部方法:WriteString, ReadString, Command, ToBase64
}
public struct AttachmentInfo
{
public string FileName { get; set; }
public string Bytes { get; set; } // Base64 編碼內容
// 多重建構子:路徑、Stream、byte[]
}
}
TcpClient(server, port),預設 25 埠。AUTH LOGIN → 334--unique-boundary-1 (外層 mixed)--unique-boundary-2 (內層 alternative: text/plain + text/html)--unique-boundary-2-- 結束 alternative--unique-boundary-1 + 標頭 + Base64 內容)--unique-boundary-1-- 結束 mixed. 結束 DATA → 250
protected bool Command(NetworkStream ns, string cmd, string expectedCode)
{
WriteString(ns, cmd);
string resp = ReadString(ns);
return resp.IndexOf(expectedCode) != -1;
}
protected void WriteString(NetworkStream ns, string str)
{
str += "\r\n";
byte[] buf = Encoding.GetEncoding(LanguageEncoding).GetBytes(str);
const int chunk = 75;
for (int i = 0; i < buf.Length; i += chunk)
ns.Write(buf, i, Math.Min(chunk, buf.Length - i));
}
foreach (AttachmentInfo att in Attachments)
{
WriteString(ns, "--unique-boundary-1");
WriteString(ns, $"Content-Type: application/octet-stream; name=\"{att.FileName}\"");
WriteString(ns, "Content-Transfer-Encoding: base64");
WriteString(ns, $"Content-Disposition: attachment; filename=\"{att.FileName}\"");
WriteString(ns, "");
WriteString(ns, att.Bytes); // 已為 Base64 字串
WriteString(ns, "");
}
public AttachmentInfo(string path) // 從檔案
{
FileName = Path.GetFileName(path);
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
byte[] raw = new byte[fs.Length];
fs.Read(raw, 0, raw.Length);
Bytes = Convert.ToBase64String(raw);
}
}
public AttachmentInfo(string name, Stream stream) // 從記憶體流
{
FileName = Path.GetFileName(name);
byte[] raw = new byte[stream.Length];
stream.Read(raw, 0, raw.Length);
Bytes = Convert.ToBase64String(raw);
}
public AttachmentInfo(string name, byte[] raw) // 從位元組陣列
{
FileName = Path.GetFileName(name);
Bytes = Convert.ToBase64String(raw);
}
var mail = new MailSender
{
Server = "smtp.gmail.com",
Port = 587, // 或 25, 465 (SSL 需自行在 TcpClient 上包裝 SslStream)
UserName = "your@gmail.com",
PassWord = "app-password",
From = "your@gmail.com",
FromName = "您的名字",
To = "target@example.com",
ToName = "收件人",
Subject = "測試 ESMTP 元件",
Body = "這是純文字內容",
HtmlBody = "這是 HTML 內容
支援 粗體。
",
IsHtml = true,
LanguageEncoding = "UTF-8",
Priority = 1 // 高優先序
};
mail.Attachments.Add(new MailSender.AttachmentInfo(@"C:\report.pdf"));
mail.Attachments.Add(new MailSender.AttachmentInfo("logo.png", File.OpenRead(@"C:\logo.png")));
try
{
mail.SendMail();
Console.WriteLine("發送成功");
}
catch (Exception ex)
{
Console.WriteLine($"失敗: {ex.Message}");
}
| 限制 | 解決方案 |
|---|---|
| 同步阻塞 | 改為 async/await + NetworkStream.ReadAsync/WriteAsync |
| 無 SSL/TLS (STARTTLS) | 在 EHLO 後檢查 250-STARTTLS,啟動 SslStream 升級連線 |
| 單一收件人 | To 屬性改為 List,RCPT TO 迴圈發送 |
| 無 CC/BCC | 新增 Cc/Bcc 集合,標頭加入 Cc:,RCPT 階段同步發送 |
| 大附件記憶體壓力 | 附件改為串流式分塊 Base64 編碼寫入,避免一次載入完整 byte[] |
| 錯誤處理過於簡略 | 拋出自訂 SmtpException 包含狀態碼、伺服器回應、階段資訊 |
MailSender.cs:主類別 + AttachmentInfo 結構。System.Net.Mail,建議以介面 IMailSender 抽象,雙實作並存,便於單元測試 Mock。這個元件雖然樸實,但完整展示了 SMTP/ESMTP 協定核心流程、MIME 多部分結構組裝、Base64 編碼與網路串流操作,是理解「郵件如何送達」的最佳教學級程式碼,也是受限環境下無法引用高階庫時的實用備選方案。