Ir a la documentación
Python

Conversaciones

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

Lectura

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'])

La API pagina los hilos con un pageToken. El cliente te lo entrega como nextCursor y lo recibe de vuelta como cursor, igual que cualquier otra lista, y list_all e iterate lo siguen por ti. Es opaco: devuelve lo que te dieron y nunca construyas uno.

Los filtros de la lista son argumentos nombrados en snake_case (label_ids=, date_from=), mientras que las claves del cuerpo de una solicitud conservan los nombres en camelCase de la API (addLabelIds en update). Una página y un hilo vuelven como diccionarios, así que se leen con page['nextCursor'] y 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 y from_contacts son los controles propios de la lista de conversaciones. sort es newest, oldest, sender o subject, las fechas aceptan un datetime o una cadena ISO 8601 y ambos extremos están incluidos, y from_contacts conserva el correo cuyo mensaje más reciente vino de un contacto guardado. Cada orden se pagina hasta el final sin saltarse ni repetir una conversación. Un datetime sin tzinfo se interpreta como hora local.

Organización

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_…')

El estado de lectura ES una etiqueta en todos los backends de aquí, así que viaja con las listas de etiquetas y el orden es determinista cuando fijas ambas cosas. Al menos uno de los tres campos debe estar presente.

addLabelIds acepta ids de labels.list y los ids de sistema como ARCHIVE y STARRED. Un id que no nombra ninguna etiqueta se rechaza con un 422 label_not_found en lugar de crearse, así que crea antes la etiqueta con labels.create. threads.list(folder='USER_DONE') lista todos los hilos que llevan una etiqueta, estén en la carpeta que estén.

Adjuntos de un mensaje

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 es base64, y una cadena vacía cuando no se han podido encontrar los bytes almacenados, así que comprueba su longitud antes de decodificar. El texto cifrado de un mensaje cifrado SÍ está en esta lista y se descarga como cualquier otro archivo; la parte de versión PGP/MIME y cualquier firma separada, no. Conservan sus ids en encryption.parts y nada más.

Un mensaje que llegó cifrado

Este SDK no cifra ni descifra: no puede abrir un mensaje que haya cifrado otra persona ni puede enviar uno cifrado. La solicitud de envío se rechaza si lleva un marcador de cifrado, porque un cliente sin clave no tiene por qué afirmar que la tiene. Las claves generadas en la aplicación de OpenEmail viven en el navegador que las creó y no llegan a nada de aquí, y cuando ese navegador abre un mensaje sellado el texto plano se queda en él, y el mensaje almacenado que lee esta llamada sigue siendo texto cifrado. Lo que te da threads.get es el sobre, reconocido. Un mensaje que llegó envuelto en PGP o S/MIME lleva un diccionario encryption, de modo que un decodedBody vacío deja de ser lo único que recibes. Es la única clave del diccionario cuya ausencia no te puedes permitir adivinar, y MessageEncryption en openemail.types la describe.

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)

Ramifica con is_sealed, nunca según la presencia del campo. Dos de los cinco formatos, pgp-signed y smime-signed, describen un cuerpo que llegó EN CLARO junto a una firma separada, así que condicionar por la presencia oculta correo que nadie necesitaba ocultar, y el usuario no puede verlo ni explicárselo. is_sealed existe exactamente por eso: el servidor declara una sola vez el conjunto sellado, y una tercera copia escrita a partir de la unión es la copia que se desvía.

La ausencia no significa texto plano. encryption falta en todos los mensajes almacenados antes de que se publicara la detección, y en todo lo que llegó al buzón por una ruta donde el detector nunca se ejecutó. Registra que nadie miró, un hecho sobre nuestra cobertura y no sobre el correo, y nada lo rellena a posteriori.

En qué se diferencian del resto

  • Cada entrada de ThreadResource.messages es un MessageResource, un dict[str, Any] simple cuyo tipo no nombra ningún campo, ni siquiera encryption. Tipar los campos sería el cliente afirmando una normalización que nadie realiza. Lee encryption con message.get('encryption') y decide con is_sealed, porque un cliente que no puede decidir en función de él lee un mensaje sellado como uno vacío.
  • Una solicitud que no puede atenderse fielmente es un 422 capability_unsupported, no una respuesta que parece correcta y está silenciosamente equivocada.

