Aller à la documentation
API

Fils de discussion

Lire et organiser le courrier.

GETapi.openemail.uk/threads

Exécute n'importe lequel des 7 appels de cette page sur votre espace de travail, avec votre propre clé.

Listage

GET /threads?folder=inbox. Passer query interroge le même index local. Les mots simples doivent tous apparaître, et chacun correspond de façon souple, sans tenir compte de la casse, des accents ni des séparateurs : min trouve donc « Benjamin ». Une expression entre guillemets est comparée telle qu'elle est écrite, à la casse et aux accents près, si bien que "ben jamin" ne trouve pas « Ben-Jamin ». Les mots vides comme the ou emails sont écartés d'une liste de mots simples dès qu'il reste autre chose à chercher. Des opérateurs comme from:, to:, subject:, label:, is:unread, has:pdf, after:2026/01/31 et newer_than:7d la restreignent, et OR, les parenthèses et un - en préfixe les combinent. Les destinataires sont stockés en une seule liste sans rôles et ne contiennent jamais de Bcc : cc: lit donc le même champ que to: et bcc: ne correspond à rien en propre. from:me désigne le courrier que vous avez envoyé, et to:me le courrier portant l'une de vos propres adresses, alias compris, parmi ses destinataires ou comme adresse de remise.

Les mots et les opérateurs from:, to:, cc:, subject: et body: lisent le message le plus récent de chaque fil : son expéditeur, ses destinataires, son objet et les 4 000 premiers caractères de son corps. filename: et has: lisent toutes les pièces jointes de la conversation entière, et label:, in: et is: lisent la conversation entière. folder s'applique toujours, sauf si la requête en nomme un avec in:, ou avec un is: qui est un dossier comme is:sent, et in:anywhere cherche dans tous les dossiers, seul comme à côté d'autres termes. Le listage des brouillons fait exception et reste dans les brouillons quoi que nomme la requête.

Une valeur que la recherche ne sait pas utiliser est ignorée au lieu de restreindre : une faute de frappe dans une valeur élargit donc le résultat au lieu de le vider. C'est le cas de category:, larger:, smaller:, size:, messagesize:, list:, rfc822msgid:, received:, sent:, des mots de catégorie comme is:promotions, d'un mot has: ne nommant aucun type de pièce jointe, d'un importance: autre que high ou low, d'une date illisible et d'une durée dont l'unité n'est ni h, ni d, ni w, ni m, ni y. Un nom d'opérateur qu'elle ne connaît pas, project: par exemple, est recherché comme du texte simple. Les dates lisent la dernière activité du fil, en UTC, after: incluant le jour qu'il nomme et before: l'excluant ; écrivez-en une sous la forme YYYY/MM/DD, YYYY-MM-DD, YYYYMMDD, une année seule, ou des secondes ou millisecondes epoch.

nextPageToken est opaque. Renvoyez exactement ce qu'on vous a donné ; n'en construisez et n'en modifiez jamais un. Sa forme ne fait pas partie du contrat.

Récupération

GET /threads/{id} renvoie tous les messages du fil, pas seulement le plus récent, ainsi que ses libellés et l'indication qu'un élément y est non lu.

Les messages arrivés chiffrés

Cette API ne chiffre ni ne déchiffre. Elle ne peut pas ouvrir un message chiffré par quelqu'un d'autre, et elle ne peut pas en envoyer un chiffré. Une requête portant un marqueur de chiffrement est refusée par un 422, parce que les seules surfaces autorisées à en poser un sont celles qui détiennent les clés, et aucun client API n'en détient. Ce qu'elle fait, c'est RECONNAÎTRE une enveloppe scellée à l'entrée, d'après le Content-Type de premier niveau et rien de plus, puis le signaler sur le message.

OpenEmail détient désormais des clés, et il vaut la peine d'être précis sur laquelle des deux moitiés et sur l'endroit. Le propriétaire d'une boîte génère une identité OpenPGP dans son navigateur et publie la clé PUBLIQUE dans un annuaire que les autres expéditeurs OpenEmail connectés peuvent interroger. La moitié privée est fabriquée dans ce navigateur, n'est jamais envoyée ici et n'est jamais récupérable : rien dans cette API ne peut donc déchiffrer quoi que ce soit, et aucune demande de support, aucune injonction ni aucune de nos sauvegardes ne produit une clé qui le pourrait. L'application web peut désormais OUVRIR un message PGP/MIME ou PGP inline quand la clé se trouve dans le navigateur du lecteur, mais ce déchiffrement a lieu dans l'onglet et son texte en clair n'est jamais réécrit : le message stocké reste du texte chiffré, et aucune réponse de cette API ne transporte jamais le texte ouvert. L'application peut désormais sceller un nouveau message dans le navigateur et l'envoyer : le compositeur chiffre vers les clés publiées des destinataires et le courrier part en PGP/MIME. Cette API ne peut toujours rien sceller : le champ ci-dessous décrit donc à la fois le courrier chiffré par quelqu'un d'autre et le courrier scellé dans un onglet OpenEmail.

