220 就緒。HELO/EHLO domain → 250。MAIL FROM: → 250。RCPT TO: (可多次) → 250。DATA → 354 進入資料模式。\r\n.\r\n 結束 → 250。QUIT → 221 關閉。
// SMTP.h
#pragma once
#include // CSocket
class CSMTP
{
public:
CSMTP();
virtual ~CSMTP();
// 連線管理
BOOL Connect(CString strHost, CString strLocalName = _T("localhost"));
BOOL Connect(); // 使用 SetHost/SetFrom 預設值
BOOL Disconnect();
// 寄件者/收件者
void SetFrom(CString strFrom); CString GetFrom();
BOOL SetTo(CString strTo); CString GetTo(); // 支援多收件者內部用陣列
void AddTo(CString strTo); // 追加收件者
// 郵件內容
BOOL Data(CString strSubject, CString strBody); // 組裝並發送 DATA
// 低階命令 (進階用途)
BOOL Hello(); // EHLO
BOOL Mail(); // MAIL FROM
BOOL Rcpt(); // RCPT TO
BOOL Quit();
// 錯誤處理
CString GetErrorMessage() const;
private:
// 內部狀態
CSocket m_socket;
CString m_strHost;
CString m_strFrom;
CStringArray m_arrTo; // 多收件者
CString m_strErrorMsg;
int m_nState; // 狀態機: CONNECTED, HELO_DONE, MAIL_DONE, RCPT_DONE, DATA_DONE
// 輔助
BOOL SendCommand(LPCSTR pszCmd, CString& strResponse);
BOOL CheckResponse(int nExpectedCode, const CString& strResponse);
CString FormatAddress(const CString& strAddr); // 加 < >
};
BOOL CSMTP::Connect(CString strHost, CString strLocalName)
{
m_strHost = strHost;
m_strErrorMsg.Empty();
m_nState = 0;
if (!m_socket.Create()) { m_strErrorMsg = _T("Create socket failed"); return FALSE; }
if (!m_socket.Connect(strHost, 25)) { m_strErrorMsg = _T("Connect failed: ") + m_strHost; return FALSE; }
CString resp;
if (!ReadResponse(resp) || !CheckResponse(220, resp)) return FALSE;
m_nState = 1; // CONNECTED
return Hello(); // 發送 EHLO
}
BOOL CSMTP::ReadResponse(CString& strResp)
{
strResp.Empty();
char buf[1024];
while (true) {
int n = m_socket.Receive(buf, sizeof(buf)-1, 0);
if (n <= 0) return FALSE;
buf[n] = '\0';
strResp += CA2T(buf);
// SMTP 回應以 "XXX " (代碼+空格) 結尾為完整行,或最後一行 "XXX\r\n"
if (strResp.GetLength() >= 4 && strResp[3] == ' ') break; // 簡化判斷
}
return TRUE;
}
BOOL CSMTP::SendCommand(LPCSTR pszCmd, CString& strResp)
{
CString cmd(pszCmd); cmd += _T("\r\n");
if (m_socket.Send(cmd, cmd.GetLength()) == SOCKET_ERROR) return FALSE;
return ReadResponse(strResp);
}
BOOL CSMTP::CheckResponse(int nExpected, const CString& resp)
{
if (resp.GetLength() < 3) return FALSE;
int code = _ttoi(resp.Left(3));
if (code != nExpected) {
m_strErrorMsg.Format(_T("SMTP Error %d (expected %d): %s"), code, nExpected, resp);
return FALSE;
}
return TRUE;
}
BOOL CSMTP::Hello()
{
CString cmd; cmd.Format(_T("EHLO %s\r\n"), m_strLocalName);
CString resp;
if (!SendCommand(cmd, resp) || !CheckResponse(250, resp)) return FALSE;
m_nState = 2; return TRUE;
}
BOOL CSMTP::Mail()
{
CString cmd; cmd.Format(_T("MAIL FROM:<%s>\r\n"), m_strFrom);
CString resp;
if (!SendCommand(cmd, resp) || !CheckResponse(250, resp)) return FALSE;
m_nState = 3; return TRUE;
}
BOOL CSMTP::Rcpt()
{
for (int i = 0; i < m_arrTo.GetSize(); ++i) {
CString cmd; cmd.Format(_T("RCPT TO:<%s>\r\n"), m_arrTo[i]);
CString resp;
if (!SendCommand(cmd, resp) || !CheckResponse(250, resp)) return FALSE;
}
m_nState = 4; return TRUE;
}
BOOL CSMTP::Data(CString strSubject, CString strBody)
{
CString resp;
if (!SendCommand(_T("DATA"), resp) || !CheckResponse(354, resp)) return FALSE;
// 組裝郵件內容 (RFC 5322)
CString msg;
msg.Format(_T("From: <%s>\r\n"), m_strFrom);
for (int i = 0; i < m_arrTo.GetSize(); ++i)
msg.Format(_T("%sTo: <%s>\r\n"), msg, m_arrTo[i]);
msg.Format(_T("%sSubject: %s\r\n"), msg, strSubject);
msg += _T("MIME-Version: 1.0\r\n");
msg += _T("Content-Type: text/plain; charset=\"utf-8\"\r\n");
msg += _T("\r\n"); // 空行分隔 Header/Body
msg += strBody;
msg += _T("\r\n.\r\n"); // 結束標記
if (m_socket.Send(msg, msg.GetLength()) == SOCKET_ERROR) return FALSE;
if (!ReadResponse(resp) || !CheckResponse(250, resp)) return FALSE;
m_nState = 5; return TRUE;
}
void CSendMailDlg::OnSend()
{
CSMTP smtp;
smtp.SetHost(_T("smtp.gmail.com")); // 或公司郵件伺服器
smtp.SetFrom(_T("sender@example.com"));
smtp.AddTo(_T("recipient1@domain.com"));
smtp.AddTo(_T("recipient2@domain.com"));
if (!smtp.Connect()) {
AfxMessageBox(_T("連線失敗: ") + smtp.GetErrorMessage());
return;
}
if (!smtp.Data(_T("測試主旨"), _T("這是郵件內容測試。\n第二行。"))) {
AfxMessageBox(_T("發送失敗: ") + smtp.GetErrorMessage());
}
else {
AfxMessageBox(_T("發送成功!"));
}
smtp.Disconnect();
}
| 功能 | 實作方向 |
|---|---|
| SMTP AUTH (LOGIN/PLAIN/CRAM-MD5) | EHLO 後檢查 250-AUTH,發送 AUTH LOGIN,Base64 編碼用戶名/密碼。 |
| SMTPS (Port 465, Implicit SSL) | 使用 CSslSocket (MFC 無內建,需 OpenSSL/SChannel 封裝) 或 STARTTLS 升級。 |
| STARTTLS (Port 587, Explicit SSL) | EHLO 後發送 STARTTLS,收到 220 後在現有 Socket 上進行 TLS 握手。 |
| 附件 (MIME Multipart) | Content-Type: multipart/mixed,Body 分為多個 Part,各含 Content-Disposition: attachment、Base64 編碼檔案內容。 |
| HTML 郵件 | Content-Type: text/html 或 multipart/alternative 同時提供 text/plain 版本。 |
m_socket.Send/Receive 預設阻塞,建議設定 SO_RCVTIMEO/SO_SNDTIMEO 或使用非阻塞 + select。=?utf-8?B?base64?= (RFC 2047)。CSMTP 以 CSocket 為基礎,實作 SMTP 指令狀態機、應答碼驗證、郵件內容組裝。核心難點在於同步阻塞 I/O 的流程控制、多行應答解析、MIME 編碼、SSL/TLS 整合。生產環境建議:使用成熟庫 (VMime, POCO, Casablanca) 或系統 CDO/MAPI,自行封裝僅適合教學、輕量嵌入或特定舊版相容場景。