Архив

Zero Inbox. Гайд по наведению порядка в почте

Проблематика

Моему почтовому ящику на gmail много лет. Более десяти лет самостоятельного существования, а также в нем лежат архивы из других почтовых систем. Все эти годы я использовал его так как и нужно использовать умные продукты:

  • Не сортировал письма, так как для этого есть хороший поиск
  • Не удалял письма с большими вложениями, так как для этого есть куча пространства
  • Не категоризировал письма и позволял сервису обучаться на моих привычках, для автоматических эвристик

И это было чудесное время, пока мне не захотелось навести в нем порядок.

Задача навести порядок не была самоцелью, скорее меня начало раздражать что весь inbox завален каким-то мусором: заказы из магазинов, рекламные письма, обновления от почты, все это вперемешку с периодическими дайджестами и личной перепиской.

gmail inbox ^^ Это не настоящий скрин моей почты. Просто картинка для превью.

Задача

Прежде чем переходить к действиям я постарался сформулировать задачу которую хочу решить, получилось следующее:

  • Навести порядок в Лейблах. Сгруппировать их в логические блоки, добавить нужные и удалить лишние
  • Создать правила для автоматического присвоения ярлыков от основных источников. Пересмотреть правила попадания в Inbox для этих правил
  • Отписаться от лишних рассылок
  • Удалить письма которые посчитаю мусорными

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

Наведение порядка в Лейблах

В первую очередь я создал несколько логических групп для лейблов, у меня получился такой список:

gmaiil labels

Внутри каждого из этих лейблов есть пачка 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 есть подробнейшее описание, от идеи до примеров использования.

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

Где взять 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 vs 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.

distribution На графике отображено распределение писем. Каждая точка - это уникальный отправитель. По шкале Y отображается количество писем от этого отправителя

Разгребание этого хвоста оказалось унылой механической работой, гораздо скучнее чем программировать. Фильтруешь по отправителю через поиск и принимаешь решение:

  • Не нужно. ⟶ Удалить
  • Нужно. Можно создать фильтр на будущее. ⟶ Создаем фильтр. Проставляем лейбл. Архивируем.
  • Нужно. Фильтр не требуется. ⟶ Проставляем лейбл. Архивируем.

Выводы

C момента завершения процесса, до написания поста прошел почти месяц, поэтому я могу поделиться взвешенными выводами.

  • Во-первых, теперь "Inbox" воспринимается совсем по другому. Все что в нем есть - это незавершенные дела. Если рассылка, то нужно её прочитать или отписаться или удалить. Если переписка - то значит надо довести её до логического завершения и отправить в архив.

  • Во-вторых, непрочитанные письма, которые лежат вне инбокса (в архиве), больше не напрягают. У меня сейчас есть пачка рассылок которые сразу переходят в "Архив" и лежат там. Я их открываю почитать когда есть время.

  • В-третьих, пока я разбирался с этим всем, я отписался от огромного количества рассылок.

empty inbox

Занимательный факт:

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

Ха-ха. Лох.

Уже постфактум, наведя порядок в ящике и опубликовав скрипт я наткнулся на аналогичное решение написанное на Go. Самое печальное, что перед тем как переписывать на Python 3.x, я бегло посмотрел что есть в Go на тему парсинга mbox, но, в тот раз, не нашлось ничего вразумительного.

Предположив что придется самому разбираться с чтением и парсингом mbox формата, я не стал изучать этот вопрос и сфокусировался на Python версии.

В дальнейшем, я обновил решение на Go до актуального состояния. Golang версия умеет почти все что и Python, работает в разы быстрее и я рекомендую использовать её.

💡 https://github.com/Rpsl/mboximporter

Рассказывать про него отдельно, в рамках этой статьи, нет смысла. Возможно, позднее удастся доработать все @todo и тогда напишу отдельным постом.

shrug

Другие статьи в блоге:

⟵ Накрутка рефералов или "нубский CPA"