Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

C/C++

A POP3 Mail Client using WinSock


APR95: A POP3 Mail Client using WinSock

Encapsulate the API you need--and ignore the rest

Robert is vice president of engineering at TeacherSoft Corp., where he develops Internet-access software and browser technology. He can be contacted at [email protected].


As the Internet continues to grow exponentially, the demand for internetworked applications is at an all-time high, with no slowdown in sight. There are now many opportunities for developers to implement a new generation of client/server applications. When Windows 95 is released with its increased emphasis on connectivity, the need for internetworked applications will only heighten.

In this article, I'll present a simple C++ foundation for Internet client applications running on Microsoft Windows, based on classes that encapsulate the Windows Sockets API. Building on a simple socket class, I'll demonstrate how to implement a mail client that understands the Post Office Protocol (POP), a powerful Internet protocol for processing e-mail now available on most Internet mail servers. My mail client is constructed using Borland's ObjectWindows Library (OWL). Although my classes comprise a small piece in the grand scheme of Internet development, they can serve as a foundation for more-sophisticated applications.

The Windows Sockets API

Internet client applications such as Mosaic, archie, ftp, telnet, and finger, are built on the TCP/IP suite of network protocols in conjunction with higher-level protocols such as HTTP, FTP, SMTP, SNMP, NNTP, POP, and so on. On UNIX, the low-level TCP/IP protocols are usually encapsulated by a library implementing an API such as Berkeley sockets or TLI. Developing equivalent Internet applications under Windows involves an API known as "Windows Sockets." Windows Sockets has become the standard API for Internet development under Windows and is now being ported to other platforms such as OS/2 and the Macintosh.

