Faut que vous y allez rapidement. Synchronisation rapide des e-mails IMAP

salut! Je m'appelle Ilya. Il y a deux ans, j'ai rejoint le client mobile IMAP. Les versions antérieures de l'application téléchargeaient longtemps la liste des lettres et dépensaient beaucoup de trafic pour mettre à jour la boîte aux lettres. La question s'est posée de l'optimisation du travail avec le protocole et des capacités de ce protocole en général. Je ne savais rien du protocole et je me suis plongé dans la lecture de la documentation. Il s'avère que pendant tout ce temps le client a utilisé le protocole sans interruption et n'a pas du tout pris en compte les fonctionnalités d'implémentation. Ces fonctionnalités ont permis d'accélérer le téléchargement des e-mails de 2 à 3 fois. A propos de ce qu'est IMAP et quelles sont les puces pour l'optimiser plus tard dans mon article.

Je ne plongerai pas trop profondément dans le protocole. Un article plutôt de la catégorie «Je voudrais lire cet article il y a deux ans». Il est peu probable que les gourous IMAP trouvent de nouvelles informations par eux-mêmes. Cet article s'appuie sur la description du protocole de RFC 3501 .

Connexion au serveur


IMAP est un protocole avec état. Ce fut une découverte pour moi, avant que je n'avais pas vu ou travaillé avec de tels protocoles. Considérez le schéma de travail avec le serveur. 


Allons dans l'ordre, et surtout, avec des exemples. Vous devez d'abord créer une connexion au serveur. Pour ce faire, utilisez la bibliothèque openSSL.

openssl s_client -connect imap.server.com:993 -crlf 

Très bien, la connexion est établie et vous pouvez observer la réponse OK avec une ligne qui commence par la réponse CAPABILITY

OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE  SPECIAL-USE AUTH=PLAIN AUTH=LOGIN]

Il existe une feuille de triche pratique pour chacun des CAPABILITY , où toutes les valeurs de CAPABILITY possibles sont écrites avec des liens vers le RFC. Par exemple, IMAP4rev1 indique au client que le serveur fonctionne conformément à la norme IMAP4 et IDLE signale que vous pouvez vous abonner aux modifications qui se produisent dans la boîte aux lettres.

Autorisation du serveur


Après vous être connecté au serveur, vous devez vous rendre dans votre boîte aux lettres. Cela se fait à l'aide de la commande LOGIN.

a1 LOGIN email pass

Alors, arrêtez, connectez-vous, je comprends, et a1 qu'est-ce que c'est? - Peut - être demandez-vous. Et c'est le tag d'équipe. Dans l'intérêt du client, les balises doivent être différentes, car la réponse arrive avec la même balise que la demande, ce qui signifie qu'elle peut être mise en correspondance pour l'analyse entre les équipes. Le serveur peut également renvoyer une réponse avec un astérisque au début, comme * OK, cela s'appelle une réponse non balisée. Fondamentalement, une telle réponse est renvoyée pour les équipes qui attendent plusieurs entités dans la réponse, par exemple, LIST. 

Demande de liste de dossiers


Pour demander une liste de lettres dans un dossier, vous devez d'abord trouver ces dossiers. Cela se fait par la commande LIST. Cette commande renvoie une liste de dossiers sur le serveur.

A2 LIST «» *
* LIST (\HasNoChildren \Trash) «/» Trash
* LIST (\HasNoChildren \Sent) «/» Sent
* LIST (\HasNoChildren \Drafts) «/» Drafts
* LIST (\HasNoChildren \Junk) «/» Junk
* LIST (\HasNoChildren) «/» INBOX
A2 OK List completed (0.001 + 0.000 + 0.001 secs).

