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

SMTP 服务器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.43/5 (27投票s)

2007年9月23日

GPL3

4分钟阅读

viewsIcon

147712

downloadIcon

5289

本文展示了如何设计 SMTP 服务器。

引言

在本文中,我将展示一个基于 RFC 821 定义的简单邮件传输协议 (SMTP) 的实现。根据该文档 - 简单邮件传输协议 (SMTP) 的目标是可靠且高效地传输邮件。SMTP 独立于特定的传输子系统,只需要一个可靠的有序数据流通道。在本文中,我们仅使用 TCP/IP 进行消息分发。

SMTP 提供了邮件传输机制;当两个主机连接到同一传输服务时,可以直接从发送用户的宿主传输到接收用户的宿主;当源主机和目标主机未连接到同一传输服务时,可以通过一个或多个中继 SMTP 服务器进行传输。为了提供中继能力,必须向 SMTP 服务器提供最终目标主机的名称以及目标邮箱的名称。

该协议使用 7 位 ASCII 字符。如果传输层提供了 8 位传输通道,则将 MSB 设置为零并使用。

Screenshot - smtp-model.gif

SMTP 客户端 (Sender-SMTP) 使用预定义的严格的不区分大小写的命令集与 SMTP 服务器 (Receiver-SMTP) 进行通信。其中 HELOEHLOMAILRCPTDATAQUIT 始终由所有 SMTP 服务器实现。作为响应,服务器会向客户端发送一些预定义的响应代码。例如,如果一切正常,则返回 250。回复代码后面可能跟有简短的描述。所有命令都以 CRLF 字符结束。我将在此按顺序描述这些命令。

在本文中,我将展示如何实现一个简单的 SMTP 服务器。您可以使用 Outlook Express 等任何 SMTP 客户端进行测试。要测试服务器,请先停止 Windows SMTP 服务。您可能还需要创建一些文件夹。例如,如果您想发送邮件给 kuasha@exampledomain.com,则应该有一个相对于服务器可执行文件当前目录的有效目录./exampledomain.com/kuasha/mbox/。简而言之,如果您看到类似“无法将 xxxx 文件复制到 dddd 目录”的错误消息,则需要创建 dddd 目录结构。我鼓励您查看代码。如果我们能修复所有错误,就可以将其声明为稳定的 SMTP。

这里还有一个 POP3 服务器实现 链接。如果您将两个可执行文件放在同一个目录中,并先设置 SMTP,然后设置 POP3,您就可以使用该机器的机器 IP 地址,通过邮件客户端在用户之间发送和接收邮件。

实现

当客户端和服务器之间的传输通道建立后,服务器会向客户端发送一个就绪信号(220响应)。

    220 kuashaonline.com Ready. <CRLF>

然后客户端可以开始会话,发送 HELO 命令。

HELO 或 EHLO 命令

这两个命令(任选其一)用于在客户端和服务器之间建立对话会话。客户端以以下格式发送 HELO 命令以开始会话。

    HELO <SP> <domain> <CRLF>

这里的 <domain> 是希望发送消息的用户的域。如果服务器允许来自此域的用户,则发送 OK 响应。

    250 OK You are not kicked off :) <CRLF>

这是我的 HELO 请求的简单实现。请注意,为简化起见,我没有检查错误条件。

int CMailSession::ProcessHELO(char *buf, int len)
{
    Log("Received HELO\n");
    buf+=5;
    //User session established
    m_nStatus=SMTP_STATUS_HELO;
    m_FromAddress.SetAddress("");
    m_nRcptCount=0;
    // Prepare a new message now.
    CreateNewMessage();
    return SendResponse(250);
}

现在会话已建立,客户端可以发送消息了。

MAIL 命令

客户端开始使用 MAIL 命令发送邮件消息。

    Format: MAIL <SP> FROM:<reverse-path> <CRLF>
    Example MAIL FROM:<manir@exampledomain.com> <CRLF>

在这里,客户端指定 FROM 地址。假设我们接受此 from 地址并发送 OK

    250 OK<CRLF>

如果,例如,from 地址格式无效,服务器可能发送无效参数响应 (501)。

    501 Syntax error in parameters or arguments <CRLF>

这是我的简单实现。

/*
MAIL
S: 250
F: 552, 451, 452
E: 500, 501, 421
*/
int CMailSession::ProcessMAIL(char *buf, int len)
{
    char address[MAX_ADDRESS_LENGTH+5];
    char *st,*en;
    __w64 int alen;
    if(m_nStatus!=SMTP_STATUS_HELO)
    {
        return SendResponse(503);
    }
    memset(address,0,sizeof(address));
    st=strchr(buf,'<');
    en=strchr(buf,'>');
    st++;
    alen=en-st;
    strncpy(address,st,alen);
    printf("FROM [%s]",address);
    if(!CMailAddress::AddressValid(address))
    {
        return SendResponse(501);
    }
    m_FromAddress.SetAddress(address);
    return SendResponse(250);
}

OK,from 地址已设置。现在我们需要 to 地址。

RCPT 命令

客户端使用此命令设置 to 地址。

    Format: RCPT <SP> TO:<forward-path> <CRLF>
    Example: RCPT TO:kuasha@exampledomain.com<CRLF> 

假设我们的 SMTP 服务器可以接受消息(如果它是本地的)进行存储,或者以某种方式将消息中继到目标 SMTP 服务器。因此,我们接受它。

    250 OK<CRLF>

