Threads
`threads.list`, `list_all`, `iterate`, `get`, `update`, `trash`, `snooze`, `unsnooze` und `list_attachments`.
Lesen
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'])Die API paginiert Threads mit einem pageToken. Der Client reicht ihn als nextCursor heraus und nimmt ihn als cursor wieder entgegen, wie bei jeder anderen Liste, und list_all sowie iterate folgen ihm automatisch. Er ist opak: Zurückgegeben wird genau der erhaltene Wert; ein Token darf nie selbst gebaut werden.
Die Listenfilter sind Schlüsselwortargumente in snake_case (label_ids=, date_from=), während die Schlüssel eines Request-Bodys die camelCase-Namen der API behalten (addLabelIds bei update). Eine Seite und ein Thread kommen als dicts zurück, Sie lesen sie also mit page['nextCursor'] und 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 und from_contacts sind die Steuerelemente der Threadliste selbst. sort ist newest, oldest, sender oder subject, die Datumswerte nehmen ein datetime oder einen ISO-8601-String und schließen beide Enden ein, und from_contacts behält Mail, deren neueste Nachricht von einem gespeicherten Kontakt kam. Jede Sortierung lässt sich bis zum Ende blättern, ohne einen Thread zu überspringen oder zu wiederholen. Ein datetime ohne tzinfo wird als Ortszeit gelesen.
Organisieren
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_…')Der Gelesen-Status IST hier auf jedem Backend ein Label, reist also mit den Label-Listen mit, und die Reihenfolge ist deterministisch, wenn beides gesetzt wird. Mindestens eines der drei Felder muss vorhanden sein.
addLabelIds nimmt IDs aus labels.list und die System-IDs wie ARCHIVE und STARRED. Eine ID, die kein Label benennt, wird mit 422 label_not_found abgelehnt statt angelegt, legen Sie das Label also zuerst mit labels.create an. threads.list(folder='USER_DONE') listet jeden Thread mit einem Label, egal in welchem Ordner.
Anhänge an einer Nachricht
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 ist base64 und ein leerer String, wenn die gespeicherten Bytes nicht gefunden werden konnten; vor dem Dekodieren sollte daher die Länge geprüft werden. Der Ciphertext einer verschlüsselten Nachricht STEHT in dieser Liste und wird wie jede andere Datei heruntergeladen; der Versions-Part von PGP/MIME und eine etwaige abgetrennte Signatur dagegen nicht. Sie behalten ihre ids in encryption.parts und sonst nichts.
Eine verschlüsselt eingegangene Nachricht
Dieses SDK verschlüsselt und entschlüsselt nicht: Es kann keine Nachricht öffnen, die jemand anderes verschlüsselt hat, und keine verschlüsselte senden. Die Sendeanfrage wird abgelehnt, wenn sie einen Verschlüsselungsmarker mitführt, denn ein Client ohne Schlüssel hat nichts zu behaupten. Schlüssel, die in der OpenEmail-App erzeugt wurden, leben in dem Browser, der sie erzeugt hat, und erreichen hier nichts; öffnet dieser Browser eine versiegelte Nachricht, bleibt der Klartext in ihm, und die gespeicherte Nachricht, die dieser Aufruf liest, ist weiterhin Chiffretext. Was threads.get liefert, ist der Umschlag, als solcher erkannt. Eine Nachricht, die PGP- oder S/MIME-verpackt eingegangen ist, führt ein encryption-dict mit, ein leerer decodedBody ist damit nicht mehr das Einzige, was man in die Hand bekommt. Es ist der eine Schlüssel im dict, dessen Fehlen man durch Raten nicht übersteht, und MessageEncryption in openemail.types beschreibt ihn.
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)Verzweigt wird über is_sealed, nie über das Vorhandensein des Feldes. Zwei der fünf Formate, pgp-signed und smime-signed, beschreiben einen Body, der IM KLARTEXT neben einer abgetrennten Signatur eingegangen ist; eine Prüfung auf bloßes Vorhandensein verbirgt also Mail, die niemand verbergen musste, und die Nutzerin oder der Nutzer kann sie weder sehen noch erklären. Genau dafür gibt es is_sealed: Der Server nennt die versiegelte Menge einmal, und eine dritte, aus der Union abgeschriebene Kopie ist die, die auseinanderdriftet.
Fehlen bedeutet nicht Klartext. encryption fehlt bei jeder Nachricht, die vor dem Ausliefern der Erkennung gespeichert wurde, und bei allem, was das Postfach über einen Weg erreicht hat, auf dem der Detektor nie lief. Es hält fest, dass niemand nachgesehen hat – eine Tatsache über unsere Abdeckung und nicht über die Mail –, und nichts trägt es nachträglich nach.
Worin sich diese vom Rest unterscheiden
- Jeder Eintrag in
ThreadResource.messagesist eineMessageResource, ein einfachesdict[str, Any], dessen Typ kein Feld benennt, nicht einmalencryption. Die Felder zu typisieren hieße, dass der Client eine Normalisierung behauptet, die niemand durchführt. Lesen Sieencryptionmitmessage.get('encryption')und verzweigen Sie mitis_sealed, denn ein Client, der danach nicht verzweigen kann, liest eine versiegelte Nachricht als leere. - Eine Anfrage, die nicht getreu bedient werden kann, ergibt ein 422
capability_unsupportedund keine Antwort, die richtig aussieht und stillschweigend falsch ist.
Parameter: threads.list
folderstr- Welcher Ordner aufgelistet wird. Der Server setzt standardmäßig `inbox`; ein Weglassen schränkt die Auflistung also ein, statt sie auf alles auszuweiten. Der Wert gilt auch für eine `query`-Suche, sofern die Query nicht selbst einen Ordner mit `in:` oder ein Ordner-`is:` wie `is:sent` benennt.
querystr- Die Suchsyntax des Postfachs. Einfache Wörter müssen alle vorkommen und passen jeweils unscharf: Groß-/Kleinschreibung, Akzente und Trennzeichen werden ignoriert, und ein Teil eines längeren Wortes zählt mit, sodass `min` und `ben jamin` beide "Benjamin" finden. Eine Phrase in Anführungszeichen wird bis auf Groß-/Kleinschreibung und Akzente wörtlich gesucht, `"ben jamin"` findet also "Ben-Jamin" nicht, und Füllwörter werden verworfen, wenn sonst noch etwas zum Suchen bleibt. Wenn nichts genau passt, werden stattdessen ähnliche Schreibweisen geliefert, `benjimin` findet also "Benjamin": Ein einfaches Wort oder der Wert von `from:`, `to:`, `cc:`, `subject:`, `body:`, `filename:` oder `label:` darf vom Anfang eines Wortes um einen Tippfehler (einen geänderten, fehlenden, zusätzlichen oder vertauschten Buchstaben) abweichen, wenn es vier bis sieben Buchstaben hat, und um zwei, wenn es acht oder mehr hat, während eine Phrase in Anführungszeichen, ein Wort mit einer Ziffer, ein kürzeres Wort und ein ausgeschlossenes Wort weiterhin genau passen müssen, und die folgenden Seiten suchen auf dieselbe Weise weiter. Eingegrenzt wird mit Operatoren wie `from:ada`, `label:Invoices`, `is:unread`, `has:pdf`, `before:2026/01/31` und `older_than:1y`, kombinierbar mit `OR`, Klammern und einem vorangestellten `-`; ein Wert, den die Suche nicht verwenden kann, wird ignoriert und schränkt nicht ein. Wörter sowie die Operatoren `from:`, `to:`, `cc:`, `subject:` und `body:` lesen Absender, Empfänger und Betreff der neuesten Nachricht sowie die ersten 4.000 Zeichen ihres Bodys ohne Markup, während `filename:` und `has:` jeden Anhang der gesamten Konversation lesen und Labels und Ordner die gesamte Konversation. Eingegrenzt wird derselbe Index, den die ungefilterte Auflistung liest. Versiegelte Nachrichten speichern keinen Body-Text, daher können nur ihr Absender, ihre Empfänger und ihr Betreff treffen. Ein einfaches Wort trifft außerdem den Namen jedes Anhangs in der Unterhaltung, ganz gleich, welche Nachricht ihn trug.
label_idsstr | Sequence[str]- Schränkt die Auflistung auf Threads mit diesen Labels ein. Der Endpunkt nimmt einen kommagetrennten String entgegen, und der Client fügt eine Liste oder ein Tupel für Sie zu einem solchen zusammen. Die Anzahl der genannten Labels ist unbegrenzt.
limitint- Wie viele Threads zurückgegeben werden, von 1 bis 100. Ohne Angabe verwendet der Handler 25. Der Standardwert liegt im Handler und nicht im Schema, daher verhalten sich ein fehlender Wert und eine ausdrückliche 25 gleich.
cursorstr- Der `nextCursor` der vorherigen Seite, unverändert zurückgegeben. Es ist das `pageToken` der API unter dem Namen, den jede andere Liste verwendet, und es ist opak; ein Token darf daher nie konstruiert oder bearbeitet werden.
Antwort: Page[ThreadSummaryResource]
itemslist[ThreadSummaryResource]- Ein Eintrag pro Thread auf dieser Seite, aus dem `data`-Umschlag der API herausgehoben. Jeder Eintrag besteht nur aus einem Objektmarker und einer id. Die Auflistung führt weder Betreff noch Snippet, Teilnehmer oder Labels mit; für mehr ist `threads.get` auf den gewünschten Threads aufzurufen.
items[].idstr- Die id des Threads, unverändert an `threads.get`, `threads.update` und die übrigen Aufrufe weiterzureichen. Es ist dieselbe id, ob die Zeile aus einer gefilterten Auflistung oder aus einer `query`-Suche stammt.
hasMorebool- Ob es eine weitere Seite gibt, abgeleitet aus `nextCursor`, wo die API es nicht angibt.
nextCursorstr | None- Das `nextPageToken` der API, als `cursor` für die folgende Seite zurückzusenden, oder `None`, wenn es keine weitere Seite gibt. Ein leeres Token wird zu `None` normalisiert, sodass eine Falsy-Prüfung und eine `None`-Prüfung übereinstimmen.
Referenz
threads.list()Vollständige Referenzthreads.list_all()Vollständige Referenzthreads.iterate()Vollständige Referenzthreads.get()Vollständige Referenzthreads.update()Vollständige Referenzthreads.trash()Vollständige Referenzthreads.snooze()Vollständige Referenzthreads.unsnooze()Vollständige Referenzthreads.list_attachments()Vollständige Referenz