angle-up arrow-clockwise arrow-counterclockwise arrow-down-up arrow-left at calendar card-list chat check envelope folder house info-circle pencil people person person-plus phone plus question-circle search tag trash x

IMAPClient und Holen body parts

26 Juni 2020 Aktualisiert 25 Juli 2020 durch Peter
In Mail

Das Holen des IMAP -Servers body parts mit Python und IMAPClient lohnt sich. Die Reduzierung der Download-Zeiten ist signifikant.

post main image
https://unsplash.com/@tobiastu

Ich beschloss, den Schwerpunkt vorübergehend von der Entwicklung der Software für mein CMS/Blog auf ein kleineres Projekt zu verlagern. Der Hauptgrund ist, dass ich hoffte, neue Dinge über Python zu erfahren, die nützlich sind.

Ich wollte schon immer meine eigene IMAP Client-Software haben. Vielleicht war meine Wahl auch stark von einigen Ärgernissen über den IMAP -Client Dekko2 für Ubuntu Touch, das Betriebssystem meines Mobiltelefons, beeinflusst. Ich weiß, dass ich mit der Existenz von Dekko2 zufrieden sein sollte, und ich bin mir wirklich sicher, dass es auf meinem OnePlus One gut läuft. Aber einige Aktionen sind langsam oder liefern nicht das gewünschte Ergebnis. Zum Beispiel ist die Suche langsam. Und manchmal scheinen die Ergebnisse nicht vollständig zu sein.

Dekko2 ist ein nettes Stück Software, es spart Bandbreite und Platz auf Ihrem Telefon, aber persönlich interessiert mich das nicht. Mobilfunkabonnements werden immer billiger, und der Telefonspeicher ist oft mindestens 8 GB groß.

Mein Ansatz

Schreiben Sie einen einfachen IMAP -Client. Speichern Sie die Daten des E-Mail-Kopfes und den KÖRPER-TEXT (text/plain) und den KÖRPER HTML (text/html) in einer lokalen Datenbank. Dann haben wir die meisten Daten, die wir benötigen, auf unserem PC oder Telefon. Wir laden nur die Anhänge herunter, Inline-Bilder auf Anfrage.

SQLite ist natürlich der richtige Weg, gut unterstützt und stabil (aber niemals in einer hochleistungsfähigen Multithreading-Anwendung verwenden). Und die Suche ist schnell und nimmt keine Bandbreite in Anspruch.

Ich muss sagen, dass mich auch die Aussage auf der Website SQLite fasziniert hat, dass die Speicherung von Daten in einem BLOB schneller sei als die Speicherung als Datei. Nun, zumindest solange die Größe nicht zu groß ist. Die meisten E-Mails, die ich erhalte, sind weniger als 20-30 KB groß, so dass dies der Regel entspricht.

Ich habe mich entschieden, das Paket IMAPClient zu verwenden, um mein Leben etwas einfacher zu machen. Es ist sehr einfach zu benutzen ... wenn man anfängt.

Über welche Datenmenge sprechen wir

Ich habe ein E-Mail-Konto mit einem INBOX von etwa 5000 Nachrichten. Mit dem Paket IMAPClient ist es einfach, das Paket ENVELOPE herunterzuladen. Dieses enthält Informationen wie:

  • Datum und Uhrzeit
  • Thema
  • E-Mail-Adressen: an, von, cc, bcc, usw.

Wenn wir den ENVELOPE abrufen, können wir auch den BODYSTRUCTURE abrufen. Dies ist unerlässlich, da wir die BODYSTRUCTURE benötigen, um die Teile BODY TEXT und BODY HTML herunterzuladen. Wir benötigen diese Teile für die Suche.

Ich weiß nichts über Sie, aber ich interessiere mich nicht für Bilder. Und wenn wir ein Bild benötigen, können wir es jederzeit herunterladen. Dasselbe gilt für Anhänge.

Ich entschied mich, den BODYSTRUCTURE zusammen mit der Nachricht zu speichern und diesen später zu entschlüsseln, als ich den Download der benötigten Teile anforderte. Das Laden von Nachrichten besteht also aus zwei Stufen.

In der ersten Phase laden wir die ENVELOPE und BODYSTRUCTURE herunter. Der ENVELOPE wird in Datum, Betreff, E-Mail-Adressen dekodiert und dann werden all diese Informationen gespeichert. Nach dieser Phase haben wir eine schöne Liste von Nachrichten zu zeigen, aber ohne body parts. In der zweiten Phase dekodieren wir BODYSTRUCTURE, laden den KÖRPERTEXT und KÖRPER HTML herunter und speichern diese Informationen in der Datenbank.