这是我的实现

/*
RCPT
S: 250, 251
F: 550, 551, 552, 553, 450, 451, 452
E: 500, 501, 503, 421
*/
int CMailSession::ProcessRCPT(char *buf, int len)
{
    char address[MAX_ADDRESS_LENGTH+5];
    char user[MAX_USER_LENGTH+5];
    char tdom[MAX_DOMAIN_LENGTH+5];
    char szUserPath[MAX_PATH+1];
    char *st,*en, *domain=tdom;
    __w64 int alen;
    if(m_nStatus!=SMTP_STATUS_HELO)
    {
        //503 Bad Command
        return SendResponse(503);
    }
    if(m_nRcptCount>=MAX_RCPT_ALLOWED)
    {
        //552 Requested mail action aborted: exceeded storage allocation
        return SendResponse(552);
    }
    memset(address,0,sizeof(address));
    st=strchr(buf,'<');
    en=strchr(buf,'>');
    st++;
    alen=en-st;
    strncpy(address,st,alen);
    domain=strchr(address,'@');
    domain+=1;
    memset(user,0,sizeof(user));
    strncpy(user,address,strlen(address)-strlen(domain)-1);
    printf("RCPT [%s] User [%s] Domain [%s]\n",address, user, domain);
    char domain_path[300];
    sprintf(domain_path,"%s%s",DIRECTORY_ROOT,domain);
    if(PathFileExists(domain_path))
    {
        sprintf(szUserPath,"%s\\%s",domain_path,user);
        printf("User MBox path [%s]\n",szUserPath);
        if(!PathFileExists(szUserPath))
        {
            TRACE("PathFileExists(%s) FALSE\n",szUserPath);
            printf("User not found on this domain\n");
            return SendResponse(550);
        }
    }
    else
    {
        TRACE("PathFileExists(%s) FALSE\n",domain_path);
        return SendResponse(551);
    }
    m_ToAddress[m_nRcptCount].SetMBoxPath(szUserPath);
    m_ToAddress[m_nRcptCount].SetAddress(address);
    m_nRcptCount++;
    return SendResponse(250);
}

OK。现在是时候接收来自客户端的 DATA 了。

DATA 命令

客户端发送 DATA 命令以开始发送数据。

    DATA <CRLF>

服务器现在将状态设置为接收数据,并使用 354返回码向客户端发送肯定答复。

    354 Start mail input; end with [CRLF].[CRLF] <CRLF>

当客户端收到此回复后,客户端开始发送邮件正文。最后,客户端发送一个 [CRLF].[CRLF] 序列来告知服务器数据发送已完成。

这是我处理 DATA 命令的实现。

int CMailSession::ProcessDATA(char *buf, int len)
{
    DWORD dwIn=len, dwOut;
    if(m_nStatus!=SMTP_STATUS_DATA)
    {
        m_nStatus=SMTP_STATUS_DATA;
        return SendResponse(354);
    }
    //client should send term in separate line 
    if(strstr(buf,SMTP_DATA_TERMINATOR)) //if a [CRLF].CRLF] found
    {
        printf("Data End\n");
        m_nStatus=SMTP_STATUS_DATA_END;
        return ProcessDATAEnd();
    }
    // We write the data to a file
    WriteFile(m_hMsgFile,buf,dwIn, &dwOut,NULL);
    return 220;
}

收到终止序列 ([CRLF].CRLF]) 后,邮件接收完成。附件也在此时接收。SMTP 服务器可能不会将附件部分分开。这是邮件查看器的责任。所有内容都被视为数据。

QUIT 命令

OK,邮件发送完成。客户端现在应该发送 QUIT 命令。

    QUIT<CRLF>

服务器通过发送 221响应关闭会话。

    221 Service closing transmission channel.

OK。以上是关于正常情况的全部内容。没有错误。但可能会出现一些错误或失败。服务器必须发送适当的响应来通知这一点。这是我响应方法中的一些示例代码。

int CMailSession::SendResponse(int nResponseType)
{
    char buf[100];
    int len;
    if(nResponseType==220)
        sprintf(buf,"220 %s Welcome to %s %s \r\n",
			DOMAIN_NAME,APP_TITLE, APP_VERSION);
    else if(nResponseType==221)
        strcpy(buf,"221 Service closing transmission channel\r\n");
    else if (nResponseType==250) 
        strcpy(buf,"250 OK\r\n");
    else if (nResponseType==354)
        strcpy(buf,"354 Start mail input; end with <CRLF>.<CRLF>\r\n");
    else if(nResponseType==501)
        strcpy(buf,"501 Syntax error in parameters or arguments\r\n"); 
    else if(nResponseType==502)
        strcpy(buf,"502 Command not implemented\r\n"); 
    else if(nResponseType==503)
        strcpy(buf,"503 Bad sequence of commands\r\n"); 
    else if(nResponseType==550)
        strcpy(buf,"550 No such user\r\n");
    else if(nResponseType==551)
        strcpy(buf,"551 User not local. Can not forward the mail\r\n");
    else
        sprintf(buf,"%d No description\r\n",nResponseType);
    len=(int)strlen(buf);
    printf("Sending: %s",buf);
    send(m_socConnection,buf,len,0);
    return nResponseType;
}

参考

历史

  • 2007 年 9 月 23 日:文章发布于 CodeProject

此服务器仅用于测试目的,是我在 2003 年还是大二学生时编写的。因此,它一点也不稳定。

© . All rights reserved.