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

IMAPClient y la obtención de body parts

Atrapar el servidor IMAP con Python y IMAPClient vale la pena. La reducción de los tiempos de descarga es significativa.

26 junio 2020 Actualizado 25 julio 2020
En Email
post main image
https://unsplash.com/@tobiastu

Decidí cambiar temporalmente el enfoque de desarrollar el software para mi CMS / Blog a un proyecto más pequeño. La razón principal es que esperaba aprender nuevas cosas sobre Python que fueran útiles.

Siempre quise tener mi propio software cliente IMAP . Tal vez mi elección también fue fuertemente influenciada por alguna molestia sobre el cliente IMAP Dekko2 para Ubuntu Touch, el sistema operativo de mi teléfono móvil. Sé que debería estar contento con la existencia de Dekko2 y estoy, realmente, funcionando bien con mi OnePlus One. Pero algunas acciones son lentas o no entregan el resultado solicitado. Por ejemplo, la búsqueda es lenta. Y a veces los resultados parecen no estar completos.

Dekko2 es una buena pieza de software, ahorra ancho de banda y espacio en tu teléfono pero personalmente no me importa esto. Las suscripciones de los móviles son cada vez más baratas y la memoria del teléfono suele ser de 8 GB mínimo.

Mi enfoque

Escriba un cliente básico IMAP . Almacenar los datos del CABEZAL DE CORREO y el TEXTO CORPORAL (text/plain) y el CUERPO HTML (text/html) en una base de datos local. Entonces tenemos la mayoría de los datos que necesitamos en nuestro PC o teléfono. Sólo descargamos los archivos adjuntos, imágenes en línea a petición.

SQLite es el camino a seguir, por supuesto, bien soportado y estable (pero nunca lo use en una aplicación multi-hilo de alto rendimiento). Y la búsqueda es rápida y no requiere ningún ancho de banda.

Debo decir que también me fascinó la declaración en el sitio web de SQLite de que almacenar datos en un BLOB era más rápido que almacenarlos en un archivo. Bueno, al menos mientras el tamaño no sea demasiado grande. La mayoría de los emails que recibo están por debajo de los 20-30 KB, así que esto se ajusta a la regla.

Decidí usar el paquete IMAPClient para hacer mi vida un poco más fácil. Es muy fácil de usar... cuando empiezas.

Acerca de qué cantidad de datos estamos hablando

Tengo una cuenta de correo electrónico con un INBOX de unos 5000 mensajes. Con el paquete IMAPClient es fácil descargar el ENVELOPE. Este contiene información como:

  • Fecha y hora
  • Asunto
  • Direcciones de correo electrónico: a, desde, cc, bcc, etc.

Cuando traemos el ENVELOPE podemos traer el BODYSTRUCTURE también. Esto es esencial ya que necesitamos el BODYSTRUCTURE para descargar el TEXTO CORPORAL y las partes del cuerpo HTML . Necesitamos estas partes para la búsqueda.

No sé ustedes pero a mí no me importan las imágenes. Y si necesitamos una imagen siempre podemos descargarla. Lo mismo ocurre con los archivos adjuntos.

Decidí almacenar el BODYSTRUCTURE con el mensaje y luego decodificarlo cuando solicite descargar las partes requeridas. Así que la carga de los mensajes consiste en dos etapas.

En la primera etapa descargamos los ENVELOPE y BODYSTRUCTURE. El ENVELOPE se decodifica en fecha, asunto, direcciones de correo electrónico y luego se almacena toda esta información. Después de esta etapa tenemos una bonita lista de mensajes para mostrar, pero sin ningún body parts. En la segunda etapa decodificamos el BODYSTRUCTURE, descargamos el TEXTO CORPORAL y el CUERPO HTML, y almacenamos esta información en la base de datos.

Es hora de hacer algunas pruebas.

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

Ahí lo tienen. Mi PC es un i7 y obtiene sus datos a través de una conexión a Internet. Mi teléfono es un OnePlus One corriendo Ubuntu Touch y obtiene sus datos a través de 3G/4G. Por supuesto que hay muchos factores que influyen en estos tiempos, así que esto es sólo una indicación.

Hay dos veces por artículo. La primera es con los mensajes de depuración activados, el archivo de registro es de 1,4 GB, la segunda es sin mensajes de depuración.

Los tiempos son para cargar una base de datos vacía con datos de 5000 mensajes. En una situación típica, ya tienes todos los mensajes y sólo obtienes los nuevos (o borras los eliminados).

No descargué archivos adjuntos incluyendo mensajes adjuntos (reenviados). ¿Cuál es la diferencia de tamaño con una descarga completa? Mi servidor de correo electrónico utiliza Dovecot. El directorio Maildir para mi INBOX:

  • new: 5.7M
  • cur: 681M

Esto es un total de 690 MB. Esto significa que no descargamos 690 MB - 170 MB = 520 MB. En otras palabras, descargando 170/690 = 25% de todos los datos, podemos buscar por dirección de correo electrónico y buscar el TEXTO CORPORAL y el CUERPO HTML sin conectarnos al servidor IMAP consultando nuestra base de datos.

SQLite y optimizaciones

Para almacenar los datos utilizo las siguientes tablas:

  • imap_server
  • imap_server_folder
  • imap_mail_msg
  • imap_mail_msg_addr

