angle-uparrow-clockwisearrow-counterclockwisearrow-down-uparrow-leftatcalendarcard-listchatcheckenvelopefolderhouseinfo-circlepencilpeoplepersonperson-fillperson-plusphoneplusquestion-circlesearchtagtrashx

IMAPClient en het ophalen van body parts

Het ophalen van IMAP server body parts met Python en IMAPClient is de moeite waard. De vermindering van de downloadtijden is aanzienlijk.

26 juni 2020 Bijgewerkt 25 juli 2020
In Email
post main image
https://unsplash.com/@tobiastu

Ik heb besloten om de focus tijdelijk te verleggen van het ontwikkelen van de software voor mijn CMS / Blog naar een kleiner project. De belangrijkste reden is dat ik hoopte nieuwe dingen te leren over Python die nuttig zijn.

Ik wilde altijd al mijn eigen IMAP client software hebben. Misschien was mijn keuze ook sterk beïnvloed door enige ergernis over de IMAP client Dekko2 voor Ubuntu Touch, het OS van mijn mobiele telefoon. Ik weet dat ik blij zou moeten zijn met het bestaan van Dekko2 en ik ben echt goed bezig met mijn OnePlus One. Maar sommige acties zijn traag of leveren niet het gevraagde resultaat op. Zo is het zoeken bijvoorbeeld traag. En soms lijken de resultaten niet volledig te zijn.

Dekko2 is een mooi stukje software, het bespaart bandbreedte en ruimte op je telefoon, maar persoonlijk kan het me niet schelen. Mobiele abonnementen worden steeds goedkoper en het geheugen van de telefoon is vaak minimaal 8 GB.

Mijn aanpak

Schrijf een basis IMAP client. Sla de e-mail HEADER-gegevens en BODY TEXT (text/plain) en BODY HTML (text/html) op in een lokale database. Dan hebben we de meeste gegevens die we nodig hebben op onze PC of telefoon. We downloaden alleen de bijlagen, inline beelden op aanvraag.

SQLite is de manier om te gaan natuurlijk, goed ondersteund en stabiel (maar gebruik het nooit in een high performance multi-threaded applicatie). En het zoeken is snel en neemt geen bandbreedte in beslag.

Ik moet zeggen dat ik ook gefascineerd was door de verklaring op de SQLite website dat het opslaan van gegevens in een BLOB sneller was dan het opslaan ervan als een bestand. Nou ja, in ieder geval zolang de omvang niet te groot is. De meeste e-mails die ik ontvang zijn onder de 20-30 KB, dus dit past in de regel.

Ik besloot om het IMAPClient pakket te gebruiken om mijn leven wat gemakkelijker te maken. Het is zeer eenvoudig te gebruiken ... als je begint.

Over welke hoeveelheid gegevens hebben we het?

Ik heb een e-mailaccount met een INBOX van zo'n 5000 berichten. Met het IMAPClient pakket is het eenvoudig om de ENVELOPE te downloaden. Dit bevat informatie zoals:

  • Datum en tijd
  • Onderwerp
  • E-mailadressen: naar, van, cc, bcc, etc.

Wanneer we de ENVELOPE halen, kunnen we ook de BODYSTRUCTURE halen. Dit is essentieel omdat we de BODYSTRUCTURE nodig hebben om de BODY TEXT en BODY HTML onderdelen te downloaden. We hebben deze onderdelen nodig voor het zoeken.

Ik weet niet hoe het met jou zit, maar ik geef niet om beelden. En als we een beeld nodig hebben, kunnen we het altijd downloaden. Hetzelfde geldt voor bijlagen.

Ik heb besloten om de BODYSTRUCTURE op te slaan met het bericht en deze later te decoderen als ik vraag om de benodigde onderdelen te downloaden. Het laden van berichten bestaat dus uit twee stappen.

In de eerste fase downloaden we de ENVELOPE en BODYSTRUCTURE. De ENVELOPE wordt gedecodeerd in datum, onderwerp, e-mailadressen en vervolgens wordt al deze informatie opgeslagen. Na deze fase hebben we een mooie lijst met berichten om te laten zien, maar dan zonder body parts. In de tweede fase decoderen we de BODYSTRUCTURE, downloaden we de BODY TEXT en BODY HTML, en slaan we deze informatie op in de database.

Tijd voor enkele tests.

+-------+---------------------------------+---------+--------------+---------------+
| Stage | Action                          | DB size | Time on PC   | Time on phone |
+-------+---------------------------------+---------+--------------+---------------+
|   1   | Download and store              | 25 MB   | 1.5 minutes  | 3 minutes     |
|       |  ENVELOPE  and  BODYSTRUCTURE  data |         | 0.45 seconds | 3 minutes     |
+-------+---------------------------------+---------+--------------+---------------+
|   2   | Download and store              | 182 MB  | 5 minutes    | 15 minutes    |
|       | BODY TEXT and BODY  HTML          |         | 4 minutes    | 12 minutes    |
+-------+---------------------------------+---------+--------------+---------------+