The Windows Sockets API, sometimes known as "WinSock," was defined in 1992 by Martin Hall, Mark Towfiq, and several other engineers representing companies such as JSB, Microdyne, Microsoft, FTP Software, and NetManage. Version 1.1 of the spec was written in January 1993 and has been implemented by many vendors. There is also a popular shareware implementation, known as "Trumpet Winsock," available at many ftp sites on the Internet. For additional background information on Windows Sockets, refer to the article "Untangling the Windows Sockets API," by Mike Calbaum, Frank Porcaro, Mark Ruegsegger, and Bruce Backman (DDJ, February 1993). Likewise, the article, "Building an Internet Global Phone," by Sing Li (Dr. Dobb's Sourcebook on the Information Highway, Winter 1994), presents a discussion of Mark Clouden's freely available WSNETWRK library, a Windows DLL that encapsulates and expands upon the functionality of the WinSock API.

The design of Windows Sockets is based on Berkeley sockets, the networking facility found in classic Berkeley UNIX. As with Berkeley sockets, the Windows Sockets API is not too large or difficult to understand. One difference between the two results from the fact that the design of Berkeley sockets assumes a multithreaded operating system. Because of this, if your code uses only the Berkeley-equivalent API calls, it may cause your Windows application to block for unreasonable periods of time while waiting for specific network requests to be fulfilled.

To address this problem, the WinSock designers added a set of asynchronous extensions which provide the additional functionality needed to build a responsive network application under Windows. The extensions are all named with a "WSA" prefix. Except for WSAStartup() and WSACleanup(), the services are entirely optional. The WSAStartup() function initializes the socket system, while WSACleanup() closes everything down. Other than these two Windows-specific extensions, Windows Sockets behaves almost identically to Berkeley sockets running on UNIX--except, of course, for the lack of true multithreading. Even so, you can still easily port a lot of existing UNIX code to Windows and convert necessary portions to the asynchronous model as needed.

A Thin Wrapper

My first goal in this article is to design a C++ class that provides a thin wrapper around the Windows Sockets API. This class does not encapsulate the entire API, because I think it is best to service only the areas you need for a specific job. If you need added functionality, you can derive more-specialized classes from the base API.

After presenting the socket class, I'll show how applications can use this base-level functionality by defining a child class that provides access to Internet e-mail using POP. POP provides a high-level API for storage and forwarding of mail, and it is a good example of how to extend our basic socket class.

The vanilla socket class provides initialization code and basic connection features. Using this class, you can construct a socket, connect it to a known host, and read and write data to that host. All interaction with the socket must be managed by the developer. The base socket class, TAsyncSocket, is shown in Listing One . Only excerpts from the source code accompany this article. The complete listings are available electronically; see "Availability," page 3.

The public interface for TAsyncSocket is small. The class provides member functions for establishing an asynchronous connection and for synchronous sending and receiving. The synchronous methods are handy for very small pieces of data that are normally cached by the WinSocket system and do not usually cause blocking (although it is never guaranteed that the socket will not block).

The other public function of note is LastError(), which returns the last error encountered by the socket. It is simply an encapsulation of WSAGetLastError(). Basic time-out error handling is also provided. If the socket system blocks for a specific amount of time, the blocking call is canceled, and the socket is closed. More-robust error handling is a must for a complete set of tools, but is beyond the scope of this discussion.

Managing Socket Information

Managing the information between the socket owner and the socket can be problematic. While the object-oriented nature of the socket makes it reusable, it hides some of the details we might find useful. For instance, what happens when an error is encountered or when the socket needs to read or write data? A mechanism to deal with those situations is found in the TSocketCatch class, which provides a communications path between the socket classes and the other classes or the code that manages them. One parameter of the TAsyncSocket class is a TSocketCatch pointer. TAsyncSocket uses this class to pass information back to its manager. Listing Two presents the TSocketCatch class. It's pretty simple, but it provides the basic mechanism for passing error information back to higher-level code. Here again, you can add more-sophisticated functionality in a derived class. The class for catching POP socket information is a good example; see Listing Three . The TPOPSocketCatch class adds notification methods for message retrieval and for the number of messages to be retrieved.

The POP Socket

The POP protocol is text based, similar to other higher-level Internet protocols such as SMTP. Our POP socket class provides support for retrieving mail and hides most of the complexity of the POP protocol. The examples and classes are based on protocol version POP3, specified in the Internet document RFC1081. This document, along with other Internet specifications, is available via ftp from ds.internic.net in the rfc directory. In this case, the file is rfc1081.txt, and it is worth studying.

The POP protocol steps through a series of states based on information the client and server exchange. Commands are ASCII based and made up of a keyword, possibly followed by an argument and terminated with a CR/LF pair. The success or failure of a POP command is indicated with either the positive \ "+OK" or the negative \ "--ERR". Additional relevant information may follow. As with commands, all responses are terminated with a CR/LF pair. Some responses can be multiline, but those cases are clearly indicated in RFC1081.

Listing Four is the POP socket class, derived from TAsyncSocket. We extended the basic socket significantly. The key member function is RetrieveMail(), which provides a single call to retrieve all mail for a specific user. To achieve this, the socket goes through several states. During the life of a POP connection, the POP server and client coexist in different states based on the information passed between them. The states are as follows: Connection, Authorization, Transaction, and Update.

Once you call RetrieveMail, a series of events is started that ends with either mail being delivered, or a message of "No Mail" being returned. This, of course, assumes that the host name, user name, and password provided are all valid. If not, you'll get an error message.

The Connect State

In a typical RetrieveMail mail session, the first state entered into is the Connect state. As you can see from Listing Five , when RetrieveMail() is called, it sets up string holders for the host, user, and password information, and then calls Connect(). The Connect() function (see Listing Six) calls the parent function TAsyncSocket::Connect() (see Listing Seven). Notice we provide the POP port and protocol information, simplifying the connect function at this level.

It is TAsyncSocket::Connect() that actually creates the socket, via a call to the Windows Sockets function, Socket(). If socket() returns a valid socket identifier, Connect() starts the event chain rolling by calling TAsyncSocket::AsyncGetHostByName().

The AsyncGetHostByName function is in Listing Eight . This function requests an SM_HOSTNAME message to be sent when the name resolution has occurred. This is key because the member function that handles that message is virtual. This call gets us back into the TAsyncPOPSocket realm--specifically, its host name handler; see Listing Nine.

Again, we call our parent to take advantage of its built-in functionality--in this case, the parent function is TAsyncSocket::SmHostName(). If all goes well, we post an FD_READ using WSAAsyncSelect() and wait on the final stage of the connection state. The SmPopConnect() routine in Listing Ten verifies that we have connected to a POP server by checking for the standard "+OK" POP reply. If so, the authorization process is started, via a call to Authorize().

The Authorization State

Authorization consists of sending the user and password information to the POP server. The process is started by a call to TAsyncPOPSocket::Authorize(), whose implementation is in Listing Eleven . This calls WSAAsyncSelect, which results in a data ready (FD_READ) message at SmPopAuthorize() in Listing Twelve . The final stage of authorization is to pair the user name with a valid password. This routine posts the password to the server and sets up SmPopPassword() to check on the result. As shown in Listing Thirteen , SmPopPassword() checks for a POP_OK response; if one is received, we enter the transaction state.

Thus far, the discussion has focused on getting the POP session to the transaction state. For the sake of space, I will leave the rest of the code browsing up to you. To thoroughly understand the sequence of events in this protocol, I suggest you step through the code with a debugger.

Once your code is in the Transaction state, your program can check on mail stats, retrieve and delete mail, and possibly invoke some extended commands, based on your POP server's support for these extensions. The listings show the source for a complete mail-retrieval session.

After the Transaction state, the next state is Update. A "Quit" command posted to the POP server moves you into the Update state. All transactions that have taken place are committed and the POP server will accept new authorization information if desired. If your application has finished processing user mail, your code should close the socket at this point.

In summary, to retrieve mail, a mail client must:

  1. 1. Acquire POP mail host, user name, and password from the user.
  2. 2. Create, connect, and instigate the mail transaction based on gathered information.
  3. 3. For each mail message to be retrieved, open a new window and display the message to the user.
The source code gives complete details for each step. In implementing the mail client, I used the Application Expert in the Borland IDE to generate a shell MDI application. From there, I added the bits of socket code needed to retrieve all mail for a specific user from a host and then show it in a simple text window. You can easily extend this program to provide support for message saving, printing, and other useful services.

Conclusion

The classes presented here only cover the specific areas needed to get a basic socket up and running. By staying away from much of the clutter involved in Windows Sockets, we can focus on the task of retrieving mail using the POP3 protocol.

You can extend these socket classes to support protocols other than POP. One area you might investigate is a generic text socket that handles the line-oriented interface to many Internet protocols. Another area that needs attention is real error handling. The comments in the code point out where error-handling code is needed. You can build exception handling into the code or go about it in a more traditional way. In either case, I've included a couple of classes in the socket.cpp file that you may find useful.

Listing One


class _OWLCLASS TAsyncSocket : public TWindow
{
public:
        TAsyncSocket(TWindow* pParent,TSocketCatch* pCatcher);
        ~TAsyncSocket();
        int Connect(const char* pHost, int nPort, int nProtocol);
        int Disconnect();
        int SyncSend(const char far* pData, int nLen);

        int SyncRecv(char far* pData, int nMaxLen);
        int SyncSendLine(const char* pData);
        int LastError();
protected:
        void EvTimer(uint uTimerId);
        void SetupWindow();
        void CleanupWindow();
        int AsyncGetHostByName(const char* pServerName);
        virtual LRESULT SmHostName(WPARAM,LPARAM);
        virtual LRESULT SmDataReady(WPARAM,LPARAM){return TRUE;};

        // data members
        char* pWorkBuffer;            // work buffer
        HANDLE hAsync;                // handle used for async Winsocket calls
        TSocketCatch* pCatcher;       // ptr to our info catcher
        BOOL bValid;                  // true if socket is initialized
        BOOL bConnected;              // true if socket is connected
        static BOOL bBlocking;        // true if socket is blocking
        WSADATA wsaData;              // Winsocket data
        HINSTANCE hSockInstance;      // HINSTANCE for socket
        SOCKET sock;                  // socket identifier
        hostent* pHost;               // ptr to host entry
        protoent* pProto;             // ptr to proto entry
        struct hostent Host;          // instance of hostent
        struct protoent Proto;        // instance of protoent
        struct sockaddr_in inetAddr;  // instance of sockeaddr_in strucutre
        struct sockaddr sa;           // instance of sockaddr structure
        DECLARE_RESPONSE_TABLE(TAsyncSocket);
};


Listing Two


class _OWLCLASS TSocketCatch
{
public:
        virtual void WinSocketError(int nError, const char* pErrText){};
};


Listing Three


class _OWLCLASS TPOPSocketCatch : public TSocketCatch
{
public:
        virtual void MessageRetrieved(int nID, const char* pFilename){};
        virtual void MessageCount(int nCount){};
};


Listing Four


class _OWLCLASS TAsyncPOPSocket : public TAsyncSocket
{

public:
        enum {NONE,RETRIEVE,SEND};
        TAsyncPOPSocket(TWindow* pParent = NULL, TPOPSocketCatch* pC = 0);
        ~TAsyncPOPSocket();
        Connect(const char* pHost);
        int Hangup();
        int Authorize(const char* pName, const char* pPassword);
        int RetrieveMail(const char* pHost, const char* pUser, 
                         const char* pPassword);
        void RetrieveNext();
        long MessageTotal();
        int GetMessageCount();
        int RetrieveMessages();
        int Quit();
protected:
        int MailResponse();
        virtual LRESULT SmHostName(WPARAM,LPARAM);
        LRESULT SmPopConnect(WPARAM,LPARAM);
        LRESULT SmPopAuthorize(WPARAM,LPARAM);
        LRESULT SmPopPassword(WPARAM,LPARAM);
        LRESULT SmPopMessageCount(WPARAM,LPARAM);
        LRESULT SmPopGetMessage(WPARAM,LPARAM);
        LRESULT SmPopStartMessage(WPARAM,LPARAM);
        
        // data members
        string sServer;
        string sUser;
        string sPassword;
        TPOPSocketCatch* pMyCatcher;
        bool bAuthorized;
        string* pWorkString;
        long lMessageTotal;
        long lMessageCount;
        int nMessagesPending;
        char* pCurrentMessage;
        ofstream out;

        DECLARE_RESPONSE_TABLE(TAsyncPOPSocket);
};


Listing Five


int TAsyncPOPSocket::RetrieveMail(const char* pHost, const char*
pUser, const char* pPassword)
{
        sServer = pHost;
        sUser = pUser;
        sPassword = pPassword;
        return Connect(sServer.c_str());
}


Listing Six


int TAsyncPOPSocket::Connect(const char* pHost)
{
        return TAsyncSocket::Connect(pHost,POP_PORT,PF_INET);
}


Listing Seven


int TAsyncSocket::Connect(const char* pHost,int nPort,int nProtocol = PF_INET)
{
        memset(&inetAddr,0,sizeof(inetAddr));   // cleanup structure
        inetAddr.sin_family = nProtocol;  // set protocol(almost always PF_INET)
        inetAddr.sin_port = htons(nPort);       // setup port
        sock = socket(nProtocol,SOCK_STREAM,0); // create the socket, 
                                                // passed protocol, STREAM,
                                                // no protocol specification (0)
        if (sock != INVALID_SOCKET)             // socket was created ok
        {
          return AsyncGetHostByName(pHost);     // acquire the host
                                                // asynchronously
        }
        else
        {
          return LastError();                 // return WSAError
        }
}


Listing Eight


int TAsyncSocket::AsyncGetHostByName(const char* pHost)
{

        hAsync = WSAAsyncGetHostByName( HWindow, SM_HOSTNAME, pHost,
                                        pWorkBuffer, WORKBUFF_SIZE);
        if (hAsync == 0)            // if we did not receive a handle
               return LastError();  // return socket error
        else                        // otherwise
               return 0;            // return success
}
</PRE>
<P>

<h4><a name="0182_007e">Listing Nine<a name="0182_007e"></h4>
<P>
<pre>

LRESULT TAsyncPOPSocket::SmHostName(WPARAM wp, LPARAM lp)
{
        TAsyncSocket::SmHostName(wp,lp);    // call parent 
        if (bConnected)                     // normal connection was ok
        {                                   // so start our series of commands

              WSAAsyncSelect(sock,HWindow,SM_POPCONNECT,FD_READ);
        }                                   //
        else                                // connect did not happen
        {                                   // if we have a catcher,
              if (pCatcher)                 // then let them know
                  pCatcher->WinSocketError(LastError(),"");
        } 
        return TRUE; 
}


Listing Ten


LRESULT TAsyncPOPSocket::SmPopConnect(WPARAM /* wp */, LPARAM lp)
{
        WSAAsyncSelect(sock,HWindow,0,0);          // turn off async msg's
        if (HIWORD(lp) == 0 && LOWORD(lp) == FD_READ) // check for error
        {                                      
           if (MailResponse() == POP_OK)           // did we get an OK?
           {                                       // yes, authorize the account
                 Authorize(sUser.c_str(),sPassword.c_str());
           }
           else 
                 return TRUE;                      // ERROR HANDLING needed here
        }
        return TRUE; 
}


Listing Eleven


int TAsyncPOPSocket::Authorize(const char* pName, const char* pPassword)
{
        if (!bConnected)              // error catch for no connection
        {
             // ERROR HANDLER  needed here.
        }                                  
        string s = POP_USER;
        s += pName;
        SyncSendLine(s.c_str());
        WSAAsyncSelect(sock,HWindow,SM_POPAUTHORIZE,FD_READ);
        return 0;
}

Listing Twelve


LRESULT TAsyncPOPSocket::SmPopAuthorize(WPARAM /* wp */, LPARAM lp)
{
        WSAAsyncSelect(sock,HWindow,0,0);               // turn off async msgs
        if (HIWORD(lp) == 0 && LOWORD(lp) == FD_READ)   // check for error
        {                                               //
          if (MailResponse() == POP_OK)                 // did we get POP_OK
          {                                             // yes

                string s = POP_PASS;                    // format password
                s += sPassword;                         // string
                SyncSendLine(s.c_str());                // and send it
                                           // response goes to SmPopPassword
                WSAAsyncSelect(sock,HWindow,SM_POPPASSWORD,FD_READ); 
                return TRUE;                            //
           }                                    
           // else needs ERROR HANDLER
        }                                              //   
        return TRUE;                                   // ERROR HANDLER  
}


Listing Thirteen


LRESULT TAsyncPOPSocket::SmPopPassword(WPARAM /* wp */, LPARAM lp)
{
        WSAAsyncSelect(sock,HWindow,0,0);     // turn off async msgs
        if (HIWORD(lp) == 0 && LOWORD(lp) == FD_READ) // check for error
        {                                     //
          if (MailResponse() == POP_OK)       // check for POP_OK
          {                                   //
                bAuthorized = TRUE;           // got it, set flag
                GetMessageCount();            // start message retrieval
                return TRUE;                  //
          }                                   //
          else                                //
            return TRUE;                      // ERROR HANDLER
        }                                     //
        return TRUE;                          // ERROR HANDLER  
}


Copyright © 1995, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.