Tengo que ir rápido. Sincronización rápida de correo electrónico IMAP

¡Hola! Soy Ilya Hace dos años, me uní al cliente móvil IMAP. Las versiones anteriores de la aplicación descargaban la lista de cartas durante mucho tiempo y gastaban mucho tráfico para actualizar el buzón. Surgió la pregunta sobre la optimización del trabajo con el protocolo y sobre las capacidades de este protocolo en general. No sabía nada sobre el protocolo y me puse a leer la documentación. Resulta que todo este tiempo el cliente usó el protocolo sin interrupción y no tuvo en cuenta las características de implementación. Estas características ayudaron a acelerar las descargas de correo de 2 a 3 veces. Sobre lo que es IMAP y cuáles son los chips para optimizarlo más adelante en mi artículo.

No me sumergiré en el protocolo demasiado profundamente. Un artículo de la categoría "Me gustaría leer este artículo hace dos años". Es poco probable que los gurús de IMAP encuentren nueva información por sí mismos. Este artículo se basa en la descripción del protocolo de RFC 3501 .

Conexión al servidor


IMAP es un protocolo con estado. Esto fue un descubrimiento para mí, antes de eso no había visto ni trabajado con tales protocolos. Considere el esquema de trabajar con el servidor. 


Vayamos en orden, y lo más importante, con ejemplos. Primero debe crear una conexión con el servidor. Para hacer esto, use la biblioteca openSSL.

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

Genial, la conexión está establecida y puede observar la respuesta correcta con una línea que comienza con la respuesta CAPACIDAD

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

Hay una hoja de trucos conveniente para cada CAPACIDAD , donde todos los valores de CAPACIDAD posibles se escriben con enlaces al RFC. Por ejemplo, IMAP4rev1 le dice al cliente que el servidor funciona de acuerdo con el estándar IMAP4, e IDLE le indica que puede suscribirse a los cambios que ocurren en el buzón.

Autorización del servidor


Después de conectarse al servidor, debe ir a su buzón. Esto se hace usando el comando LOGIN.

a1 LOGIN email pass

Entonces, pare, inicie sesión, entiendo, y a1 ¿qué es esto? - Quizás lo preguntes. Y esta es la etiqueta del equipo. En interés del cliente, las etiquetas deben ser diferentes, ya que la respuesta llega con la misma etiqueta que la solicitud, lo que significa que puede coincidir con el análisis entre equipos. El servidor también puede devolver una respuesta con un asterisco al principio, como * OK, esto se denomina respuesta sin etiquetar. Básicamente, dicha respuesta se devuelve para los equipos que esperan varias entidades en la respuesta, por ejemplo, LIST. 

Solicitud de lista de carpetas


Para solicitar una lista de letras en una carpeta, primero debe encontrar estas carpetas. Esto se hace mediante el comando LIST. Este comando devuelve una lista de carpetas en el servidor.

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

El primer parámetro en el comando es el espacio de nombres. Si el servidor admite el espacio de nombres, sus valores se pueden solicitar mediante la consulta NAMESPACE. El espacio de nombres estándar parece una cadena vacía. A continuación, entra en juego el parámetro comodines. Con él, podemos decirle al servidor qué carpetas debemos devolver. Por ejemplo, podemos obtener: una rama de árbol de carpetas, solo raíces, o simplemente todo, como en el ejemplo anterior. Es mejor no hacer esto, porque quién sabe cuántas carpetas tiene el usuario en la caja. Los autores del protocolo recomiendan utilizar "%"; en este caso, obtendrá todas las carpetas de nivel superior del buzón. 

De la respuesta, entendemos que esta es una respuesta sin etiquetar donde cada línea es su carpeta en el cuadro. Primero, hay banderas por las cuales leemos la metainformación de la carpeta, por ejemplo, en el ejemplo todas las carpetas no tienen descendientes y algunas carpetas de propósito especial (como Papelera, Basura, etc.). Luego viene el carácter separador de carpeta. Este símbolo se usa para subcarpetas. Por ejemplo, para un descendiente de la carpeta Papelera, el nombre se vería como "Papelera / Nueva carpeta". Después de todas las carpetas, el servidor nos devolverá OK con la etiqueta que le asignamos al comando y el tiempo de ejecución de este comando.  

Selección de carpeta


Además, de acuerdo con el esquema, debemos seleccionar una carpeta desde la cual ajustaremos nuestros mensajes. Esto se hace usando el comando 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).

Cuando selecciona una carpeta, se devuelve toda la información sobre ella. Vamos en orden.

  • Responda con banderas que están permitidas dentro de la carpeta para letras.  
  • Responda con banderas que el cliente puede cambiar para siempre
  • Responda con el número de letras en la carpeta
  • La respuesta es con el número de cartas recientes, es decir, las que recibimos entre las selecciones de carpetas.
  • Responda con la cantidad de mensajes no leídos

Bueno, por ahora, detengámonos en esto. El resto de la información no la necesitamos.

Cartas de solicitud


Ahora lo más interesante es la solicitud de cartas. Debe tener mucho cuidado aquí, especialmente en clientes móviles. De acuerdo, es poco probable que cuando ingrese a la aplicación reciba miles de mensajes del servidor a su base de datos. Además, no tiene sentido descargar la carta completa, ya que puede no ser práctico mostrar, por ejemplo, una lista de todas las cartas. Por ejemplo, para mostrar rápidamente las cartas del usuario, solo le pediremos un "sobre". En este sobre queremos ver: remitente, destinatario, asunto de la carta y fecha de envío. Cargaremos las primeras 10 publicaciones.

5 FETCH 16337:16327 (ENVELOPE)

Los dos puntos enumeran el segmento de los números de letras que queremos recibir, y entre paréntesis lo que queremos leer de estas letras, en este caso, el sobre de la letra.

Daré la respuesta en forma abreviada:

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

Está claro que nada está claro. Y es que el formato de sobre está dictado por RFC 2822. No lo consideraré en este artículo. Este sobre tiene toda la información necesaria: fecha de recepción de la carta, asunto de la carta, remitente, destinatario e incluso Id. De mensaje. Sus clientes usan para mostrar una conversación.

Entonces, pudimos mostrar al usuario información básica sobre la carta, pero ¿qué pasa con el cuerpo?
Podemos descargar de inmediato todo el cuerpo de la carta, independientemente de su tamaño, por supuesto, esto no es por mucho tiempo, pero no obstante es costoso a través de la red y la memoria. Por cierto, esto se hace con el mismo comando FETCH. 

6 FETCH 16337:16327 (BODY[]) 

Pruebe dicho comando en su bandeja de entrada, y comprenderá lo que quise decir con "costoso", incluso con 10 mensajes obtenemos una respuesta bastante voluminosa con absolutamente toda la información sobre la carta. Hablando de ella.

¿Con qué frecuencia descargaste la fuente de la carta en algún cliente que conoces para ver cómo se ve en su forma original? Si no, saquemos una carta de prueba. En él, agregué una imagen directamente a la carta y una imagen como archivo adjunto. Guárdelo en formato eml y luego ábralo con cualquier editor de texto. Dependiendo del cliente, recibirá diferentes fuentes de la carta, pero en general serán similares. 

Comencemos con el encabezado del correo electrónico:

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)

Toda la metainformación se describe en el encabezado de la carta, de quién, a quién, cuándo, tipo de contenido del mensaje, asunto y prioridad de la carta. El campo de límite indica el límite de la letra.

Además, entienda lo que esto significa.

----=_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

Cada límite es el borde habitual de una pieza de escritura. Comienzan con dos guiones "-". El borde de cierre tiene estos dos guiones al final. Se describe con más detalle en RFC1341. A

esto se le puede llamar la parte principal de la letra, aquí se describen partes de la letra y sus tipos MIME.

Acerca de los tipos MIME
Un tipo MIME es un tipo de medio que se ha descrito en MIME ( Extensiones multipropósito de correo de Internet) para describir los tipos de contenido dentro de un mensaje de correo electrónico. 

  • multipart / mixed nos dice que la letra tiene una estructura mixta, es decir, diferentes partes de las letras pueden ser diferentes representaciones de esta o aquella información. 

  • multipart/related , , , 

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


No tenemos texto simple aquí, por lo que es más lógico tomar una representación html. En esta presentación html solo hay una imagen con el parámetro Content-Disposition: inline, es decir, se encuentra directamente en el cuerpo de la carta y no en los documentos adjuntos.

El enlace a esta imagen no es muy simple. Se describe mediante el parámetro Content-ID, que es igual a jua-uid-q1nz1guinitrcfd3-1567975257318 . Este es un enlace a la siguiente parte de la carta, una imagen codificada en base-64. Para salvar mis nervios, no incluí todo el código base 64.

La última parte de la carta tiene la forma 

----=_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

que ya tiene Content-Disposition no en línea, como la imagen de arriba, sino un archivo adjunto. Esta imagen solo debe ir al panel de archivos adjuntos, por cierto, también está codificada en base-64 y tiene un gran tamaño. Aquí queda claro que no debe volver a cargar todo el cuerpo de la carta si solo queremos mostrar información básica. 

Regresar al protocolo


Después de trabajar en las letras, debe cerrar la carpeta seleccionada y decir adiós al servidor. Para cerrar la carpeta, necesitamos ingresar el comando CERRAR. Si, es muy simple


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

Por cierto, si trabajó con la consola en paralelo conmigo y leyó el artículo, entonces podría haber sucedido un evento no tan agradable, el servidor podría cerrar su conexión por tiempo de espera. Esto es completamente normal, y cada servidor tiene su propio tiempo de espera, por ejemplo, tenemos 30 minutos. 
Por lo tanto, se recomienda hacer el comando NOOP en segundo plano

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

Literalmente no hace nada, pero le permite mantener la conexión sin un tiempo de espera tanto como lo necesitemos. Si actualmente selecciona una carpeta, NOOP puede funcionar como una solicitud periódica de cambios en esta carpeta 

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

Aquí, en la respuesta, se nos notifican dos mensajes eliminados, uno nuevo y que el número de mensajes en esta carpeta es 16 472.

También noto que puede trabajar con solo una carpeta seleccionada, no hay trabajo paralelo aquí.

Bueno, al final, cierre la sesión con el servidor y le diremos adiós.

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

Vemos la triste respuesta BYE sin etiquetar, lo que significa que es hora de terminar el trabajo.

Sincronización rápida con CONDSOTORE y QRESYNC


Puede usar la operación NOOP para rastrear cambios en un cuadro en una carpeta seleccionada. Pero, ¿qué sucede si queremos averiguar qué ha cambiado en la carpeta mientras trabajábamos con otra? La opción más obvia es ordenar todas las letras en el almacenamiento local, ya sea un caché o una base de datos, y comparar con lo que devolverá el servidor. Por un lado, esta es realmente una solución, y en algunos servidores será literalmente la única verdadera. Por otro lado, queremos mostrar letras tan rápido como el protocolo generalmente lo permite. Afortunadamente, nuestro servidor admite extensiones de protocolo como CONDSTORE y QRESYNC, que se agregaron a RFC7162. El primero agrega un número especial de 63 bits al mensaje y a la carpeta, llamado secuencia de modulación, que aumenta con cada operación en esta letra. La secuencia de modulación más alta entre todos los mensajes se agrega a la carpeta. Como resultado, cada vez que se conecta a una carpeta en un servidor que admite CONDSTORE, podemos averiguar fácilmente si algo ha cambiado o no, simplemente comparando los valores de secuencia de mod para las carpetas locales y del servidor.

Además, esta extensión agrega parámetros adicionales para los comandos STORE y FETCH: secuencia mod CHANGEDSINCE y secuencia mod UNCHANGEDSINCE, que le permiten realizar una operación si la secuencia mod de los mensajes transmitidos es mayor y menor que esta, respectivamente. Veamos un ejemplo.

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

Simulé una situación en la que entramos en el buzón y no sabíamos nada al respecto antes, es decir, nuestra secuencia de modulación local es 0. Como puede ver, el servidor generalmente nos devuelve todos los mensajes que están en el buzón, ya que antes no recibíamos nada y no sé nada sobre la caja. En respuesta a una solicitud de letras UID de CHANGEDSINCE, una respuesta sin etiquetar OK también viene con un HIGHESTMODESEQ que ahora guardaremos, y para cada mensaje nuestro MODSEQ. Realizaremos

algunas operaciones con el buzón: agregue nuevas letras, cambie las banderas. Hagamos una nueva solicitud pero con la secuencia mod anterior

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

y ya vemos la diferencia, en lugar de generar 20 comunidades antiguas y nuevas que acaban de llegar (un asterisco en 17221: * significa tomar letras del número 17221 al máximo posible) recibimos letras cuyo MODSEQ es mayor que el anterior. Esto ayuda bastante bien a sincronizar una carpeta en la que no hemos estado durante algún tiempo y obtener una especie de reparto de las letras cambiadas, en lugar de intentar todas las posibles.

Parecería, mucho mejor? Pero QRESYNC hace que la operación de sincronización sea aún más rápida, le permite especificar los parámetros MODSEQ y los UID de mensaje que conocemos justo durante la selección de la carpeta. Vamos a explicar con un ejemplo. Primero, QRESYNC debe habilitarse con el comando 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).

Como no sabíamos nada sobre la carpeta antes de eso, el servidor solo nos devuelve información sobre la carpeta, sin una pepita de sus cambios. Supongamos que preguntamos los primeros veinte mensajes y recordamos su UID y también HIGHESTMODESEQ. Dejamos la carpeta, nos enviamos un mensaje, eliminamos el mensaje, cambiamos las banderas y regresamos con la información anterior sobre la carpeta

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

Y ahora, al elegir una carpeta modificada, obtenemos inmediatamente una pepita de cambios, en forma de una respuesta DESAPARECIDA (ANTERIOR) para los mensajes que se eliminaron, y FETCH para los mensajes que se agregaron o cambiaron. Ahora es aún más fácil sincronizar la carpeta si el usuario no la ha visitado durante mucho tiempo. Esta es una forma muy interesante si tiene un montón de mensajes almacenados localmente en la memoria caché y no desea compararlos con los mensajes en el servidor.

El primer parámetro de esta solicitud es UIDVALIDITY, que se usa esencialmente para verificar que el uid que recibió anteriormente no cambió en la carpeta. Esto puede suceder si el servidor cambia la sesión uid de sesión a sesión para todos los mensajes o si la carpeta se ha eliminado y se ha creado una carpeta con el mismo nombre en su lugar.

El segundo parámetro es el HIGHESTMODSEQ que conocemos y el último es el intervalo de UID conocidos, se pueden escribir como dos puntos, si el intervalo es continuo o separado por una coma.

Conclusión


En mi ejemplo, me encontré con una situación en la que la ignorancia del área temática conduce a un funcionamiento incorrecto y subóptimo de la aplicación. No cubrí todas las opciones posibles para usar el protocolo con este artículo. Pero espero que para el próximo desarrollador del cliente IMAP la información anterior sea útil.

IMAP tiene un montón de cosas interesantes. Los comandos para la sincronización rápida son solo el comienzo, de hecho, puede optimizar aún más los diferentes comandos IMAP, dependiendo de las capacidades del servidor, y hacer que trabajar con el correo sea más rápido, más económico en la red y la memoria, y en general más agradable. Pero hablaré de esto más tarde.

All Articles