Daar heb je het. Mijn PC is een i7 en krijgt zijn gegevens via een internetverbinding. Mijn telefoon is een OnePlus One met Ubuntu Touch en krijgt zijn gegevens via 3G/4G. Natuurlijk zijn er veel factoren die van invloed zijn op deze tijden, dus dit is slechts een indicatie.

Er zijn twee keer per item. De eerste is met 'debug-berichten aan', het logbestand is 1,4 GB, de tweede is zonder debug-berichten.

De tijden zijn voor het laden van een lege database met gegevens voor 5000 berichten. In een typische situatie heb je alle berichten al en krijg je alleen nieuwe berichten (of verwijderde berichten).

Ik heb geen bijlagen met bijgevoegde (doorgestuurde) berichten gedownload. Wat is het verschil in grootte met een volledige download? Mijn mailserver gebruikt Dovecot. De Maildir directory voor mijn INBOX:

  • new: 5.7M
  • cur: 681M

Dit is een totaal van 690 MB. Dit betekent dat we 690 MB - 170 MB = 520 MB niet hebben gedownload. Met andere woorden, door het downloaden van 170/690 = 25% van alle data, kunnen we zoeken op email adres en de BODY TEXT en BODY HTML doorzoeken zonder verbinding te maken met de IMAP server door onze database te bevragen.

SQLite en optimalisaties

Om de gegevens op te slaan gebruik ik de volgende tabellen:

  • imap_server
  • imap_server_folder
  • imap_mail_msg
  • imap_mail_msg_addr

Prestatieoptimalisaties met SQLite zijn eenvoudig wanneer u excutemany gebruikt. Ik heb een tabel voor de berichten en een tabel voor de e-mailadressen en pas na 100 berichten vastleggen. Het verschil in tijd was ongeveer 50%.

Maar de grootste prestatiewinst die ik heb behaald was door het beperken van het aantal e-mailadressen. Ik ben lid van sommige e-mail lijsten en sommige lijsten sturen een bericht naar meer dan 400 zichtbare e-mail adressen, in het to-veld of cc-veld. Ik besloot de ENVELOPE lokaal op te slaan en het aantal e-mailadressen per, cc, bcc, enz. te beperken tot 20. Als ik meer wil zien, waarschijnlijk niet, dan kan ik de ENVELOPE altijd weer lezen en opslaan en laten zien. Net als bij het tonen van de e-mail, kunnen we nog eens 20 e-mailadressen tonen met een link naar 380 meer. Vrij nutteloos in de meeste gevallen.

Ik heb de werking van UPDATE niet geoptimaliseerd bij het opslaan van de BODY TEXT en BODY HTML. Ik heb ergens gelezen dat dit niet veel zou veranderen, maar elke seconde telt dus ik zal dit later onderzoeken.

IMAPClient en het ophalen van body parts BODY TEXT (text/plain) en BODY HTML (text/html)

Het gebruik van IMAPClient om de uids van alle berichten op te halen is eenvoudig. Maar om body parts op te halen is een uitdaging. De teruggestuurde BODYSTRUCTURE wordt door IMAPClient omgezet in tuples, lists. Om een body part op te halen hebben we het lichaamsnummer nodig en dit nummer staat niet in de BODYSTRUCTURE.

Dit betekent dat we de BODYSTRUCTURE zelf moeten afvlakken en lichaamsnummers moeten toewijzen. Ik vond wat code op het internet die erg nuttig was, zie onderstaande links: eenvoudige moderne commandoregelcliënt'.

Na het afvlakken moeten we de BODY TEXT (text/plain) en BODY HTML (text/html) selecteren om te downloaden. Ik heb besloten een body parts Class te maken die alle relevante gegevens bevat om een onderdeel te downloaden. Bij het updaten van de tabel imap_mail_msg met de opgehaalde BODY TEXT en BODY HTML wordt deze Class ook ingemaakt en opgeslagen in tabel imap_mail_msg voor het geval we later bijlagen willen downloaden. Hier is de Class die ik gebruik:

class BodystructurePart:

    def __init__(self, 
        part=None,
        body_number=None,
        content_type=None,
         charset=None,
        size=None,
        decoder=None,
        is_inline_or_attachment=None,
        is_inline=None,
        inline_or_attachment_info=None,
        is_attached_message=None
        ):
        self.part = part
        self.body_number = body_number
        self.content_type = content_type
        self.charset  =  charset
        self.size = size
        self.decoder = decoder
        self.is_inline_or_attachment = is_inline_or_attachment
        self.is_inline = is_inline
        self.inline_or_attachment_info = inline_or_attachment_info
        self.is_attached_message = is_attached_message

Inline_or_attachment_info is een woordenboek met eigenschappen van de bijlage. Ik heb nog niet gekeken naar het decoderen van doorgestuurde berichten.

Downloaden en decoderen van body parts

