Переписки
`threads.list`, `listAll`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze` и `listAttachments`.
Чтение
const page = await openemail.threads.list({ folder: 'inbox', query: 'from:ada', labelIds: ['INBOX', 'IMPORTANT'], limit: 25,}) const next = page.nextCursor ? await openemail.threads.list({ folder: 'inbox', cursor: page.nextCursor }) : null const thread = await openemail.threads.get('thread_…')console.log(thread.messageCount, thread.hasUnread, thread.totalReplies)API разбивает переписки на страницы через pageToken. Клиент отдаёт его вам как nextCursor и принимает обратно как cursor, как и в любом другом списке, а listAll и iterate проходят по страницам за вас. Он непрозрачен: возвращайте то, что вам дали, и никогда не составляйте его сами.
Упорядочивание
await openemail.threads.update('thread_…', { read: true, addLabelIds: ['Done'], removeLabelIds: ['INBOX'],}) await openemail.threads.trash('thread_…')await openemail.threads.snooze('thread_…', new Date(Date.now() + 86_400_000))await openemail.threads.unsnooze('thread_…')Состояние прочтения здесь ЯВЛЯЕТСЯ ярлыком на любом бэкенде, поэтому оно передаётся вместе со списками ярлыков, и порядок детерминирован, когда вы задаёте и то, и другое. Должно присутствовать хотя бы одно из трёх полей.
Вложения письма
const files = await openemail.threads.listAttachments('thread_…', 'message_…') for (const file of files) { console.log(file.filename, file.contentType, file.size) if (file.content) await save(file.filename, Buffer.from(file.content, 'base64'))}content — это base64, а при невозможности найти сохранённые байты — пустая строка, поэтому перед декодированием проверяйте её длину. Шифротекст зашифрованного письма В этом списке ЕСТЬ и скачивается, как любой другой файл; часть с версией PGP/MIME и отделённая подпись — нет. За ними остаются только их идентификаторы в encryption.parts, и ничего больше.
Письмо, пришедшее зашифрованным
Этот SDK ничего не шифрует и не расшифровывает: он не может открыть письмо, зашифрованное кем-то другим, и не может отправить зашифрованное. Запрос на отправку отклоняется, если он несёт маркер шифрования, потому что клиенту без ключа незачем что-либо о нём утверждать. Ключи, созданные в приложении OpenEmail, живут в том браузере, который их создал, и сюда не попадают, а когда этот браузер открывает запечатанное письмо, открытый текст остаётся в нём, и сохранённое письмо, которое читает этот вызов, по-прежнему остаётся шифротекстом. То, что даёт вам threads.get, — это распознанный конверт. Письмо, пришедшее в обёртке PGP или S/MIME, несёт объект encryption, поэтому пустой decodedBody перестаёт быть единственным, что вам вручили; encryption — единственное поле MessageResource с настоящим типом, потому что это единственное поле, чьё отсутствие нельзя пережить, угадывая.
import { isSealed, openemail } from '@openemail/sdk' const thread = await openemail.threads.get('thread_…') for (const message of thread.messages) { if (!message.encryption) continue if (!isSealed(message)) continue console.warn('cannot read this one:', message.encryption.format)}Ветвитесь по isSealed, а не по наличию поля. Два из пяти форматов, pgp-signed и smime-signed, описывают тело, пришедшее ОТКРЫТЫМ рядом с отделённой подписью, поэтому проверка на наличие поля прячет почту, которую прятать было не нужно, а пользователь не может её ни увидеть, ни объяснить. isSealed поставляется именно поэтому: сервер задаёт набор запечатанных форматов один раз, а третья копия, выписанная из объединения типов, — это та самая копия, которая разъедется.
Отсутствие — это не открытый текст. encryption отсутствует у всех писем, сохранённых до выпуска определителя, и у всего, что попало в ящик путём, где определитель не отрабатывал. Это фиксирует, что никто не смотрел, — факт о нашем покрытии, а не о почте, и задним числом поле ничем не заполняется.
Чем это отличается от остального
- Каждая запись в
ThreadResource.messages— этоMessageResource, то естьRecord<string, unknown>ровно с одним именованным полем. Типизировать остальное значило бы, что клиент утверждает нормализацию, которой никто не выполняет, аencryptionвсё же названо, потому что клиент, который не может по нему ветвиться, читает запечатанное письмо как пустое. - Запрос, который нельзя обслужить добросовестно, даёт 422
capability_unsupported, а не ответ, который выглядит правильным и тихо неверен.
Параметры: threads.list (ThreadListOptions)
folderstring- Какую папку выводить. Сервер по умолчанию подставляет `inbox`, поэтому пропуск параметра сужает выдачу, а не расширяет её до всего. Он применяется и к поиску через `query`, если только сам запрос не называет папку через `in:` или через папочный `is:`, например `is:sent`.
querystring- Синтаксис поиска по почтовому ящику. Все простые слова должны встретиться, и каждое сопоставляется нестрого: регистр, диакритика и разделители игнорируются, а часть более длинного слова засчитывается, поэтому и `min`, и `ben jamin` находят «Benjamin». Фраза в кавычках сопоставляется как написана, с точностью до регистра и диакритики, поэтому `"ben jamin"` не находит «Ben-Jamin», а служебные слова отбрасываются, если остаётся что-то ещё, по чему искать. Сужайте выдачу операторами вроде `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:` читают все вложения всей переписки, а ярлыки и папки читают переписку целиком. Поиск сужает тот же индекс, который читает выдача без фильтров. Запечатанные письма не хранят текста тела, поэтому в них могут совпасть только отправитель, получатели и тема.
labelIdsstring | string[]- Ограничить выдачу переписками, несущими эти ярлыки. Эндпоинт принимает строку с разделителями-запятыми, а клиент склеивает массив в неё за вас; ограничения на количество названных ярлыков нет.
limitnumber- Сколько переписок вернуть, от 1 до 100. Если не указано, обработчик использует 25. Значение по умолчанию задано в обработчике, а не в схеме, поэтому отсутствие значения и явная 25 ведут себя одинаково.
cursorstring- `nextCursor` предыдущей страницы, переданный обратно дословно. Это `pageToken` из API под тем именем, которое использует любой другой список; он непрозрачен, поэтому никогда не составляйте и не правьте его.
Ответ: Page<ThreadSummaryResource>
itemsThreadSummaryResource[]- По одной записи на каждую переписку на этой странице, вынутые из конверта `data` в API. Каждая запись — это только маркер объекта и идентификатор. В выдаче нет ни темы, ни фрагмента, ни участников, ни ярлыков, поэтому за чем-то большим придётся вызвать `threads.get` для нужных переписок.
items[].idstring- Идентификатор переписки, который без изменений передаётся в `threads.get`, `threads.update` и остальные методы. Он один и тот же, пришла ли строка из отфильтрованной выдачи или из поиска через `query`.
hasMoreboolean- Есть ли следующая страница; выводится из `nextCursor` там, где API этого не сообщает.
nextCursorstring | null- `nextPageToken` из API, который отправляется обратно как `cursor` за следующей страницей, или null, когда следующей страницы нет. Пустой токен нормализуется в null, поэтому проверка на ложность и проверка на null дают один результат.