SMTP 服务器






4.43/5 (27投票s)
本文展示了如何设计 SMTP 服务器。
引言
在本文中,我将展示一个基于 RFC 821 定义的简单邮件传输协议 (SMTP) 的实现。根据该文档 - 简单邮件传输协议 (SMTP) 的目标是可靠且高效地传输邮件。SMTP 独立于特定的传输子系统,只需要一个可靠的有序数据流通道。在本文中,我们仅使用 TCP/IP 进行消息分发。
SMTP 提供了邮件传输机制;当两个主机连接到同一传输服务时,可以直接从发送用户的宿主传输到接收用户的宿主;当源主机和目标主机未连接到同一传输服务时,可以通过一个或多个中继 SMTP 服务器进行传输。为了提供中继能力,必须向 SMTP 服务器提供最终目标主机的名称以及目标邮箱的名称。
该协议使用 7 位 ASCII 字符。如果传输层提供了 8 位传输通道,则将 MSB 设置为零并使用。

SMTP 客户端 (Sender-SMTP) 使用预定义的严格的不区分大小写的命令集与 SMTP 服务器 (Receiver-SMTP) 进行通信。其中 HELO
、EHLO
、MAIL
、RCPT
、DATA
、QUIT
始终由所有 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 年还是大二学生时编写的。因此,它一点也不稳定。