Las optimizaciones de rendimiento con SQLite son fáciles cuando se usa excutemany. Tengo una tabla para los mensajes y una tabla para las direcciones de correo electrónico y sólo se confirman después de 100 mensajes. La diferencia de tiempo fue de un 50%.

Pero la mayor ganancia de rendimiento que obtuve fue limitando el número de direcciones de correo electrónico. Soy miembro de algunas listas de correo electrónico y algunas listas envían un mensaje a más de 400 direcciones de correo electrónico visibles, en el campo to-field o cc-field. Decidí almacenar el ENVELOPE local y limitar el número de direcciones de correo electrónico por a, cc, bcc, etc. a 20. Si quiero ver más, probablemente no, entonces siempre puedo leer el ENVELOPE de nuevo y almacenarlo y mostrarlo. Como cuando se muestra el correo electrónico, podemos mostrar 20 direcciones de correo electrónico adicionales con un enlace a 380 más. Bastante inútil en la mayoría de los casos.

No optimicé la operación UPDATE al almacenar el TEXTO CORPORAL y el CUERPO HTML. Leí en alguna parte que esto no cambiaría mucho, pero cada segundo cuenta, así que investigaré esto más tarde.

IMAPClient y traer body parts TEXTO CORPORAL (text/plain) y CUERPO HTML (text/html)

Usar IMAPClient para obtener el uids de todos los mensajes es fácil. Pero traer el body parts es un desafío. El BODYSTRUCTURE devuelto es convertido por IMAPClient en tuples, lists. Para obtener un body part necesitamos el número de cuerpo y este número no está en el BODYSTRUCTURE.

Esto significa que debemos aplanar el BODYSTRUCTURE nosotros mismos y asignar números de cuerpo. Encontré un código en Internet que fue muy útil, ver los enlaces de abajo: "Cliente de línea de comando simple y moderno".

Después de la operación de aplanamiento, debemos seleccionar el TEXTO DEL CUERPO (text/plain) y el CUERPO HTML (text/html) para su descarga. Decidí crear un body parts Class que contiene todos los datos relevantes para descargar una pieza. Al actualizar la tabla imap_mail_msg con el BODY TEXT y BODY HTML, este Class también es decapado y almacenado en la tabla imap_mail_msg en caso de que queramos descargar los archivos adjuntos más tarde. Aquí está el Class que uso:

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 es un diccionario con las propiedades del accesorio. No he investigado todavía la decodificación de los mensajes reenviados.

Descargando y decodificando body parts

Esto funcionó bien para muchos mensajes, pero para 20 de los 5000, el 0,4%, hubo una excepción de decodificación. Por ejemplo, el mensaje decía que el charset era 'us-ascii'. Pero la decodificación con este charset causó el siguiente error:

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

Afortunadamente hay un paquete llamado chardet que intenta detectar la codificación de una cadena. Sugirió que la codificación charset era 'ISO-8859-1' y la decodificación no dio errores. Otro mensaje decía que el charset era 'utf-8' pero daba error de decodificación:

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

Chardet sugirió que la codificación charset era 'Windows-1252' y la decodificación con este charset no dio errores. Revisé manualmente los mensajes decodificados y se veían bien. Esta parte del código:

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

¿Cómo sabemos que podemos decodificar todo el correo?

Aquí tocamos un gran problema de desarrollo de un cliente de correo electrónico. Mi código era capaz de decodificar los 5000 mensajes sin errores. Pero, ¿funcionará también para el mensaje 5001? Hay un problema aún más grande. Si el mensaje es decodificado sin errores, ¿cómo sabemos que el mensaje decodificado es correcto?

Hay pocas maneras de resolver esto. Una forma es crear un enorme conjunto de prueba de mensajes de correo electrónico y aprobar manualmente las partes del mensaje decodificado. Pero una forma mejor y ciertamente más rápida es usar un cliente de correo electrónico ya probado, alimentarlo con nuestros correos electrónicos y comparar las partes del mensaje decodificado con nuestros resultados.

Ver los correos electrónicos

Flask y Bootstrap son las herramientas perfectas para esto, En unas pocas horas construyo un frontend que muestra una sola página que consiste en dos partes. La parte superior es la lista de correos electrónicos, la parte inferior es un IFRAME que muestra el correo electrónico TEXTO CORPORAL o CUERPO HTML.

Resumen

La mayoría de los ejemplos en Internet sólo tratan de descargar el mensaje completo y luego decodificarlo. Obtener y decodificar el correo electrónico IMAP es un desafío porque debemos convertir el BODYSTRUCTURE y luego aplanarlo para obtener los números del body parts. El paquete IMAPClient ciertamente ayuda pero carece de buenos ejemplos. Debemos seleccionar un método para comprobar automáticamente si nuestros correos electrónicos de prueba son decodificados correctamente. Al almacenar los datos de ENVELOPE y TEXTO CORPORAL y CUERPO HTML es una base de datos de SQLite tengo casi toda la información que quiero, la búsqueda es muy rápida porque no tiene que interactuar con el servidor de IMAP .

Enlaces / créditos

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

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

Deje un comentario

Comente de forma anónima o inicie sesión para comentar.

Comentarios

Deje una respuesta.

Responda de forma anónima o inicie sesión para responder.