Parámetros: threads.list

folderstr
Qué carpeta listar. El servidor usa `inbox` por defecto, así que omitirlo restringe el listado en lugar de ampliarlo a todo. También se aplica a una búsqueda con `query`, salvo que la propia consulta nombre una carpeta con `in:` o con un `is:` de carpeta como `is:sent`.
querystr
La sintaxis de búsqueda del buzón. Todas las palabras sueltas deben aparecer, y cada una coincide de forma laxa: se ignoran mayúsculas, acentos y separadores, y una parte de una palabra más larga cuenta, así que tanto `min` como `ben jamin` encuentran "Benjamin". Una frase entre comillas coincide tal como está escrita, salvo por mayúsculas y acentos, de modo que `"ben jamin"` no encuentra "Ben-Jamin", y las palabras vacías se descartan cuando queda algo más que buscar. Cuando nada coincide exactamente, se devuelven en su lugar grafías parecidas, así que `benjimin` encuentra "Benjamin": una palabra suelta, o el valor de `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:` o `label:`, puede diferir del comienzo de una palabra en una errata (una letra cambiada, omitida, sobrante o intercambiada) si tiene de cuatro a siete letras y en dos si tiene ocho o más, mientras que una frase entre comillas, una palabra con un dígito, una palabra más corta y una palabra excluida siguen coincidiendo solo de forma exacta, y las páginas siguientes buscan de la misma manera. Restringe con operadores como `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31` y `older_than:1y`, y combínalos con `OR`, paréntesis y un `-` inicial; un valor que la búsqueda no pueda usar se ignora en lugar de restringir. Las palabras y los operadores `from:`, `to:`, `cc:`, `subject:` y `body:` leen el remitente, los destinatarios y el asunto del mensaje más reciente, y los primeros 4.000 caracteres de su cuerpo con el marcado eliminado, mientras que `filename:` y `has:` leen todos los adjuntos de la conversación completa, y las etiquetas y las carpetas leen la conversación completa. Restringe el mismo índice que lee el listado sin filtrar. Los mensajes sellados no almacenan texto del cuerpo, así que solo pueden coincidir su remitente, sus destinatarios y su asunto. Una palabra suelta también coincide con el nombre de cualquier adjunto de la conversación, sea cual sea el mensaje que lo trajo.
label_idsstr | Sequence[str]
Restringe el listado a los hilos que llevan estas etiquetas. El endpoint recibe una cadena separada por comas, y el cliente une por ti una lista o una tupla en una sola. No hay límite de cuántas nombres.
limitint
Cuántos hilos devolver, de 1 a 100. Si se omite, el handler usa 25. El valor por defecto vive en el handler y no en el esquema, así que un valor ausente y un 25 explícito se comportan igual.
cursorstr
El `nextCursor` de la página anterior, devuelto literalmente. Es el `pageToken` de la API bajo el nombre que usa cualquier otra lista, y es opaco, así que nunca construyas ni edites uno.

Respuesta: Page[ThreadSummaryResource]

itemslist[ThreadSummaryResource]
Una entrada por hilo en esta página, extraída del sobre `data` de la API. Cada entrada es solo un marcador de objeto y un id. El listado no lleva asunto, fragmento, participantes ni etiquetas, así que cualquier otra cosa implica llamar a `threads.get` sobre los hilos que te interesen.
items[].idstr
El id del hilo, para pasárselo sin cambios a `threads.get`, `threads.update` y los demás. Es el mismo id tanto si la fila vino de un listado filtrado como de una búsqueda con `query`.
hasMorebool
Si hay otra página más, derivado de `nextCursor` allí donde la API no lo indica.
nextCursorstr | None
El `nextPageToken` de la API, para reenviarlo como `cursor` en la página siguiente, o `None` cuando no hay más páginas. Un token vacío se normaliza a `None`, así que una comprobación de valor falsy y una de `None` coinciden.

Referencia