POP3 (Post Office Protocol v3) 埠號 110,純文字指令回應模式。核心流程:
+OKUSER username → +OKPASS password → +OKSTAT → +OK msgCount totalBytesQUIT → 斷線using System;
using System.IO;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
public class Pop3Client : IDisposable
{
private readonly string _host;
private readonly int _port;
private readonly string _user;
private readonly string _pass;
private TcpClient _client;
private NetworkStream _stream;
private StreamReader _reader;
private StreamWriter _writer;
private bool _connected;
public Pop3Client(string host, int port, string user, string pass)
{
_host = host;
_port = port;
_user = user;
_pass = pass;
}
public async Task ConnectAsync()
{
_client = new TcpClient();
await _client.ConnectAsync(_host, _port);
_stream = _client.GetStream();
_reader = new StreamReader(_stream, Encoding.ASCII);
_writer = new StreamWriter(_stream, Encoding.ASCII) { AutoFlush = true };
string welcome = await ReadResponseAsync();
if (!welcome.StartsWith("+OK")) throw new Exception($"連線失敗: {welcome}");
await SendCommandAsync($"USER {_user}");
string userResp = await ReadResponseAsync();
if (!userResp.StartsWith("+OK")) throw new Exception($"帳號錯誤: {userResp}");
await SendCommandAsync($"PASS {_pass}");
string passResp = await ReadResponseAsync();
if (!passResp.StartsWith("+OK")) throw new Exception($"密碼錯誤: {passResp}");
_connected = true;
}
public async Task GetMessageCountAsync()
{
EnsureConnected();
await SendCommandAsync("STAT");
string resp = await ReadResponseAsync();
// +OK 3 3456
if (!resp.StartsWith("+OK")) throw new Exception($"STAT 失敗: {resp}");
var parts = resp.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return int.Parse(parts[1]); // 郵件數
}
public async Task DisconnectAsync()
{
if (!_connected) return;
await SendCommandAsync("QUIT");
await ReadResponseAsync();
Dispose();
}
private async Task SendCommandAsync(string cmd)
{
await _writer.WriteLineAsync(cmd);
}
private async Task ReadResponseAsync()
{
return await _reader.ReadLineAsync() ?? string.Empty;
}
private void EnsureConnected()
{
if (!_connected) throw new InvalidOperationException("未連線");
}
public void Dispose()
{
_reader?.Dispose();
_writer?.Dispose();
_stream?.Dispose();
_client?.Dispose();
_connected = false;
}
}
// 使用範例
public static async Task Main()
{
var pop = new Pop3Client("pop.gmail.com", 995, "user@gmail.com", "app-password"); // SSL 需另行處理
try
{
await pop.ConnectAsync();
int count = await pop.GetMessageCountAsync();
Console.WriteLine($"新郵件數: {count}");
}
catch (Exception ex)
{
Console.WriteLine($"錯誤: {ex.Message}");
}
finally
{
await pop.DisconnectAsync();
}
}
| 原始碼問題 | 改進方案 |
|---|---|
同步阻塞 TcpClient | async/await 非阻塞,適配 UI / ASP.NET Core |
硬編碼編碼 ASCII.GetBytes | StreamReader/Writer(Encoding.ASCII) 統一管理 |
無錯誤碼解析,僅 InvalidOperationException | 依回應碼 +OK/-ERR 拋出具體異常 |
資源未釋放 (無 using/Dispose) | 實作 IDisposable,finally 確保釋放 |
| 單一職責違規 (連線+統計混在一起) | 分離 Connect, GetMessageCount, Disconnect |
| 不支援 SSL/TLS (現代郵件伺服器強制) | 下節說明 |
現代 POP3 (Gmail, Outlook, Yahoo) 皆強制 **SSL (995) 或 STARTTLS (110)**。
.NET 核心庫無內建 SslStream 包裝 POP3,需自行升級:
// ConnectAsync 內部修改
_client = new TcpClient();
await _client.ConnectAsync(_host, _port);
_stream = _client.GetStream();
if (_port == 995) // Implicit SSL
{
var ssl = new System.Net.Security.SslStream(_stream, false, (a,b,c,d) => true);
await ssl.AuthenticateAsClientAsync(_host);
_stream = ssl;
}
// 之後 _reader/_writer 包裝 _stream 即可
LIST / UIDL:取得郵件清單與唯一 ID。RETR n:下載第 n 封完整郵件 (需 MIME 解析,建議用 MimeKit)。DELE n:標記刪除 (需 QUIT 生效)。TOP n lines:只取標頭 + 前 N 行內文。// NuGet: MailKit (含 POP3/IMAP/SMTP, 完整 SSL, MIME 解析, 非同步)
using MailKit.Net.Pop3;
using MailKit;
using (var client = new Pop3Client())
{
await client.ConnectAsync("pop.gmail.com", 995, true); // true = SSL
await client.AuthenticateAsync("user", "pass");
int count = client.Count;
// var msg = client.GetMessage(0); // MimeMessage 物件
await client.DisconnectAsync(true);
}
手寫 Socket 適合學習協議、嵌入式環境或極度客製化需求;正式專案請使用 MailKit 或 OpenPop.NET,避免重造輪子並踩坑。