The intention of this article is to finish with the use of POP3 (Post Office Protocol 3), implementing everything we have seen so far.
Objecdtives
At this point, we still have to talk about something very important: the processing of the files received in a message. This has an additional level of complexity when the files are binary files, since they will come encoded with Encode64. Once we finish with the theory, we can see how to implement the classes so that they can be used by the test application.
The issues we will develop will be:
Receiving attached files
Receiving files is nothing else than parsing the text that we have received, with certain rules. The first is that you have to detect the "boundary" that will tell us the separator for every part of the e-mail.
Figure 1: Body part
Once we have done this, we can scan through all the attachments that the e-mail has, until we find the one that ends with the boundary name plus two hyphens, "--", indicating that there are no further attachments in the message. Every attachment is located between two boundaries, as shown in the figure.
Figure 2: Attachment
The message has properties that indicate how the attachment is encoded, what is the original filename, and whether it really is an attachment or not, for what is within the limit may be the message body.
Text files and binary files
The content of the boundary allows us to obtain the information from the attached file, but, as we saw, have been encoded because it was a binary file, or it may have been sent as a plain text file. The second case is easier, because, if the file was sent as 8-bit ASCII, the content within the boundaries is copied directly to the file without any additional processing. The problem appears when the file was encoded, for in this case, the text is not the real file content, but rather, it was processed. In this case, you have to do a DeCode64 (in the SMTP, when the binary file is sent, it has to do an Endoce64).
The text file doesn't make much sense when we analyze it, therefore, we will concentrate on the encoding. For this purpose, we need to see how a Decode of the received file is done; this is basically the inverse process of what we did when we sent the encoded file (we saw this in the section on sending e-mail with SMTP).
We will analyze the function first in C++, since it is a little more natural, and then, we will see some implementation details that some of you may already have seen in the SMTP Encode implementation.
bool InternalDecode64(const unsigned char *in, unsigned inLen, unsigned char *out, unsigned &outmax) { char *pdest; int lnBloque = (int)( inLen / 4 ), lnInRel; int lnCantRelleno = 0; int lnPos1, lnPos2, lnPos3, lnPos4; // Counts how many time the padding is used.- for( lnInRel = inLen-1; (unsigned int)lnInRel <= inLen; lnInRel-- ) { if( in[lnInRel] == '=' ) lnCantRelleno++; else break; } for( lnInRel = 0; lnInRel < lnBloque; lnInRel ++) { pdest = strchr( m_base64Table, in[lnInRel * 4] ); lnPos1 = (int)(pdest - m_base64Table); pdest = strchr( m_base64Table, in[lnInRel * 4 + 1] ); lnPos2 = (int)(pdest - m_base64Table); pdest = strchr( m_base64Table, in[lnInRel * 4 + 2] ); lnPos3 = (int)(pdest - m_base64Table); pdest = strchr( m_base64Table, in[lnInRel * 4 + 3] ); lnPos4 = (int)(pdest - m_base64Table); out[lnInRel * 3 ] = (char)( ( lnPos1 << 2 ) + ( ( lnPos2 & 48 ) >> 4 ) ); out[lnInRel * 3 + 1] = (char)( ( ( lnPos2 & 15 ) << 4) + ( ( lnPos3 & 60) >> 2 ) ); out[lnInRel * 3 + 2] = (char)( ( ( lnPos3 & 3 ) << 6) + lnPos4 ); } // Eliminates the padding.- outmax = (lnBloque * 3) - lnCantRelleno; out[ outmax ] = '\0'; return true; }
For those of you who don't know C++, I will briefly explain some of the operators used in the code:
For example, the numbers 3 and 6 would be as follows: 011 110 010 && result of the binary "And"
The variable m_base64Table contains a table of characters that will be used to decode the original string.
The Base64 alphabet
Now we can return to the algorithm. The first thing it does is obtain the amount of padding that was generated by Encode (the "=" characters at the end of the string), so that the string can later be traversed from the beginning to the end. In each pass, a relationship is sought between every byte and the string where the 64 bytes are stored. These positions are then combined, forming three new bytes, which are part of the original string that was then encoded.
After seeing this, we can have an idea how to implement the class in VFP. One possible implementation might be like this:
PROCEDURE Decode64( lsIn, lsOut ) LOCAL inLen, inLen2, lnInRel, lnBloque LOCAL lnByte1, lnByte2, lnByte3 LOCAL lnPos1, lnPos2, lnPos3, lnPos4 LOCAL lnCantRelleno inLen = LEN( lsIn ) lnBloque = ROUND( inLen / 4, 0 ) inLen2 = lnBloque * 3 lnInRel = 1 lsOut = "" lnCantRelleno = OCCURS( "=", lsIn ) FOR lnInRel = 0 TO lnBloque *|-- Search at the relevant positions.- lnPos1 = AT( SUBSTR( lsIn, lnInRel * 4 + 1, 1 ), This.m_base64List ) - 1 lnPos2 = AT( SUBSTR( lsIn, lnInRel * 4 + 2, 1 ), This.m_base64List ) - 1 lnPos3 = AT( SUBSTR( lsIn, lnInRel * 4 + 3, 1 ), This.m_base64List ) - 1 lnPos4 = AT( SUBSTR( lsIn, lnInRel * 4 + 4, 1 ), This.m_base64List ) - 1 lnPos1 = IIF( lnPos1 < 0, 0, lnPos1 ) lnPos2 = IIF( lnPos2 < 0, 0, lnPos2 ) lnPos3 = IIF( lnPos3 < 0, 0, lnPos3 ) lnPos4 = IIF( lnPos4 < 0, 0, lnPos4 ) lnByte1 = BITLSHIFT( lnPos1, 2 ) + BITRSHIFT( BITAND( lnPos2, 0x30 ), 4 ) lnByte2 = BITLSHIFT( BITAND( lnPos2, 0xF ), 4 ) + BITRSHIFT( BITAND( lnPos3, 0x3C ), 2 ) lnByte3 = BITLSHIFT( BITAND( lnPos3, 0x3 ), 6 ) + lnPos4 lsOut = lsOut + CHR( lnByte1 ) + CHR( lnByte2 ) + CHR( lnByte3 ) NEXT *|-- Eliminate the padding.- lsOut = SUBSTR( lsOut, 1, inLen2 - lnCantRelleno ) inLen2 = 1 ENDPROC
Now we are able to transform the string of the encoded file into its original form without any problem; we only have to take care of eliminating the "ENTER" in the string, sent by the POP3, since these are not part of the original Encode, rather, they were sent so as not to surpass the maximum line size supported by the SMTP server.
The function "Decode64" which we developed can be substituted easily by the function "STRCONV( lsEncode, 14 )", available since VFP 8.0, the only problem is that this will make our class "discriminate" against earlier versions of VFP, and I really like VFP 6.0 a lot.
Implementation of the WPop3Mail class
With the theory we have seen so far, we can know start to develop our class, which will be in charge of connecting the server and pick up the messages stored on it, allowing us to bring them to our terminal.
For this purpose I am going to show you a class that will only have the task of connecting and disconnecting from the POP3 server, and that contains a collection of "Mail" objects. Every one of those "Mail" objects will be responsible of handling only a single message located on the server. Our class will look like this:
DEFINE CLASS WPop3 AS CUSTOM OLEPUBLIC .... NAME = "WPop3" *|-- User of the connection for the POP3.- m_UsrPOP3 = "" *|-- Password for the POP3 user.- m_PassPOP3 = "" *|-- Array with the list of mails stored on the server.- DIMENSION m_MailList[1] .... ENDDEFINE
Thus, we only need to store the user name and his password, and then we can receive the messages. Therefore, our class will be in charge of generating our collection of "Mail" objects so that we can handle them one at a time. Let's see the implementation of a method that does this:
PROCEDURE RecivirMails LOCAL iMail, lsConnMsg LOCAL lnStart, lnEnd *|-- Create the Socket in the clase WSocket.- IF ( !THIS.m_SocketConn.CrearSock() ) This.m_Error = "Can't create the Socket" RETURN .F. ENDIF This.SetInfoCon() *|-- Connect to the mail server.- IF ( THIS.m_SocketConn.ConectarServer() ) lsConnMsg = THIS.m_SocketConn.Recibir() IF( This.VerificarRespuesta( lsConnMsg, NO_ERRORS ) ) lnStart = AT( "<", lsConnMsg ) IF( lnStart > 0 ) lnEnd = AT( ">", lsConnMsg ) IF( lnStart > 0 AND lnEnd > lnStart ) This.m_sTimeStamp = SUBSTR( lsConnMsg, lnStart, lnEnd - lnStart + 1 ) ENDIF ELSE *|-- Doesn't support APOP RFC1725, MD5 RFC1321 This.m_sTimeStamp = "" ENDIF IF( This.Login() ) IF( This.ObtenerCantMail() ) *|-- Get the list of e-mails.- IF( This.m_NumMsg > 0 ) DIMENSION This.m_MailList[This.m_NumMsg] FOR iMail = 1 TO This.m_NumMsg This.m_MailList[iMail] = CREATEOBJECT("WMail") This.m_MailList[iMail].m_MailNum = iMail This.m_MailList[iMail].m_SocketConn = This.m_SocketConn NEXT ENDIF ELSE This.m_Error = "Can't obtain the total amount of e-mail " RETURN .F. ENDIF ELSE This.m_Error = "Incorrect user or password" RETURN .F. ENDIF ELSE This.m_Error = "Connected to the server, but server doesn't respond adequately" RETURN .F. ENDIF ELSE This.m_Error = "Can't connect to the server" RETURN .F. ENDIF ENDPROC
As we can see, the first thing to do is to create the socket and establish the connection; in case it supports MD5, it will tell us so when starting the session, as we have seen in the previous article. Then, we send our identification to the server, and obtain the amount of pending messages that have to be downloaded (in the method ObtenerCantMail), passing the STAT command to the server.
Once we have the amount of messages, the only thing left is to assemble our collection; this is what we do in the FOR, creating the objects "WMAIL" and assigning it to every position in the array.
Now, the WMAIL class is in charge of obtaining every individual message and processing it as necessary. This class is fairly big, and it isn't worthwhile to talk about something which you can see for yourself without too much complexity, therefore, we will only see some of the things it does for us.
For example, after reading a message, we will probably want to delete it from the server; for this purpose we implement the method DeleteMail.
PROCEDURE DeleteMail() LOCAL lsMsgReturn IF( This.m_SocketConn.Enviar( "DELE " + ALLTRIM( STR( This.m_MailNum ) ) + ENTER ) ) IF( This.VerificarRespuesta( This.m_SocketConn.Recibir(), NO_ERRORS ) ) RETURN .T. ENDIF ELSE RETURN .F. ENDIF ENDPROC
As we can see, the only thing we do in this case is send the "DELE" command to the server, so that the server does the work.
We can also save the attachments that we receive in our e-mail. For this purpose, we use the class method GrabarAdjunto. This method receives, as a parameter, the number of the attachment we want to save, and the name we want to give to this attachment.
PROCEDURE GrabarAdjunto( lnNumMail, lsFileName ) LOCAL lsAdjunto, lnDesde, lnHasta LOCAL lsLine lnDesde = This.MailAdjuntos[lnNumMail, 2] lnHasta = This.MailAdjuntos[lnNumMail, 3] lsAdjunto = SUBSTR( This.MailData, lnDesde, lnHasta - lnDesde ) *|-- Cuts off the surplus.- lsLine = MLINE( lsAdjunto, MEMLINES( lsAdjunto ) ) IF( !EMPTY( lsLine ) ) lsAdjunto = SUBSTR( lsAdjunto, 1, LEN( lsAdjunto ) - LEN( lsLine ) ) ENDIF *|-- Now, take away the CHR(13) and the CHR(10), since these are not part of *|-- Encode64, but rather a part of POP3.- lsAdjunto = STRTRAN( lsAdjunto, CHR(10), "" ) lsAdjunto = STRTRAN( lsAdjunto, CHR(13), "" ) RETURN FileDecode64( lsAdjunto, lsFileName ) ENDPROC
Here we mention the function "FileDecode64", which is stored in the library FoxUtils.fll. We have already seen part of the implementation, so now we only need to publish the function. We can do this more or less as follows:
FoxInfo myFoxInfo[] = { {"FileDecode64", (FPFI) FileDecode64, 2, "CC"}, }; extern "C" { FoxTable _FoxTable = { (FoxTable *) 0, sizeof(myFoxInfo)/sizeof(FoxInfo), myFoxInfo };
We will now see how we can use this class, from a simple VFP application.
oMail = CREATEOBJECT( "WPop3" ) *|-- Usario y password oMail.m_HostName = "ServerPOP3" oMail.m_UsrPOP3 = "ianni_roberto" oMail.m_PassPOP3 = "123456" oMail.m_TipoAut = 0 && Plain IF( oMail.RecivirMails() ) FOR I = 1 TO oMail.m_NumMsg *|-- Propiedades.... ?oMail.m_MailList[I].MailSubject ?oMail.m_MailList[I].MailDate ?oMail.m_MailList[I].MailBody ?oMail.m_MailList[I].MailReceiver *|-- For the attachments, the FoxUtils.fll class is required, *|-- available at http://www.foxlook.com.ar/developer/help/FoxUtils.Fll.zip *|-- Loop through the attachments.- FOR J = 1 TO oMail.m_MailList[I].MailCantAdjuntos oMail.m_MailList[I].GrabarAdjunto( J, "C:\" + oMail.m_MailList[I].MailAdjuntos[I, 1] ) NEXT *|-- Save the entire e-mail to disk. STRTOFILE( oMail.m_MailList[I].MailData, "C:\Mail" + ALLTRIM( STR( I ) ) + ".txt" ) NEXT *|-- Disconnect the POP3 oMail.QuitPOP() oMail.Desconectarme() ELSE MESSAGEBOX( "Error: " + ALLTRIM( oMail.m_Error ) ) ENDIF RELEASE oMail
As we see in the example, it is very simple to use the class that receives the messages from the POP3 server and saves them to the local disk. We only have to specify the name of the server, the user and the password in order to receive all the messages in our mailbox, including all the attachments.
Test application
As a test application, I include a simple program called "FoxLook Monitor", which stays in the SysTray, checking for messages on the server, and, in case it finds them, showing a nice little window which informs us about the new message (similar to MSN).
As some of you know, or will notice, the test application is called "FoxLook Monitor", because I called the project for sending and receiving messages "FoxLook". For those interested, the Web site is www.foxlook.com.ar/developer. There, you can obtain the complete classes and the latest versions and improvements done to the classes.