Cela mérite un champ à cause de ce qu'était l'alternative. Un message scellé ne stocke aucun corps lisible : decodedBody revient donc à "", exactement les mêmes octets qu'un message réellement sans contenu. encryption est ce qui permet de distinguer les deux avant d'agir sur l'un d'eux, et c'est une affirmation sur l'enveloppe plutôt qu'une vérification : constater qu'un message est scellé n'équivaut pas à l'avoir ouvert.

Réponse
{    "object": "thread",    "id": "thread_2f9b…",    "messages": [      {        "id": "msg_7c41…",        "subject": "Q3 numbers",        "decodedBody": "",        "encryption": {          "format": "pgp-mime",          "detectedAt": "2026-08-30T09:14:22.117Z",          "rawRetained": false,          "parts": [            { "index": 0, "attachmentId": "msg_7c41…-0", "role": "version" },            { "index": 1, "attachmentId": "msg_7c41…-1", "role": "ciphertext" }          ]        }      }    ]  }

encryption

format'pgp-mime' | 'pgp-signed' | 'pgp-inline' | 'smime-encrypted' | 'smime-signed'
Quelle enveloppe est arrivée. Lu sur le `Content-Type` de premier niveau (son paramètre `protocol` pour PGP, son `smime-type` pour S/MIME) ou, pour `pgp-inline`, sur un corps qui s'ouvre par l'en-tête d'armure PGP. Une partie `pkcs7-mime` sans aucun `smime-type` est lue comme `smime-encrypted`, ce qu'en fait la RFC 8551 par défaut.
detectedAtstring
ISO 8601, le moment où le détecteur s'est exécuté, c'est-à-dire celui de l'ingestion du message ici. Cela ne dit rien du moment où le message a été chiffré, ni par qui.
rawRetainedboolean
Si les octets RFC822 d'origine ont été conservés, de sorte que le message puisse être restitué entier. Faux sur tous les messages aujourd'hui, puisque rien ici ne conserve encore le courrier brut. Le champ est présent dès maintenant pour que le jour où cela changera ne soit pas aussi le jour où il faudra migrer de nouveau chaque message stocké.
partsobject[]
Les parties d'enveloppe qu'utilise ce format. Présent dès que `encryption` l'est, et vide quand il n'y en a aucune à nommer : `pgp-inline` n'a aucune partie distincte, puisque son armure EST le corps et arrive dans `decodedBody`.
parts[].indexnumber
Quelle partie MIME du message d'origine c'était, comptée sur les parties telles qu'elles sont arrivées et non sur `attachments`. Les deux listes diffèrent, ce qui est toute la raison de cet enregistrement.
parts[].attachmentIdstring
L'id que cette partie porte dans `attachments`, quand elle y figure : l'id du message suivi de l'index de la partie. La partie `ciphertext` est listée et se télécharge comme n'importe quel autre fichier ; `version` et `signature` sont tenus hors de la liste, si bien que leurs ids ne servent qu'à corréler les deux vues. L'endpoint des pièces jointes ne les renvoie pas.
parts[].role'version' | 'ciphertext' | 'signature'
`version` est la partie de contrôle PGP/MIME, `ciphertext` est le message, `signature` est une signature détachée. Seul `ciphertext` mérite d'être récupéré ; les deux autres sont du mobilier de protocole qui s'affichait autrefois en pièces jointes parasites et ne le fait plus.
formatCe qui est arrivéCorps
pgp-mimeUne enveloppe PGP/MIME : multipart/encrypted avec protocol=application/pgp-encrypted.Scellé
pgp-inlineL'armure dans le corps lui-même. Lue uniquement sur le texte du corps, pour qu'une réponse qui ne fait que citer un bloc armuré ne soit pas prise pour l'un d'eux.Scellé
smime-encryptedUne partie S/MIME pkcs7-mime avec smime-type=enveloped-data, ou une sans aucun smime-type.Scellé
pgp-signedUne signature PGP détachée à côté du message : multipart/signed avec protocol=application/pgp-signature.Lisible
smime-signedUne signature S/MIME détachée : un protocole pkcs7-signature, ou smime-type=signed-data.Lisible

Signé n'est pas scellé, et brancher sur la présence de encryption plutôt que sur format inverse exactement ce point. Une signature est une affirmation sur l'auteur du message, pas une enveloppe autour de lui : le corps d'un message signé est en clair et se lit comme n'importe quel autre. Traitez pgp-mime, pgp-inline et smime-encrypted comme illisibles, et les deux formats signés comme du courrier ordinaire.

Ce qui change sur un message scellé

