Skip to the documentation
Python

openemail.threads

Every method in this namespace: its signature, its parameters, what it returns and an example.

Methods

Read, search, label, trash and snooze conversations in the mailbox.

threads.list()

List one page of threads in a folder

Scopesthreads:readPages through results
Signature
def list(    *,    limit: int | None = None,    cursor: str | None = None,    folder: str | None = None,    query: str | None = None,    label_ids: str | Sequence[str] | None = None,    sort: ThreadSort | None = None,    date_from: datetime | str | None = None,    date_to: datetime | str | None = None,    from_contacts: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Page[ThreadSummaryResource]

Returns one page of threads from the mailbox index, ordered by each thread's latest message with the newest first unless sort says otherwise. A row is only object and id, so call get for the messages, labels and unread state.

sort, date_from=, date_to= and from_contacts= are the thread list's own controls: the four orders, a date range read against the newest message on each thread, and a filter to mail from saved contacts. Every order pages to the end, and a cursor carries on in the order it was handed out in, so send the same filters with it.

folder defaults to inbox and is matched against label ids after being upper cased, so sent, archive, spam, trash, draft, snoozed, starred and unread all work, bin is read as trash, and a user label id works as a folder too. A name that matches nothing returns an empty page rather than an error. label_ids= narrows the folder further: a thread must carry the folder label and every id you pass.

query takes the mailbox search syntax. Plain words must all appear, and each matches loosely: case, accents and separators are ignored and part of a longer word counts, so min finds "Benjamin". A quoted phrase is matched as written apart from case and accents, so "ben jamin" does not find "Ben-Jamin" while "quarterly invoice" finds "Quarterly invoice". When nothing matches exactly, close spellings are returned instead, so benjimin finds "Benjamin": a plain word, or the value of from:, to:, cc:, subject:, body:, filename: or label:, may differ from the start of a word by one typo when it has four to seven letters and by two when it has eight or more, while a quoted phrase, a word containing a digit, a shorter word and an excluded word still match exactly, and the pages that follow keep matching the same way. Filler words such as the, about or emails are dropped from a list of plain words when something else is left to search for, so emails from john searches for john alone. Operators such as from:, to:, subject:, label:, is:unread, has:pdf, after:2026/01/31 and newer_than:7d narrow it, and OR, parentheses and a leading - combine them. Recipients are stored as one list without roles and never hold a Bcc, so cc: reads the same field as to: and bcc: matches nothing of its own. from:me is mail you sent, and to:me is mail carrying one of your own addresses, aliases included, among its recipients or as the address it was delivered to.

Words and the from:, to:, cc:, subject: and body: operators read only the newest message on each thread: its sender, its recipients, its subject and the first 4,000 characters of its body with markup stripped. filename: and has: read every attachment on the whole conversation, and label:, in: and is: read the whole conversation. A plain word also matches the name of any attachment on the conversation, whichever message carried it. A sealed message has no body text to match. The search stays inside folder unless the query names a folder itself, with in: or a folder is: such as is:sent, and in:anywhere searches every folder, on its own as well as beside other terms. A drafts listing is the exception and stays in drafts whatever the query names.

Dates read the newest activity on the thread, in UTC. after: includes the day it names and before: excludes it, and a date can be written YYYY/MM/DD, YYYY-MM-DD, YYYYMMDD, as a bare year, or as epoch seconds or milliseconds. A short date reads day first (16/09/2026), unless the second number cannot be a month (09/16/2026), and a number above 12 settles it either way. The API pages with an opaque pageToken, which the SDK hands back as nextCursor and accepts as cursor=.

Parameters

limitint

Threads per page, a whole number from 1 to 100. Defaults to 25.

cursorstr

The nextCursor of the previous page, passed back unchanged.

folderstr

Label the threads must carry, case insensitive. Defaults to inbox, and bin is read as trash.

querystr

Mailbox search. Plain words must all appear and match loosely, a quoted phrase has to appear as written, filler words are dropped when something else is left to search for, and operators such as from:ada, has:pdf and in:anywhere narrow it.

label_idsstr | Sequence[str]

Label ids a thread must all carry on top of folder, matched exactly. A list or tuple is sent comma separated, and a string is sent as it is.

sortThreadSort

newest (the default), oldest, sender or subject, the four orders of the thread list in the app. sender and subject are alphabetical, newest first within one sender or subject.

date_fromdatetime | str

Keeps threads whose newest message arrived at or after this instant. A datetime or an ISO 8601 string with a time and an offset.

date_todatetime | str

Keeps threads whose newest message arrived at or before this instant. Both ends are included, and date_from= after date_to= is a 422.

from_contactsbool

When True, keeps only threads whose newest message came from a saved contact. A key limited to some addresses reads the contacts its owner saved.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Page[ThreadSummaryResource], a dict with items, hasMore and nextCursor. Each item is a dict with object set to thread and the thread's id.

Example

from datetime import datetime, timedelta, timezone from openemail import openemail page = openemail.threads.list(folder='inbox', query='from:ada has:pdf', limit=50)print([thread['id'] for thread in page['items']]) if page['hasMore'] and page['nextCursor']:    following = openemail.threads.list(        folder='inbox', query='from:ada has:pdf', limit=50, cursor=page['nextCursor']    )    print(len(following['items'])) week_ago = datetime.now(timezone.utc) - timedelta(days=7)oldest_first = openemail.threads.list(sort='oldest', date_from=week_ago, from_contacts=True)print(len(oldest_first['items']))

Notes

  • The server offers a cursor whenever a page comes back full, so hasMore can be True on what turns out to be the last page, and the next call then returns no items.

  • Threads that arrived in the same instant are ordered by id, so a page boundary between two of them never skips or repeats one.

  • A thread that receives mail while you page moves ahead of the cursor and is not returned by later pages.

  • A value query cannot use is ignored rather than narrowing, so a typo in a value widens the result instead of emptying it. That covers category:, larger:, smaller:, size:, messagesize:, list:, rfc822msgid:, received: and sent:, the category words such as is:promotions, a has: word naming no kind of attachment, an importance: other than high or low, an unreadable date and a duration whose unit is not h, d, w, m or y. An operator name it does not know, project: for instance, is searched as plain text.

  • A narrowed key only sees threads delivered to the addresses it covers, every address on a whole domain it holds included. A thread still has to carry the folder and every one of label_ids=, the same as for any other key.

Also available in

API
GET /threads
TypeScript
threads.list()
Ruby
threads.list
CLI
openemail threads list

threads.list_all()

Collect every thread in a folder into one list

Scopesthreads:readPages through results
Signature
def list_all(    *,    limit: int | None = None,    cursor: str | None = None,    folder: str | None = None,    query: str | None = None,    label_ids: str | Sequence[str] | None = None,    sort: ThreadSort | None = None,    date_from: datetime | str | None = None,    date_to: datetime | str | None = None,    from_contacts: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ThreadSummaryResource]

Walks every page with the same filters as list and returns once the last page is in, so the whole result sits in memory at once. That suits a label view or a small folder. For a large inbox, iterate lets you stop as soon as you have what you need.

Each request asks for limit= threads, 25 when you leave it out, so raising it to 100 needs a quarter of the round trips. Passing cursor= starts the walk from that page instead of the first. The walk ends at the first empty page or when the server stops offering a cursor.

A failure on any page raises and discards everything collected so far.

Parameters

limitint

Page size for each request, a whole number from 1 to 100. Defaults to 25.

cursorstr

A nextCursor to start the walk from instead of the first page.

folderstr

Label the threads must carry, case insensitive. Defaults to inbox.

querystr

Mailbox search, as in list. Plain words must all appear and match loosely, a quoted phrase has to appear as written, filler words are dropped when something else is left to search for, and operators such as from:ada, has:pdf and in:anywhere narrow it.

label_idsstr | Sequence[str]

Label ids a thread must all carry on top of folder, matched exactly.

sortThreadSort

newest (the default), oldest, sender or subject, the four orders of the thread list in the app. sender and subject are alphabetical, newest first within one sender or subject.

date_fromdatetime | str

Keeps threads whose newest message arrived at or after this instant. A datetime or an ISO 8601 string with a time and an offset.

date_todatetime | str

Keeps threads whose newest message arrived at or before this instant. Both ends are included, and date_from= after date_to= is a 422.

from_contactsbool

When True, keeps only threads whose newest message came from a saved contact. A key limited to some addresses reads the contacts its owner saved.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ThreadSummaryResource], every matching thread as a dict with object and id, in the order sort asks for, newest first by default.

