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

IMAPClient и получение body parts

Получение сервера IMAP body parts с Python и IMAPClient стоит того. Сокращение времени загрузки является существенным.

26 июня 2020 Обновленный 25 июля 2020
В Email
post main image
https://unsplash.com/@tobiastu

Я решил временно перенести акцент с разработки программного обеспечения для моей CMS / Блога на более мелкий проект. Основная причина в том, что я надеялся узнать что-то новое о Python , что будет полезно.

Я всегда хотел иметь свою собственную клиентскую программу IMAP . Возможно, на мой выбор также сильно повлияла некоторая неприятность клиента IMAP Dekko2 для Ubuntu Touch, операционной системы моего мобильного телефона. Я знаю, что должен быть доволен существованием Dekko2 , и на самом деле он отлично работает на моем OnePlus One. Но некоторые действия выполняются медленно или не дают запрошенного результата. Например, поиск выполняется медленно. А иногда результат получается не полным.

Dekko2 - хорошая программа, она экономит пропускную способность и место на телефоне, но лично меня это не волнует. Мобильные абонементы дешевеют, а память телефона часто составляет минимум 8 ГБ.

Мой подход

Напишите базовый клиент IMAP . Храните данные HEADER и BODY TEXT (text/plain) и BODY HTML (text/html) в локальной базе данных. Тогда у нас есть большая часть данных, которые нам нужны на нашем ПК или телефоне. Мы загружаем только вложения, встроенные изображения по запросу.

SQLite - это, конечно, хорошо поддерживаемый и стабильный способ (но никогда не используйте его в высокопроизводительном многопоточном приложении). А поиск быстрый и не занимает никакой пропускной способности.

Должен сказать, что меня также заинтересовало утверждение на сайте SQLite о том, что хранение данных в BLOB быстрее, чем хранение в виде файла. Ну, по крайней мере, до тех пор, пока размер не станет большим. Большинство писем, которые я получаю, меньше 20-30 KB, так что это соответствует правилу.

Я решил использовать пакет IMAPClient , чтобы сделать свою жизнь немного проще. Он очень прост в использовании ... когда вы начинаете.

О том, о каком количестве данных мы говорим

У меня есть почтовый ящик с INBOX из примерно 5000 сообщений. С пакетом IMAPClient легко загрузить ENVELOPE. Это содержит такую информацию как:

  • Дата и время
  • Тема
  • Адреса электронной почты: в, из, cc, bcc и т.д.

Когда мы получаем ENVELOPE , мы можем получить и BODYSTRUCTURE . Это необходимо, так как для загрузки частей BODY TEXT и BODY HTML нам понадобится BODYSTRUCTURE . Нам нужны эти части для поиска.

Не знаю как вы, но меня не волнуют изображения. И если нам понадобится изображение, мы всегда можем его скачать. То же самое справедливо и для вложений.

Я решил сохранить BODYSTRUCTURE с сообщением, а затем расшифровал его, когда попросил скачать нужные части. Таким образом, загрузка сообщений состоит из двух этапов.

На первом этапе мы загружаем ENVELOPE и BODYSTRUCTURE. ENVELOPE декодируется в дату, тему, адрес электронной почты, а затем вся эта информация сохраняется. После этого этапа у нас есть хороший список сообщений, которые нужно показать, но без body parts. На втором этапе мы декодируем BODYSTRUCTURE, загружаем BODY TEXT и BODY HTML, и храним эту информацию в базе данных.

Время для некоторых тестов.

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

Вот оно. Мой компьютер является i7 и получает свои данные через Интернет-соединение. Мой телефон OnePlus One работает под управлением Ubuntu Touch и получает свои данные через 3G/4G. Конечно, на это время влияют многие факторы, поэтому это всего лишь указание.

На каждый элемент есть два раза. Первый с 'отладочными сообщениями включён', лог-файл 1,4 ГБ, второй без отладочных сообщений.

Время загрузки пустой базы данных с данными на 5000 сообщений. В типичной ситуации все сообщения уже есть и вы получаете только новые (или удаляете удаленные).

Я не скачивал вложения, включая вложенные (пересылаемые) сообщения. Какова разница в размере при полной загрузке? Мой почтовый сервер использует Dovecot. Каталог Maildir для моего INBOX:

  • new: 5.7M
  • cur: 681M

Это всего 690 МБ. Это значит, что мы не скачали 690 МБ - 170 МБ = 520 МБ. Другими словами, скачав 170/690 = 25% всех данных, мы можем искать по электронному адресу и искать в BODY TEXT и BODY HTML без подключения к серверу IMAP , сделав запрос к нашей базе данных.

SQLite и оптимизации

Для хранения данных я использую следующие таблицы:

  • imap_server
  • imap_server_folder
  • imap_mail_msg
  • imap_mail_msg_addr

