Introduction
In this, I'll show a simple implementation of Simple Mail Transfer Protocol (SMTP) that is defined in RFC 821. According to the document - The objective of Simple Mail Transfer Protocol (SMTP) is to transfer mail reliably and efficiently. SMTP is independent of the particular transmission subsystem and requires only a reliable ordered data stream channel. In this article, we use TCP/IP only for message distribution.
The SMTP provides mechanisms for the transmission of mail; directly from the sending user's host to the receiving user's host when the two hosts are connected to the same transport service, or via one or more relay SMTP-servers when the source and destination hosts are not connected to the same transport service. To be able to provide the relay capability, the SMTP-server must be supplied with the name of the ultimate destination host as well as the destination mailbox name.
The protocol uses 7-bit ASCII characters. If the transport layer provides an 8-bit transmission channel, then the MSB is set to zero and used.
An SMTP client (Sender-SMTP) communicates with the SMTP server (Receiver-SMTP) using a predefined strict set of case insensitive commands. Among them HELO
, EHLO
, MAIL
, RCPT
, DATA
, QUIT
are always implemented by all SMTP servers. In response, the server sends some predefined response code to the client. For example, 250
is returned if everything goes OK. A short description may follow the reply code. All commands are ended with CRLF
characters. I'll describe the commands here sequentially.
In this article, I'll show how to implement a simple SMTP server. You can test it with any SMTP client like Outlook Express. To test the server, please stop the Windows SMTP Service first. You may need to create some folders also. For example, if you want to send mail to kuasha@exampledomain.com you should have ./exampledomain.com/kuasha/mbox/ as a valid directory relative to the current directory of the server executable. Simply speaking, if you see an error message like "Cannot copy xxxx file to dddd directory" then you have to make dddd directory structure. I encourage you to see the code instead. And we may declare it to be stable SMTP if we can fix all errors.
There is also a POP3 server implementation here. If you keep two executables in a same directory and first setup SMTP and then setup POP3, you can send and receive mails between users with a mail client using the machine IP address of the machine.
Implementation
When a transmission channel is established between client and server, the server sends a ready signal (220
response) to the client:
220 kuashaonline.com Ready. <CRLF>
Client can then start the session issuing a HELO
command.
The HELO or EHLO command
These two commands (any one) are used to establish a dialog session between client and server. The client sends HELO
command in the following format to start a session:
HELO <SP> <domain> <CRLF>
Here the <domain>
is users domain who wish to send a message. If the server allows a user from this domain, it sends OK
response:
250 OK You are not kicked off :) <CRLF>
Here is my simple implementation of HELO
request. Please note that for simplicity, I have not checked error conditions.
int CMailSession::ProcessHELO(char *buf, int len)
{
Log("Received HELO\n");
buf+=5;
m_nStatus=SMTP_STATUS_HELO;
m_FromAddress.SetAddress("");
m_nRcptCount=0;
CreateNewMessage();
return SendResponse(250);
}
The session is now established and the client can now send a message.
The MAIL Command
Client starts with MAIL
command to send a mail message:
Format: MAIL <SP> FROM:<reverse-path> <CRLF>
Example MAIL FROM:<manir@exampledomain.com> <CRLF>
Here client assigns the FROM
address. Let's assume that we do accept this from
address and send OK
.
250 OK<CRLF>
It is possible to send invalid parameter response (501) from server if, for example, from
address format is not valid.
501 Syntax error in parameters or arguments <CRLF>
Here is my simple implementation:
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
address is set. Now we want the to
address.
The RCPT Command
The client sets to
address with this command:
Format: RCPT <SP> TO:<forward-path> <CRLF>
Example: RCPT TO:kuasha@exampledomain.com<CRLF>
Let's assume that our SMTP server can accept the message to store the message (if it is local) or relay the message to destination SMTP server somehow. So we accept it:
250 OK<CRLF>
Here is my implementation:
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)
{
return SendResponse(503);
}
if(m_nRcptCount>=MAX_RCPT_ALLOWED)
{
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. Now it's time to receive DATA
from client.
The DATA Command
Client sends DATA
command to start sending data:
DATA <CRLF>
The server now sets its state to receive data and sends an affirmative result to client using 354
return.
354 Start mail input; end with [CRLF].[CRLF] <CRLF>
When client receives this reply, client starts to send the mail body. At the end, client sends a [CRLF].[CRLF]
sequence to tell server that data sending is finished.
Here is my implementation to process DATA
command:
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);
}
if(strstr(buf,SMTP_DATA_TERMINATOR)) {
printf("Data End\n");
m_nStatus=SMTP_STATUS_DATA_END;
return ProcessDATAEnd();
}
WriteFile(m_hMsgFile,buf,dwIn, &dwOut,NULL);
return 220;
}
When the terminator sequence ([CRLF].CRLF]
) is received, the mail reception is finished. The attachments are also received at this stage. The SMTP server may not separate the attachment part. It is the responsibility of the mail viewer. Everything is considered as data.
The QUIT Command
OK, we are finished with the sending mail. Client should now send QUIT
command:
QUIT<CRLF>
And server closes the session by sending 221
response.
221 Service closing transmission channel.
OK. That's all about the good world. No error. But there may be some errors or failure. Server must send an appropriate response to notify that. Here is some example code in my response method:
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;
}
Reference
History
- 23rd September, 2007: Article posted on CodeProject
This server was written for test purposes only when I was a 2nd year undergrad student back in the year 2003. So, it is not stable at all.