Перейти к документации
Ruby

Цепочки

`threads.list`, `list_all`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze` и `list_attachments`.

Чтение

read_threads.rb
page = client.threads.list(  folder: "inbox",  query: "from:ada",  label_ids: ["INBOX", "IMPORTANT"],  limit: 25) if page.next_cursor  next_page = client.threads.list(folder: "inbox", cursor: page.next_cursor)  puts next_page.items.sizeend thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com")puts thread[:messageCount], thread[:hasUnread], thread[:totalReplies]

API листает цепочки с помощью pageToken. Клиент отдаёт его вам как next_cursor и принимает обратно как cursor:, как у любого другого списка, а list_all и iterate следуют за ним за вас. Он непрозрачен: передавайте обратно то, что получили, и никогда не составляйте его сами.

Фильтры списка являются именованными аргументами Ruby в snake_case (label_ids:, date_from:), тогда как поля тела запроса сохраняют имена API в camelCase (addLabelIds: в update). Цепочка возвращается как Hash с ключами типа Symbol, поэтому thread[:messageCount] читает количество.

sort_threads.rb
last_week = client.threads.list_all(  sort: "oldest",  date_from: Time.now - (7 * 86_400),  date_to: Time.now,  from_contacts: true)puts last_week.size client.threads.iterate(sort: "sender") do |thread|  puts thread[:id]end

sort:, date_from:, date_to: и from_contacts: являются собственными настройками списка цепочек. sort: принимает newest, oldest, sender или subject, и OpenEmail::THREAD_SORTS их перечисляет. Даты принимают Time, DateTime или строку ISO 8601 со временем и смещением, и обе границы включаются. Date из Ruby отправляется как голая дата, которую эти поля отклоняют с 422. from_contacts: true оставляет почту, последнее сообщение которой пришло от сохранённого контакта. При любом порядке выдача листается до конца, не пропуская и не повторяя цепочки.

list_all возвращает один Array, когда получена последняя страница. iterate передаёт каждую цепочку в блок и запрашивает следующую страницу, только когда она нужна циклу. Без блока он возвращает Enumerator, поэтому first(10) или lazy останавливаются, как только получат то, что им нужно.

Упорядочивание

organise_threads.rb
thread_id = "CAHk7pQ2x9LmZ4-mail.example.com" client.threads.update(thread_id, read: true, addLabelIds: ["USER_DONE"], removeLabelIds: ["INBOX"]) client.threads.trash(thread_id)client.threads.snooze(thread_id, Time.now + 86_400)client.threads.unsnooze(thread_id)

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

addLabelIds принимает идентификаторы из labels.list и системные идентификаторы вроде ARCHIVE и STARRED. Идентификатор, который не называет ни одного ярлыка, отклоняется с 422 label_not_found, а не создаётся, поэтому сначала создайте ярлык через labels.create. client.threads.list(folder: "USER_DONE") выводит все цепочки с каким-либо ярлыком, в какой бы папке они ни находились.

Вложения письма

attachments.rb
files = client.threads.list_attachments("CAHk7pQ2x9LmZ4-mail.example.com", "message_4c1b257a") files.each do |file|  puts "#{file[:filename]} #{file[:contentType]} #{file[:size]}"  File.binwrite(file[:filename], file[:content].unpack1("m")) unless file[:content].to_s.empty?end

list_attachments возвращает Array из Hash. content закодирован в base64, который unpack1("m") превращает в двоичную String, и является пустой строкой, если сохранённые байты не удалось найти, поэтому перед декодированием проверяйте его длину. Шифротекст зашифрованного сообщения есть в этом списке и скачивается, как любой другой файл. Части с версией PGP/MIME и отделённой подписи в нём нет. От них остаются только идентификаторы в encryption.parts.

Письмо, пришедшее зашифрованным

Этот гем ничего не шифрует и не расшифровывает. Он не может открыть сообщение, зашифрованное кем-то другим, и не может отправить зашифрованное. Запрос на отправку отклоняется, если он несёт маркер шифрования, потому что клиенту без ключа незачем его заявлять. Ключи, созданные в приложении OpenEmail, живут в браузере, который их создал, и сюда не попадают. Когда этот браузер открывает запечатанное сообщение, открытый текст остаётся в нём, а сохранённое сообщение, которое читает этот вызов, по-прежнему является шифротекстом. threads.get даёт вам распознанный конверт. Сообщение, пришедшее в обёртке PGP или S/MIME, несёт Hash encryption, поэтому пустой decodedBody перестаёт быть единственным, что вы получаете. encryption является единственным полем сообщения, которое API гарантирует, потому что именно о его отсутствии нельзя позволить себе гадать.

encrypted_mail.rb
thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com") thread[:messages].each do |message|  next unless message[:encryption]  next unless OpenEmail.sealed?(message)   warn "cannot read this one: #{message[:encryption][:format]}"end

Ветвитесь по OpenEmail.sealed?, а не по наличию поля. Два из пяти форматов, pgp-signed и smime-signed, описывают тело, пришедшее открытым рядом с отделённой подписью, поэтому проверка на наличие поля прячет почту, которую прятать было не нужно, и пользователь не может ни увидеть её, ни объяснить. OpenEmail.sealed? существует именно поэтому. Сервер задаёт набор запечатанных форматов один раз, копия в геме генерируется из того же источника, а третья копия, написанная вручную, и есть та, что со временем расходится с остальными. OpenEmail::MESSAGE_ENCRYPTION_FORMATS перечисляет все пять форматов.

Отсутствие не означает открытый текст. encryption отсутствует у всех писем, сохранённых до выпуска определителя, и у всего, что попало в ящик путём, где определитель не отрабатывал. Это фиксирует, что никто не смотрел (факт о нашем покрытии, а не о почте), и задним числом поле ничем не заполняется.

Чем это отличается от остального

  • Каждая запись в messages цепочки является Hash, который сохранил почтовый ящик, без фиксированного списка полей. Обещать больше значило бы, что клиент утверждает нормализацию, которую никто не выполняет. encryption является единственным полем, которое API всё же гарантирует, потому что клиент, не способный ветвиться по нему, прочитает запечатанное сообщение как пустое.
  • Запрос, который нельзя выполнить честно, даёт 422 capability_unsupported, выбрасываемый как OpenEmail::ValidationError, а не ответ, который выглядит правильным, но втихую неверен.

Параметры: threads.list

folderString
Какую папку выводить. Сервер по умолчанию подставляет `inbox`, поэтому пропуск параметра сужает выдачу, а не расширяет её до всего. Он применяется и к поиску через `query:`, если только сам запрос не называет папку через `in:` или через папочный `is:`, например `is:sent`.
queryString
Синтаксис поиска по почтовому ящику. Все простые слова должны встретиться, и каждое сопоставляется нестрого: регистр, диакритика и разделители игнорируются, а часть более длинного слова засчитывается, поэтому и `min`, и `ben jamin` находят «Benjamin». Фраза в кавычках сопоставляется как написана, с точностью до регистра и диакритики, поэтому `"ben jamin"` не находит «Ben-Jamin», а служебные слова отбрасываются, если остаётся что-то ещё, по чему искать. Если точных совпадений нет, вместо них возвращаются близкие написания, так что `benjimin` находит «Benjamin»: обычное слово или значение `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:` или `label:` может отличаться от начала слова на одну опечатку (заменённую, пропущенную, лишнюю или переставленную букву), если в нём от четырёх до семи букв, и на две, если восемь и больше. Фраза в кавычках, слово с цифрой, более короткое слово и исключённое слово по-прежнему совпадают только точно, а следующие страницы ищут тем же способом. Сужайте выдачу операторами вроде `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31` и `older_than:1y` и комбинируйте их через `OR`, скобки и ведущий `-`. Значение, которое поиск не может использовать, игнорируется, а не сужает выдачу. Слова и операторы `from:`, `to:`, `cc:`, `subject:` и `body:` читают отправителя, получателей, тему последнего сообщения и первые 4 000 символов его тела с вырезанной разметкой, тогда как `filename:` и `has:` читают все вложения всей переписки, а ярлыки и папки читают переписку целиком. Поиск сужает тот же индекс, который читает выдача без фильтров. Запечатанные сообщения не хранят текста тела, поэтому в них могут совпасть только отправитель, получатели и тема. Обычное слово также совпадает с именем любого вложения в переписке, в каком бы сообщении оно ни пришло.
label_idsString or Array<String>
Ограничить выдачу цепочками с этими ярлыками. Эндпоинт принимает строку с разделителями-запятыми, а клиент склеивает в неё Array или Set за вас. Ограничения на количество названных ярлыков нет.
limitInteger
Сколько цепочек вернуть, от 1 до 100. Если не указано, обработчик использует 25. Значение по умолчанию задано в обработчике, а не в схеме, поэтому отсутствующее значение и явное 25 ведут себя одинаково.
cursorString
`next_cursor` предыдущей страницы, переданный обратно без изменений. Это `pageToken` из API под именем, которое используют все остальные списки, и он непрозрачен, поэтому никогда не составляйте и не правьте его.

Ответ: OpenEmail::Page

itemsArray<Hash>
По одному Hash на каждую цепочку этой страницы, извлечённому из конверта `data` API. Каждый содержит только маркер `object` и `id`. Выдача не несёт ни темы, ни фрагмента текста, ни участников, ни ярлыков, поэтому за всем остальным вызывайте `threads.get` для нужных цепочек.
items[].idString
Идентификатор цепочки, читается как `item[:id]`, чтобы без изменений передать его в `threads.get`, `threads.update` и остальные методы. Это тот же идентификатор, пришла ли строка из отфильтрованной выдачи или из поиска через `query:`.
has_more?Boolean
Есть ли следующая страница. Берётся из API, если он это сообщает, а иначе выводится из `next_cursor`.
next_cursorString or nil
`nextPageToken` из API, который нужно отправить обратно как `cursor:` для следующей страницы, или nil, если следующей страницы нет. Пустой токен нормализуется в nil, поэтому `if page.next_cursor` и проверка на nil дают одинаковый результат.