首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

Lumisoft.NET 郵件發送完整實作指南:直投模式與 SMTP 認證模式雙軌並行、多線程批量發送架構

     編輯:ASP.NET 基礎

專案背景與功能概覽

本文基於實際專案經驗,詳細記錄如何使用 Lumisoft.NET 組件建構一套功能完整的郵件發送工具,核心功能包含:

  • 雙發送模式:直投模式 (無需 SMTP 帳號,直接連線收件者 MX 伺服器) 與 SMTP 認證模式 (傳統帳密登入)。
  • 多線程批量發送:採用 SmartThreadPool 管理工作佇列,支援暫停/繼續/停止、動態調整執行緒數。
  • 自動重撥號:針對 ADSL/撥號環境,定時檢查連線並自動重撥。
  • 完整日誌與狀態追蹤:成功/失敗計數、即時顯示發送進度。
  • EML 檔案儲存/載入發送:支援郵件序列化為 .eml 檔,利於離線編輯、審核、重發。

核心架構圖解

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 更新成功/失敗/剩餘計數

關鍵程式碼實作

1. 多線程生產者-消費者啟動邏輯

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);
}

2. 核心發送邏輯:直投 vs SMTP 認證

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);
    }
}

3. 郵件訊息建構:支援 HTML/Plain/附件/內嵌圖片/回執

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;
}

4. EML 檔案儲存與載入發送

// 儲存為 .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 發送完成");
}

5. 停止發送與資源釋放

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

現代化遷移路徑建議

  1. 介面抽象:定義 IMailSender { Task SendAsync(MailMessage msg); }。
  2. 實作切換:LumisoftMailSender (現有) → MailKitMailSender (新) → SmtpClientMailSender (簡單場景)。
  3. 背景服務化:將發送邏輯移至 ASP.NET Core BackgroundService / Worker Service,配合 RabbitMQ/Kafka 佇列,脫離 WinForms 桌面限制。
  4. 可觀測性:引入 Serilog + OpenTelemetry,記錄發送耗時、狀態碼、重試次數,接入 Grafana/Prometheus。

結語

Lumisoft.NET 提供極為靈活的 MIME 組裝與 SMTP/POP3/IMAP 協定控制,適合需要深度定製郵件結構、EML 檔案流轉、直投實驗 的場景。但在高併發、跨平台、現代認證 (OAuth2) 等新需求下,建議漸進式遷移至 MailKit (MimeKit),並將發送邏輯服務化、容器化,獲得更穩健的維運體驗。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved