Цепочки
`threads.list`, `list_all`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze` и `list_attachments`.
Чтение
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'].
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 читается как местное время.
Упорядочивание
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') выдаёт все цепочки с меткой, в какой бы папке они ни лежали.
Вложения письма
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.
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` дают один результат.
Справочник
threads.list()Полный справочникthreads.list_all()Полный справочникthreads.iterate()Полный справочникthreads.get()Полный справочникthreads.update()Полный справочникthreads.trash()Полный справочникthreads.snooze()Полный справочникthreads.unsnooze()Полный справочникthreads.list_attachments()Полный справочник