Zero Inbox. Гайд по наведению порядка в почте
- вторник, 2 июня 2020 г. в 00:33:56
Моему почтовому ящику на gmail много лет. Более десяти лет самостоятельного существования, а также в нем лежат архивы из других почтовых систем. Все эти годы я использовал его так как и нужно использовать умные продукты:
И это было чудесное время, пока мне не захотелось навести в нем порядок.
Задача навести порядок не была самоцелью, скорее меня начало раздражать что весь 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
. Туда отправляются письма которые не попадают под правила кластеризации и их слишком мало чтобы выносить в отдельный ярлык
В первую очередь я занялся самым простым: дайджестами, нотификациями от крупных сервисов и уведомлениями от сервисов которыми часто пользуюсь. Тут никакой автоматизации не требуется, абсолютно механическая работа. Я просто открыл две вкладки с почтой: в первой работал со списком писем, во второй настраивал фильтры и занимался сортировкой.
Руки быстро привыкают к одинаковой последовательности действий и за пару часов мне удалось разобрать большой массив писем.
К сожаления я не записывал прогресс чтобы поделиться подробной статистикой :(
Во время этих действий я активно пользовался поиском в почте, так что рекомендую ознакомиться с документацией по фильтрам:
После решения самых простых задач мне пришлось решать каким образом дальше организовать процесс.
Реальность была такова, что у меня оставалось несколько десятков тысяч писем во "входящих" и отсутствие понимания как их кластеризовать.
Было необходимо это как-нибудь автоматизировать. Я предположил что наверняка есть готовые скрипты, которые умеют подключаться по imap к почтовому ящику и делать что-нибудь. Поверхностный поиск на github навел меня на репозиторий:
Elasticsearch For Beginners: Indexing your Gmail Inbox
Это набор скриптов на Python 2.x (на момент действий описываемых в статье), который позволяет распарсить mbox формат и загрузить письма в Elasticsearch.
В README.md есть подробнейшее описание, от идеи до примеров использования.
После того как письма будут загружены в хранилище — можно использовать любые агрегатные функции и строить аналитику по своему почтовому ящику.
Поигравшись некоторое время с 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 Mapping Chart — отдельно хочу порекомендовать таблицу с сопоставлением запросов на sql и в MongoDB. Может быть очень полезна, если до этого опыта работы с MongoDB не было.
В версии python 2.7, для чтения mbox
файла, имеется метод mailbox.Unixmailbox
, а в версии 3.1 его уже нету, и предлагается использовать mailbox.mbox
При этом, версия 2.7 для разбора писем используется построчное чтение, а в версии 3.1 сначала генерируется карта всех позиций (начало письма -> конец письма) и только после этого можно совершать навигацию по письмам.
С одной стороны крутое улучшение, с другой, на моем ноутбуке и почтовом ящике размером в несколько гигабайт, каждый запуск скрипта приводил к нагрузке CPU под 100% и ожиданию в несколько минут пока он соизволит распарсить весь файл и начнет работать с ним.
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()
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, но в тот раз не нашлось ничего вразумительного.
Предположив что придется самому разбираться с чтением и парсингом mbox формата, я не стал изучать этот вопрос и сфокусировался на Python версии.
В дальнейшем, я обновил решение на Go до актуального состояния. Golang версия умеет почти все что и Python, работает в разы быстрее и я рекомендую использовать её.
https://github.com/Rpsl/mboximporter
Рассказывать про него отдельно, в рамках этой статьи, нет смысла. Возможно, позднее удастся доработать все todo и тогда напишу отдельным постом.