Gotta go fast. Поиск в почтовом клиенте IMAP
Приветствую вас в заключительной статье о возможностях IMAP. В предыдущих статьях Быстрая синхронизация писем по IMAP и Оптимизация запросов содержимого письма по IMAP мы рассмотрели базовые операции над папками и письмами, научились слушать новые письма, оптимизировать синхронизацию и загрузку контента писем. В этой статье я поведаю вам как работает поиск и как его можно накачать стероидами.
Кто ищет, тот запускает SEARCH
Поиск по письмам в IMAP без расширений производится командой SEARCH. Я не буду описывать все возможные параметры для поиска, а лишь пробегусь по самым используемым. Для начала конечно нужно залогинится и выбрать папку. В стандарте IMAP без расширений нет возможности искать по всем папкам сразу. Давайте попробуем запустить простенький поиск, например найдем только непрочитанные письма. Для этого воспользуемся параметром UNSEEN.
1 SEARCH UNSEEN
* SEARCH 18687 18690 18691 18694 18695 18696 18707 18720 18724 18725 18727 18738 18759 18761 18765 18767 18769 …
Из этого куска понятно, что сервер возвращает нам не сами письма, а только их порядковые номера. Для получения контента писем дополнительно на каждый такой номер нужно вызвать команду FETCH.
Ладно, это совсем просто, а что нужно сделать чтобы найти письма по тексту? Для этого в стандарте прописано аж два параметра: BODY и TEXT. Разница лишь в том, что второй параметр ищет в том числе и в заголовках письма, когда как первый только в контенте. Допустим найдем письмо с текстом «mail»
1 SEARCH BODY mail
* SEARCH 1 3 4 5 6 7 8 9 … 21412 21413 21414 21415
1 OK Search completed (5.900 + 0.001 + 0.538 secs).
Ого, их так много. И все это заняло у нас 6 секунд, нехило. Все дело в том что поиск выдает нам совпадение по подстрокам. То есть любое письмо, которое содержит любое вхождение слова «mail» вернется нам в ответе.
Ну хорошо, рассмотрим как искать письма по датам. Поможет в этом нам два параметра: BEFORE и SINCE. Чтобы найти письма до указанной даты, нам, вестимо, следует использовать BEFORE, когда как после этой даты, SINCE. Дата пишется в формате RFC2822. Дата не включает время и таймзону.
1 search before 1-Feb-2020
* SEARCH 1 2 3 4 5 6 7 8 9 10 …
1 SEARCH SINCE 1-Feb-2020
* SEARCH 18383 18384 18385 18386 …
Выглядит просто. IMAP позволяет нам искать даже по заголовкам письма. Это дает нам возможность искать только те письма, у которых, например, есть аттач. Делается это с помощью параметра
HEADER
где field-name это имя заголовка, а string — его значение. Для наших целей мы будем искать по Content-Disposition с значением attachment
1 SEARCH HEADER Content-Disposition attachment
* SEARCH 21202 21211 21212 21213 21215 21216 21219 …
Таким образом мы можем создать какую-нибудь виртуальную папку, где будут отображаться только письма с каким-либо аттачем. При заходе в такую папку мы вместо загрузки писем с помощью FETCH просто воспользуемся командой SEARCH с параметрами выше.
Ещё раз обращу внимание: без расширений стандарта IMAP искать можно только по выбранной папке. Если вы хотите искать вообще по всем папкам, придется выходить из текущей и заходить в следующую, и так для каждой.
Когда мы говорим о таких простых запросах, мы забываем что поиск может происходить и по нескольким параметрам. Что предлагаем нам IMAP? Использовать логические операции, само собой! Для этого предусмотрено целых два параметра: NOT и OR. Думаю из их названия понятно что они делают. Не существует логической операции AND, для таких запросов нужно просто перечислять параметры через пробел.
1 SEARCH BEFORE 1-Feb-2020 FLAGGED
* SEARCH 706 708 1346 1347 1348 1351 …
1 SEARCH OR BEFORE 1-Feb-2020 SINCE 1-Feb-2020
* SEARCH 1 2 3 4 5 6 7 8 9 10 11 …
1 SEARCH NOT BEFORE 1-Feb-2020
* SEARCH 18383 18384 18385 18386 …
Обратите внимание что для OR параметры записываются справа от логической операции, как в префиксной нотации. Например для трех параметров выглядеть это будет так
1 SEARCH OR OR HEADER Content-Disposition attachment BEFORE 1-Feb-2020 FLAGGED
* SEARCH 1 2 3 4 5 …
Наверняка вы заметили, что результаты поиска отсортированы лишь по номеру сообщений. Что делать если мы хотим отсортировать, допустим, по теме письма, или ещё каким-нибудь замысловатым образом. Для этого нужно обратить внимание на команду SORT. Она делает то, что мы от него ожидаем — сортирует результаты поиска по какому либо параметру.
1 SORT (SUBJECT) UTF-8 BEFORE 1-Feb-2020
* SORT 395 396 397 398 399 400 401 402 403
Сравните результат вывода с обычным SEARCH. Сервер отсортировал нам сообщения по теме письма. Дополнительно в этой команде нам следует прописать ещё и чарсет параметра (UTF-8). Другие параметры вы можете посмотреть в tools.ietf.org/html/rfc5256»>RFC. В них включены DATE, ARRIVAL, CC, FROM, SIZE, TO, и REVERSE. Последний параметр просто реверсит сортировку, остальные, думаю, понятны по названию.
Если вы отвечаете или пересылаете сообщение создается цепочка сообщений. Для запроса таких цепочек используется команда THREAD. Данная команда использует два параметра: ORDEREDSUBJECT и REFERENCES. Они описывают алгоритм построения цепочки. Сама команда THREAD работает как SEARCH, только в ответе она выводит номера сообщений, отформатированных в цепочки. Сообщения берутся из заданных поисковых параметров. Давайте я попробую получить какую-нибудь цепочку по теме сообщений. Для её вывода я использую поисковый параметр SUBJECT, чтобы ограничить вывод только конкретными письмами. Заранее сделаю себе цепочку из трех писем.
1 THREAD ORDEREDSUBJECT UTF-8 SUBJECT «Thread 1»
* THREAD (21416 (21417)(21418))
В ответе приходят номера сообщений, где корень цепочки находится первым в скобках, а в каждой новой скобке находятся его листья. То есть каждая открывающая скобка обозначает корень цепочки. Добавим ещё одно сообщение в цепочку и посмотрим как поменялся вывод
* THREAD (21416 (21417)(21418)(21419))
Хорошо, новое письмо есть, оно находится в цепочке. Попробуем добавить подцепь в какой-нибудь месседж из цепочки и посмотрим снова результат.
* THREAD (21416 (21417)(21418)(21419))
Упс, что-то пошло не так. Дело тут в том, что алгоритм ORDEREDSUBJECT не всегда может составить цепочку по теме. В примере выше достаточно добавить подцепь, которая будет иметь другую тему и мы уже его не видим в цепочке. И тут нужно обратить внимание на параметр REFERENCES. Он для построения цепочки использует специальный заголовок References в письме. Выглядит он примерно так
References: <922cf7ab0065f8424be7bae17d1cd298@xxx.yyy.com>
<0faa73efb3e21eb12ee00e9ac4e7c5d0@xxx.yyy.com>
Такой заголовок есть в каждом письме в цепочки, и он обозначает какие письма связаны с данным. Каждый такой reference представляет из себя заголовок Mesasge-ID письма. Теперь перейдем к запросу, он будет почти такой же, только мы возьмем параметр REFERENCES.
1 THREAD REFERENCES UTF-8 SUBJECT «Thread 1»
* THREAD (21416 21417 (21418)(21419 21420))
Можно выдохнуть, подцепочка не потерялась. Здесь у 16 сообщения есть в цепочке сообщения 17, у 17 есть подцепочка 18 и подцепочка из 19 и 20. Чтобы въехать в эту структуру со скобками в RFC по SORT есть хороший пример где цепочке вида
* THREAD (2)(3 6 (4 23)(44 7 96))
соответствует такое дерево
Достаем конвейер
Давайте рассмотрим что у нас там по поиску по контенту. В последний раз когда мы искали что-то внутри письма по тексту у нас заняло это 6 секунд
1 OK Search completed (5.900 + 0.001 + 0.538 secs)
Сразу возникают плохие мысли. Это что-же, нам теперь для десяти папок нужно ждать целую минуту на поиск? А если больше десяти? Для облегчения нашей жизни было создано расширение SEARCHRES. Оно позволяет запомнить найденные письма и запросить их как-нибудь потом. В этом расширении добавляется команда RETURN с параметрами ALL, SAVE, MIN, MAX, COUNT. ALL используется для форматированного вывода номеров сообщений, подходящих под запрос, SAVE для сохранение этих номеров для следующей операции, MIN для вывода самого минимального номера, MAX для самого максимального и, наконец, COUNT выводит количество найденных сообщений. Какой профит от всего этого? А их несколько:
- Снижение трафика, так как вывод такого запроса не включает номера сообщений
- Возможность создавать конвейер из запросов (FETCH, COPY, MOVE, e.t.c)
- Не нужно разбирать список номеров сообщений, чтобы потом использовать его в других запросах
- Сервер может распараллелить несколько таких поисковых команд, а не блокировать сессию для ожидания результата.
Рассмотрим на простых примерах. Чтобы сохранить результат поиска нужно использовать в запросе параметр RETURN (SAVE)
1 SEARCH RETURN (SAVE) BODY «mail»
1 OK Search completed (5.518 + 0.001 + 0.100 secs).
Стоп, но ведь никак время не поменялось. А оно и не изменится, но зато после запроса SEARCH мы можем делать любые другие операции, пока сервер занят поиском, этот вызов не блокирует нам сессию, а значит эти 5 секунд мы можем потратить на что-то полезное.
Теперь, чтобы использовать результат поиска, мы в запросе FETCH вместо кучи номеров сообщений просто используем символ »$»
1 FETCH $ (UID)
* 11005 FETCH (UID 12023)
* 11006 FETCH (UID 12024)
* 11007 FETCH (UID 12025)
* 11008 FETCH (UID 12026)
…
Стало гораздо проще. Раньше нам стоило запомнить огромный список, который нам выводил запрос, и по каждому из номеров сообщений запускать команду FETCH. Теперь же нам достаточно использовать ранее сохраненный результат поиска и не тратить сетевой ресурс. Тот результат, который мы сохранили, можно использовать несколько раз, выстраивая при этом цепочку операций. Каждый такой новый запрос перетирает результат старого, так что будьте внимательны.
С ALL все чуть посложнее, но все еще приятнее чем стандартный SEARCH. Он будет работать на сервере и когда запрос закончится, он нас оповестит.
1 SEARCH RETURN (ALL) BODY «mail»
* ESEARCH (TAG «1») ALL 1,3:77,79:112,114:118,120:144,146:159, ....
1 OK Search completed (5.771 + 0.001 + 0.115 secs)
Тут он нам выводит список номеров сообщений, и если сообщения можно схлопнуть, то есть использовать интервал, он за нас это сделает. Как это в теории может помочь: например мы хотим сделать что-то типа паджинации, запрашивать и выводить не весь список целиком, а только, например, первые 20. Тогда нам нужно распарсить этот список и отсчитать 20 первых сообщений, и потом их получить с помощью FETCH. В примере выше можно запросить так
1 FETCH 1 (UID)
* 1 FETCH (UID 1)
1 OK Fetch completed (0.001 + 0.000 secs).
1 FETCH 3:22 (UID)
* 3 FETCH (UID 3)
* 4 FETCH (UID 4)
* 5 FETCH (UID 5)
* 6 FETCH (UID 6)
* 7 FETCH (UID 7)
…
В этом случае мы можем после долгого поиска сделать достаточно быстрый вывод результатов, вместо огромной пачки писем в обычном запросе.
Для MIN и MAX мне трудно придумать кейс. Их результат обычно это один месседж, с минимальным или максимальным номером сообщения соответственно. Эти параметры можно комбинировать. Например вызвать RETURN (SAVE ALL) или RETURN (SAVE MIN MAX). Результат будет отличаться в зависимости от параметров в скобках, например для (MIN MAX) вернется только два письма, для (MIN MAX ALL) вернуться минимальный, максимальный и все письма.
Чтобы доказать что данное расширение не блокирует сессию я проведу два поиска одновременно
1 SEARCH RETURN (ALL) BODY «mail»
2 SEARCH RETURN (ALL) FLAGGED
* ESEARCH (TAG «2») ALL 706,708,1346:1348, ...
* ESEARCH (TAG «1») ALL 1,3:77,79:112,114:118, …
Можно заметить, что относительно легкий запрос прошёл раньше и мы уже можем использовать его результат. Второй тяжелый запрос пришёл позже и не помешал нам с первым.
Мы тебя везде найдем
В этой части статьи мы рассмотрим достаточно редкое, но полезное расширение стандарта IMAP MULTISEARCH. Не думаю что вы сможете найти сервер с таким capability, но для общего развития я все же его рассмотрю.
Недостатком предыдущих команд была невозможность поиска сразу по всем папкам. Приходится открывать каждую папку и запускать отдельный поиск. Данное расширение стандарта позволяет нам миновать такую обидную проблему. Я, к своему сожалению, не смог найти сервер, который бы смог работать с такой командой, поэтому я с вашего позволения просто буду использовать код запросов из документа. Важно чтобы ваш сервер поддерживал два capability: ESEARCH и MULTISEARCH. Это расширение позволяет выбрать конкретные папки для поиска
C: tag1 ESEARCH IN (mailboxes «folder1» subtree «folder2») unseen
C: tag2 ESEARCH IN (mailboxes «folder1» subtree-one «folder2») subject «chad»
S: * ESEARCH (TAG «tag1» MAILBOX «folder1» UIDVALIDITY 1) UID ALL 4001,4003,4005,4007,4009
S: * ESEARCH (TAG «tag2» MAILBOX «folder1» UIDVALIDITY 1) UID ALL 3001:3004,3788
S: * ESEARCH (TAG «tag1» MAILBOX «folder2/banana» UIDVALIDITY 503) UID ALL 3002,4004
S: * ESEARCH (TAG «tag1» MAILBOX «folder2/peach» UIDVALIDITY 3) UID ALL 921691
S: tag1 OK done
S: * ESEARCH (TAG «tag2» MAILBOX «folder2/salmon» UIDVALIDITY 1111111) UID ALL 50003,50006,50009,50012
S: tag2 OK done
В первом запросе мы ищем по папке «folder1» в конкретной ветке подпапок «folder2». Второй запрос добавляет условие, что нужно искать конкретно в подпапке «folder2». Как и предыдущие примеры, эти две команды работают параллельно, ничего не мешает запустить и больше запросов.
Заключение
Данной статьей я заканчиваю серию по IMAP. Далеко не все возможности этого мощного протокола были рассмотрены. Как я и говорил в первой части, я хотел написать такую статью, которая бы помогла мне в самом начале моего пути. Этими простыми примерами и базовыми командами я хотел показать возможности по оптимизации работы с почтой и снова подчеркнуть как важно понимать свою предметную область. Для тех, кто прочитал всю серию, я рекомендую самим побаловаться с этим протоколом и, может быть, заметить фишки которые я пропустил и больше вникнуть в работу IMAP. Спасибо что дочитали до конца!