Aller à la documentation
Python

Fils

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

Lecture

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

L'API pagine les conversations avec un pageToken. Le client vous le remet sous le nom nextCursor et le reprend sous le nom cursor, comme pour toutes les autres listes, et list_all et iterate le suivent à votre place. Il est opaque : renvoyez ce qu'on vous a donné et n'en fabriquez jamais un.

Les filtres de liste sont des arguments nommés en snake_case (label_ids=, date_from=), tandis que les clés d'un corps de requête gardent les noms en camelCase de l'API (addLabelIds sur update). Une page et un fil reviennent sous forme de dictionnaires : page['nextCursor'] et thread['messageCount'] permettent donc de les lire.

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 et from_contacts sont les commandes propres à la liste des fils. sort vaut newest, oldest, sender ou subject, les dates prennent un datetime ou une chaîne ISO 8601 et les deux bornes sont incluses, et from_contacts garde le courrier dont le message le plus récent vient d'un contact enregistré. Chaque ordre se pagine jusqu'au bout sans sauter ni répéter un fil. Un datetime sans tzinfo est lu comme une heure locale.

Organisation

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

L'état lu/non lu EST un libellé sur tous les backends ici : il voyage donc avec les listes de libellés et l'ordre est déterministe quand vous définissez les deux. Au moins un des trois champs doit être présent.

addLabelIds prend des identifiants issus de labels.list et les identifiants système comme ARCHIVE et STARRED. Un identifiant qui ne désigne aucun libellé est refusé avec un 422 label_not_found au lieu d'être créé : créez donc d'abord le libellé avec labels.create. threads.list(folder='USER_DONE') liste tous les fils qui portent un libellé, quel que soit leur dossier.

Pièces jointes d'un message

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 est en base64, et vaut une chaîne vide quand les octets stockés sont introuvables : vérifiez donc sa longueur avant de décoder. Le chiffré d'un message chiffré EST bien dans cette liste et se télécharge comme n'importe quel autre fichier ; la partie version PGP/MIME et toute signature détachée, non. Celles-ci gardent leurs ids dans encryption.parts, et rien de plus.

Un message arrivé chiffré

Ce SDK ne chiffre ni ne déchiffre : il ne peut pas ouvrir un message chiffré par quelqu'un d'autre, ni en envoyer un chiffré. La requête d'envoi est refusée si elle porte un marqueur de chiffrement, car un client sans clé n'a pas à en affirmer un. Les clés générées dans l'application OpenEmail vivent dans le navigateur qui les a créées et n'atteignent rien ici ; quand ce navigateur ouvre un message scellé, le texte en clair y reste, et le message stocké que cet appel lit demeure du chiffré. Ce que threads.get vous donne, c'est l'enveloppe, reconnue. Un message arrivé enveloppé en PGP ou S/MIME porte un dictionnaire encryption, si bien qu'un decodedBody vide cesse d'être la seule chose qu'on vous remet. C'est la seule clé dont l'absence ne se devine pas impunément, et MessageEncryption dans openemail.types la décrit.

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)

Branchez avec is_sealed, jamais sur la présence du champ. Deux des cinq formats, pgp-signed et smime-signed, décrivent un corps arrivé EN CLAIR à côté d'une signature détachée : conditionner sur la présence masque donc du courrier que personne n'avait besoin de masquer, et l'utilisateur ne peut ni le voir ni l'expliquer. is_sealed est fourni exactement pour cette raison : le serveur énonce une fois l'ensemble des formats scellés, et une troisième copie écrite à la main à partir de l'union est celle qui dérive.

L'absence ne signifie pas texte en clair. encryption manque sur tous les messages stockés avant la mise en service de la détection, et sur tout ce qui a rejoint la boîte par un chemin où le détecteur n'a jamais tourné. Cela consigne que personne n'a regardé (un fait sur notre couverture, pas sur le courrier), et rien ne vient le rétro-remplir.

En quoi ces méthodes diffèrent des autres

  • Chaque entrée de ThreadResource.messages est un MessageResource, un simple dict[str, Any] dont le type ne nomme aucun champ, pas même encryption. Typer les champs reviendrait, pour le client, à affirmer une normalisation que personne n'effectue. Lisez encryption avec message.get('encryption') et branchez avec is_sealed, car un client incapable de se brancher dessus lit un message scellé comme un message vide.
  • Une requête qui ne peut pas être servie fidèlement donne un 422 capability_unsupported, et non une réponse qui a l'air correcte et qui est discrètement fausse.