Le premier paramètre de la commande est l'espace de noms. Si le serveur prend en charge l'espace de noms, ses valeurs peuvent être demandées à l'aide de la requête NAMESPACE. L'espace de noms standard ressemble à une chaîne vide. Ensuite, le paramètre des caractères génériques entre en jeu. Avec lui, nous pouvons dire au serveur quels dossiers nous devons retourner. Par exemple, nous pouvons obtenir: une branche d'arborescence de dossiers, uniquement des racines, ou tout simplement, comme dans l'exemple ci-dessus. Il vaut mieux ne pas le faire, car qui sait combien de dossiers l’utilisateur a dans la boîte. Les auteurs du protocole recommandent d'utiliser «%» - dans ce cas, vous obtiendrez tous les dossiers de niveau supérieur de la boîte aux lettres. 

D'après la réponse, nous comprenons qu'il s'agit d'une réponse non balisée où chaque ligne est votre dossier dans la zone. Tout d'abord, il existe des indicateurs par lesquels nous lisons les méta-informations du dossier, par exemple, dans l'exemple, tous les dossiers n'ont pas de descendants et certains dossiers à usage spécial (tels que la corbeille, le courrier indésirable, etc.). Vient ensuite le caractère séparateur de dossier. Ce symbole est utilisé pour les sous-dossiers. Par exemple, pour un descendant du dossier Corbeille, le nom ressemblerait à «Corbeille / Nouveau dossier». Après tous les dossiers, le serveur nous renverra OK avec la balise que nous avons affectée à la commande et le temps d'exécution de cette commande.  

Sélection de dossier


De plus, selon le schéma, nous devons sélectionner un dossier à partir duquel nous resserrerons nos messages. Cela se fait à l'aide de la commande SELECT.

4 SELECT INBOX
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 16337 EXISTS
* 2 RECENT
* OK [UNSEEN 6037] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 17412] Predicted next UID
* OK [HIGHESTMODSEQ 21503] Highest
4 OK [READ-WRITE] Select completed (0.015 + 0.000 + 0.014 secs).

Lorsque vous sélectionnez un dossier, toutes les informations le concernant sont renvoyées. Allons dans l'ordre.

  • Répondez avec des drapeaux autorisés dans le dossier pour les lettres.  
  • Répondez avec des indicateurs que le client peut changer pour toujours
  • Répondre avec le nombre de lettres dans le dossier
  • La réponse est avec le nombre de lettres récentes, c'est-à-dire celles que nous avons reçues entre les sélections de dossiers
  • Répondre avec le nombre de messages non lus

Eh bien, pour l'instant, nous allons nous attarder sur cela. Le reste des informations dont nous n'avons pas besoin.

Demander des lettres


Maintenant, la chose la plus intéressante est la demande de lettres. Vous devez être extrêmement prudent ici, en particulier sur les clients mobiles. D'accord, il est peu probable que lorsque vous entrez dans l'application, vous recevrez des milliers de messages du serveur vers votre base de données. De plus, cela n'a aucun sens de télécharger la lettre entière, car il peut ne pas être pratique d'afficher, par exemple, une liste de toutes les lettres. Par exemple, pour afficher rapidement les lettres des utilisateurs, nous ne demanderons qu'une "enveloppe". Dans cette enveloppe, nous voulons voir: l'expéditeur, le destinataire, l'objet de la lettre et la date d'envoi. Nous chargerons les 10 premiers messages.

5 FETCH 16337:16327 (ENVELOPE)

Le deux-points énumère le segment du nombre de lettres que nous voulons recevoir et entre parenthèses ce que nous voulons lire de ces lettres, dans ce cas l'enveloppe de la lettre.

Je donnerai la réponse sous forme abrégée:

* 16334 FETCH (ENVELOPE ("Sat, 07 Sep 2019 23:07:48 +0000" "Hello from Fabric.io" (("Fabric" NIL "notifier" "fabric.io")) (("Fabric" NIL "notifier" "fabric.io")) (("Fabric" NIL "notifier" "fabric.io")) ((NIL NIL "me" "me@mail")) NIL NIL NIL "<5d7438441b07c_2d872ad30967b9646405c6@answers-notifier2012.mail>"))

Il est clair que rien n'est clair. Et le fait est que le format d'enveloppe est dicté par la RFC 2822. Je ne le considérerai pas dans cet article. Cette enveloppe contient toutes les informations nécessaires: date de réception de la lettre, objet de la lettre, expéditeur, destinataire et même messageId. Ses clients utilisent pour afficher une conversation.