Dit werkte prima voor veel berichten, maar voor 20 van de 5000, 0,4%, was er een decoderingsuitzondering. Zo stond in het bericht dat de charset 'us-ascii' was. Maar het decoderen met deze charset veroorzaakte de volgende fout:

'ascii' codec can't decode byte 0xfb in position 6494: ordinal not in range(128)

Gelukkig is er een pakket genaamd chardet dat de codering van een string probeert te detecteren. Het suggereerde dat de codering charset 'ISO-8859-1' was en de decodering gaf geen fouten. Een ander bericht zei dat de charset 'utf-8' was, maar gaf een decodeerfout:

'utf-8' codec can't decode byte 0xe8 in position 2773: invalid continuation byte

Chardet stelde voor de codering charset was 'Windows-1252' en het decoderen met deze charset gaf geen fouten. Ik heb de gedecodeerde berichten handmatig gecontroleerd en ze zagen er prima uit. Dit deel van de code:

    if bodystructure_part.content_type in ['text/plain', 'text/html']:

        BODY = 'BODY[{}]'.format(bodystructure_part.body_number)
        fetch_result =  imap_server.fetch([msg_uid], [BODY])

        if msg_uid not in fetch_result:
            if dbg: logging.error(fname  +  ': msg_uid = {} not in fetch_result'.format(msg_uid))
            continue

        if BODY not in fetch_result[msg_uid]:
            if dbg: logging.error(fname  +  ': BODY not in fetch_result[msg_uid = {}]'.format(msg_uid))
            continue

        data = fetch_result[msg_uid][BODY]

        if bodystructure_part.decoder == b'base64':
            decoded_data = base64.b64decode(data)
        elif bodystructure_part.decoder == b'quoted-printable':
            decoded_data = quopri.decodestring(data)
        else:
            decoded_data = data
        
        # this may fail if  charset  is wrong
        is_decoded = False
        try:
            text = decoded_data.decode(bodystructure_part.charset)
            is_decoded = True
        except Exception as e:
            logging.error(fname  +  ': msg_uid = {}, problem decoding decoded_data with bodystructure_part.charset  = {}, e = {}, decoded_data = {}'.format(msg_uid, bodystructure_part.charset, e, decoded_data))

        if not is_decoded:
            # try to get encoding
            r =  chardet.detect(decoded_data)
             charset  = r['encoding']
            try:
                text = decoded_data.decode(charset)
                is_decoded = True
            except Exception as e:
                logging.error(fname  +  ': msg_uid = {}, problem decoding decoded_data with detected  charset  = {}, e = {}, decoded_data = {}'.format(msg_uid,  charset, e, decoded_data))
                
        if not is_decoded:
            logging.error(fname  +  ': msg_uid = {}, cannot decode'.format(msg_uid))

Hoe weten we dat we alle berichten kunnen decoderen?

Hier raken we een groot probleem van het ontwikkelen van een e-mail client. Mijn code was in staat om alle 5000 berichten zonder fouten te decoderen. Maar werkt het ook voor bericht 5001? Er is nog een groter probleem. Als het bericht zonder fouten wordt gedecodeerd, hoe weten we dan dat het gedecodeerde bericht correct is?

Er zijn weinig manieren om dit op te lossen. Een manier is om een enorme testset van e-mailberichten te maken en de gedecodeerde berichtonderdelen handmatig goed te keuren. Maar een betere en zeker snellere manier is om een bestaande bewezen e-mailclient te gebruiken, deze te voeden met onze e-mails en de gedecodeerde berichtdelen te vergelijken met onze resultaten.

De e-mails bekijken

Flask en Bootstrap zijn hiervoor het perfecte gereedschap, In een paar uur tijd bouw ik een frontend die een enkele pagina laat zien die uit twee delen bestaat. Het bovenste deel is de lijst met e-mails, het onderste deel is een IFRAME die de e-mail BODY TEXT of BODY HTML laat zien.

Samenvatting

De meeste voorbeelden op het internet gaan alleen over het downloaden van het volledige bericht en vervolgens over het decoderen ervan. Het ophalen en decoderen van IMAP e-mail is een uitdaging omdat we de BODYSTRUCTURE moeten omzetten en vervolgens moeten afvlakken om de nummers van de body parts te krijgen. Het IMAPClient pakket helpt zeker, maar mist goede voorbeelden. We moeten een methode selecteren om automatisch te controleren of onze testmails correct zijn gedecodeerd. Door het opslaan van ENVELOPE en BODY TEXT en BODY HTML data is een SQLite database heb ik bijna alle informatie die ik wil, het zoeken is erg snel omdat het niet hoeft te interageren met de IMAP server.

Links / credits

IMAPClient
https://imapclient.readthedocs.io/en/2.1.0/

simple modern command line client
https://github.com/christianwengert/mail

Laat een reactie achter

Reageer anoniem of log in om commentaar te geven.

Opmerkingen

Laat een antwoord achter

Antwoord anoniem of log in om te antwoorden.