Zeit für einige 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    |
+-------+---------------------------------+---------+--------------+---------------+

Da haben Sie es. Mein PC ist ein i7 und bezieht seine Daten über eine Internetverbindung. Mein Telefon ist ein OnePlus One , das unter Ubuntu Touch läuft und seine Daten über 3G/4G erhält. Natürlich gibt es viele Faktoren, die diese Zeiten beeinflussen, so dass dies nur ein Anhaltspunkt ist.

Es gibt zwei Zeiten pro Artikel. Das erste ist mit 'Debug-Meldungen an', die Log-Datei ist 1,4 GB groß, das zweite ist ohne Debug-Meldungen.

Die Zeiten gelten für das Laden einer leeren Datenbank mit Daten für 5000 Meldungen. In einer typischen Situation haben Sie bereits alle Nachrichten und erhalten nur neue (oder löschen entfernte).

Ich habe keine Anhänge einschließlich angehängter (weitergeleiteter) Nachrichten heruntergeladen. Was ist der Größenunterschied bei einem vollständigen Download? Mein Mailserver verwendet Dovecot. Das Verzeichnis Maildir für mein INBOX:

  • new: 5.7M
  • cur: 681M

Das sind insgesamt 690 MB. Das bedeutet, dass wir nicht 690 MB heruntergeladen haben - 170 MB = 520 MB. Mit anderen Worten, wenn wir 170/690 = 25% aller Daten herunterladen, können wir nach der E-Mail-Adresse suchen und den KÖRPERTEXT und KÖRPER HTML durchsuchen, ohne uns mit dem Server IMAP zu verbinden, indem wir unsere Datenbank abfragen.

SQLite und Optimierungen

Zur Speicherung der Daten verwende ich die folgenden Tabellen:

  • imap_server
  • imap_server_folder: imap_server_folder
  • imap_mail_msg
  • imap_mail_msg_addr

Performance-Optimierungen mit SQLite sind einfach, wenn Sie excutemany verwenden. Ich habe eine Tabelle für die Nachrichten und eine Tabelle für die E-Mail-Adressen und committe erst nach 100 Nachrichten. Der Zeitunterschied betrug etwa 50%.

Den größten Leistungsgewinn habe ich jedoch durch die Begrenzung der Anzahl der E-Mail-Adressen erzielt. Ich bin Mitglied einiger E-Mail-Listen, und einige Listen senden eine Nachricht an über 400 sichtbare E-Mail-Adressen im to-Feld oder cc-Feld. Ich beschloss, ENVELOPE lokal zu speichern und die Anzahl der E-Mail-Adressen pro to, cc, bcc usw. auf 20 zu begrenzen. Wenn ich mehr sehen möchte, wahrscheinlich nicht, dann kann ich die ENVELOPE immer wieder lesen und speichern und anzeigen. Wie beim Anzeigen der E-Mail können wir weitere 20 E-Mail-Adressen mit einem Link zu 380 weiteren anzeigen. Ziemlich nutzlos in den meisten Fällen.

Ich habe die Operation UPDATE nicht optimiert, wenn ich den KÖRPERTEXT und KÖRPER HTML speicherte. Ich habe irgendwo gelesen, dass sich dadurch nicht viel ändern würde, aber jede Sekunde zählt, also werde ich das später untersuchen.

IMAPClient und Abrufen von body parts KÖRPER TEXT (text/plain) und KÖRPER HTML (text/html)

Die Verwendung von IMAPClient zum Abrufen von uids aller Nachrichten ist einfach. Aber das Abrufen von body parts ist eine Herausforderung. Die zurückgegebene BODYSTRUCTURE wird von IMAPClient in tuples, lists umgewandelt. Um ein body part zu erhalten, benötigen wir die Körpernummer und diese Nummer ist nicht im BODYSTRUCTURE enthalten.

Das bedeutet, dass wir die BODYSTRUCTURE selbst abflachen und Körpernummern vergeben müssen. Ich habe im Internet einen Code gefunden, der sehr hilfreich war, siehe Links unten: 'einfacher moderner Kommandozeilen-Client'.

