65.9K
CodeProject 正在变化。 阅读更多。
Home

SMTP 客户端

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (59投票s)

2008 年 8 月 24 日

CPOL

4分钟阅读

viewsIcon

750723

downloadIcon

22602

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 类的实现)。

表 1. 实现中的差异。
Windows Linux
需要进行 Winsock 初始化 无需 Winsock 初始化
使用函数 closesocket 使用函数 close
使用函数 ioctlsocket 使用函数 ioctl
定义了有用的类型别名;例如,SOCKETSOCKADDR_INLPHOSTENTLPSERVENTLPIN_ADDRLPSOCKADDR 应定义其他类型别名

下面将展示连接到远程 SMTP 服务器时需要执行的步骤。

  1. 仅在 Windows 中,初始化 Winsock2(函数:WSAStartup)。
  2. 获取本地机器上的套接字描述符(函数:socket)。
  3. 将端口值(例如,25)转换为 TCP/IP 字节顺序(函数:htons)。
  4. 获取远程机器的 Internet 地址(函数:inet_addrgethostbyname)。
  5. 如果使用非阻塞模式,则设置套接字参数(函数:ioctl/ioctlsocket)。务必检查在调用 ioctl/ioctlsocket 之后调用的每个函数的返回值(请参阅下一节 - 使用非阻塞模式)。
  6. 连接到远程服务器(函数:connect)。
  7. 进行自我介绍 - EHLO <SP> <domain> <CRLF>
  8. 发送 AUTH LOGIN <CRLF> 和另一条在“发送电子邮件时的典型场景”部分中描述的命令(函数:sendrecv)。
  9. QUIT <CRLF> 结束对话。
  10. 关闭与远程机器的连接(函数:close/closesocket)。
  11. 仅在 Windows 中,释放 Winsock2 资源(函数:WSACleanup)。

使用非阻塞模式

在程序的最新版本中,我使用了一种非阻塞连接。有许多策略可以实现非阻塞模式(例如,Select 模型、WSAAsyncSelect 模型、WSAEventSelect 模型或 Completion port I/O 模型)。在我的代码中,我决定使用 Select 模型。它不像其他方法那么复杂,并且对于基本连接(一个客户端到一个服务器)来说效率很高。使用非阻塞模式的优点是:如果远程服务器停止响应,程序不会挂起,数据可以分批发送。这种方法的缺点是其复杂性。将套接字置于非阻塞模式后,下一个 API 调用将立即返回。通常,这些调用会因错误 WSAEWOULDBLOCK(Windows)或 EINPROGRESS(Linux)而失败,这意味着请求的操作尚未完成。因此,在非阻塞模式下,应高度关注分析 API 函数返回的错误。在 Select 模型中,我们在调用 sendrecvconnectaccept 等 API 函数后使用 select 函数 [11]。select 中的 ndfs 参数在 Windows 中被忽略,但在 Linux 中,它是三个集合(fd_set *readfdsfd_set *writefdsfd_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;
}

作者注

  1. 如果您在发送电子邮件时遇到问题,请使用 Visual Studio 的调试器分析您的 SMTP 服务器与客户端之间的对话;也许您的服务器需要另一种类型的身份验证,或者根本不需要身份验证。
  2. 您不允许使用 CSmtp 类进行垃圾邮件发送。

参考文献

  1. CFastSmtp - 快速简单的 SMTP 类...
  2. 简单邮件传输协议 RFC 821
  3. ARPA Internet 文本消息格式标准 RFC 822
  4. 用于身份验证的 SMTP 服务扩展 RFC 2554
  5. MIME:Internet 消息正文格式 RFC 2045
  6. MIME:媒体类型 RFC 2046
  7. MIME:非 ASCII 文本的报头扩展 RFC 2047
  8. MIME:注册程序 RFC 2048
  9. MIME:符合性标准和示例 RFC 2049
  10. Berkeley Sockets 应用程序编程接口 (API)
  11. select 函数
© . All rights reserved.