Nous avons donc pu montrer à l'utilisateur des informations de base sur la lettre, mais qu'en est-il du corps?
On peut télécharger immédiatement tout le corps de la lettre, quelle que soit sa taille, ce n'est bien sûr pas pour longtemps mais néanmoins coûteux sur le réseau et la mémoire. Soit dit en passant, cela se fait avec la même commande FETCH. 

6 FETCH 16337:16327 (BODY[]) 

Essayez une telle commande dans votre boîte de réception, et vous comprendrez ce que j'entendais par «coûteux», même avec 10 messages, nous obtenons une réponse assez volumineuse avec absolument toutes les informations sur la lettre. En parlant d'elle.

À quelle fréquence avez-vous téléchargé la source de la lettre chez un client que vous connaissez pour voir à quoi elle ressemble dans sa forme originale? Sinon, tirons-en une lettre de test. Dans ce document, j'ai ajouté une photo directement à la lettre et une photo en pièce jointe. Enregistrez-le au format eml, puis ouvrez-le avec n'importe quel éditeur de texte. Selon le client, vous recevrez différentes sources de la lettre, mais en général elles seront similaires. 

Commençons par l'en-tête de l'e-mail:

Return-Path: <myemail>
Delivered-To:myemail
Received: from localhost (localhost [127.0.0.1])
	byimap.server.com (imap.server.com) with ESMTP id 6C2BE2A0363
	for <myemail>; Sun,  8 Sep 2019 23:41:29 +0300 (MSK)
X-Virus-Scanned: amavisd-new at imap.server.com
Received: from imap.server.com ([127.0.0.1])
	by localhost ( imap.server.com [127.0.0.1]) (amavisd-new, port 10026)
	with ESMTP id abx8HQQT_k5A for <myemail>;
	Sun,  8 Sep 2019 23:41:29 +0300 (MSK)
Mime-Version: 1.0
Date: Sun, 08 Sep 2019 20:41:28 +0000
Content-Type: multipart/mixed;
 boundary=»--=_Part_722_554093397.1567975288»
Message-ID: <9e4e3872e603eac2c20f26bb1d65548d>
From: "Me" <myemail>
Subject: Hey, Habr!
To: myemail
X-Priority: 3 (Normal)

Toutes les méta-informations sont décrites dans l'en-tête de la lettre, de qui, à qui, quand, type de contenu du message, sujet et priorité de la lettre. Le champ limite indique la limite de la lettre.

Comprenez mieux ce que cela signifie.

----=_Part_722_554093397.1567975288
Content-Type: multipart/related;
 boundary=»--=_Part_583_946112260.1567975288»
----=_Part_583_946112260.1567975288
Content-Type: multipart/alternative;
 boundary=»--=_Part_881_599167713.1567975288»
----=_Part_881_599167713.1567975288
Content-Type: text/plain; charset=«utf-8»
Content-Transfer-Encoding: quoted-printable
----=_Part_881_599167713.1567975288
Content-Type: text/html; charset=«utf-8»
Content-Transfer-Encoding: quoted-printable
<!DOCTYPE html><html><head><meta http-equiv=3D"Content-Type" content=3D"t=
ext/html; charset=3Dutf-8" /></head><body><div data-crea=3D"font-wrapper"=
 style=3D«font-family: XO Tahion; font-size: 16px; direction: ltr»> <img =
src=3D"cid:jua-uid-q1nz1guinitrcfd3-1567975257318"><br><br><div></div> <b=
r> </div></body></html>
----=_Part_881_599167713.1567975288--
----=_Part_583_946112260.1567975288
Content-Type: image/jpeg; name=«2018-09-04 22.46.36.jpg»
Content-Disposition: inline; filename=«2018-09-04 22.46.36.jpg»
Content-ID: <jua-uid-q1nz1guinitrcfd3-1567975257318>
Content-Transfer-Encoding: base64