Seuls les trois formats scellés changent quelque chose, et le changement a lieu à l'ingestion plutôt que dans cette réponse. Tout ce qui aurait lu le corps se met en retrait, au lieu de lire du texte chiffré et de rapporter un résultat qu'il n'aurait pas pu obtenir :

  • La recherche dans le corps. Le message est indexé avec un extrait de corps vide : il reste donc trouvable par expéditeur, objet, adresse et libellé, et non par ce qu'il contient.
  • La passe sur le corps du détecteur d'hameçonnage. Le verdict arrive quand même et dit ce qu'il n'a pas pu faire : risk.signals porte body-encrypted et risk.aiChecked est false.
  • La vérification d'écriture par IA, qui s'abstient plutôt que de deviner : aiWritten.level vaut unknown et aiWritten.skipped vaut encrypted.
  • Les conditions sur le corps dans les règles. Les conditions sur l'enveloppe et les en-têtes s'exécutent exactement comme avant ; une règle qui interrogeait le corps est enregistrée comme non évaluée plutôt que comptée comme non concordante, car « n'a pas correspondu » et « n'a pas pu être lu » sont deux réponses différentes.
  • L'import d'invitations de calendrier. L'invitation est à l'intérieur du texte chiffré, et construire un événement à partir de l'enveloppe inscrirait une entrée fausse dans un vrai calendrier.
  • Les résumés de fil et les embeddings, pour le fil entier. Une seule réponse scellée suffit. Un résumé est la lecture du texte en clair par un modèle, stockée en métadonnées en clair : c'est le seul endroit de ce pipeline où un corps fuirait dans un magasin que personne ne prend pour un corps.

Tout ce qui n'a pas besoin du corps reste intact :

  • DMARC, DKIM et SPF. Ils se lisent sur Authentication-Results, que le texte chiffré ne masque pas : un message chiffré obtient donc un vrai verdict d'authentification plutôt qu'aucun.
  • Le regroupement en fils, le classement du spam et la liste de blocage : tout cela relève de l'enveloppe et des en-têtes.
  • Les pièces jointes. La partie chiffrée reste dans attachments, nommée encrypted-message.asc quand elle arrive sans nom, et se télécharge par l'endpoint ci-dessous. C'est exactement ce que le lecteur de l'application web récupère et déchiffre dans le navigateur ; pour un client API, qui ne détient aucune clé, ce téléchargement reste le seul moyen de lire ce courrier. Ouvrez-le dans un client qui en possède une.
  • Un message signé ne perd rien de tout cela. Chacune des vérifications ci-dessus continue de s'y appliquer, et rien n'est retenu : c'est pourquoi la liste des scellés compte trois formats et non cinq.

L'absence de encryption n'affirme pas que le message est en clair. Elle signifie que personne n'a regardé : le message est antérieur à la détection, ou a atteint la messagerie par un chemin qui n'exécute pas le détecteur. Rien ne le remplit rétroactivement : un champ qui dit « nous n'avons pas vérifié » ne doit jamais se lire « nous avons vérifié et n'avons rien trouvé ».

Marquer et étiqueter

PATCH /threads/{id} accepte read, addLabelIds et removeLabelIds. L'état de lecture est un libellé sur tous les backends que ce produit prend en charge : régler read et déplacer des libellés dans un même appel rend donc l'ordre déterministe.

PATCH
{ "read": true, "addLabelIds": ["USER_INVOICES"] }

TRASH et SNOOZED sont refusés ici avec label_not_directly_settable. Aucun de ces états n'est porté par son seul libellé (la mise à la corbeille efface aussi les libellés de dossier, et une mise en attente exige une heure de réveil stockée à côté) : les poser à la main laisse donc un fil dans un état que l'application ne produit jamais et dont elle ne sait pas revenir. Utilisez les endpoints ci-dessous.

Corbeille et mise en attente

EndpointEffet
POST /threads/{id}/trashDéplace vers la corbeille, en effaçant d'un coup INBOX, SPAM, SNOOZED et ARCHIVE.
POST /threads/{id}/snoozeCorps { "wakeAt": "…" }. Le masque et programme son retour.
POST /threads/{id}/unsnoozeLe ramène tout de suite et annule le retour programmé.

La mise en attente écrit deux choses : le libellé qui masque le fil, et l'entrée qui le ramène. Faire l'une sans l'autre est exactement la raison pour laquelle ce sont des endpoints et non des modifications de libellés.

Pièces jointes

GET /threads/{id}/messages/{messageId}/attachments renvoie chaque pièce jointe avec filename, contentType, size et content en base64. content est une chaîne vide lorsque les octets stockés sont introuvables : vérifiez sa longueur avant de décoder.

Une enveloppe chiffrée n'est pas ici en entier. Le texte chiffré l'est (c'est le message, et le télécharger est le seul moyen pour un client API de lire ce courrier), mais la partie de version PGP/MIME et toute signature détachée sont tenues hors de la liste, parce qu'elles s'affichaient en pièces jointes parasites et qu'un appelant n'en peut rien faire. Toutes deux conservent leurs ids dans encryption.parts, ce qui corrèle les deux vues ; cet endpoint ne les renvoie pas.