SMTP 客户端






4.82/5 (59投票s)
CSmtp 类允许发送带附件的电子邮件。它只提供 AUTH LOGIN 身份验证。
引言
CSmtp
类允许从您的程序发送电子邮件。编写 CSmtp
类的灵感来自于文章:CFastSmtp - 一个快速简单的 SMTP 类... 我使用了 CFastSmtp
的代码并进行了以下修改
- 修复了一些 bug(例如,释放内存)
- 实现了带身份验证的登录(AUTH LOGIN)
- 添加了发送附件的功能
- 修改了错误处理
- 添加了 MIME 规范中的新标题(例如,X-Priority)
- 添加了非阻塞模式
- 使用了异常处理
- 确保了与 Linux 系统的兼容性
发送电子邮件时的典型场景
成功连接到 SMTP 服务器后,我们的客户端开始与远程 SMTP 服务器进行对话。客户端发送的每一行都应该以“\r\n
”结尾。如果您想了解更多细节,请参阅参考文献:[2]、[3]、[4]、[5]、[6]、[7]、[8] 和 [9]。[2] 描述了原始的 SMTP 协议 (1982),[4] 讨论了用于身份验证的 SMTP 扩展,而 [5]-[9] 改进了 MIME 规范。下面展示了发送电子邮件时的典型场景。示例 3 失败是因为 CSmtp
类中没有实现 TLS 过程。如果您想添加 TLS,请参阅 OpenSSL。我引入了以下表示法:S 代表远程服务器,C 代表我们的客户端,xxx 表示被审查的信息。
示例 1 - 连接到 smtp.wp.pl 并使用错误的登录名或密码
S: 220 smtp.wp.pl ESMTP
C: EHLO: mydomain.com
S: 250-smtp.wp.pl
250-PIPELINING
250-AUTH=LOGIN PLAIN
250-AUTH LOGIN PLAIN
250-STARTTLS
250-SIZE
250-X-RCPTLIMIT 100
250-8BITMIME
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
C: Kioq
S: 334 UGFzc3dvcmQ6
C: Kioq
S: 535 blad autoryzacji, niepoprawny login lub haslo / auth failure
示例 2 - 连接到 smtp.wp.pl 并使用正确的登录名和密码
S: 220 smtp.wp.pl ESMTP
C: EHLO: mydomain.com
S: 250-smtp.wp.pl
250-PIPELINING
250-AUTH=LOGIN PLAIN
250-AUTH LOGIN PLAIN
250-STARTTLS
250-SIZE
250-X-RCPTLIMIT 100
250-8BITMIME
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
C: xxx
S: 334 UGFzc3dvcmQ6
C: xxx
S: 235 go ahead
C: MAIL FROM:<me@mydomain.com>
S: 250 ok
C: RCPR TO:<friend@domain.com>
S: 250 ok
C: DATA
S: 234 go ahead
C: Date: Sun, 24 Aug 2008 22:43:45
From: JP<mail@domain.com>
X-Mailer: The Bat! (v3.02) Professional
Replay-to:mail@domain.com
X-Priority: 3 (Normal)
To:<friend@domain.com>
Subject: The message
MIME Version 1.0
Content-Type: multipart/mixed; boundary="__MESSAGE__ID__54yg6f6h6y456345"
--__MESSAGE__ID__54yg6f6h6y456345
Content-type: text/plain; charset=US-ASCII
Content-Transfer-Encoding: 7bit
This is my message.
--__MESSAGE__ID__54yg6f6h6y456345
Content-Type: application/x-msdownload; name="test.exe"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test.exe"
TVqQAAMAAAAEAAAA//8AALgAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
(...)
SU5HWFhQQURESU5HUEFERElOR1hYUEFERElOR1BBRERJTkdYWFBBRERJTkdQQURESU5HWA==
--__MESSAGE__ID__54yg6f6h6y456345
Content-Type: application/x-msdownload; name="test2.jpg"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="test2.jpg"
/9j/4Sv+RXhpZgAASUkqAAgAAAAJAA8BAgAGAAAAegAAABABAgAWAAAAgAAAABIBAwABAAAA
(...)
A6YxR5YJJ5zUu6ZW4+NjC24E4q5Dcox5I+lRI0iWAAV9aay+lTctoYTjrml+9irRmz//2Q==
--__MESSAGE__ID__54yg6f6h6y456345--
.
S: 250 ok xxx qp xxx
C: QUIT
S: 221 smtp.wp.pl
示例 3 - 连接到 smtp.gmail.com
S: 220 mx.google.com ESMTP
w28sm1561195uge.4
C: EHLO: mydomain.com
S: 250-mx.google.com at your service [xxx.xxx.xxx.xxx],
250-SIZE 28311552
250-8BITMIME
250-STARTTLS
250 ENHANCEDSTATUSCODES
C: AUTH LOGIN
S: 530 5.7.0 Must issue a STARTTLS command first. w28sm1561195uge.4
示例 4 - 连接到 smtp.bizmail.yahoo.com 并使用错误的登录名或密码
S: 220 smtp103.biz.mail.re2.yahoo.com ESMTP
C: EHLO: mydomain.com
S: 250-smtp103.biz.mail.re2.yahoo.com
250-AUTH LOGIN PLAIN XYMCOOKIE
250-PIPELINING
250-8BITMIME
C: AUTH LOGIN
S: 334 VXNlcm5hbWU6
C: dG9t
S: 334 UGFzc3dvcmQ6
C: bmVyb24xMg==
S: 535 authorization failed (#5.7.0)
CSmtp 类的实现
CSmtp
类的实现对于 Windows 和 Linux 操作系统非常相似。这并不令人意外,因为 Windows 使用通用的 Berkeley sockets 应用程序编程接口 (API) [10]。差异如表 1 所示(仅适用于 CSmtp
类的实现)。
Windows | Linux |
---|---|
需要进行 Winsock 初始化 | 无需 Winsock 初始化 |
使用函数 closesocket |
使用函数 close |
使用函数 ioctlsocket |
使用函数 ioctl |
定义了有用的类型别名;例如,SOCKET 、SOCKADDR_IN 、LPHOSTENT 、LPSERVENT 、LPIN_ADDR 、LPSOCKADDR |
应定义其他类型别名 |
下面将展示连接到远程 SMTP 服务器时需要执行的步骤。
- 仅在 Windows 中,初始化
Winsock2
(函数:WSAStartup
)。 - 获取本地机器上的套接字描述符(函数:
socket
)。 - 将端口值(例如,25)转换为 TCP/IP 字节顺序(函数:
htons
)。 - 获取远程机器的 Internet 地址(函数:
inet_addr
、gethostbyname
)。 - 如果使用非阻塞模式,则设置套接字参数(函数:
ioctl
/ioctlsocket
)。务必检查在调用ioctl
/ioctlsocket
之后调用的每个函数的返回值(请参阅下一节 - 使用非阻塞模式)。 - 连接到远程服务器(函数:
connect
)。 - 进行自我介绍 - EHLO <SP> <domain> <CRLF>。
- 发送 AUTH LOGIN <CRLF> 和另一条在“发送电子邮件时的典型场景”部分中描述的命令(函数:
send
、recv
)。 - 以 QUIT <CRLF> 结束对话。
- 关闭与远程机器的连接(函数:
close
/closesocket
)。 - 仅在 Windows 中,释放
Winsock2
资源(函数:WSACleanup
)。
使用非阻塞模式
在程序的最新版本中,我使用了一种非阻塞连接。有许多策略可以实现非阻塞模式(例如,Select 模型、WSAAsyncSelect 模型、WSAEventSelect 模型或 Completion port I/O 模型)。在我的代码中,我决定使用 Select 模型。它不像其他方法那么复杂,并且对于基本连接(一个客户端到一个服务器)来说效率很高。使用非阻塞模式的优点是:如果远程服务器停止响应,程序不会挂起,数据可以分批发送。这种方法的缺点是其复杂性。将套接字置于非阻塞模式后,下一个 API 调用将立即返回。通常,这些调用会因错误 WSAEWOULDBLOCK
(Windows)或 EINPROGRESS
(Linux)而失败,这意味着请求的操作尚未完成。因此,在非阻塞模式下,应高度关注分析 API 函数返回的错误。在 Select 模型中,我们在调用 send
、recv
、connect
、accept
等 API 函数后使用 select
函数 [11]。select
中的 ndfs
参数在 Windows 中被忽略,但在 Linux 中,它是三个集合(fd_set *readfds
、fd_set *writefds
、fd_set *exceptfds
)中最高编号的文件描述符加 1。为了说明阻塞模式和非阻塞模式之间的区别,此处展示了连接到远程服务器的两种方法。为了提高可读性,我只展示了 Windows 版本(已省略预处理器指令)。
/*Connecting to the remote server in blocking mode*/
SOCKET CSmtp::ConnectRemoteServer(const char *szServer,const unsigned short nPort_)
{
unsigned short nPort = 0;
LPSERVENT lpServEnt;
SOCKADDR_IN sockAddr;
unsigned long ul = 1;
int res = 0;
SOCKET hSocket = INVALID_SOCKET;
if((hSocket = socket(PF_INET, SOCK_STREAM,0)) == INVALID_SOCKET)
throw ECSmtp(ECSmtp::WSA_INVALID_SOCKET);
if(nPort_ != 0)
nPort = htons(nPort_);
else
{
lpServEnt = getservbyname("mail", 0);
if (lpServEnt == NULL)
nPort = htons(25);
else
nPort = lpServEnt->s_port;
}
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = nPort;
if((sockAddr.sin_addr.s_addr = inet_addr(szServer)) == INADDR_NONE)
{
LPHOSTENT host;
host = gethostbyname(szServer);
if (host)
memcpy(&sockAddr.sin_addr,host->h_addr_list[0],host->h_length);
else
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_GETHOSTBY_NAME_ADDR);
}
}
if(connect(hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)) == SOCKET_ERROR)
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_CONNECT);
}
return hSocket;
}
/*Connecting to the remote server in non-blocking mode*/
SOCKET CSmtp::ConnectRemoteServer(const char *szServer,const unsigned short nPort_)
{
unsigned short nPort = 0;
LPSERVENT lpServEnt;
SOCKADDR_IN sockAddr;
unsigned long ul = 1;
fd_set fdwrite,fdexcept;
timeval timeout;
int res = 0;
timeout.tv_sec = TIME_IN_SEC;
timeout.tv_usec = 0;
SOCKET hSocket = INVALID_SOCKET;
if((hSocket = socket(PF_INET, SOCK_STREAM,0)) == INVALID_SOCKET)
throw ECSmtp(ECSmtp::WSA_INVALID_SOCKET);
if(nPort_ != 0)
nPort = htons(nPort_);
else
{
lpServEnt = getservbyname("mail", 0);
if (lpServEnt == NULL)
nPort = htons(25);
else
nPort = lpServEnt->s_port;
}
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = nPort;
if((sockAddr.sin_addr.s_addr = inet_addr(szServer)) == INADDR_NONE)
{
LPHOSTENT host;
host = gethostbyname(szServer);
if (host)
memcpy(&sockAddr.sin_addr,host->h_addr_list[0],host->h_length);
else
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_GETHOSTBY_NAME_ADDR);
}
}
// start non-blocking mode for socket:
if(ioctlsocket(hSocket,FIONBIO, (unsigned long*)&ul) == SOCKET_ERROR)
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_IOCTLSOCKET);
}
if(connect(hSocket,(LPSOCKADDR)&sockAddr,sizeof(sockAddr)) == SOCKET_ERROR)
{
if(WSAGetLastError() != WSAEWOULDBLOCK)
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_CONNECT);
}
}
else
return hSocket;
while(true)
{
FD_ZERO(&fdwrite);
FD_ZERO(&fdexcept);
FD_SET(hSocket,&fdwrite);
FD_SET(hSocket,&fdexcept);
if((res = select(hSocket+1,NULL,&fdwrite,&fdexcept,&timeout)) == SOCKET_ERROR)
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_SELECT);
}
if(!res)
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::SELECT_TIMEOUT);
}
if(res && FD_ISSET(hSocket,&fdwrite))
break;
if(res && FD_ISSET(hSocket,&fdexcept))
{
closesocket(hSocket);
throw ECSmtp(ECSmtp::WSA_SELECT);
}
} // while
FD_CLR(hSocket,&fdwrite);
FD_CLR(hSocket,&fdexcept);
return hSocket;
}
用法
#include "CSmtp.h"
#include <iostream>
int main()
{
bool bError = false;
try
{
CSmtp mail;
mail.SetSMTPServer("smtp.domain.com",25);
mail.SetLogin("***");
mail.SetPassword("***");
mail.SetSenderName("User");
mail.SetSenderMail("user@domain.com");
mail.SetReplyTo("user@domain.com");
mail.SetSubject("The message");
mail.AddRecipient("friend@domain2.com");
mail.SetXPriority(XPRIORITY_NORMAL);
mail.SetXMailer("The Bat! (v3.02) Professional");
mail.AddMsgLine("Hello,");
mail.AddMsgLine("");
mail.AddMsgLine("How are you today?");
mail.AddMsgLine("");
mail.AddMsgLine("Regards");
mail.AddMsgLine("--");
mail.AddMsgLine("User");
mail.AddAttachment("c:\\test.exe");
mail.AddAttachment("c:\\test2.jpg");
mail.Send();
}
catch(ECSmtp e)
{
std::cout << "Error: " << e.GetErrorText().c_str() << ".\n";
bError = true;
}
if(!bError)
{
std::cout << "Mail was send successfully.\n";
return 0;
}
else
return 1;
}
作者注
- 如果您在发送电子邮件时遇到问题,请使用 Visual Studio 的调试器分析您的 SMTP 服务器与客户端之间的对话;也许您的服务器需要另一种类型的身份验证,或者根本不需要身份验证。
- 您不允许使用
CSmtp
类进行垃圾邮件发送。