Chaque frontière est la frontière habituelle d'un écrit. Ils commencent par deux tirets "-". La bordure de fermeture comporte ces deux tirets à la fin. Il est décrit plus en détail dans la RFC 1341.

Cela peut être appelé la partie principale de la lettre, des parties de la lettre et leurs types MIME sont décrits ici.

À propos des types MIME
Un type MIME est un type de support qui a été décrit dans MIME ( Multipurpose Internet Mail Extensions) pour décrire les types de contenu à l'intérieur d'un message électronique. 

  • multipart / mixed nous indique que la lettre a une structure mixte, c'est-à-dire que différentes parties des lettres peuvent être différentes représentations de telle ou telle information. 

  • multipart/related , , , 

  • multipart/alternative , , , text/plain text/html, . 


Nous n'avons pas de texte simple ici, il est donc plus logique de prendre une représentation html. Dans cette présentation html, il y a juste une image avec le paramètre Content-Disposition: inline, c'est-à-dire qu'il est situé directement dans le corps de la lettre, et non dans les documents joints.

Le lien vers cette image n'est pas assez simple. Il est décrit par le paramètre Content-ID, qui est égal à jua-uid-q1nz1guinitrcfd3-1567975257318 . Ceci est un lien vers la partie suivante de la lettre - une image encodée en base 64. Pour sauver mes nerfs, je n'ai pas inclus tout le code base 64.

La dernière partie de la lettre a la forme 

----=_Part_722_554093397.1567975288
Content-Type: image/png; name=«2018-07-02 11.08.23 pm.png»
Content-Disposition: attachment; filename=«2018-07-02 11.08.23 pm.png»
Content-Transfer-Encoding: base64

qui a déjà Content-Disposition non pas en ligne, comme l'image ci-dessus, mais une pièce jointe. Cette image devrait simplement aller dans le panneau de pièce jointe, soit dit en passant, elle est également encodée en base-64 et a une grande taille. Ici, il devient clair que vous ne devez pas charger à nouveau le corps entier de la lettre si nous voulons afficher uniquement des informations de base. 

Retour au protocole


Après avoir travaillé sur les lettres, vous devez fermer le dossier sélectionné et dire au revoir au serveur. Pour fermer le dossier, nous devons entrer la commande FERMER. Oui, c'est si simple


7 CLOSE
7 OK Close completed (0.001 + 0.000 secs).

Soit dit en passant, si vous avez travaillé avec la console en parallèle avec moi et lu l'article, un événement pas si agréable aurait pu se produire, le serveur pourrait fermer votre connexion par timeout. C'est tout à fait normal, et chaque serveur a son propre délai d'expiration, par exemple, nous avons 30 minutes. 
Par conséquent, il est recommandé de faire la commande NOOP en arrière-plan

1 NOOP
1 OK NOOP completed (0.001 + 0.000 secs).

Il ne fait littéralement rien, mais vous permet de conserver la connexion sans délai autant que nous en avons besoin. Si vous sélectionnez actuellement un dossier, NOOP peut fonctionner comme une demande périodique de modifications de ce dossier 

1 NOOP
* 16472 EXPUNGE
* 16471 EXPUNGE
* 16472 EXISTS
* 1 RECENT
1 OK NOOP completed (0.004 + 0.000 + 0.003 secs).

Ici, dans la réponse, nous sommes informés de deux messages supprimés, un nouveau et que le nombre de messages dans ce dossier est de 16 472.

Je note également que vous ne pouvez travailler qu'avec un seul dossier sélectionné, il n'y a pas de travail parallèle ici.

Eh bien, à la fin, fermez la session avec le serveur et nous lui dirons au revoir.

8 LOGOUT
* BYE Logging out
8 OK Logout completed (0.001 + 0.000 secs).

Nous voyons la triste réponse BYE sans étiquette, ce qui signifie qu'il est temps de terminer le travail.

Synchronisation rapide avec CONDSOTORE et QRESYNC