Оптимизация производительности с SQLite проста при использовании excutemany. У меня есть таблица для сообщений и таблица для адресов электронной почты и фиксация только после 100 сообщений. Разница во времени составила около 50%.

Но наибольшего прироста производительности я добился, ограничив количество адресов электронной почты. Я являюсь членом некоторых списков электронной почты, а некоторые списки посылают сообщения на более чем 400 видимых адресов электронной почты, в поле to-поле или cc-поле. Я решил сохранить ENVELOPE локальным и ограничить количество адресов электронной почты на один, cc, bcc и т.д. до 20. Если я захочу увидеть больше, скорее всего нет, то всегда смогу снова прочитать ENVELOPE и сохранить и показать их. Как и при показе электронной почты, мы можем показать еще 20 электронных адресов со ссылкой на 380 больше. В большинстве случаев это бесполезно.

Я не оптимизировал работу UPDATE при хранении BODY TEXT и BODY HTML. Я где-то читал, что это не сильно изменится, но каждая секунда имеет значение, так что я разберусь с этим позже.

IMAPClient и получение body parts BODY TEXT (text/plain) и BODY HTML (text/html)

Использование IMAPClient для получения uids всех сообщений является простым. Но получить body parts - это непростая задача. Возвращаемый BODYSTRUCTURE конвертируется IMAPClient в tuples, lists. Для получения body part нам нужен номер тела, а этого номера нет в BODYSTRUCTURE.

Это означает, что мы должны сами сгладить BODYSTRUCTURE и присвоить номера тел. Я нашел в интернете код, который был очень полезен, смотрите ссылки ниже: 'простой современный клиент командной строки'.

После выполнения операции сплющивания мы должны выбрать для загрузки BODY TEXT (text/plain) и BODY HTML (text/html). Я решил создать body parts Class , в котором будут храниться все соответствующие данные для загрузки части. При обновлении таблицы imap_mail_msg с извлеченными BODY TEXT и BODY HTML, эта Class также маринуется и хранится в таблице imap_mail_msg на тот случай, если мы захотим загрузить вложения позже. Здесь используется 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 - словарь со свойствами вложения. Я еще не рассматривал возможность декодирования пересылаемых сообщений.

Скачивание и декодирование body parts

Для многих сообщений это работало хорошо, но для 20 из 5000, 0,4%, было исключение по декодированию. Например, в сообщении говорилось, что charset - 'us-ascii'. Но декодирование с этим charset вызвало следующую ошибку:

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

К счастью, есть пакет chardet , который пытается определить кодировку строки. Он предположил, что кодировка charset была 'ISO-8859-1' и декодирование не дало ошибок. В другом сообщении charset была 'utf-8', но дала ошибку декодирования:

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

Chardet предложила кодировку charset 'Windows-1252' и декодирование с этим charset не дало ошибок. Я вручную проверил декодированные сообщения и они выглядели нормально. Эта часть кода:

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

Откуда нам знать, что мы можем декодировать всю почту?

Здесь мы касаемся большой проблемы разработки почтового клиента. Мой код был способен декодировать все 5000 сообщений без ошибок. Но будет ли он работать и для сообщения 5001? Есть еще большая проблема. Если сообщение декодировано без ошибок, то откуда нам знать, что декодированное сообщение корректно?

Есть несколько способов решить эту проблему. Один способ - создать огромный тестовый набор email сообщений и вручную одобрить декодированные части сообщения. Но лучший и, конечно же, более быстрый способ - это использовать существующий проверенный почтовый клиент, скормить его нашим письмам и сравнить расшифрованные части сообщений с нашими результатами.

Просмотр электронных писем

Flask и Bootstrap являются идеальными инструментами для этого, за несколько часов я создаю фронтенд, который показывает одну страницу, состоящую из двух частей. Верхняя часть - это список писем, нижняя часть - IFRAME , которые показывают BODY TEXT или BODY HTML.

Резюме

Большинство примеров в интернете касаются только загрузки полного сообщения, а затем его декодирования. Получение и декодирование письма IMAP является проблемой, потому что мы должны преобразовать BODYSTRUCTURE и затем сгладить его, чтобы получить номера body parts. Пакет IMAPClient конечно помогает, но не хватает хороших примеров. Мы должны выбрать метод автоматической проверки правильности декодирования наших тестовых писем. Хранясь в базе данных ENVELOPE и BODY TEXT и BODY HTML , я получаю почти всю необходимую мне информацию, поиск происходит очень быстро, потому что не нужно взаимодействовать с сервером IMAP .

Ссылки / кредиты

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

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

Оставить комментарий

Комментируйте анонимно или войдите в систему, чтобы прокомментировать.

Комментарии

Оставьте ответ

Ответьте анонимно или войдите в систему, чтобы ответить.