IMAPClient и получение body parts
Получение сервера IMAP body parts с Python и IMAPClient стоит того. Сокращение времени загрузки является существенным.
Я решил временно перенести акцент с разработки программного обеспечения для моей 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
Подробнее
Email IMAP Ubuntu Touch
Недавний
- Скрытие первичных ключей базы данных UUID вашего веб-приложения
- Don't Repeat Yourself (DRY) с Jinja2
- SQLAlchemy, PostgreSQL, максимальное количество строк для user
- Показать значения в динамических фильтрах SQLAlchemy
- Безопасная передача данных с помощью шифрования Public Key и pyNaCl
- rqlite: альтернатива dist с высокой степенью готовности и SQLite
Большинство просмотренных
- Используя Python pyOpenSSL для проверки SSL-сертификатов, загруженных с хоста
- Использование UUID вместо Integer Autoincrement Primary Keys с SQLAlchemy и MariaDb
- Подключение к службе на хосте Docker из контейнера Docker
- Использование PyInstaller и Cython для создания исполняемого файла Python
- SQLAlchemy: Использование Cascade Deletes для удаления связанных объектов
- Flask Удовлетворительный запрос API проверка параметров запроса с помощью схем Маршмэллоу