Paramètres : threads.list

folderstr
Le dossier à lister. Le serveur le fixe par défaut à `inbox` : l'omettre restreint donc le listing au lieu de l'élargir à tout. Cela vaut aussi pour une recherche `query`, sauf si la requête nomme elle-même un dossier avec `in:` ou un `is:` de dossier tel que `is:sent`.
querystr
La syntaxe de recherche de la boîte aux lettres. Les mots simples doivent tous apparaître, et chacun correspond de façon souple : la casse, les accents et les séparateurs sont ignorés, et une portion d'un mot plus long compte, si bien que `min` comme `ben jamin` trouvent « Benjamin ». Une expression entre guillemets est recherchée telle qu'elle est écrite, à la casse et aux accents près : `"ben jamin"` ne trouve donc pas « Ben-Jamin », et les mots outils sont écartés dès qu'il reste autre chose à chercher. Quand rien ne correspond exactement, des graphies proches sont renvoyées à la place, si bien que `benjimin` trouve « Benjamin » : un mot simple, ou la valeur de `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:` ou `label:`, peut s'écarter du début d'un mot d'une faute de frappe (une lettre changée, manquante, en trop ou inversée) s'il compte de quatre à sept lettres, et de deux s'il en compte huit ou plus, tandis qu'une expression entre guillemets, un mot contenant un chiffre, un mot plus court et un mot exclu doivent toujours correspondre exactement, et les pages suivantes cherchent de la même façon. Affinez avec des opérateurs comme `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31` et `older_than:1y`, et combinez-les avec `OR`, des parenthèses et un `-` en préfixe ; une valeur que la recherche ne peut pas exploiter est ignorée plutôt que restrictive. Les mots ainsi que les opérateurs `from:`, `to:`, `cc:`, `subject:` et `body:` lisent l'expéditeur, les destinataires et l'objet du message le plus récent ainsi que les 4 000 premiers caractères de son corps, balisage retiré, tandis que `filename:` et `has:` lisent toutes les pièces jointes de la conversation entière, et que les libellés et les dossiers portent sur la conversation entière. La recherche restreint le même index que lit le listing sans filtre. Les messages scellés ne stockent aucun texte de corps : seuls leur expéditeur, leurs destinataires et leur objet peuvent correspondre. Un mot simple correspond aussi au nom de n'importe quelle pièce jointe de la conversation, quel que soit le message qui la portait.
label_idsstr | Sequence[str]
Restreint le listing aux conversations portant ces libellés. L'endpoint prend une chaîne séparée par des virgules, et le client assemble une liste ou un tuple en une seule chaîne pour vous. Il n'y a pas de limite au nombre de libellés que vous nommez.
limitint
Combien de conversations renvoyer, de 1 à 100. En l'absence de valeur, le handler utilise 25. La valeur par défaut vit dans le handler plutôt que dans le schéma : une valeur absente et un 25 explicite se comportent donc de la même façon.
cursorstr
Le `nextCursor` de la page précédente, renvoyé tel quel. C'est le `pageToken` de l'API sous le nom qu'utilisent toutes les autres listes, et il est opaque : n'en construisez ni n'en modifiez jamais un.

Réponse : Page[ThreadSummaryResource]

itemslist[ThreadSummaryResource]
Une entrée par conversation de cette page, extraite de l'enveloppe `data` de l'API. Chaque entrée n'est qu'un marqueur d'objet et un id. Le listing ne porte ni objet, ni extrait, ni participants, ni libellés : pour tout le reste, il faut appeler `threads.get` sur les conversations voulues.
items[].idstr
L'id de la conversation, à passer tel quel à `threads.get`, `threads.update` et aux autres. C'est le même id, que la ligne vienne d'un listing filtré ou d'une recherche `query`.
hasMorebool
Indique s'il existe une page suivante, déduit de `nextCursor` là où l'API ne le précise pas.
nextCursorstr | None
Le `nextPageToken` de l'API, à renvoyer comme `cursor` pour la page suivante, ou `None` lorsqu'il n'y a plus de page. Un jeton vide est normalisé en `None` : un test de valeur falsy et un test sur `None` s'accordent donc.

Référence