Example

from openemail import openemail snoozed = openemail.threads.list_all(folder='snoozed', limit=100) print(f'{len(snoozed)} threads are waiting to wake')

Notes

  • Each page is its own request with its own retries, so a network blip on page five does not restart the walk from page one.

  • Rows are ids only. Reading the threads afterwards is one get per id.

Also available in

API
GET /threads
TypeScript
threads.listAll()
Ruby
threads.list_all

threads.iterate()

Stream threads one at a time across pages

Scopesthreads:readPages through results
Signature
def iterate(    *,    limit: int | None = None,    cursor: str | None = None,    folder: str | None = None,    query: str | None = None,    label_ids: str | Sequence[str] | None = None,    sort: ThreadSort | None = None,    date_from: datetime | str | None = None,    date_to: datetime | str | None = None,    from_contacts: bool | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> Iterator[ThreadSummaryResource]

Returns a generator that yields threads one by one and requests the next page only when the current one is used up. Nothing is fetched until the loop starts, and breaking out of it stops further requests, so this is the way to scan a large folder for the first match.

Filters behave as in list, and each request asks for limit= threads, 25 by default. The cursor marks a position in time rather than a row count, so trashing, archiving or relabelling threads inside the loop does not make the walk skip the ones after them. A thread that receives new mail during the walk moves ahead of the cursor and is not yielded again.

Parameters

limitint

Page size for each request, a whole number from 1 to 100. Defaults to 25.

cursorstr

A nextCursor to start from instead of the first page.

folderstr

Label the threads must carry, case insensitive. Defaults to inbox.

querystr

Mailbox search, as in list. Plain words must all appear and match loosely, a quoted phrase has to appear as written, filler words are dropped when something else is left to search for, and operators such as from:ada, has:pdf and in:anywhere narrow it.

label_idsstr | Sequence[str]

Label ids a thread must all carry on top of folder, matched exactly.

sortThreadSort

newest (the default), oldest, sender or subject, the four orders of the thread list in the app. sender and subject are alphabetical, newest first within one sender or subject.

date_fromdatetime | str

Keeps threads whose newest message arrived at or after this instant. A datetime or an ISO 8601 string with a time and an offset.

date_todatetime | str

Keeps threads whose newest message arrived at or before this instant. Both ends are included, and date_from= after date_to= is a 422.

from_contactsbool

When True, keeps only threads whose newest message came from a saved contact. A key limited to some addresses reads the contacts its owner saved.

api_keystr

Overrides the client's API key for every page of this walk.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

Iterator[ThreadSummaryResource], a generator yielding a dict with object and id per step.

Example

from openemail import openemail receipts = openemail.threads.iterate(folder='inbox', query='receipt newer_than:30d', limit=100) for summary in receipts:    thread = openemail.threads.get(summary['id'])     if thread['hasUnread']:        openemail.threads.update(summary['id'], {'read': True})

Notes

  • The generator is lazy, so an abandoned loop costs only the pages it consumed.

  • timeout= applies to each page's request. A page that still times out after the SDK's retries raises OpenEmailNetworkError with is_timeout out of the loop rather than ending it quietly.

Also available in

API
GET /threads
TypeScript
threads.iterate()
Ruby
threads.iterate

threads.get()

Read a thread with every message on it

Scopesthreads:read
Signature
def get(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ThreadResource

Returns the whole conversation, oldest message first, with the labels the thread sits in and its unread state. Unsent draft replies are included in messages with 'isDraft': True, which is why messageCount, the length of messages, can be higher than totalReplies, which counts only real messages.

Messages are passed through as the mailbox stored them, so MessageResource is a plain dict[str, Any] rather than a fixed field list. The one key the API commits to is encryption. When message['encryption']['format'] is pgp-mime, pgp-inline or smime-encrypted, the message is sealed and its body is empty or holds armour, and is_sealed(message) from openemail returns True. pgp-signed and smime-signed are ordinary readable mail. A message with no encryption at all was stored before detection existed, so its absence says nothing about whether it was plaintext.

Each message's tags and unread are overwritten on read with the thread's current labels and unread flag, so they describe the thread and not that one message.

Parameters

idstrRequired

Thread id, as returned by list or carried on a message.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ThreadResource with id echoed from the request, messages, labels (each with id, name and color), messageCount, hasUnread, totalReplies and deliveredTo, the workspace address the thread belongs to (None when none was recorded).

Example

from openemail import is_sealed, openemail thread = openemail.threads.get('thr_8f2c41d0a3b94e6f') for message in thread['messages']:    state = 'sealed' if is_sealed(message) else 'readable'    print(message.get('id'), message.get('subject'), state) print([label['id'] for label in thread['labels']], thread['deliveredTo'])

Notes

  • A thread delivered to no address the key covers is a 404, exactly like one that does not exist.

  • hasUnread mirrors the UNREAD label, and update with read is how you change it.

  • Draft ids from drafts.list open here too, because a draft is stored as a thread labelled DRAFT.

Also available in

API
GET /threads/{id}
TypeScript
threads.get()
Ruby
threads.get
CLI
openemail threads get

threads.update()

Mark a thread read or unread and change its labels

Scopesthreads:write
Signature
def update(    id: str,    patch: ThreadPatch,    *,    api_key: str | None = None,    timeout: float | None = None,) -> UpdatedThreadResource

Adds and removes labels on a thread in one call. read is shorthand for the UNREAD label: True removes it and False adds it. At least one of read, a non empty addLabelIds or a non empty removeLabelIds is required, each list takes at most 50 ids, and any other key is a 422 because the body is strict.

Folders are labels too, so archiving is {'addLabelIds': ['ARCHIVE'], 'removeLabelIds': ['INBOX']}, the same pair the app uses. TRASH, SNOOZED and DRAFT are refused in either list with 422 label_not_directly_settable, because each needs a step this route cannot take. Use trash and snooze for those.

User label ids come from labels.list, and the system ids such as ARCHIVE, STARRED and UNREAD are taken in any case. An id in addLabelIds that names no label is refused with 422 label_not_found and nothing on the thread changes, so create the label with labels.create first. An unknown id in removeLabelIds is not an error, since the thread cannot carry it. Removals are applied before additions, so an id in both lists ends up on the thread.

Parameters

idstrRequired

Thread id.

patch['read']bool

True removes UNREAD, False adds it.

patch['addLabelIds']list[str]

Label ids to put on the thread, at most 50, each naming a label, never TRASH, SNOOZED or DRAFT.

patch['removeLabelIds']list[str]

Label ids to take off the thread, at most 50, never TRASH, SNOOZED or DRAFT.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

UpdatedThreadResource with id, addedLabelIds and removedLabelIds. Both lists echo the request, with system ids upper cased, plus the UNREAD change that read implies, not what actually changed.

Example

from openemail import openemail updated = openemail.threads.update(    'thr_8f2c41d0a3b94e6f',    {'read': True, 'addLabelIds': ['ARCHIVE', 'USER_RECEIPTS'], 'removeLabelIds': ['INBOX']},) print(updated['addedLabelIds'], updated['removedLabelIds'])

Notes

  • The SDK retries this call after a network failure or a retryable status, which is safe because adding a label already present or removing one already gone changes nothing.

  • The thread is looked up before the body is validated, so a wrong id is a 404 even when the patch is also invalid.

  • User label ids look like USER_RECEIPTS. Take them from labels.list rather than building them.

Also available in

API
PATCH /threads/{id}
TypeScript
threads.update()
Ruby
threads.update
CLI
openemail threads update

threads.trash()

Move a thread to the Bin

Scopesthreads:write
Signature
def trash(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> TrashedThreadResource

Adds TRASH and removes INBOX, SPAM, SNOOZED and ARCHIVE in one step, which is what the app's delete does. Doing half of that through update would leave the thread listed in both the Bin and its old folder, which is why update refuses TRASH.

Nothing is deleted. The thread stays readable with get and lists under folder='trash'. Calling this on a thread that is already in the Bin changes nothing and returns the same body, which is why the SDK retries it.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

TrashedThreadResource with object set to thread, the id, and trashed set to True.

Example

from openemail import OpenEmailApiError, openemail try:    result = openemail.threads.trash('thr_8f2c41d0a3b94e6f')except OpenEmailApiError as error:    if not error.is_not_found:        raise    print('No such thread, or the key does not reach it')else:    print(result['id'], result['trashed'])

Notes

  • restore takes a thread back out of the Bin. update refuses TRASH in removeLabelIds as well as in addLabelIds.

  • Trashing a snoozed thread also cancels its scheduled wake, so it does not reappear in the inbox later.

  • A thread delivered to no address the key covers is a 404.

Also available in

API
POST /threads/{id}/trash
TypeScript
threads.trash()
Ruby
threads.trash
CLI
openemail threads trash

threads.snooze()

Hide a thread until a set time

Scopesthreads:write
Signature
def snooze(    id: str,    wake_at: datetime | str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> SnoozedThreadResource

Adds SNOOZED, removes INBOX and stores a wake time, all in one call. Both halves matter: the label hides the thread and the stored wake time is what brings it back. A thread labelled SNOOZED any other way would never return, which is why update refuses that label.

wake_at takes a datetime or an ISO 8601 string. The SDK sends a datetime as a UTC ISO 8601 string, and the server answers 422 invalid_parameter on wakeAt when the value does not parse or is not in the future. Snoozing a thread that is already snoozed replaces its wake time.

Threads are woken by an hourly sweep, so one comes back at the first sweep after wake_at, up to about an hour late. Opening the Snoozed folder in the app wakes overdue threads straight away. A thread always wakes into the inbox.

Parameters

idstrRequired

Thread id.

wake_atdatetime | strRequired

When the thread should return, a future instant.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

SnoozedThreadResource with id and snoozedUntil, the wake time normalised to a UTC ISO string.

Example

from datetime import datetime, timedelta, timezone from openemail import openemail tomorrow = datetime.now(timezone.utc) + timedelta(days=1)wake_at = tomorrow.replace(hour=9, minute=0, second=0, microsecond=0) snoozed = openemail.threads.snooze('thr_8f2c41d0a3b94e6f', wake_at)print(snoozed['snoozedUntil'])

Notes

  • Only INBOX is removed, so a thread snoozed from another folder keeps that folder's label while it sleeps and wakes carrying both.

  • A string with no zone offset is read in the server's local zone, so send Z or an explicit offset. A naive datetime is read in the local zone of the machine running the SDK, so give it a tzinfo.

  • The SDK retries this call, which is safe because a repeat stores the same wake time.

Also available in

API
POST /threads/{id}/snooze
TypeScript
threads.snooze()
Ruby
threads.snooze
CLI
openemail threads snooze

threads.unsnooze()

Bring a snoozed thread back now

Scopesthreads:write
Signature
def unsnooze(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> SnoozedThreadResource

Adds INBOX, removes SNOOZED and deletes the stored wake time, so the thread returns immediately and the hourly sweep leaves it alone afterwards.

A thread that is not snoozed is left where it is, so an archived thread stays archived, and the response still reports snoozedUntil as None.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

SnoozedThreadResource with id and snoozedUntil set to None.

Example

from openemail import openemail for summary in openemail.threads.iterate(folder='snoozed'):    result = openemail.threads.unsnooze(summary['id'])    print('back in the inbox:', result['id'])

Notes

  • Removing SNOOZED through update is refused with 422 label_not_directly_settable, because it would leave the wake time scheduled.

  • Safe to retry, and the SDK does: a second call applies the same labels and deletes a wake time that is already gone.

Also available in

API
POST /threads/{id}/unsnooze
TypeScript
threads.unsnooze()
Ruby
threads.unsnooze
CLI
openemail threads unsnooze

threads.list_attachments()

List a message's attachments with their content

Scopesthreads:read
Signature
def list_attachments(    id: str,    message_id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[AttachmentResource]

Returns the attachments of one message with each file's bytes inlined as base64 in content, which base64.b64decode turns back into bytes. The thread is checked first and then the message, so a message_id that is not on that thread is a 404 even when it exists elsewhere in the mailbox. Take message ids from the messages of threads.get.

This is the display list. The ciphertext of an encrypted envelope is in it and downloads like any other file, named encrypted-message.asc when it arrived without a name. The PGP/MIME version part and any detached signature are held out on purpose. Every part keeps its id in the message's encryption['parts'], and for those two the id is a correlation key only: no route returns their bytes.

Every file comes back whole in a single response, with no size cap and no range reads, so a message carrying large files makes a large response.

Parameters

idstrRequired

Thread id the message belongs to.

message_idstrRequired

Message id from that thread's messages.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[AttachmentResource], each with attachmentId, filename, contentType, size and base64 content.

Example

import base64 from openemail import openemail files = openemail.threads.list_attachments(    'thr_8f2c41d0a3b94e6f', 'msg_3f9a1c07d2b84e6a9c5b1f20') for file in files:    data = base64.b64decode(file['content'] or '')    print(file['filename'], file['contentType'], len(data))

Notes

  • When the stored bytes for a file cannot be found, content is an empty string rather than None, so check its length before decoding.

  • Both ids must match: a real message id paired with the wrong thread id is a 404 resource_not_found.

  • A thread delivered to no address the key covers is a 404 before the message is looked at.

Also available in

API
GET /threads/{id}/messages/{messageId}/attachments
TypeScript
threads.listAttachments()
Ruby
threads.list_attachments
CLI
openemail threads list-attachments

threads.list_notes()

List the notes on a thread

Scopesthreads:read
Signature
def list_notes(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ThreadNoteResource]

Returns every note on one thread, the Notes panel of the reading pane: pinned notes first, then the rest in the order they were arranged.

Notes are private to a person. A key reads and writes the notes of the workspace owner, and an app those of the person who connected it. A key limited to particular addresses reaches only the notes on threads that arrived at them.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ThreadNoteResource], each with id, threadId, content, color, pinned, order, createdAt and updatedAt.

Example

from openemail import openemail notes = openemail.threads.list_notes('thr_8f2c41d0a3b94e6f') for note in notes:    print('pinned' if note['pinned'] else '', note['content'])

Notes

  • A thread delivered to no address the key covers is a 404.

Also available in

API
GET /threads/{id}/notes
TypeScript
threads.listNotes()
Ruby
threads.list_notes
CLI
openemail threads list-notes

threads.create_note()

Add a note to a thread

Scopesthreads:write
Signature
def create_note(    id: str,    body: ThreadNoteCreate,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ThreadNoteResource

Pins a private note to a thread, as the Notes panel does. A new note goes after the others, and pinned keeps it at the top.

Notes are private to a person. A key reads and writes the notes of the workspace owner, and an app those of the person who connected it. A key limited to particular addresses reaches only the notes on threads that arrived at them.

Parameters

idstrRequired

Thread id.

body['content']strRequired

The text of the note, up to 20,000 characters. Leading and trailing spaces are trimmed.

body['color']ThreadNoteColor

One of the eight the app offers: default, red, orange, yellow, green, blue, purple or pink. Defaults to default.

body['pinned']bool

Keeps the note above the others. Defaults to False.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ThreadNoteResource for the new note.

Example

from openemail import openemail note = openemail.threads.create_note(    'thr_8f2c41d0a3b94e6f',    {        'content': 'Waiting on the signed contract before replying.',        'color': 'yellow',        'pinned': True,    },) print(note['id'], note['order'])

Notes

  • The SDK does not retry it, because a second call would add a second note.

Also available in

API
POST /threads/{id}/notes
TypeScript
threads.createNote()
Ruby
threads.create_note
CLI
openemail threads create-note

threads.update_note()

Change a note

Scopesthreads:write
Signature
def update_note(    id: str,    note_id: str,    patch: ThreadNoteUpdate,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ThreadNoteResource

Changes the text of a note, its colour or whether it is pinned. Give at least one of the three. A note id that is not on this thread is a 404, even when the note exists on another thread.

Parameters

idstrRequired

Thread id.

note_idstrRequired

The note's id, from list_notes.

patch['content']str

The new text, up to 20,000 characters.

patch['color']ThreadNoteColor

The new colour.

patch['pinned']bool

Pins or unpins it.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ThreadNoteResource as it is now.

Example

from openemail import openemail note = openemail.threads.update_note(    'thr_8f2c41d0a3b94e6f', 'b3d1f0c2-7a4e-4f7b-9c1d-2e8f6a5b4c3d', {'pinned': False}) print(note['pinned'], note['updatedAt'])

Notes

  • Safe to repeat: setting the same values twice leaves the note as it was.

Also available in

API
PATCH /threads/{id}/notes/{noteId}
TypeScript
threads.updateNote()
Ruby
threads.update_note
CLI
openemail threads update-note

threads.delete_note()

Delete a note

Scopesthreads:write
Signature
def delete_note(    id: str,    note_id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> DeletedThreadNoteResource

Deletes a note for good. There is no bin for notes.

Parameters

idstrRequired

Thread id.

note_idstrRequired

The note's id, from list_notes.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

DeletedThreadNoteResource with object set to note, the id and threadId, and deleted set to True.

Example

from openemail import openemail deleted = openemail.threads.delete_note(    'thr_8f2c41d0a3b94e6f', 'b3d1f0c2-7a4e-4f7b-9c1d-2e8f6a5b4c3d') print(deleted['id'], deleted['deleted'])

Notes

  • The SDK does not retry a delete. A 404 on your own second attempt after a lost response means the first one worked.

Also available in

API
DELETE /threads/{id}/notes/{noteId}
TypeScript
threads.deleteNote()
Ruby
threads.delete_note
CLI
openemail threads delete-note

threads.reorder_notes()

Arrange the notes on a thread

Scopesthreads:write
Signature
def reorder_notes(    id: str,    ids: Sequence[str],    *,    api_key: str | None = None,    timeout: float | None = None,) -> builtins.list[ThreadNoteResource]

Sets the order of every note on a thread at once, first to last, as dragging them in the Notes panel does. Pinned notes still come first.

ids has to name every note on the thread exactly once. Anything else is a 422 invalid_parameter on ids, and nothing moves.

Parameters

idstrRequired

Thread id.

idsSequence[str]Required

Every note id on the thread, in the order you want them, as a list or tuple.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

list[ThreadNoteResource] in the new order.

Example

from openemail import openemail notes = openemail.threads.list_notes('thr_8f2c41d0a3b94e6f')newest_first = [note['id'] for note in reversed(notes)] reordered = openemail.threads.reorder_notes('thr_8f2c41d0a3b94e6f', newest_first)print([note['content'] for note in reordered])

Notes

  • Safe to repeat: the same order twice leaves the notes as they were.

Also available in

API
POST /threads/{id}/notes/reorder
TypeScript
threads.reorderNotes()
Ruby
threads.reorder_notes
CLI
openemail threads reorder-notes

threads.counts()

Count the mail in each folder

Scopesthreads:read
Signature
def counts(    *,    address: str | None = None,    api_key: str | None = None,    timeout: float | None = None,) -> MailboxCountsResource

Returns what the sidebar of the app shows: how many conversations each folder holds and how many of them are unread, and how many inbox conversations arrived at each address of the workspace.

folders has one row per folder, named by its label id in lower case: inbox, sent, spam, archive, trash and snoozed, then unread, whose count is the unread conversations in the inbox. addresses counts the inbox per address the mail was delivered to, with None for mail that recorded none.

A key limited to particular addresses counts only the mail that arrived at them. address narrows every count to one address, and one the key does not reach counts nothing rather than failing.

Parameters

addressstr

Count only the mail delivered to this address.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

MailboxCountsResource with object set to mailbox_counts, folders, each with label, count and unread, and addresses, each with address and count.

Example

from openemail import openemail counts = openemail.threads.counts()inbox = next((row for row in counts['folders'] if row['label'] == 'inbox'), None) if inbox is not None:    print(inbox['unread'], 'unread of', inbox['count']) for row in counts['addresses']:    print(row['address'] or '(none)', row['count'])

Notes

  • Read only, so the SDK retries it after a network failure like any other read.

Also available in

API
GET /threads/counts
TypeScript
threads.counts()
Ruby
threads.counts
CLI
openemail threads counts

threads.summary()

Read the summary of a thread

Scopesthreads:read
Signature
def summary(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> ThreadAiSummaryResource

Returns the short AI summary the reading pane shows above a thread. A summary is written once and kept, and written again when a new message arrives, so reading one is cheap.

state is ready with the text in summary, pending while one is being written, or none when there is nothing to summarise, such as a thread holding a message that arrived encrypted, whose body OpenEmail never reads. A pending answer starts the writing, so ask again a few seconds later.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

ThreadAiSummaryResource with object set to thread_summary, threadId, state and summary, which is None unless state is ready.

Example

import time from openemail import openemail result = openemail.threads.summary('thr_8f2c41d0a3b94e6f') if result['state'] == 'pending':    time.sleep(5)    result = openemail.threads.summary('thr_8f2c41d0a3b94e6f') print(result['summary'] or result['state'])

Notes

  • Writing a summary spends one of the workspace's AI actions. Reading one that is already written spends nothing.

  • A thread delivered to no address the key covers is a 404.

Also available in

API
GET /threads/{id}/summary
TypeScript
threads.summary()
Ruby
threads.summary
CLI
openemail threads summary

threads.restore()

Take a thread out of the Bin

Scopesthreads:write
Signature
def restore(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> RestoredThreadResource

Puts a thread back in the inbox, out of the Bin and out of Spam, which is what Restore from Bin and Move to inbox do in the app. It is the undo of trash.

Calling it on a thread that is already in the inbox changes nothing and returns the same body, which is why the SDK retries it.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

RestoredThreadResource with object set to thread, the id, and restored set to True.

Example

from openemail import openemail openemail.threads.trash('thr_8f2c41d0a3b94e6f') restored = openemail.threads.restore('thr_8f2c41d0a3b94e6f')print(restored['id'], restored['restored'])

Notes

  • A thread that was archived before it went to the Bin comes back to the inbox, not to the archive.

  • A thread delivered to no address the key covers is a 404.

Also available in

API
POST /threads/{id}/restore
TypeScript
threads.restore()
Ruby
threads.restore
CLI
openemail threads restore

threads.delete()

Delete a thread for good

Scopesthreads:write
Signature
def delete(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> DeletedThreadResource

Deletes every message in a thread, with its attachments, which is what Delete from Bin does in the app. It cannot be undone.

It works on a thread in any folder, so call trash instead when you only mean to put the thread out of the way: a trashed thread stays readable and restore brings it back.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

DeletedThreadResource with object set to thread, the id, and deleted set to True.

Example

from openemail import OpenEmailApiError, openemail try:    deleted = openemail.threads.delete('thr_8f2c41d0a3b94e6f')except OpenEmailApiError as error:    if not error.is_not_found:        raise    print('Already gone, or the key does not reach it')else:    print(deleted['id'], deleted['deleted'])

Notes

  • The SDK does not retry a delete. A 404 on your own second attempt after a lost response means the first one worked.

  • A 500 thread_delete_failed means nothing was removed, so the call is safe to repeat.

  • A thread delivered to no address the key covers is a 404.

Also available in

API
DELETE /threads/{id}
TypeScript
threads.delete()
Ruby
threads.delete
CLI
openemail threads delete

threads.unsubscribe()

Unsubscribe from the sender of a thread

Scopesthreads:write
Signature
def unsubscribe(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> UnsubscribeResultResource

Does what the Unsubscribe button of the reading pane does: finds the subscription behind the newest message of the thread that carries a List-Unsubscribe header and unsubscribes from it the way the sender asks for, with a one-click request or an unsubscribe email. A sender that only offers a page cannot be unsubscribed by a program, so method is link and url is the page a person has to open.

A thread with no unsubscribe header is a 422 unsubscribe_unsupported, and a thread delivered to no address the key covers is a 404.

Parameters

idstrRequired

Thread id.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

UnsubscribeResultResource with object set to unsubscribe, threadId, subscriptionId, method, url and binned.

Example

from openemail import openemail result = openemail.threads.unsubscribe('thr_8f2c41d0a3b94e6f') if result['method'] == 'link':    print('Open this page to finish:', result['url'])else:    print('Unsubscribed by', result['method'])

Notes

  • The SDK does not retry it, because a second call can send a second unsubscribe email.

Also available in

API
POST /threads/{id}/unsubscribe
TypeScript
threads.unsubscribe()
Ruby
threads.unsubscribe
CLI
openemail threads unsubscribe

threads.get_event()

Read the event a thread carries

Scopescalendar:read
Signature
def get_event(    id: str,    *,    api_key: str | None = None,    timeout: float | None = None,) -> CalendarEventResource

Returns the calendar event that came with an invitation in the thread, as the invitation card of the reading pane shows it. Answer it with calendar.respond_to_event.

Parameters

idstrRequired

Thread id, as threads.list returns it.

api_keystr

Overrides the client's API key for this call only.

timeoutfloat

Seconds this call may take, the response included, before it raises OpenEmailNetworkError with is_timeout. It overrides the client's timeout for this call, and 0 turns the limit off.

Returns

CalendarEventResource, as calendar.get_event returns it.

Example

from openemail import openemail event = openemail.threads.get_event('thr_8f2c41d0a3b94e6f') print(event['summary'], event['start'], event['location'])

Notes

  • A thread with no event, or one the key does not reach, is a 404.

Also available in

API
GET /threads/{id}/event
TypeScript
threads.getEvent()
Ruby
threads.get_event
CLI
openemail threads get-event