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

IMAPClient et recherche body parts

Récupérez le serveur IMAP avec Python et IMAPClient , cela vaut la peine. La réduction des temps de téléchargement est significative.

26 juin 2020 Mise à jour 25 juillet 2020
Dans Email
post main image
https://unsplash.com/@tobiastu

J'ai décidé de passer temporairement du développement du logiciel pour mon CMS / Blog à un projet plus petit. La raison principale est que j'espérais apprendre de nouvelles choses utiles sur Python .

J'ai toujours voulu avoir mon propre logiciel client IMAP . Peut-être que mon choix a aussi été fortement influencé par une certaine contrariété concernant le client IMAP pour Ubuntu Touch, le système d'exploitation de mon téléphone portable. Je sais que je devrais être content de l'existence de Dekko2 et je suis, vraiment, son bon fonctionnement sur mon OnePlus One. Mais certaines actions sont lentes ou ne donnent pas le résultat escompté. Par exemple, la recherche est lente. Et parfois, les résultats ne semblent pas complets.

Dekko2 est un beau logiciel, il permet d'économiser de la bande passante et de l'espace sur votre téléphone, mais personnellement, je ne m'en soucie pas. Les abonnements de téléphonie mobile sont de moins en moins chers et la mémoire du téléphone est souvent de 8 Go minimum.

Mon approche

Écrivez un client IMAP de base. Stocker les données de l'en-tête du courriel et le texte du corps (text/plain) et le corps HTML (text/html) dans une base de données locale. Nous avons alors la plupart des données dont nous avons besoin sur notre PC ou notre téléphone. Nous ne téléchargeons que les pièces jointes, les images en ligne sur demande.

