Home | Projects

SMTP Server

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 host 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 8-bit transmission channel then the MSB is set to zero and used.

Screenshot - smtp-model.gif
An SMTP client (Sender-SMTP) communicates with the SMTP server (Receiver-SMTP) using a predefined strict set of case insensative commands. Among them HELO, EHLO, MAIL, RCPT, DATA, QUIT is always implemented by all SMTP servers. In response the server sends some predefined response code to 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 current directory of the server executable. Simply speaking if you see error message like "Can not copy xxxx file to dddd directory" then you have to make dddd directory structure. I encourage 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 at pop3-server.asp. 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 cleint and server the server send a ready signal (220 response) to the client-

    220 kuashaonline.com Ready. <CRLF>

Client then can 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 an 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 check error conditions.

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);
}


The session is now established and the client now can send 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. Lets 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-

/*
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 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> 

Lets assume that our SMTP server can accept the message to store the message (if it is local) or can relay the message to destination SMTP server somehow. So we accept it

    250 OK<CRLF>

Here is my implementation.

/*
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. Now its 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 send affirmative result to client using 354 return

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

When client receives this reply client start 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);
    }
    //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;
}

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. Thats all about good world. No error. But there may be some errors or failure. Server must send appropriate response to nolify that. Here is some example codes 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;
}

References

RFC 821 - http://www.ietf.org/rfc/rfc0821.txt

History

This server is written for test purpose only when I was a 2nd year undergrad student back in year 2003. So, it is not stable at all.

Articles Copyright © Maruf Maniruzzaman (http://kuashaonline.com/projects)