Nach dem Abflachen müssen wir den KÖRPER TEXT (text/plain) und KÖRPER HTML (text/html) zum Herunterladen auswählen. Ich beschloss, einen body parts ( Class ) zu erstellen, der alle relevanten Daten zum Herunterladen eines Teils enthält. Bei der Aktualisierung der Tabelle imap_mail_msg mit dem geholten KÖRPER TEXT und KÖRPER HTML wird dieser Class ebenfalls gebeizt und in der Tabelle imap_mail_msg gespeichert, falls wir später Anlagen herunterladen wollen. Hier ist die von mir verwendete Class :

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 ist ein Wörterbuch mit Eigenschaften des Anhangs. Ich habe mich noch nicht mit der Dekodierung weitergeleiteter Nachrichten befasst.

Herunterladen und Entschlüsselung body parts

Dies funktionierte bei vielen Nachrichten gut, aber bei 20 der 5000, also 0,4%, gab es eine Dekodierungsausnahme. Zum Beispiel hieß es in der Nachricht, dass die Nachricht charset 'us-ascii' war. Aber die Decodierung mit diesem charset verursachte den folgenden Fehler:

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

Glücklicherweise gibt es ein Paket namens chardet , das versucht, die Kodierung einer Zeichenkette zu erkennen. Es schlug vor, dass die Kodierung charset 'ISO-8859-1' sei und die Dekodierung keine Fehler ergab. In einer anderen Nachricht hieß es, dass charset "utf-8" sei, die Dekodierung jedoch einen Fehler ergab:

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

Chardet schlug vor, dass die Kodierung von charset 'Windows-1252' sei, und die Dekodierung mit diesem charset ergab keine Fehler. Ich habe die dekodierten Nachrichten manuell überprüft, und sie sahen gut aus. Dieser Teil des Codes:

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

Woher wissen wir, dass wir die gesamte Post entschlüsseln können?

Hier berühren wir ein großes Problem der Entwicklung eines E-Mail-Clients. Mein Code war in der Lage, alle 5000 Nachrichten fehlerfrei zu entschlüsseln. Aber wird er auch für Nachricht 5001 funktionieren? Es gibt sogar ein noch größeres Problem. Wenn die Nachricht fehlerfrei dekodiert wird, woher wissen wir dann, dass die dekodierte Nachricht korrekt ist?

Es gibt nur wenige Möglichkeiten, dies zu lösen. Eine Möglichkeit besteht darin, eine riesige Testmenge von E-Mail-Nachrichten zu erstellen und die entschlüsselten Nachrichtenteile manuell zu genehmigen. Ein besserer und sicherlich schnellerer Weg ist es jedoch, einen vorhandenen bewährten E-Mail-Client zu verwenden, ihn mit unseren E-Mails zu füttern und die entschlüsselten Nachrichtenteile mit unseren Ergebnissen zu vergleichen.

Anzeigen der E-Mails

Flask und Bootstrap sind die perfekten Werkzeuge dafür. In wenigen Stunden baue ich ein Frontend, das eine einzige Seite zeigt, die aus zwei Teilen besteht. Der obere Teil ist die Liste der E-Mails, der untere Teil ist ein IFRAME , der die E-Mail BODY TEXT oder BODY HTML anzeigt.

Zusammenfassung

Die meisten Beispiele im Internet befassen sich nur mit dem Herunterladen der vollständigen Nachricht und entschlüsseln diese dann. Das Abrufen und Entschlüsseln der E-Mail IMAP ist eine Herausforderung, weil wir die E-Mail BODYSTRUCTURE konvertieren und dann flach machen müssen, um die Nummern der E-Mail body parts zu erhalten. Das Paket IMAPClient ist sicherlich hilfreich, aber es fehlen gute Beispiele. Wir müssen eine Methode wählen, um automatisch zu prüfen, ob unsere Testmails korrekt dekodiert werden. Durch die Speicherung von ENVELOPE und BODY TEXT und BODY HTML Daten ist eine SQLite Datenbank Ich habe fast alle Informationen, die ich will, die Suche ist sehr schnell, weil sie nicht mit dem IMAP Server interagieren muss.

Links / Impressum

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

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

Mehr erfahren:
IMAP Mail

Einen Kommentar hinterlassen

Kommentieren Sie anonym oder melden Sie sich zum Kommentieren an.

Kommentare

Eine Antwort hinterlassen

Antworten Sie anonym oder melden Sie sich an, um zu antworten.