Lumisoft.NET 不僅支援 SMTP 發信,其 POP3_Client 類別提供完整的 POP3 協定實作,適合建立郵件接收服務、自動化處理郵件機器人、備份郵件至資料庫等場景。本文完整示範從連線登入、列舉信件、解析頭部/正文/附件、以 UID 去重、到標記刪除的端到端流程,並針對常見編碼亂碼、日期解析失敗提供修正方案。
public List<ReceivedMail> ReceiveMails(Pop3Setting setting, List<string> existingUids)
{
var result = new List<ReceivedMail>>();
using (var client = new POP3_Client())
{
// 選擇性啟用通訊記錄 (除錯用)
client.Logger = new Logger { WriteLog = (s, e) => Debug.WriteLine(e.LogEntry.Text) };
client.Connect(setting.Server, setting.Port, setting.UseSsl);
client.Login(setting.UserName, setting.Password);
foreach (var msg in client.Messages)
{
// 1. 先只下載頭部 (輕量、快速)
var headerBytes = msg.HeaderToByte();
var mimeHeader = Mail_Message.ParseFromByte(headerBytes);
// 2. UID 去重
string uid = msg.UID;
if (existingUids.Contains(uid)) continue; // 已處理過
// 3. 解析頭部關鍵欄位 (含編碼修正)
var mail = new ReceivedMail
{
Uid = uid,
Subject = DecodeMimeHeader(mimeHeader.Subject),
From = ParseMailAddress(mimeHeader.From),
To = ParseMailAddresses(mimeHeader.To),
Cc = ParseMailAddresses(mimeHeader.Cc),
Date = SafeParseDate(mimeHeader.Date), // 見下方修正方法
Size = msg.Size
};
// 4. 需要正文/附件時,再下載完整訊息
if (NeedBodyOrAttachments)
{
var fullBytes = msg.MessageToByte();
var mimeFull = Mail_Message.ParseFromByte(fullBytes);
// 正文:優先 HTML,退而求其次 Plain Text
mail.BodyHtml = mimeFull.BodyHtmlText;
mail.BodyText = mimeFull.BodyText;
if (string.IsNullOrEmpty(mail.BodyHtml)) mail.BodyHtml = mail.BodyText;
// 附件與內嵌圖片分離
foreach (var entity in mimeFull.GetAttachments(true, true))
{
if (entity.ContentDisposition == null) continue;
string fileName = entity.ContentDisposition.Param_FileName;
if (string.IsNullOrEmpty(fileName)) continue;
var bytePart = entity.Body as MIME_b_SinglepartBase;
byte[] data = bytePart?.Data;
bool isInline = entity.ContentDisposition.DispositionType == MIME_DispositionTypes.Inline;
if (isInline)
mail.InlineImages.Add(new MailAttachment { FileName = fileName, Data = data, ContentId = entity.ContentID });
else
mail.Attachments.Add(new MailAttachment { FileName = fileName, Data = data });
}
}
result.Add(mail);
}
}
return result;
}
private string DecodeMimeHeader(string encoded)
{
if (string.IsNullOrEmpty(encoded)) return encoded;
// Lumisoft ParseFromByte 預設用 UTF-8 解碼,若原始為 Big5/GB2312 會產生亂碼
// 策略:先取得原始 Header 字串,再以 Default 編碼轉 Byte,再以 UTF-8 重解
// 這裡提供通用 Regex 解碼備援
var regex = new Regex(@"=\?(?<charset>[^?]+)\?(?<enc>[BQ])\?(?<data>[^?]+)\?=", RegexOptions.IgnoreCase);
return regex.Replace(encoded, m =>
{
string charset = m.Groups["charset"].Value.ToLower();
string enc = m.Groups["enc"].Value;
string data = m.Groups["data"].Value;
// 正規化字集名稱
if (charset.Contains("gb2312") || charset.Contains("gbk") || charset.Contains("euc-cn") || charset.Contains("euccn"))
charset = "gb2312";
else if (charset.Contains("utf8") || charset.Contains("utf-8"))
charset = "utf-8";
else if (charset.Contains("big5"))
charset = "big5";
try
{
byte[] bytes = enc == "B" ? Convert.FromBase64String(data) : DecodeQuotedPrintable(data);
return Encoding.GetEncoding(charset).GetString(bytes);
}
catch { return m.Value; } // 解碼失敗保留原文
});
}
private byte[] DecodeQuotedPrintable(string input)
{
using (var ms = new MemoryStream())
{
for (int i = 0; i < input.Length; i++)
{
if (input[i] == '=' && i + 2 < input.Length)
{
string hex = input.Substring(i + 1, 2);
if (byte.TryParse(hex, System.Globalization.NumberStyles.HexNumber, null, out byte b))
{
ms.WriteByte(b);
i += 2;
continue;
}
}
ms.WriteByte((byte)input[i]);
}
return ms.ToArray();
}
}
private DateTime SafeParseDate(DateTime? lumiDate)
{
if (lumiDate.HasValue && lumiDate.Value != DateTime.MinValue) return lumiDate.Value;
// 若 Lumisoft 解析失敗,嘗試從原始 Header 字串自行解析
// 這裡需配合上層傳入原始 Header 字串,或在 ParseFromByte 前預處理
// 範例:若原始 Date 標頭為 "2023-05-06 19:01:44",DateTime.TryParse 即可成功
return DateTime.MinValue; // 實務上應傳入原始字串再嘗試
}
// 建議在呼叫 ParseFromByte 前預處理 Header 字串:
// byte[] utf8Bytes = Encoding.UTF8.GetBytes(rawHeaderString);
// var mime = Mail_Message.ParseFromByte(utf8Bytes); // 強制以 UTF-8 解碼標頭,避免預設編碼錯誤
private string ParseMailAddress(Mail_t_MailboxList list)
{
if (list == null || list.Count == 0) return string.Empty;
var mb = list[0];
return string.IsNullOrEmpty(mb.DisplayName) ? mb.Address : $"{mb.DisplayName} <{mb.Address}>";
}
private string ParseMailAddresses(Mail_t_AddressList list)
{
if (list == null) return string.Empty;
var sb = new StringBuilder();
foreach (var mb in list.Mailboxes)
{
if (sb.Length > 0) sb.Append("; ");
sb.Append(string.IsNullOrEmpty(mb.DisplayName) ? mb.Address : $"{mb.DisplayName} <{mb.Address}>");
}
return sb.ToString();
}
POP3 協定採用「標記刪除 + QUIT 時真正刪除」機制。Lumisoft 封裝為 MarkForDeletion():
public void DeleteMails(Pop3Setting setting, List<string> uidsToDelete)
{
using (var client = new POP3_Client())
{
client.Connect(setting.Server, setting.Port, setting.UseSsl);
client.Login(setting.UserName, setting.Password);
foreach (var msg in client.Messages)
{
if (uidsToDelete.Contains(msg.UID))
{
try
{
msg.MarkForDeletion(); // 標記
}
catch (Exception ex)
{
Log.Error($"刪除 UID {msg.UID} 失敗: {ex.Message}");
}
}
}
// 離開 using 區塊時,Dispose 會自動發送 QUIT,伺服器才真正刪除
}
}
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 中文主旨/寄件者顯示亂碼 | MIME 標頭採用 Base64/QP 編碼,字集非 UTF-8 (如 Big5/GB2312),Lumisoft 預設 UTF-8 解碼 | 使用 DecodeMimeHeader 正規化字集後手動解碼;或預處理 Header 字串以 UTF-8 強制解碼 |
| Date 解析拋出 ParseException | 郵件 Date 標頭不符 RFC2822 (如 "2023-05-06 19:01:44") | 修改 Lumisoft 原始碼 Mail_Message.Date 屬性加入 DateTime.TryParse 容錯;或預處理 Header |
| 附件檔名亂碼 | Content-Disposition filename* 參數編碼 (RFC 5987) 或舊式 encoded-word | 優先讀取 filename* (UTF-8');否則對 filename 做 DecodeMimeHeader |
| 大量郵件下載記憶體飆升 | 一次 MessageToByte() 載入完整郵件 (含大附件) | 先用 HeaderToByte() 篩選;需下載附件時改用串流分塊讀取 (Lumisoft 目前無內建串流 API,建議改用 MailKit) |
| UID 重複導致重複處理 | POP3 UID 僅在單一 Session 內唯一,跨 Session 可能重用 | 結合 UID + 郵件大小 + Date + Subject 產生複合鍵;或改用 IMAP (支援永久 UID) |
| SSL/TLS 連線失敗 | Port 與 UseSsl 不匹配 (如 Port 110 卻設 UseSsl=true) | 參考常見設定:Gmail 995/SSL、Outlook 995/SSL、企業郵箱 110/STARTTLS (Port 110, UseSsl=false, 需手動 STLS 指令,Lumisoft 需自行實作) |
client.Messages 列舉時已自動下載頭部,不需額外呼叫。MarkForDeletion,最後一次 QUIT 提交。POP3_Client 實例,避免重複 TCP/SSL 握手。Task.Run 或遷移至 MailKit 獲得原生 async/await。IMAP (含 IDLE 推播)、OAuth2、S/MIME、OpenPGP。async/await、CancellationToken、IAsyncEnumerable。System.Drawing 依賴。Lumisoft.NET 的 POP3_Client 提供功能完整的 POP3 客戶端實作,配合本文提供的編碼修正、日期容錯、UID 去重、附件分離等技巧,可快速建構郵件接收處理管線。但受限於同步 API、編碼處理不夠健全、缺乏 IMAP IDLE/OAuth2 等現代功能,長期專案強烈建議遷移至 MailKit,並在架構層面抽象 IMailReceiver 介面以平滑切換。