本文基於實際專案經驗,詳細記錄如何使用 Lumisoft.NET 組件建構一套功能完整的郵件發送工具,核心功能包含:
SmartThreadPool 管理工作佇列,支援暫停/繼續/停止、動態調整執行緒數。UI (WinForms)
│
├─ 發送參數收集 (發件人清單、收件人清單、郵件內容、附件、內嵌圖片、模式選擇)
│
├─ SmartThreadPool (STPStartInfo: MaxWorkerThreads=100, IdleTimeout=10s)
│ │
│ ├─ WorkItemsProducer Thread → 依收件人數量產生 SendJobInfo → QueueWorkItem(DoWork)
│ │
│ └─ Worker Threads → DoWork(SendJobInfo)
│ │
│ ├─ 直投模式 → SMTP_Client.QuickSend(Mail_Message)
│ │
│ └─ SMTP 模式 → SMTP_Client 逐指令 (Connect → Auth → MailFrom → RcptTo → SendMessage → Disconnect)
│
├─ TimerHelper (間隔 N 分鐘) → 檢查失敗次數 → RasDial 重撥號 → 重置計數
│
└─ 狀態回呼 (ShowSendStatus) → UI 更新成功/失敗/剩餘計數
private SmartThreadPool _smartThreadPool;
private IWorkItemsGroup _workItemsGroup;
private Thread _workItemsProducerThread;
private TimerHelper _timer;
private int _workItemsGenerated, _workItemsCompleted;
private void btnSend_Click(object sender, EventArgs e)
{
// 重置計數器
_workItemsGenerated = _workItemsCompleted = 0;
FailedItems = SuccessItems = 0;
// 初始化執行緒池
var stpInfo = new STPStartInfo
{
IdleTimeout = 10,
MaxWorkerThreads = 100,
MinWorkerThreads = 0
};
_smartThreadPool = new SmartThreadPool(stpInfo);
_workItemsGroup = _smartThreadPool;
// 啟動生產者執行緒
_workItemsProducerThread = new Thread(WorkItemsProducer) { IsBackground = true };
_workItemsProducerThread.Start();
// 啟動定時重撥號 (若設定)
int intervalMs = SystemConfig.Default.IntervalRedial * 60 * 1000;
if (intervalMs > 0)
{
_timer?.Stop(); _timer?.Dispose();
_timer = new TimerHelper(intervalMs, false);
_timer.Execute += Timer_Execute;
_timer.Start();
}
}
private void WorkItemsProducer()
{
InvokeUI(() => { txtSendDetail.Clear(); EnableControls(false); });
var addresses = GetAddressList(); // 收件人清單
var mailInfos = GetMailInfo(); // 郵件內容變體 (支援隨機混淆)
for (int i = 0; i < addresses.Count; i++)
{
var job = new SendJobInfo
{
DomainList = _mailDomainList, // SMTP 帳號池 (輪詢)
MailTo = addresses[i],
MailInfo = GetOneMail(mailInfos, i), // 取得對應內容
ShowSendStatus = ShowSendStatus,
CurrentDomain = i % _mailDomainList.Count,
UseDirectSendType = SystemConfig.Default.EmailDirectSend,
MailFromDisplay = string.IsNullOrEmpty(SystemConfig.Default.UserEmailFrom)
? SystemConfig.Default.DefaultFromDisplayName
: SystemConfig.Default.UserEmailFrom
};
_workItemsGroup.QueueWorkItem(DoWork, job);
Interlocked.Increment(ref _workItemsGenerated);
Thread.Sleep(100); // 避免瞬間湧入過多任務
}
// 等待所有工作完成
_workItemsGroup.WaitForIdle();
_smartThreadPool.Shutdown();
InvokeUI(UpdateFinishStatus);
}
private void DoWork(object state)
{
var job = (SendJobInfo)state;
bool success = false;
try
{
if (job.UseDirectSendType)
{
// === 直投模式:無需 SMTP 帳號,直接連收件者 MX ===
var msg = CreateMailMessage(job.MailTo, job.MailFromDisplay, job.MailInfo);
SMTP_Client.QuickSend(msg); // 內部會解析 MX、嘗試遞送
success = true;
}
else
{
// === SMTP 認證模式 ===
var domain = job.DomainList[job.CurrentDomain];
using (var client = new SMTP_Client())
{
int port = domain.Ssl ? WellKnownPorts.SMTP_SSL : WellKnownPorts.SMTP;
if (domain.Port > 0) port = domain.Port;
client.Connect(domain.SmtpServer, port, domain.Ssl);
client.EhloHelo(domain.SmtpServer);
client.Auth(new AUTH_SASL_Client_Plain(domain.Username, domain.Password));
client.MailFrom(job.MailInfo.From, -1);
client.RcptTo(job.MailTo);
using (var ms = CreateMimeStream(job.MailTo, job.MailFromDisplay, job.MailInfo))
{
client.SendMessage(ms);
}
client.Disconnect();
success = true;
}
}
}
catch (Exception ex)
{
LogTextHelper.Error(ex);
success = false;
}
finally
{
Interlocked.Increment(ref _workItemsCompleted);
job.ShowSendStatus?.Invoke(success, job.MailTo, job.MailInfo.Subject);
}
}
private Mail_Message CreateMailMessage(string to, string fromDisplay, MailContentInfo content)
{
var msg = new Mail_Message
{
MimeVersion = "1.0",
MessageID = MIME_Utils.CreateMessageID(),
Date = DateTime.Now,
Subject = content.Title
};
msg.From = new Mail_t_MailboxList { new Mail_t_Mailbox(fromDisplay, content.From) };
msg.To = new Mail_t_AddressList { new Mail_t_Mailbox(to, to) };
// 回執通知
if (!string.IsNullOrEmpty(SystemConfig.Default.DispositionNotificationTo))
msg.DispositionNotificationTo.Add(new Mail_t_Mailbox(SystemConfig.Default.DispositionNotificationTo, SystemConfig.Default.DispositionNotificationTo));
// === multipart/mixed (最外層) ===
var mixedCt = new MIME_h_ContentType(MIME_MediaTypes.Multipart.mixed);
mixedCt.Param_Boundary = Guid.NewGuid().ToString("N");
var mixed = new MIME_b_MultipartMixed(mixedCt);
msg.Body = mixed;
// === multipart/alternative (HTML + Plain) ===
var altCt = new MIME_h_ContentType(MIME_MediaTypes.Multipart.alternative);
altCt.Param_Boundary = Guid.NewGuid().ToString("N");
var alt = new MIME_b_MultipartAlternative(altCt);
mixed.BodyParts.Add(new MIME_Entity { Body = alt });
// Plain text
var plainEntity = new MIME_Entity { Body = new MIME_b_Text(MIME_MediaTypes.Text.plain) };
string plainText = SystemConfig.Default.PlaintTextTips ?? "請使用支援 HTML 的郵件用戶端檢視。";
plainEntity.Body.SetText(MIME_TransferEncodings.QuotedPrintable, Encoding.UTF8, plainText);
alt.BodyParts.Add(plainEntity);
// HTML
var htmlEntity = new MIME_Entity { Body = new MIME_b_Text(MIME_MediaTypes.Text.html) };
htmlEntity.Body.SetText(MIME_TransferEncodings.QuotedPrintable, Encoding.UTF8, content.Content);
alt.BodyParts.Add(htmlEntity);
// 附件
foreach (var att in content.Attachments)
mixed.BodyParts.Add(Mail_Message.CreateAttachment(att));
// 內嵌圖片
foreach (var img in content.EmbedImages)
{
var imgEntity = new MIME_Entity
{
ContentDisposition = new MIME_h_ContentDisposition(MIME_DispositionTypes.Inline),
ContentID = BytesTools.BytesToHex(Encoding.Default.GetBytes(Path.GetFileName(img)))
};
var imgBody = new MIME_b_Image(MIME_MediaTypes.Image.jpeg);
imgEntity.Body = imgBody;
imgBody.SetDataFromFile(img, MIME_TransferEncodings.Base64);
mixed.BodyParts.Add(imgEntity);
}
return msg;
}
// SMTP 模式需回傳 MemoryStream
private MemoryStream CreateMimeStream(string to, string fromDisplay, MailContentInfo content)
{
var m = new Mime();
var main = m.MainEntity;
main.From = new AddressList { new MailboxAddress(fromDisplay, content.From) };
main.To = new AddressList { new MailboxAddress(to, to) };
main.Subject = content.Title;
main.ContentType = MediaType_enum.Multipart_mixed;
// HTML 內容
var htmlEntity = main.ChildEntities.Add();
htmlEntity.ContentType = MediaType_enum.Text_html;
htmlEntity.ContentTransferEncoding = ContentTransferEncoding_enum.QuotedPrintable;
htmlEntity.DataText = content.Content;
// 附件
foreach (var att in content.Attachments)
{
var attEntity = main.ChildEntities.Add();
attEntity.ContentType = MediaType_enum.Application_octet_stream;
attEntity.ContentDisposition = ContentDisposition_enum.Attachment;
attEntity.ContentTransferEncoding = ContentTransferEncoding_enum.Base64;
attEntity.ContentDisposition_FileName = Path.GetFileName(att);
attEntity.DataFromFile(att);
}
var ms = new MemoryStream();
m.ToStream(ms);
ms.Position = 0;
return ms;
}
// 儲存為 .eml
private void btnCreateEml_Click(object sender, EventArgs e)
{
var msg = CreateMailMessage("test@example.com", "測試寄件者", GetTestContent());
string dir = Path.Combine(Application.StartupPath, "Eml");
Directory.CreateDirectory(dir);
string file = Path.Combine(dir, $"{DateTime.Now:yyyyMMddHHmmssfff}.eml");
msg.ToFile(file);
MessageBox.Show($"EML 已儲存:{file}");
}
// 載入 .eml 並發送
private void btnSendEml_Click(object sender, EventArgs e)
{
string dir = Path.Combine(Application.StartupPath, "Eml");
var files = Directory.GetFiles(dir, "*.eml");
if (files.Length == 0) return;
using (var client = new SMTP_Client())
{
client.Connect("smtp.example.com", 587, false);
client.Auth(new AUTH_SASL_Client_Plain("user", "pwd"));
client.MailFrom("from@example.com", -1);
client.RcptTo("to@example.com");
var msg = Mail_Message.ParseFromFile(files[0]);
using (var ms = new MemoryStream())
{
msg.ToStream(ms, new MIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8), Encoding.UTF8);
ms.Position = 0;
client.SendMessage(ms);
}
client.Disconnect();
}
MessageBox.Show("EML 發送完成");
}
private void btnStop_Click(object sender, EventArgs e)
{
try
{
_smartThreadPool?.Shutdown();
_smartThreadPool?.Dispose();
_smartThreadPool = null;
_timer?.Stop();
_timer?.Dispose();
_timer = null;
}
catch (Exception ex) { LogTextHelper.Error(ex); }
finally { UpdateFinishStatus(); }
}
| 議題 | 經驗教訓 | 建議做法 |
|---|---|---|
| 直投模式送達率低 | 許多 ISP 封鎖動態 IP 直投,郵件易進垃圾箱或被拒收 | 僅用於測試/內網;正式環境務必使用信譽良好的 SMTP 服務 (SendGrid, Mailgun, 企業郵箱) |
| 多線程併發過高觸發限流 | 同一 IP 短時間大量連線會被 SMTP 伺服器暫時封鎖 (421/450 錯誤) | 限制 MaxWorkerThreads ≤ 10;加入隨機延遲;實作指數退避重試 |
| 附件過大導致 OOM | 一次讀取 50MB+ 附件進 MemoryStream 造成大物件堆積 | 改用 FileStream 分塊讀取,或限制單信附件總大小 ≤ 20MB |
| 編碼亂碼 (中文檔名/內容) | MIME 標頭需 RFC 2047 編碼;Lumisoft 預設 UTF-8 但檔名可能為 Big5 | 統一使用 MIME_Encoding_EncodedWord(MIME_EncodedWordEncoding.Q, Encoding.UTF8);檔名先 URL Encode |
| SmartThreadPool 非標準庫 | 專案相依第三方 DLL,維護風險 | 新專案改用 System.Threading.Tasks.Dataflow 或 Channels 內建非同步管線 |
| WinForms UI 凍結 | 長時間工作未正確回報 UI 執行緒 | 所有 UI 更新必須透過 Control.Invoke 或 SynchronizationContext.Post |
IMailSender { Task SendAsync(MailMessage msg); }。LumisoftMailSender (現有) → MailKitMailSender (新) → SmtpClientMailSender (簡單場景)。BackgroundService / Worker Service,配合 RabbitMQ/Kafka 佇列,脫離 WinForms 桌面限制。Lumisoft.NET 提供極為靈活的 MIME 組裝與 SMTP/POP3/IMAP 協定控制,適合需要深度定製郵件結構、EML 檔案流轉、直投實驗 的場景。但在高併發、跨平台、現代認證 (OAuth2) 等新需求下,建議漸進式遷移至 MailKit (MimeKit),並將發送邏輯服務化、容器化,獲得更穩健的維運體驗。