Vous pouvez utiliser l'opération NOOP pour suivre les modifications dans une zone d'un dossier sélectionné. Mais que faire si nous voulons découvrir ce qui a changé dans le dossier pendant que nous travaillions avec un autre? L'option la plus évidente consiste à trier toutes les lettres du stockage local, qu'il s'agisse d'un cache ou d'une base de données, et de comparer avec ce que le serveur renverra. D'une part, c'est en effet une solution, et sur certains serveurs, ce sera littéralement la seule vraie. D'un autre côté, nous voulons afficher les lettres aussi rapidement que le protocole le permet généralement. Heureusement, notre serveur prend en charge les extensions de protocole telles que CONDSTORE et QRESYNC, qui ont été ajoutées à RFC7162. Le premier ajoute un numéro spécial de 63 bits au message et au dossier, appelé la séquence mod, qui augmente avec chaque opération sur cette lettre. La séquence de modules la plus élevée parmi tous les messages est ajoutée au dossier. Par conséquent, chaque fois que vous vous connectez à un dossier sur un serveur qui prend en charge CONDSTORE, nous pouvons facilement savoir si quelque chose a changé ou non, simplement en comparant les valeurs de séquence de mod pour les dossiers local et serveur.

De plus, cette extension ajoute des paramètres supplémentaires pour les commandes STORE et FETCH - CHANGEDSINCE mod-sequence et UNCHANGEDSINCE mod-sequence, qui vous permettent d'effectuer une opération si la séquence de modules des messages transmis est respectivement plus grande et plus petite que celle-ci. Regardons un exemple.

FETCH 17221:17241 (UID) (CHANGEDSINCE 0)
* OK [HIGHESTMODSEQ 22746] Highest
* 17222 FETCH (UID 18319 MODSEQ (22580))
* 17223 FETCH (UID 18320 MODSEQ (22601))
* 17224 FETCH (UID 18324 MODSEQ (22607))
* 17225 FETCH (UID 18325 MODSEQ (22604))
* 17226 FETCH (UID 18326 MODSEQ (22608))
* 17227 FETCH (UID 18327 MODSEQ (22614))
* 17228 FETCH (UID 18328 MODSEQ (22613))
* 17229 FETCH (UID 18336 MODSEQ (22628))
* 17230 FETCH (UID 18338 MODSEQ (22628))
* 17231 FETCH (UID 18340 MODSEQ (22628)
* 17232 FETCH (UID 18341 MODSEQ (22628))
* 17221 FETCH (UID 18318 MODSEQ (22583))

J'ai simulé une situation dans laquelle nous allons dans la boîte aux lettres et je n'en savais rien auparavant, c'est-à-dire que notre séquence de mod locale est 0. Comme vous pouvez le voir, le serveur nous renvoie généralement tous les messages qui se trouvent dans la boîte aux lettres, car avant cela, nous ne recevions rien et je ne sais rien de la boîte. En réponse à une demande de lettres UID de CHANGEDSINCE, une réponse OK non étiquetée est également accompagnée d'un HIGHESTMODESEQ que nous allons maintenant enregistrer, et pour chaque message notre MODSEQ.

Nous allons effectuer certaines opérations avec la boîte aux lettres: ajouter de nouvelles lettres, changer les drapeaux. Faisons une nouvelle demande mais avec la séquence de mod précédente

1 fetch 17221:* (UID FLAGS) (CHANGEDSINCE 22746)
* 17267 FETCH (UID 18378 FLAGS () MODSEQ (22753))
* 17270 FETCH (UID 18381 FLAGS (\Seen) MODSEQ (22754))
* 17271 FETCH (UID 18382 FLAGS () MODSEQ (22751))
* 17273 FETCH (UID 18384 FLAGS () MODSEQ (22750))

et nous voyons déjà la différence, au lieu de sortir 20 anciennes et nouvelles communautés qui viennent d'arriver (astérisque en 17221: * signifie prendre les lettres du numéro 17221 au maximum possible) nous recevons des lettres dont le MODSEQ est supérieur au précédent. Cela aide assez bien à synchroniser un dossier dans lequel nous ne sommes pas depuis un certain temps et à obtenir une sorte de distribution des lettres modifiées, au lieu d'essayer toutes les lettres possibles.

Il semblerait, beaucoup mieux? Mais QRESYNC rend l'opération de synchronisation encore plus rapide, il vous permet de spécifier les paramètres MODSEQ et les UID de message que nous connaissons lors de la sélection du dossier. Expliquons avec un exemple. D'abord, QRESYNC doit être activé avec la commande ENABLE. 

1 ENABLE QRESYNC
* ENABLED QRESYNC
1 OK Enabled (0.001 + 0.000 secs).
1 SELECT INBOX (QRESYNC (0 0))
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 17271 EXISTS
* 0 RECENT
* OK [UNSEEN 17241] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 18385] Predicted next UID
* OK [HIGHESTMODSEQ 22754] Highest
1 OK [READ-WRITE] Select completed (0.001 + 0.000 secs).

