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

Цепочки

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

Чтение

read_threads.py
from openemail import openemail page = openemail.threads.list(    folder='inbox',    query='from:ada',    label_ids=['INBOX', 'IMPORTANT'],    limit=25,) next_page = (    openemail.threads.list(folder='inbox', cursor=page['nextCursor'])    if page['nextCursor']    else None) thread = openemail.threads.get('thread_…')print(thread['messageCount'], thread['hasUnread'], thread['totalReplies'])

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

Фильтры списка задаются именованными аргументами в snake_case (label_ids=, date_from=), а ключи тела запроса сохраняют имена API в camelCase (addLabelIds в update). Страница и переписка возвращаются как словари, так что читайте их через page['nextCursor'] и thread['messageCount'].

sort_threads.py
from datetime import datetime, timedelta, timezone from openemail import openemail now = datetime.now(timezone.utc) last_week = openemail.threads.list_all(    sort='oldest',    date_from=now - timedelta(days=7),    date_to=now,    from_contacts=True,) for thread in openemail.threads.iterate(sort='sender'):    print(thread['id'])

sort, date_from, date_to и from_contacts являются собственными настройками списка переписок. sort принимает newest, oldest, sender или subject, даты принимают datetime или строку ISO 8601, оба конца включительно, а from_contacts оставляет письма, чьё самое новое сообщение пришло от сохранённого контакта. Любой порядок листается до конца, не пропуская и не повторяя переписку. datetime без tzinfo читается как местное время.

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

organise_threads.py
from datetime import datetime, timedelta, timezone from openemail import openemail openemail.threads.update('thread_…', {    'read': True,    'addLabelIds': ['USER_DONE'],    'removeLabelIds': ['INBOX'],}) openemail.threads.trash('thread_…')openemail.threads.snooze('thread_…', datetime.now(timezone.utc) + timedelta(days=1))openemail.threads.unsnooze('thread_…')

Состояние прочтения здесь ЯВЛЯЕТСЯ ярлыком на любом бэкенде, поэтому оно передаётся вместе со списками ярлыков, и порядок детерминирован, когда вы задаёте и то, и другое. Должно присутствовать хотя бы одно из трёх полей.

addLabelIds принимает идентификаторы из labels.list и системные вроде ARCHIVE и STARRED. Идентификатор, не называющий ни одной метки, отклоняется с 422 label_not_found, а не создаётся, так что сначала создайте метку через labels.create. threads.list(folder='USER_DONE') выдаёт все цепочки с меткой, в какой бы папке они ни лежали.

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

attachments.py
import base64from pathlib import Path from openemail import openemail files = openemail.threads.list_attachments('thread_…', 'message_…') for file in files:    print(file['filename'], file['contentType'], file['size'])     if file['content']:        name = Path(file['filename']).name        Path(name).write_bytes(base64.b64decode(file['content']))

content содержит base64, а если сохранённые байты найти не удалось, в нём пустая строка, поэтому перед декодированием проверяйте его длину. Шифротекст зашифрованного письма В этом списке ЕСТЬ и скачивается, как любой другой файл; части с версией PGP/MIME и отделённой подписи в нём нет. За ними остаются только их идентификаторы в encryption.parts, и ничего больше.

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

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

encrypted_mail.py
import sys from openemail import is_sealed, openemail thread = openemail.threads.get('thread_…') for message in thread['messages']:    if not message.get('encryption'):        continue    if not is_sealed(message):        continue     print('cannot read this one:', message['encryption']['format'], file=sys.stderr)

Ветвитесь по is_sealed, а не по наличию поля. Два из пяти форматов, pgp-signed и smime-signed, описывают тело, пришедшее ОТКРЫТЫМ рядом с отделённой подписью, поэтому проверка на наличие поля прячет почту, которую прятать было не нужно, а пользователь не может её ни увидеть, ни объяснить. is_sealed поставляется именно поэтому: сервер задаёт набор запечатанных форматов один раз, а третья копия, выписанная из объединения типов, и есть та самая копия, которая разъедется.

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

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

  • Каждая запись в ThreadResource.messages является MessageResource, обычным dict[str, Any], тип которого не называет ни одного поля, даже encryption. Типизировать поля значило бы, что клиент утверждает нормализацию, которой никто не выполняет. Читайте encryption через message.get('encryption') и ветвитесь по is_sealed, потому что клиент, который не может по нему ветвиться, читает запечатанное письмо как пустое.
  • Запрос, который нельзя обслужить добросовестно, даёт 422 capability_unsupported, а не ответ, который выглядит правильным и тихо неверен.

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

folderstr
Какую папку выводить. Сервер по умолчанию подставляет `inbox`, поэтому пропуск параметра сужает выдачу, а не расширяет её до всего. Он применяется и к поиску через `query`, если только сам запрос не называет папку через `in:` или через папочный `is:`, например `is:sent`.
querystr
Синтаксис поиска по почтовому ящику. Все простые слова должны встретиться, и каждое сопоставляется нестрого: регистр, диакритика и разделители игнорируются, а часть более длинного слова засчитывается, поэтому и `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_idsstr | Sequence[str]
Ограничивает выдачу переписками с этими метками. Эндпоинт принимает строку через запятую, а клиент сам склеивает в неё список или кортеж. Ограничения на число названных меток нет.
limitint
Сколько переписок вернуть, от 1 до 100. Если не указано, обработчик использует 25. Значение по умолчанию задано в обработчике, а не в схеме, поэтому отсутствие значения и явная 25 ведут себя одинаково.
cursorstr
`nextCursor` предыдущей страницы, переданный обратно дословно. Это `pageToken` из API под тем именем, которое использует любой другой список; он непрозрачен, поэтому никогда не составляйте и не правьте его.

Ответ: Page[ThreadSummaryResource]

itemslist[ThreadSummaryResource]
По одной записи на каждую переписку на этой странице, вынутые из конверта `data` в API. Каждая запись содержит только маркер объекта и идентификатор. В выдаче нет ни темы, ни фрагмента, ни участников, ни ярлыков, поэтому за чем-то большим придётся вызвать `threads.get` для нужных переписок.
items[].idstr
Идентификатор переписки, который без изменений передаётся в `threads.get`, `threads.update` и остальные методы. Он один и тот же, пришла ли строка из отфильтрованной выдачи или из поиска через `query`.
hasMorebool
Есть ли следующая страница; выводится из `nextCursor` там, где API этого не сообщает.
nextCursorstr | None
`nextPageToken` из API, который отправляется обратно как `cursor` за следующей страницей, или `None`, когда следующей страницы нет. Пустой токен нормализуется в `None`, поэтому проверка на ложность и проверка на `None` дают один результат.

Справочник