Проблематика
Моему почтовому ящику на gmail много лет. Более десяти лет самостоятельного существования, а также в нем лежат архивы из других почтовых систем. Все эти годы я использовал его так как и нужно использовать умные продукты:
- Не сортировал письма, так как для этого есть хороший поиск
- Не удалял письма с большими вложениями, так как для этого есть куча пространства
- Не категоризировал письма и позволял сервису обучаться на моих привычках, для автоматических эвристик
И это было чудесное время, пока мне не захотелось навести в нем порядок.
Задача навести порядок не была самоцелью, скорее меня начало раздражать что весь inbox завален каким-то мусором: заказы из магазинов, рекламные письма, обновления от почты, все это вперемешку с периодическими дайджестами и личной перепиской.
^^ Это не настоящий скрин моей почты. Просто картинка для превью.
Задача
Прежде чем переходить к действиям я постарался сформулировать задачу которую хочу решить, получилось следующее:
- Навести порядок в Лейблах. Сгруппировать их в логические блоки, добавить нужные и удалить лишние
- Создать правила для автоматического присвоения ярлыков от основных источников. Пересмотреть правила попадания в Inbox для этих правил
- Отписаться от лишних рассылок
- Удалить письма которые посчитаю мусорными
Во втором пункте, говоря про "правила попадания в inbox", я имею ввиду, что новые письма, которые попадают под фильтры, могут сразу переходить в архив, но, при этом, остаются непрочитанными. Их не будет видно во "входящих", но к ним всегда можно вернуться. Это удобно для различных уведомлений или дайджестов.
Наведение порядка в Лейблах
В первую очередь я создал несколько логических групп для лейблов, у меня получился такой список:
Внутри каждого из этих лейблов есть пачка sub-labels и для избежания путаницы я решил что все письма будут лежать на уровне label/sub-label
.
На скриншоте отображена конфигурация на момент написания статьи, хотя изначально было по-другому:
-
Я пытался разграничить письма по доменам и хостингу в разные фильтры
Services/Domains
&Services/Hosting
, но в процессе понял что это одинаковая сущность и нет смысла её дробить. Оставил толькоServices/Hosting
, как более общую, на мой взгляд -
Notifications/Receipts
&Notifications/Shops
. Изначально, Receipts я заводил чтобы туда падали уведомления от различных платежных систем, такси и прочих сервисов. Уже в процессе сортировки я понял что сам путаюсь куда какое письмо относится и, вероятно, нужно эти две категории слить в одну.Уведомление от магазина "Кони и заводы": Ваш счет оплачен. Вот куда это? В магазины или в чеки? В итоге сам придумал логическую разницу - в
Receipts
отправляется все что не привязано к магазинам, либо не является физическим предметом. Стало понятно, так и оставил. -
Work/Unsorted
&Services/Anything
. Эти лейблы появились как раз из-за того что я решил все хранить вsub-label
. Туда отправляются письма которые не попадают под правила кластеризации и их слишком мало чтобы выносить в отдельный ярлык
Первый подход
В первую очередь я занялся самым простым: дайджестами, нотификациями от крупных сервисов и уведомлениями от сервисов которыми часто пользуюсь. Тут никакой автоматизации не требуется, абсолютно механическая работа. Я просто открыл две вкладки с почтой: в первой работал со списком писем, во второй настраивал фильтры и занимался сортировкой.
Руки быстро привыкают к одинаковой последовательности действий и за пару часов мне удалось разобрать большой массив писем.
К сожаления я не записывал прогресс чтобы поделиться подробной статистикой :(
Во время этих действий я активно пользовался поиском в почте, так что рекомендую ознакомиться с документацией по фильтрам:
- Search operators you can use with Gmail
- Все входящие без лейблов in:inbox -has:userlabels
- Весь архив без лейблов -in:inbox -has:userlabels
Второй подход
После решения самых простых задач мне пришлось решать каким образом дальше организовать процесс.
Реальность была такова, что у меня оставалось несколько десятков тысяч писем во "входящих" и отсутствие понимания как их кластеризовать.
Было необхоидмо это как-нибудь автоматизировать. Я предположил что наверняка есть готовые скрипты, которые умеют подключаться по imap к почтовому ящику и делать что-нибудь. Поверхностный поиск на github навел меня на репозиторий:
💡 Elasticsearch For Beginners: Indexing your Gmail Inbox
Это набор скриптов на Python 2.x (на момент действий описываемых в статье), который позволяет распарсить mbox формат и загрузить письма в Elasticsearch.
В README.md есть подробнейшее описание, от идеи до примеров использования.
После того как письма будут загружены в хранилище — можно использовать любые агрегатные функции и строить аналитику по своему почтовому ящику.
Где взять mbox?
- Если у вас Google Mail, то идете на takeout.google.com, там запрашиваете выгрузку вашего почтового ящика и через несколько часов получаете готовый архив
- Можно поискать скрипты для выгрузки сообщений, что-нибудь в стиле imapbackup или imap to mbox.
- Подключиться почтовым клиентом к ящику и сделать выгрузку через него - Mail.app такое умеет
MongoDB
Поигравшись некоторое время с Elasticsearch, мне захотелось модифицировать данный скрипт под работу с MongoDB. Это связано с тем что у меня нет опыта работы с Elasticsearch и мне не хотелось заниматься его изучением, как минимум, до тех пор пока не завершен процесс уборки в ящике.
Первым делом я модифицировал исходный скрипт чтобы он загружал данные в MongoDB, убедившись что концепт работает, я переписал скрипт, попутно проведя рефакторинг, обновление зависимостей и перевод на Python 3.x.
💡 https://github.com/Rpsl/mongodb-gmail
Скрипт умеет все что умеет его аналог, только работает с MongoDB как с основным хранилищем. После загрузки данных, используя aggregation framework, можно строить любую аналитику по вашему почтовому ящику.
Usage: cli.py [OPTIONS] FILENAME
Print FILENAME.
FILENAME path to mbox file
Options:
--mongodb TEXT Connection string for mongodb instance [default:
mongodb://root:example@127.0.0.1]
--db-name TEXT MongoDB database name [default: google-mail]
--collection-name TEXT MongoDB collection name [default: mails]
--init BOOLEAN Force deleting and re-initializing the MongoDB
collection [default: False]
--body BOOLEAN Will index all body content, stripped of HTML/CSS/JS
etc. Adds fields: "body" and "body_size" [default:
False]
--help Show this message and exit.
Например, чтобы сгруппировать письма во "входящих" по отправителю, нужно выполнить следующий запрос (значение в labels
может отличаться, в зависимости от ваших языковых настроек):
db.mails.aggregate([
{ $match: { labels: { $in: ['inbox'] } } },
{ $group: { _id: "$from", total: { $sum : 1 } } },
{ $sort : { "total": -1 } }
])
Результат будет в сгруппированном и отсортированном виде.
{ "_id" : "****@****", "total" : 3360 }
{ "_id" : "****@****", "total" : 2240 }
{ "_id" : "inform@money.yandex.ru", "total" : 360 }
{ "_id" : "****@gmail.com", "total" : 342 }
{ "_id" : "notification@russianpost.ru", "total" : 337 }
{ "_id" : "transaction@notice.aliexpress.com", "total" : 318 }
{ "_id" : "gmail@rpsl.info", "total" : 229 }
{ "_id" : "****", "total" : 223 }
{ "_id" : "notifications@github.com", "total" : 190 }
{ "_id" : "****", "total" : 133 }
{ "_id" : "****", "total" : 129 }
{ "_id" : "****", "total" : 119 }
{ "_id" : "info@letyshops.com", "total" : 115 }
{ "_id" : "noreply@reg.ru", "total" : 104 }
{ "_id" : "service@paypal.com", "total" : 96 }
{ "_id" : "****", "total" : 95 }
{ "_id" : "noreply@habr.com", "total" : 91 }
{ "_id" : "info@letyshops.ru", "total" : 74 }
{ "_id" : "info@site.hh.ru", "total" : 70 }
{ "_id" : "no-reply@taxi.yandex.ru", "total" : 66 }
Type "it" for more
>
Для того чтобы упростить себе жизнь и не смотреть все данные в терминале, можно экспортировать их в csv и далее работать с готовой таблицей.
Для экспорта необходимо добавить параметр $out
к нашему запросу. Тогда он запишет результат запроса в отдельную коллекцию.
db.mails.aggregate([
{ $match: { labels: { $in: ['inbox'] } } },
{ $group: { _id: "$from", total: { $sum : 1 } } },
{ $sort: { "total": -1 } },
{ $out: "export" }
])
После этого, с помощью утилиты mongoexport
, можно выгрузить всю коллекцию в виде csv файла.
mongoexport -d google-mail \
-c export \
-u root \
-p example \
--authenticationDatabase admin \
--fields "_id,total" \
--type=csv \
--sort='{total:-1}' \
-o ~/path/to/file.csv
Далее, отредактируем по шаблону строчки в csv, добавив ссылки на страницу с поиском по отправителю:
https://mail.google.com/mail/u/0/#search/from%3Atest%40example.com
SQL to MongoDB Cheatsheet
SQL to MongoDB Mapping Chart - отдельно хочу порекомендовать таблицу с сопоставлением запросов на sql и в MongoDB. Может быть очень полезна, если до этого опыта работы с MongoDB не было.
Занимательный факт:
В версии python 2.7, для чтения mbox
файла, имеется метод mailbox.Unixmailbox
, а в версии 3.1 его уже нету, и предлагается использовать mailbox.mbox
При этом, версия 2.7 для разбора писем используется построчное чтение, а в версии 3.1 сначала генерируется карта всех позиций (начало письма -> конец письма) и только после этого можно совершать навигацию по письмам.
С одной стороны крутое улучшение, с другой, на моем ноутбуке и почтовом ящике размером в несколько гигабайт, каждый запуск скрипта приводил к нагрузке CPU под 100% и ожиданию в несколько минут пока он соизволит распарсить весь файл и начнет работать с ним.
Python 3.1
https://github.com/python/cpython/blob/f0fa1b2f670334f9f4b123e6ecb65c3beef979ed/Lib/mailbox.py
def _generate_toc(self):
"""Generate key-to-(start, stop) table of contents."""
starts, stops = [], []
self._file.seek(0)
while True:
line_pos = self._file.tell()
line = self._file.readline()
if line.startswith('From '):
if len(stops) < len(starts):
stops.append(line_pos - len(os.linesep))
starts.append(line_pos)
elif not line:
stops.append(line_pos)
break
self._toc = dict(enumerate(zip(starts, stops)))
self._next_key = len(self._toc)
self._file_length = self._file.tell()
Python 2.7
https://github.com/python/cpython/blob/6a336f6484a13c01516b6bfc3b767075cc2cb4f7/Lib/mailbox.py
def _search_start(self):
while 1:
pos = self.fp.tell()
line = self.fp.readline()
if not line:
raise EOFError
if line[:5] == 'From ' and self._isrealfromline(line):
self.fp.seek(pos)
return
def _search_end(self):
self.fp.readline() # Throw away header line
while 1:
pos = self.fp.tell()
line = self.fp.readline()
if not line:
return
if line[:5] == 'From ' and self._isrealfromline(line):
self.fp.seek(pos)
return
🤔 Если бы у меня был доступ к аналитике всего мира, мне было бы очень интересно посмотреть сколько энергии было потрачено в мире из-за этого изменения.
Финальный рывок
Используя механизмы разработанные во "втором подходе", мне удалось за несколько вечеров перебрать все оставшиеся письма в ящике.
Если посмотреть на диаграмму распределения, то видно что у нас есть сотня-другая отправителей от которых очень много сообщений (их я разобрал в "первом подходе") и огромный хвост не системных сообщений.
На масштабе не видно, там, в среднем, < 100 сообщений от отправителя и плавно убывает до 1.
На графике отображено распределение писем. Каждая точка - это уникальный отправитель. По шкале Y отображается количество писем от этого отправителя
Разгребание этого хвоста оказалось унылой механической работой, гораздо скучнее чем программировать. Фильтруешь по отправителю через поиск и принимаешь решение:
- Не нужно. ⟶ Удалить
- Нужно. Можно создать фильтр на будущее. ⟶ Создаем фильтр. Проставляем лейбл. Архивируем.
- Нужно. Фильтр не требуется. ⟶ Проставляем лейбл. Архивируем.
Выводы
C момента завершения процесса, до написания поста прошел почти месяц, поэтому я могу поделиться взвешенными выводами.
-
Во-первых, теперь "Inbox" воспринимается совсем по другому. Все что в нем есть - это незавершенные дела. Если рассылка, то нужно её прочитать или отписаться или удалить. Если переписка - то значит надо довести её до логического завершения и отправить в архив.
-
Во-вторых, непрочитанные письма, которые лежат вне инбокса (в архиве), больше не напрягают. У меня сейчас есть пачка рассылок которые сразу переходят в "Архив" и лежат там. Я их открываю почитать когда есть время.
-
В-третьих, пока я разбирался с этим всем, я отписался от огромного количества рассылок.
Занимательный факт:
По почтовым сообщениям можно видеть четкую корреляцию как месенджеры заходили в нашу жизнь. В начале 201x годов, у меня есть множество писем, где мы шарим друг-другу фотографии и различные файлы или ссылки, а потом они все пропадают, вероятно тогда мы узнали про WhatsApp.
Ха-ха. Лох.
Уже постфактум, наведя порядок в ящике и опубликовав скрипт я наткнулся на аналогичное решение написанное на Go. Самое печальное, что перед тем как переписывать на Python 3.x, я бегло посмотрел что есть в Go на тему парсинга mbox, но, в тот раз, не нашлось ничего вразумительного.
Про Zero Inbox начал писать пост в блог, по пути понял что в некоторых местах можно было сделать лучше.
— Виктор Диктор (@Rpsl) February 28, 2020
Теперь не могу решиться: оставить как есть и добавить параграф «Ха-ха. Лох.» или сделать по нормальному, хоть и без цели, но и написать сразу об этом.
Предположив что придется самому разбираться с чтением и парсингом mbox формата, я не стал изучать этот вопрос и сфокусировался на Python версии.
В дальнейшем, я обновил решение на Go до актуального состояния. Golang версия умеет почти все что и Python, работает в разы быстрее и я рекомендую использовать её.
💡 https://github.com/Rpsl/mboximporter
Рассказывать про него отдельно, в рамках этой статьи, нет смысла. Возможно, позднее удастся доработать все @todo и тогда напишу отдельным постом.