SQLite est la solution idéale, bien sûr, bien supportée et stable (mais ne l'utilisez jamais dans une application multi-threading à hautes performances). De plus, la recherche est rapide et ne nécessite pas de bande passante.

Je dois dire que j'ai également été fasciné par la déclaration sur le site web SQLite selon laquelle le stockage des données dans un BLOB était plus rapide que le stockage sous forme de fichier. Du moins, tant que la taille n'est pas trop grande. La plupart des courriels que je reçois ont une taille inférieure à 20-30 Ko, ce qui correspond à la règle.

J'ai décidé d'utiliser le paquet IMAPClient pour me faciliter un peu la vie. Il est très facile à utiliser ... quand vous commencez.

De quelle quantité de données parlons-nous

J'ai un compte de courrier électronique avec un INBOX d'environ 5000 messages. Avec le paquet IMAPClient , il est facile de télécharger le ENVELOPE. Celui-ci contient des informations comme :

  • Date et heure
  • Sujet
  • Adresses électroniques : à, de, cc, bcc, etc.

Quand on va chercher le ENVELOPE , on peut aussi aller chercher le BODYSTRUCTURE . Ceci est essentiel car nous avons besoin du BODYSTRUCTURE pour télécharger les parties BODY TEXT et BODY HTML . Nous avons besoin de ces parties pour la recherche.

Je ne sais pas pour vous, mais je ne me soucie pas des images. Et si nous avons besoin d'une image, nous pouvons toujours la télécharger. Il en va de même pour les pièces jointes.

J'ai décidé de stocker le BODYSTRUCTURE avec le message et de le décoder plus tard lorsque je demande à télécharger les parties requises. Le chargement des messages se fait donc en deux étapes.

Dans la première étape, nous téléchargeons les fichiers ENVELOPE et BODYSTRUCTURE. Le ENVELOPE est décodé en date, objet, adresses électroniques, puis toutes ces informations sont stockées. Après cette étape, nous avons une belle liste de messages à afficher, mais sans aucun body parts. Dans la deuxième étape, nous décodons le BODYSTRUCTURE, nous téléchargeons le BODY TEXT et le BODY HTML, et nous stockons ces informations dans la base de données.

Il est temps de faire quelques 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    |
+-------+---------------------------------+---------+--------------+---------------+

Voilà, vous l'avez. Mon PC est un i7 et reçoit ses données via une connexion Internet. Mon téléphone est un OnePlus One fonctionnant avec le Ubuntu Touch et reçoit ses données via la 3G/4G. Bien sûr, de nombreux facteurs influencent ces temps, ce n'est donc qu'une indication.

Il y a deux fois par article. La première fois avec "messages de débogage activés", le fichier journal est de 1,4 Go, la seconde fois sans messages de débogage.

Les temps sont ceux du chargement d'une base de données vide avec des données pour 5000 messages. Dans une situation typique, vous avez déjà tous les messages et vous ne recevez que les nouveaux (ou vous effacez ceux qui ont été supprimés).

Je n'ai pas téléchargé les pièces jointes, y compris les messages joints (transmis). Quelle est la différence de taille avec un téléchargement complet ? Mon serveur de messagerie utilise Dovecot. Le répertoire Maildir pour mon INBOX :

  • new: 5.7M
  • cur: 681M

Cela représente un total de 690 Mo. Cela signifie que nous n'avons pas téléchargé 690 Mo - 170 Mo = 520 Mo. En d'autres termes, en téléchargeant 170/690 = 25 % de toutes les données, nous pouvons effectuer une recherche par adresse électronique et rechercher le TEXTE DU CORPS et le CORPS HTML sans nous connecter au serveur IMAP en interrogeant notre base de données.

SQLite et les optimisations

Pour stocker les données, j'utilise les tableaux suivants :

  • imap_server
  • imap_server_folder
  • imap_mail_msg
  • imap_mail_msg_addr

Les optimisations des performances avec SQLite sont faciles lorsque vous utilisez excutemany. Je dispose d'une table pour les messages et d'une table pour les adresses électroniques et je ne valide qu'après 100 messages. La différence de temps est d'environ 50 %.

Mais le plus grand gain de performance que j'ai réalisé a été de limiter le nombre d'adresses électroniques. Je suis membre de certaines listes d'adresses électroniques et certaines listes envoient un message à plus de 400 adresses électroniques visibles, dans le champ to-field ou cc-field. J'ai décidé de stocker le ENVELOPE local et de limiter à 20 le nombre d'adresses électroniques par to, cc, bcc, etc. Si je veux en voir plus, probablement pas, alors je peux toujours relire les ENVELOPE et les stocker et les afficher. Comme pour l'affichage du courrier électronique, nous pouvons afficher 20 adresses électroniques supplémentaires avec un lien vers 380 autres. Plutôt inutile dans la plupart des cas.

Je n'ai pas optimisé l'opération UPDATE lors du stockage du BODY TEXT et du BODY HTML. J'ai lu quelque part que cela ne changerait pas grand-chose mais chaque seconde compte, je vais donc étudier la question plus tard.

IMAPClient et chercher body parts BODY TEXT (text/plain) et BODY HTML (text/html)

Il est facile d'utiliser le IMAPClient pour récupérer le uids de tous les messages. Mais aller chercher le body parts est un défi. Le BODYSTRUCTURE renvoyé est converti par le IMAPClient en tuples, lists. Pour obtenir un body part , nous avons besoin du numéro du corps et ce numéro ne se trouve pas dans le BODYSTRUCTURE.

Cela signifie que nous devons aplatir nous-mêmes le BODYSTRUCTURE et lui attribuer des numéros de corps. J'ai trouvé un code sur Internet qui m'a été très utile, voir les liens ci-dessous : "simple client moderne en ligne de commande".

Après l'opération d'aplatissement, nous devons sélectionner le BODY TEXT (text/plain) et le BODY HTML (text/html) pour le téléchargement. J'ai décidé de créer un body parts Class qui contient toutes les données pertinentes pour télécharger une partie. Lors de la mise à jour de la table imap_mail_msg avec le BODY TEXT et le BODY HTML récupérés, ce Class est également prélevé et stocké dans la table imap_mail_msg au cas où nous voudrions télécharger des pièces jointes plus tard. Voici la table Class que j'utilise :

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 est un dictionnaire avec les propriétés de l'attachement. Je ne me suis pas encore penché sur le décodage des messages transférés.

Téléchargement et décodage body parts

Cela a bien fonctionné pour de nombreux messages mais pour 20 des 5000, 0,4%, il y a eu une exception de décodage. Par exemple, le message disait que le charset était "us-ascii". Mais le décodage avec ce charset a provoqué l'erreur suivante :

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

Heureusement, il existe un paquet appelé chardet qui tente de détecter l'encodage d'une chaîne de caractères. Il suggère que le codage charset est "ISO-8859-1" et que le décodage ne donne aucune erreur. Un autre message indiquait que la chaîne charset était "utf-8", mais qu'une erreur de décodage s'était produite :

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

Chardet a suggéré que le codage charset était "Windows-1252" et que le décodage avec ce charset ne donnait aucune erreur. J'ai vérifié manuellement les messages décodés et ils semblaient corrects. Cette partie du 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))

Comment savoir si nous pouvons décoder tout le courrier ?

Nous touchons ici à un gros problème de développement d'un client de messagerie électronique. Mon code était capable de décoder les 5000 messages sans erreur. Mais cela fonctionnera-t-il également pour le message 5001 ? Il y a un problème encore plus important. Si le message est décodé sans erreur, comment savoir si le message décodé est correct ?

Il y a peu de moyens de résoudre ce problème. L'une d'entre elles consiste à créer un énorme ensemble de messages électroniques de test et à approuver manuellement les parties décodées du message. Mais une meilleure façon, et certainement plus rapide, est d'utiliser un client de messagerie électronique existant et éprouvé, de l'alimenter avec nos courriels et de comparer les parties décodées du message avec nos résultats.

Visualisation des courriels

Flask et Bootstrap sont les outils parfaits pour cela. En quelques heures, je construis un frontend qui montre une seule page composée de deux parties. La partie supérieure est la liste des e-mails, la partie inférieure est un IFRAME qui montre l'e-mail BODY TEXT ou BODY HTML.

Résumé

La plupart des exemples sur Internet ne concernent que le téléchargement du message complet et le décodage de celui-ci. Récupération et décodage du courrier électronique IMAP est un défi car nous devons convertir le BODYSTRUCTURE et ensuite l'aplatir pour obtenir les numéros du body parts. Le paquet IMAPClient est certainement utile, mais il manque de bons exemples. Nous devons choisir une méthode pour vérifier automatiquement si nos e-mails de test sont décodés correctement. En stockant les données ENVELOPE et BODY TEXT et BODY HTML est une base de données SQLite J'ai presque toutes les informations que je veux, la recherche est très rapide car elle ne doit pas interagir avec le serveur IMAP .

Liens / crédits

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

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

Laissez un commentaire

Commentez anonymement ou connectez-vous pour commenter.

Commentaires

Laissez une réponse

Répondez de manière anonyme ou connectez-vous pour répondre.