Тайны виртуальных конвертов: чтение и парсинг .pst файлов

Меня зовут Максим Саввин, я участник профессионального сообщества NTA.

Встретить.pst-файл весом в десяток гигабайт — это не такая уж редкость. Когда почтовый ящик нужен для того, чтобы прочитать подборку лучших мемов недели, можно не заметить серьезных ограничений. Не займет много времени и поиск ключевого слова или конкретного идентификатора по всему почтовому ящику средствами Outlook. Но что, если найти нужно не конкретный идентификатор, а все идентификаторы, для которых известна только их структура?

Открытие и чтение файла

Для открытия и чтения.pst файлов воспользуюсь pypff — python оберткой для библиотеки libpff, написанной на C. Эта библиотека позволяет работать с форматами PFF (Personal Folder File) и OFF (Offline Folder File), в которые как раз и входит формат.pst, наряду с форматами.pab (Personal Address Book) и.ost (Offline Storage Table).

# Установка библиотеки pip install libpff-python # Импортирование библиотеки import pypff

Работа с файлом будет подобна работе с древовидным архивом. Поэтому в первую очередь после чтения файла необходимо получить корневую папку:

pst = pypff.file() pst.open(“example.pst”) root = pst.get_root_folder()

Дальше порядок действий будет отличаться в зависимости от задач. Например, вы можете посмотреть список дочерних писем или папок и выбрать из них нужные и обработать только их. В случае с задачей поиска идентификаторов, буду вынужден обрабатывать все письма из всех папок, так как обрабатываемые почтовые ящики имеют разную структуру папок (в первую очередь разные названия и степени вложенности).

Получение списка писем

Для получения списка всех писем воспользуюсь рекурсивным методом, который проходит по папке и собирает содержимое из нее и её дочерних папок (код ниже).

def parse_folder(base): messages = [] for folder in base.sub_folders: if folder.number_of_sub_folders: # Извлечение писем из дочерней папки messages += parse_folder(folder) # Обработка писем в текущей папке for message in folder.sub_messages: messages.append({ "folder": folder.name, "subject": message.subject, "sender_name": message.sender_name, "sender_email": get_sender_email(message), "datetime": message.client_submit_time, "body_plain": get_body(message) }) return messages # Извлечение всех писем из файла messages = parse_folder(root)

Как можно увидеть, письма сразу превращаю в словари, извлекая нужную информацию из объектов pff.message. Для атрибутов в классе message определены также get-методы. Чтобы посмотреть полный список атрибутов, можно воспользоваться встроенной функцией __dir__(), вызвав её для соответствующего объекта. Ниже приведен список таких атрибутов и методов, для понимания возможностей работы с письмами:

Тайны виртуальных конвертов: чтение и парсинг .pst файлов

Анализ писем

Для анализа была необходима следующая информация: тема письма, тело письма, папка, дата и время и данные об отправителе. Большую часть этой информации можно получить, просто взяв сами атрибуты объекта, но такой вариант не сработает для тела письма и почтового адреса отправителя.

Как можно видеть из списка атрибутов pff.message, письмо может иметь тело в трех форматах (plain_text, html, rtf), а точнее в одном из этих трех. Для задачи меня будет интересовать получение тела письма в формате текста, поэтому необходимо конвертировать html строки (которых оказалось больше всего). Для этого воспользуемся библиотекой BeautilfulSoup: создадим объект bs на основе нашего html_body и воспользуемся методом get_text(), чтобы получить очищенный от html тегов текст письма. На этом можно было бы остановится, но в результирующих строках оставались комментарии с описанием стилей и шрифтов, поэтому дополнительно производится их удаление с помощью регулярных выражений, а также замена двойных символов перевода строки на одинарные.

# Обработка plain_text тела def process_plain_text_body(message): return re.sub(r'([\r\n]+ ?)+', r'\r\n', message.plain_text_body.decode('utf-8')) # Обработка html тела def process_html_body(message): soup = bs(message.html_body(), "lxml") plain_text = soup.get_text() # Удаление html комментариев plain_text = re.sub(r'(<!--.*-->)+', r'', plain_text, flags=re.S) plain_text = re.sub(r'([\r\n]+ ?)+', r'\r\n', plain_text) return plain_text def get_body(message): if message.get_plain_text_body(): return process_plain_text_body(message) if message.get_html_body(): return process_html_body(message)

Остается получить адрес отправителя, для которого, в отличие от имени, выделенного атрибута не оказалось. Внимательный читатель мог заметить, что в pff.message имеется поле с интригующим названием «transport_headers». Обратившись к данному атрибуту, я увидел бы содержимое, описывающее путь электронного письма (изображение взято из интернета для примера).

Тайны виртуальных конвертов: чтение и парсинг .pst файлов

Интересующее значение можно найти по ключу «Return‑Path», с тем отличием, что электронный адрес не был обрамлен треугольными скобками.

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

def extract_messages_from_file(filename): pst = pypff.file() pst.open(filename) root = pst.get_root_folder() messages = parse_folder(root) pst.close() return messages

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

Для дальнейшего визуального анализа писем и извлеченных из них данных можно конвертировать список в pandas DataFrame и воспользоваться методом «to_excel()» для записи в файл.

Заключение

Подводя итоги, скажу, что задача обработки архивов почтовых ящиков — не частый гость, и порой бывает гораздо проще открыть.pst файл через Outlook и вручную найти интересующую информацию. Но если речь заходит про обработку десятков.pst файлов и поиск данных в каждом из них, то без автоматизации такая задача может стать настоящей пыткой.

Предложенный алгоритм позволяет прочитать архив почтового ящика и извлечь из него интересующее содержимое для всех писем. Полученные данные очищены от метатегов и приведены к единому формату, пригодному для чтения аналитиком. Этот алгоритм может быть использован для обработки одного ящика или для обработки нескольких, путем последовательного вызова или с использованием методов мультипроцессинга для повышения скорости обработки.

11
2 комментария

Надо ещё чтение вложений (офисных файлов) добавить, что-то типа первые 10 абзацев, строк и слайдов.

Ответить
Автор

В данной "пробной" версии алгоритма мы работали только с телом и заголовком, но добавление такого функционала может быть следующим этапом развития инструмента.

Ответить