puisque nous ne savions rien sur le dossier avant cela, le serveur ne nous renvoie que des informations sur le dossier, sans une pépite de ses modifications. Supposons que nous ayons demandé les vingt premiers messages et que nous nous souvenions de leur UID et de HIGHESTMODESEQ. Nous quittons le dossier, nous envoyons un message, supprimons le message, modifions les indicateurs et revenons avec les informations passées sur le dossier

1 CLOSE
1 OK Close completed (0.001 + 0.000 secs).
1 SELECT INBOX (QRESYNC (1532079879 22754 18300:18385))
* FLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent)
* OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft $Forwarded $MDNSent \*)] Flags permitted.
* 17271 EXISTS
* 0 RECENT
* OK [UNSEEN 17241] First unseen.
* OK [UIDVALIDITY 1532079879] UIDs valid
* OK [UIDNEXT 18386] Predicted next UID
* OK [HIGHESTMODSEQ 22757] Highest
* VANISHED (EARLIER) 18380
* 17269 FETCH (UID 18383 FLAGS () MODSEQ (22757))
* 17271 FETCH (UID 18385 FLAGS () MODSEQ (22755))
1 OK [READ-WRITE] Select completed (0.001 + 0.000 secs).

Et maintenant, lorsque nous choisissons un dossier modifié, nous obtenons immédiatement une pépite de modifications, sous la forme d'une réponse VANISHED (EARLIER) pour les messages qui ont été supprimés et FETCH pour les messages qui ont été ajoutés ou modifiés. Il est maintenant encore plus facile de synchroniser le dossier si l'utilisateur ne l'a pas visité depuis longtemps. C'est un moyen très cool si vous avez un tas de messages stockés localement dans le cache et que vous ne voulez pas les comparer avec des messages sur le serveur.

Le premier paramètre de cette demande est UIDVALIDITY, qui est essentiellement utilisé pour vérifier que l'uid que vous avez reçu précédemment n'a pas changé dans le dossier. Cela peut se produire si le serveur modifie l'ID de session de session en session pour tous les messages ou si le dossier a été supprimé et qu'un dossier du même nom a été créé à sa place.

Le deuxième paramètre est le HIGHESTMODSEQ que nous connaissons et le dernier est l'intervalle des UID connus, ils peuvent être écrits comme deux points, si l'intervalle est continu, ou séparés par une virgule.

Conclusion


Dans mon exemple, je suis tombé sur une situation où l'ignorance du domaine conduit à un fonctionnement incorrect et sous-optimal de l'application. Je n'ai pas couvert toutes les options possibles pour l'utilisation du protocole avec cet article. Mais j'espère que pour le prochain développeur du client IMAP, les informations ci-dessus seront utiles.

IMAP a une tonne de choses intéressantes. Les commandes de synchronisation rapide ne sont qu'un début, en fait, vous pouvez optimiser davantage différentes commandes IMAP, en fonction des capacités du serveur, et rendre le travail avec le courrier plus rapide, plus économique sur le réseau et la mémoire, et généralement plus agréable. Mais j'en parlerai plus tard.

All Articles