For the POP3 client in chapter 24.5.4, the component gb.net.pop3client by Sebastian Kulesz is used. In my opinion, the advantage of the component is that all POP3 commands are encapsulated in methods according to RFC 1939. Since the Pop3Client class includes, among other things, the Exec(…) method, you can also execute all POP3 commands directly. As an extension of the component, the authentication procedure APOP was implemented by the book author Hans Lehmann. The procedure ensures that the POP3 password is transmitted in encrypted form from the POP3 client to the POP3 server. A description of the APOP authentication procedure can be found in → Chapter 24.5.3. The implementation was done in such a way that the system automatically switches to the APOP procedure if the POP3 client detects that the POP3 server also offers APOP as an authentication procedure. If you switch the 'Debug' property to True in the POP3 client trial, you can follow the complete communication between POP3 client and POP3 server:
gb.net.pop3: Connecting to mx.freenet.de gb.net.pop3: Connecting to Port 995 gb.net.pop3: Encryption: Net.SSL gb.net.pop3: Change to APOP gb.net.pop3: Authenticating... gb.net.pop3: Sending: APOP wer@ist.da 1eeddd29088529659e3bc76e84c4d44c gb.net.pop3: +OK 2 messages (66766 octets). gb.net.pop3: APOP Authentification OK gb.net.pop3: Refreshing inbox cache gb.net.pop3: Sending: STAT gb.net.pop3: +OK 2 66766 gb.net.pop3: Sending: UIDL gb.net.pop3: +OK... gb.net.pop3: Sending: UIDL gb.net.pop3: +OK... gb.net.pop3: Creating new message instance for 0 gb.net.pop3: Sending: RETR 1 gb.net.pop3: +OK 1879 octets... gb.net.pop3: Creating new message instance for 1 gb.net.pop3: Sending: RETR 2 gb.net.pop3: +OK 64887 octets... gb.net.pop3: Sending: QUIT gb.net.pop3: +OK gb.net.pop3: Disconnecting...
For the settings of the created POP3 client object, the POP3 client uses the specifications that have been stored in a configuration file via the manager for e-mail accounts → Chapter 24.3.4 'Manager for e-mail accounts'. If there is no configuration file when the programme is started for the first time, the manager starts automatically. These six settings must be assigned suitable values for POP3:
and are required in the following procedures:
Public Sub GeneratePOP3Client(_Debug As Boolean) If hPOP3Client <> Null Then If hPOP3Client.Status = Net.Connected Then If hPOP3Client.Close() = False Then hPOP3Client = Null Endif Else hPOP3Client = Null Endif Endif hPOP3Client = New Pop3Client hPOP3Client.Host = acSettings[sCurAccount & "/POP3Server"] hPOP3Client.Port = CInt(acSettings[sCurAccount & "/POP3Port"]) If Upper(acSettings[sCurAccount & "/POP3Encryption"]) = "NET.SSL" Then hPOP3Client.Encrypt = 1 If Upper(acSettings[sCurAccount & "/POP3Encryption"]) = "NET.NONE" Then hPOP3Client.Encrypt = 0 If acSettings[sCurAccount &/ "POP3Authentication"] = ("Password normal") Then \ hPOP3Client.Authent = Net.AuthBasic ' 0 If acSettings[sCurAccount &/ "POP3Authentication"] = ("Password encrypted (APOP)") Then \ hPOP3Client.Authent = Net.AuthAPOP ' 3 hPOP3Client.User = acSettings[sCurAccount & "/POP3UserName"] hPOP3Client.Password = M3.E4(acSettings[sCurAccount & "/POP3UserPassword"]) ' Decrypted password hPOP3Client.Debug = _Debug End ' GeneratePOP3Client(...) Public Function ConnectToPOP3Server() As String Try hPOP3Client.Open() If Not Error Then Return "NoError" End ' ConnectToServer()
If there is a connection from the POP3 client to the POP3 server:
You can also delete individual e-mails in the mailbox on the POP3 server by marking the e-mail as the e-mail to be deleted via the number of the e-mail. You can undo this marking for all affected e-mails by using the Reset() method. The marked e-mails are only actually deleted when you terminate the TCP-IP connection.
With this information, you are able to either selectively download a specific e-mail from the mailbox on the POP3 server to a local mailbox on your home PC for a specific e-mail account or to do this for all e-mails. The POP3 client always triggers the download for all e-mails in the mailbox - but only if an e-mail is not yet in the local mailbox. In the account settings, you can also specify that all e-mails should be deleted from the POP3 server after they have been downloaded to the mailbox.
The following paragraph describes how the download of all emails present in the selected mailbox on the POP3 server has been implemented. Saving an email in the local mailbox to a specific email account is done by saving the email source text in a file. The file name for the source text is generated from the unique ID of each email.
The complete source text for the procedure is given, which is then commented on.
Source text for downloading and storing emails:
[1] Public Sub RetrieveEMails() [2] [3] Dim i, iCount, iIndex As Integer [4] Dim sFileName, sBaseName, sMessage, sMailBoxDir, sMimeMessageText, sSavePath, sUniqueIDText, sHash As String [5] Dim dMMDate As Date [6] Dim iUID As Integer [7] Dim aValue, aElement As New Variant[] [8] Dim aDownloadList As New Variant[][] [9] Dim aBaseNameList As New String[] [10] Dim hMimeMessageHeaders As MimeMessage [11] [12] If Not MMMP.SystemOnline() Then Return [13] [14] sMailBoxDir = sBaseDir &/ sCurAccount [15] [16] '---------------------------------------------------------------------------- [17] GeneratePOP3Client(True) ' True for debugging for tests [18] '---------------------------------------------------------------------------- [19] [20] If ConnectToPOP3Server() = "NoError" Then [21] sMessage = ("Connected to ") & acSettings[sCurAccount & "/POP3Server"] [22] lblStatus.Text = sMessage [23] Else [24] sMessage = ("Error connecting to the POP3 server!") [25] lblStatus.Text = sMessage [26] Return [27] Endif [28] Wait 0.2 [29] [30] If hPOP3Client.Count > 0 Then [31] For Each sFileName In Dir(sMailBoxDir, "*.*", gb.File) ' Lokales Postfach [32] aBaseNameList.Add(Scan(sFileName, "*.*.*")[1]) [33] Next [34] [35] For i = 0 To hPOP3Client.Count - 1 [36] ' Muster ListUniqueID: 4 000008394ef05e55 [37] sUniqueIDText = Scan(hPOP3Client.ListUniqueID()[i], "* *")[1] [38] sHash = Split(Base64(Digest["MD5"].Hash(sUniqueIDText)), "=")[0] [39] sHash = Replace(sHash, "+", "A") [40] sHash = Replace(sHash, "/", "B") [41] [42] sBaseName = sHash [43] [44] ' Wenn eine EMail auf dem POP3-Server noch NICHT lokal gespeichert wurde, dann wird ihre [45] ' EMail-Nummer (UID-Zahl) auf dem Server und ihr konvertierter UID-Text als sBaseName = sHash [46] ' als Basis-Dateiname in die Download-Liste eingetragen. [47] If Not aBaseNameList.Exist(sBaseName) Then [48] aValue = New Variant[] [49] iUID = CInt(Scan(hPOP3Client.ListUniqueID()[i], "* *")[0]) [50] aValue.Add(iUID) ' EMail-Nummer [51] aValue.Add(sBaseName & ".txt") ' Dateiname als konvertierter UID-Text [52] aDownloadList.Add(aValue) [53] Endif [54] Next [55] Endif [56] [57] If aDownloadList.Count = 0 [58] lblStatus.Text = ("You have no new messages!") [59] Wait 0.5 [60] lblStatus.Text = "" [61] If Connected() Then DisconnectFromPOP3Server() [62] Return [63] Else [64] lblStatus.Text = Subst$(("You have &1 new &2."), aDownloadList.Count, \ [65] IIf(aDownloadList.Count = 1, ("message"), ("messages"))) [66] Wait 0.3 [67] Endif [68] [69] lblStatus.Text = "" [70] iCount = 0 [71] wevBody.Url = Application.Path &/ "download.html" [72] [73] For Each aElement In aDownloadList [74] Inc iCount [75] lblStatus.Text = ("Download ") & Str$(iCount) & (" of ") & Str$(aDownloadList.Count) [76] iIndex = aElement[0] [77] [78] ' ------------------------------------------------------------------------------------- [79] ' Download einer EMail (EMail-Quelltext vom Typ MimeMessageText) [80] sMimeMessageText = hPOP3Client[iIndex - 1].Text [81] Wait [82] ' ------------------------------------------------------------------------------------- [83] [84] hMimeMessageHeaders = New MimeMessage(MMMP.GetMMHeaderText(sMimeMessageText)) [85] [86] dMMDate = MMMP.ReadDate(hMimeMessageHeaders.Headers["Received"]) [87] sSavePath = sMailBoxDir &/ Format(dMMDate, "yyyy-mm-dd-hh-nn-ss") & "." & aElement[1] [88] File.Save(sSavePath, sMimeMessageText) [89] Wait [90] ' ------------------------------------------------------------------------------------------------- [91] ' Löschen der EMail *auf dem Server* nach dem Download, wenn das Löschen der EMails vorgesehen ist! [92] If bDeleteMailAllowed = True Then hPOP3Client.Remove(iIndex - 1) [93] [94] UpdateListHeaders(sBaseDir &/ sCurAccount) [95] ' Anzeige einer Übersicht der lokal gespeicherten EMails [96] ' (Anhang-Symbol(optional), Betreff, Absender, Datum und Größe) [97] ShowMailHeaders(aListHeaders) [98] [99] Next ' EMail [100] [101] If aListHeaders.Count > 0 Then [102] If Dir(sBaseDir &/ sCurAccount, "*.txt", gb.File).Count > 0 Then [103] ShowMailData() ' Anzeige der EMail im internen Browser (WebView) [104] Endif [105] Endif [106] [107] lblStatus.Text = ("Download completed!") [108] Wait 0.3 [109] [110] '------------------------------------------------ [111] If Connected() Then DisconnectFromPOP3Server() [112] DestroyPOP3Client() [113] '------------------------------------------------ [114] [115] End ' RetrieveEMails()
Comment:
After the successful download, the e-mails are in the local mailbox. Whenever e-mails are mentioned in the following paragraphs, the e-mail source text of the type MimeMessage is always meant, for the processing of which the classes of the component gb.mime are used → Chapter 24.3.0 Component gb.mime.
The algorithms necessary for displaying the emails are based on the knowledge and understanding of how emails are structured internally. For this reason, a parser was first developed that determines the structure for a given email source text of the type MimeMessage and saves it as an HTML file and falls back on the parser in → Chapter 24.3.2 Class MimePart. It is noticeable that this structure has a (recursive) nested character, which is also reflected in the project source code (recursive loop) of the structure parser for emails:
'::::: MIMEMESSAGE-STRUKTUR GENERIEREN UND ALS HTML-DATEI SPEICHERN ::::::::::::::::::::::::::::::::::: ' Funktion: Structure(...) ' Parameter: MimeMessageText Typ: String ' Funktionswert: Datei-Pfad zur Struktur-Datei Typ: String Public Function Structure(MimeMessageText As String) As String Dim sHTMLData As String $sMM = New MimeMessage(MimeMessageText) sHTMLData = "<!DOCTYPE html>" & gb.NewLine sHTMLData &= "<html lang=\"de\">" & gb.NewLine sHTMLData &= " <head>" & gb.NewLine sHTMLData &= " <meta charset=\"utf-8\">" & gb.NewLine sHTMLData &= " <style>" & gb.NewLine sHTMLData &= " body{font-family:Arial,Verdana;color:darkgreen;font-size:16px;}" & gb.NewLine sHTMLData &= " </style>" & gb.NewLine sHTMLData &= " </head>" & gb.NewLine sHTMLData &= " <body>" & gb.NewLine sHTMLData &= Replace(GetMimeMessageStructure($sMM.Part), gb.NewLine, "<br>\n") sHTMLData &= " </body>" & gb.NewLine sHTMLData &= "</html>" ' Struktur-Datei temporär speichern File.Save($sBasePath &/ _STRUCTURFILENAME, sHTMLData) If $bDebug Then _PrintDebug(("Show EMail-Structure")) Return $sBasePath &/ _STRUCTURFILENAME End ' Structure(...) ' Funktion: GetMimeMessageStructure(...) ' Parameter: Part Typ: MimePart ' Funktionswert: Text der Struktur-Datei Typ: String Private Function GetMimeMessageStructure(Part As MimePart) As String Dim sBodyType As String If $bDebug Then _PrintDebug(("Create EMail-Structure")) If Not Part Then Error.Raise("MimePart not set") sBodyType = Scan(Part.Headers["Content-Type"], "*;*")[0] $sStructure = "" $sStructureH = ("STRUCTURE MIME-MESSAGE") $sStructureH &= gb.NewLine $sStructureH &= String$(String.Len($sStructureH), "-") & gb.NewLine $sStructureH &= gb.NewLine $sStructureH &= "+ " & sBodyType & gb.NewLine $sStructureB = "" ParseStructureBody($sMM.Body) ' Parse Structure: Body $sStructureA = "" $iAttachmentCount = 0 $iFlag = 0 ParseStructureAttachments(Part) ' Parse Structure: Attachments $sStructure = $sStructureH & $sStructureB & $sStructureA Return $sStructure End ' GetMimeMessageStructure(...) ' Prozedur: ParseStructureBody(...) ' Parameter: Part Typ: MimePart ' Aktion: Parsen der Struktur des MimeMessage-Bodys. ' Das Ergebnis wird in der Variablen $sStructureB gespeichert Private Sub ParseStructureBody(Part As MimePart) Dim hChild As MimePart Dim sLine As String sLine &= String$($iLevel, "| ") & "| " & gb.NewLine sLine &= String$($iLevel, "| ") & "+-" & If(Part.Count, "+ ", "- ") sLine &= Part.ContentType & " " & IIf(Part.FileName, Part.FileName & " ", "") & Part.Count & gb.NewLine $sStructureB &= sLine Inc $iLevel For Each hChild In Part ParseStructureBody(hChild) ' Recursive loop Next Dec $iLevel End ' ParseStructureBody(...) ' Prozedur: ParseStructureAttachments(Part As MimePart) ' Parameter: Part Typ: MimePart ' Aktion: Parsen der Struktur des MM-Anhangs. ' Das Ergebnis wird in der (globalen) Variablen $sStructureA gespeichert Private Sub ParseStructureAttachments(Part As MimePart) Dim hChild As MimePart Dim sLine As String If Part.Disposition = "attachment" And Str(Part.Count) = 0 Then If $iFlag = 0 Then sLine &= "|" & gb.NewLine Inc $iFlag Endif sLine &= ("+ Attachment ") & Str($iAttachmentCount + 1) & ": " & " " & Part.ContentType sLine &= " " & Part.FileName & gb.NewLine Inc $iAttachmentCount $sStructureA &= sLine Endif Inc $iLevel For Each hChild In Part ParseStructureAttachments(hChild) ' Recursive loop Next Dec $iLevel End ' ParseStructureAttachments(..) '::::: ENDE MIMEMESSAGE-STRUKTUR GENERIEREN UND ALS HTML-DATEI SPEICHERN ::::::::::::::::::::::::::::::
As a result, you receive the following display of the structure of an email in the browser from the parser for the email source text passed as an argument:
STRUKTUR MIME-MESSAGE ---------------------- + multipart/mixed | +-+ multipart/alternative 2 | | | +-- text/plain 0 | | | +-+ multipart/related 3 | | | | | +-- text/html 0 | | | | | +-- image/png bild1.png 0 | | | | | +-- image/png bild2.png 0 | + Anhang 1: text/plain body.txt + Anhang 2: image/png chart.png
Interpretation:
The next step is to use the methods of the class 'MimeMessageParser' to isolate the individual parts of an email for display according to their recursive structure, decode them and store them temporarily in a suitable way. The parts are isolated separately for body and attachment. If one uses the classes of the component gb.mime, then one does not need to worry about the decoding of an image, for example, which is base64-encoded in the email source text. The decoding takes over the data property for each part automatically, so that you only have to turn to the storage of the decoded parts. The text parts of a message are stored in an HTML file. Multimedia objects such as pictures or videos are saved in individual files and the file path is inserted as a link in the HTML file. The path to the HTML file is then saved in a variable and passed to the URL property of a WebView as a value. The attachments (optional) are decoded and temporarily stored under their original file name in a special directory. For each attachment, a button is generated whose tag property contains the path to the attachment. You can view the content of an attachment (if it is of a suitable type) or save the attachment in the dialogue.
This is how the display of an e-mail (format text/plain) with 2 attachments appears in the browser after the e-mail source text has been parsed:
Figure 24.5.3.1: GUI POP3 client
The source code for the most important procedures of the MimeMessageParser is only complicated at first glance. In my opinion, this is mainly due to the sections in which recursions determine the programme flow (→ recursive loop):
'::::: BEGINN BODY PARSEN :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ' Funktion: MessageBody(...) ' Parameter: MimeMessageText Typ: String ' Funktionswert: Datei-Pfad zum Text-Teil - Typ: String Public Function MessageBody(MimeMessageText As String) As String $cInlineCID.Clear $bIsText = False $bIsHTML = False MMMP.DeleteTempFiles($sBasePath &/ $sInlineDir) MMMP.DeleteTempFiles($sBasePath) $sMM = New MimeMessage(MimeMessageText) ParseBody($sMM.Body) If $cInlineCID.Count > 0 Then ReplaceCID() Endif If $bIsHtml Or ($bIsHtml And $bIsText) Then Return $sBasePath &/ _HTMLFILENAME Endif If $bIsText Then Return $sBasePath &/ _TEXTFILENAME Endif End ' Prozedur: ReplaceCID() - CID steht für Content-ID ' Parameter: - ' Aktion: In der HTML-Datei wird jede CID durch den Pfad zur Inline-Datei ersetzt Private Sub ReplaceCID() Dim sHTMLText As String Dim vElement As Variant sHTMLText = File.Load($sBasePath &/ _HTMLFILENAME) For Each vElement In $cInlineCID sHTMLText = Replace(sHTMLText, "cid:" & $cInlineCID.Key, $cInlineCID[$cInlineCID.Key]) Next ' Speichern der geänderten HTML-Datei File.Save($sBasePath &/ _HTMLFILENAME, sHTMLText) End ' Prozedur: ParseBody() ' Parameter: Part Typ: MimePart ' Parsen des MimeMessage-Bodys (Inline-Dateien, Nachricht) ' Die in einer HTML-Nachricht liegenden multimedialen Objekte (image, audio, video, application) ' werden in der Variablen '$cInlineCID' (Typ: Collection) mit dem originalen Datei-Namen gespeichert. ' Die Pfade zu den Objekten werden in der Variablen $cInlineCID (Typ: Collection) gespeichert. ' Eine Nachricht kann eine Text-Nachricht oder eine HTML-Nachricht oder beides sein. ' Jede Nachricht wird in einer Datei mit den Datei-Namen "html.part.html" oder "text.part.html" ' im Basis-Verzeichnis temporär gespeichert Private Sub ParseBody(Part As MimePart) Dim hChild As MimePart Dim sTextData As String If Part.Data Then If Part.Disposition = "inline" And (Part.ContentType Like "image/*" Or Part.ContentType Like "audio/*" Or Part.ContentType Like "application/*" Or Part.ContentType Like "video/*") Then File.Save($sBasePath &/ $sInlineDir &/ Part.FileName, Part.Data) $cInlineCID[Part.ContentId] = $sBasePath &/ $sInlineDir &/ Part.FileName Endif If Part.ContentType = "text/html" Then $bIsHtml = True File.Save($sBasePath &/ _HTMLFILENAME, Part.Data) Endif If Part.ContentType = "text/plain" Then sTextData = "<!DOCTYPE html>" & gb.NewLine sTEXTData &= "<html lang=\"de\">" & gb.NewLine sTextData &= " <head>" & gb.NewLine sTextData &= " <meta charset=\"utf-8\">" & gb.NewLine sTextData &= " <style>" & gb.NewLine sTextData &= " body{font-family:Verdana,sans-serif;color:darkred;font-size:16px;}" & gb.NewLine sTextData &= " </style>" & gb.NewLine sTextData &= " </head>" & gb.NewLine sTextData &= " <body>" & gb.NewLine sTextData &= Replace(Part.Data, gb.NewLine, "<br>\n") sTextData &= " </body>" & gb.NewLine sTextData &= "</html>" $bIsText = True File.Save($sBasePath &/ _TEXTFILENAME, sTextData) Endif Endif ' Part.Data ? For Each hChild In Part ParseBody(hChild) ' Recursive loop Next End '::::: ENDE BODY PARSEN ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: '::::: ANHÄNGE TEMPORÄR SPEICHERN (DATEI) ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: ' Funktion: Attachments(...) ' Parameter: MimeMessageText Typ: String ' Funktionswert: Array mit den Datei-Pfaden zu den Anhängen - Typ: String-Array Public Function Attachments(MimeMessageText As String) As String[] $aAttachmentPaths.Clear $sMM = New MimeMessage(MimeMessageText) ParseA($sMM.Part) ' Funktionswert: String-Array mit den Pfaden zu den Anhängen Return $aAttachmentPaths End ' ParseAttachments(..) ' Prozedur: ParseA(...) ' Parameter: Part Typ: MimePart ' Aktion: Parsen des MM-Anhangs. ' Jeder Anhang wird in einer Datei mit dem originalen Datei-Namen des Anhangs gespeichert ' Das Array $aAttachmentPaths wird mit den Datei-Pfaden zu den Anhängen gefüllt Private Sub ParseA(Part As MimePart) Dim hChild As MimePart If Part.Disposition = "attachment" And Part.Count = 0 Then ' Anhang (temporär) in einer Datei mit dem Namen der Original-Datei speichern File.Save($sBasePath &/ $sAttachmentsDir &/ Part.FileName, Part.Data) ' Aktuellen Datei-Pfad zum String-Array hinzufügen $aAttachmentPaths.Add($sBasePath &/ $sAttachmentsDir &/ Part.FileName) Endif For Each hChild In Part ParseA(hChild) ' Recursive loop Next End ' ParseA(..) '::::: ENDE ANHÄNGE TEMPORÄR SPEICHERN ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
Attention: Replacing the CID (content id) in the procedure ReplaceCID() only works safely if multimedia objects have been included via a CID.