Push und Pull, und was jedes davon kostet
Es gibt nur zwei Wege, wie Ihr Code erfährt, dass eine Nachricht angekommen ist. Entweder sagt Ihnen die Gegenseite Bescheid, oder Sie fragen. Alles andere — eine Client-Bibliothek mit einem waitFor darin, ein SDK, das ein Postfach „streamt“, ein Test-Helfer, der blockiert — ist eines der beiden mit versteckter Mechanik, und es lohnt sich zu wissen, welches der beiden Sie gerade in der Hand haben, bevor Sie es debuggen müssen.
Vier Anordnungen decken fast alles ab, was es gibt.
- Ein Webhook
- Der Dienst stellt eine HTTP-Anfrage an eine Adresse, die Ihnen gehört, jedes Mal, wenn Mail eintrifft. Das ist das billigstmögliche Warten — Sie tun rein gar nichts, bis es etwas zu tun gibt —, und der Preis dafür ist eine Adresse im öffentlichen Internet, ein Listener, der genau dann erreichbar ist, wenn die Mail es ist, ein gemeinsames Geheimnis, um zu belegen, dass die Anfrage tatsächlich von dort kam, und Ihre eigene Antwort auf die Frage, was passiert, wenn Ihr Listener gerade nicht da war.
- Ein Long Poll
- Sie stellen die Anfrage, und der Server hält sie offen, bis entweder Mail eintrifft oder ein Timeout abläuft. Das braucht von Ihnen nichts außer einer ausgehenden Verbindung, und es kostet den Server einen Worker pro Wartendem — deshalb begrenzt jeder Dienst, der so etwas anbietet, sowohl die Dauer des Wartens als auch die Zahl gleichzeitiger Wartender.
- Ein einfacher Poll
- Sie fragen, wieder und wieder, und jede Anfrage wird sofort mit dem beantwortet, was gerade da ist. Das ist die einzige Anordnung, die von einem Laptop hinter einem Router aus funktioniert, von einem CI-Runner ohne eingehende Route, und von einem Agenten, der in der Sandbox eines Fremden läuft — und genau darum geht es in dieser Anleitung.
- Ein Postfach-Protokoll
- IMAP hat
IDLE, was ein Long Poll mit einem anderen Hut ist: Die Verbindung bleibt offen, und der Server meldet neue Mail direkt darauf. Das ist wirklich push-artig, und es verlangt ein Postfach mit Zugangsdaten, einen Client, der einen Socket offen halten und nach einem Abbruch neu verbinden kann, und einen Server, der den Befehl auch tatsächlich respektiert — eine ganze Menge Mechanik für einen Job, der eine einzige Nachricht braucht.
Nebeneinandergestellt, geht es bei der Wahl weniger um Eleganz als darum, was jede der beiden von der Maschine verlangt, auf der Ihr Code läuft.
| Was es von Ihnen braucht | Ein Webhook | Ein Poll |
|---|---|---|
| Eine Adresse, unter der Ihr Code erreichbar ist | Ja: eine öffentliche URL mit Zertifikat, aus dem Internet erreichbar. | Nein. Eine ausgehende Anfrage ist die ganze Anforderung. |
| Ein Geheimnis, das Sie verwalten und rotieren müssen | Ja: ein Signierschlüssel, sonst kann ein Fremder Ihnen eine gefälschte Nachricht unterschieben. | Nein. Es gibt nichts zu prüfen, weil nie etwas ungefragt eintrifft. |
| Etwas, das genau in dem Moment läuft, in dem die Mail eintrifft | Ja — und wenn es ausfällt, entscheidet nicht mehr Ihr Code, sondern die Wiederholungslogik des Absenders, ob Sie die Nachricht überhaupt bekommen. | Nein. Es geht nichts verloren, während Sie nicht hinsehen: Das Postfach hält sie ohnehin 5 Tage lang. |
| Anfragen, die gestellt werden, wenn keine Mail da ist | Gar keine. Genau das ist der ganze Reiz. | Eine pro Intervall — die eigentlichen Kosten, und genau darum geht es im Rest dieser Anleitung. |
Die Schleife, die jeder als Erstes schreibt
Es sind vier Zeilen, sie funktionieren an dem Tag, an dem sie geschrieben werden, und jedes ihrer Probleme zeigt sich später und woanders: in einer Pipeline um drei Uhr morgens, in einem Agenten, der seit elf Minuten „denkt“, in einem Postfach, das einer Kollegin mit 429 antwortet, weil Ihre Schleife das Budget aufbraucht.
import time
import requests
while True:
r = requests.get("https://grabmail.io/api/v1/mailbox",
params={"address": "signup-42@grabmail.io"})
if r.json()["messages"]:
break
time.sleep(1)Fünf Dinge sind daran falsch, und nur das erste ist offensichtlich.
- Sie gibt niemals auf
- Es gibt keine Frist, sodass diese Schleife nicht fehlschlägt, wenn die Nachricht wirklich nicht kommt — das Formular hat die Adresse abgelehnt, die Warteschlange des Absenders hängt, jemand hat die Domain falsch geschrieben. Sie hängt. Ein Job, der hängt, ist schlimmer als einer, der fehlschlägt, denn das Protokoll endet, ohne je zu sagen, warum.
- Sie zählt Versuche und nennt sie Sekunden
- Selbst mit einer Obergrenze für die Anzahl der Durchläufe sind dreißig Versuche zu je „einer Sekunde“ niemals dreißig Sekunden: Jeder Durchlauf kostet auch eine Anfrage, und eine Anfrage, die 400 ms braucht, macht aus Ihren dreißig Sekunden zweiundvierzig. Fügen Sie einen Retry hinzu, und die Rechnung ist keine Rechnung mehr.
- Jeder Runner fragt im selben Takt
- Starten Sie zwanzig Jobs aus derselben Pipeline, und sie pollen im Gleichschritt, weil sie alle innerhalb von wenigen Millisekunden begonnen haben und alle dieselbe volle Sekunde schlafen. Die Spitze ist zwanzigmal der Durchschnitt, und es ist die Spitze, die abgelehnt wird.
- Sie nimmt die neueste Nachricht, nicht Ihre
- Der erste Eintrag in der Liste ist, was immer oben in diesem Postfach steht — auf einer öffentlichen Adresse womöglich die Mail eines Fremden, auf einer wiederverwendeten Adresse die von letzter Woche. Eine Schleife, die bei der ersten Nachricht aussteigt, die sie sieht, steigt bereitwillig aus, bevor die erwartete überhaupt angekommen ist.
- Sie behandelt jede Antwort als Erfolg
- Die Nachrichtenliste aus einem
429oder einem404herauszulesen, wirft einen Fehler drei Ebenen von allem entfernt, was ihn erklären würde, und sie aus einem500herauszulesen, wirft womöglich gar keinen. Der Statuscode ist das Erste, das man sich ansehen sollte, nicht das Letzte.
An einer Uhr anhalten, nicht an einer Zählung
Nehmen Sie die Frist einmal, vor der ersten Anfrage, von einer monotonen Uhr — einer, die nicht zurückspringen kann, wenn die Maschine ihre Zeit korrigiert —, und prüfen Sie am Anfang jedes Durchlaufs gegen sie. Alles andere in der Schleife darf sich dann ändern, ohne dass sich die Dauer des Wartens ändert: Sie können das Intervall verbreitern, eine Ablehnung wiederholen oder einen zweiten Filter hinzufügen, und neunzig Sekunden bleiben neunzig Sekunden.
Wie lange lang genug ist, ist eine Frage über den Absender, nicht über Sie. Mail, die eine Maschine als Antwort auf ein Formular erzeugt, wird meist innerhalb einstelliger Sekundenzahlen zugestellt; eine Warteschlange mit Rückstand, ein Empfänger, der greylistet, oder ein stündlicher Stapel liegen in einer ganz anderen Größenordnung, und kein von Ihnen gewähltes Intervall lässt sie schneller ankommen.
| Worauf Sie warten | Eine ehrliche Frist | Was zu tun ist, wenn sie verstreicht |
|---|---|---|
| Eine Anmelde- oder Bestätigungsmail, innerhalb eines Tests | 60 bis 120 Sekunden | Lassen Sie den Test fehlschlagen und geben Sie die Adresse aus. Neunmal von zehn ist das Postfach leer, weil das Formular die Adresse abgelehnt hat, und die Adresse ist das Erste, was jeder braucht, der das Protokoll liest. |
| Ein Passwort-Reset, um den eine Person gerade gebeten hat | 30 bis 60 Sekunden | Sagen Sie ihnen, dass sie noch nicht angekommen ist, und bieten Sie an, sie erneut zu senden. Drehen Sie sich nicht endlos hinter einem stummen Bildschirm: Sie werden ohnehin um einen zweiten Code bitten, und dann gibt es zwei davon. |
| Ein Agent, der eine Anmeldung eigenständig abschließt | Zwei oder drei serverseitige Wartevorgänge, also 50 bis 75 Sekunden | Sagen Sie es in der Antwort. „Keine Bestätigungsmail nach einer Minute“ ist ein Ergebnis, mit dem der Agent etwas anfangen kann; ein Tool-Aufruf, der nie zurückkehrt, ist es nicht. |
| Ein Newsletter, eine Quittung, alles, was gesammelt verschickt wird | Minuten — oder gar nicht warten | Pollen Sie stattdessen nach einem Zeitplan und lassen Sie den Prozess enden. Etwas, das zehn Minuten an einem Socket hängt, wird von einem Proxy, einem Runner oder einem Container-Limit beendet. |
Die Frist ist auch der ehrliche Ort für Ihre Fehlermeldung. „Nichts, was zu ‚Bestätigen Sie Ihre E-Mail-Adresse’ passt, ist innerhalb von 90 s bei signup-42@grabmail.io angekommen“ nennt die Adresse, den Filter und das Budget — drei der vier Dinge, die nötig sind, um herauszufinden, was passiert ist. Das vierte — was tatsächlich angekommen ist — lohnt sich ebenfalls auszugeben: Eine Liste der Betreffzeilen, die die Schleife gesehen und verworfen hat, verwandelt „der Test ist flaky“ in einer einzigen Lektüre in „die Betreffzeile hat sich geändert“.
Wie oft man fragen sollte, und wann man verbreitert
Die Untergrenze ist das, was der Dienst zulässt, und hier ist das eine Anfrage pro Sekunde und Adresse. Das ist keine Abschreckung — einmal pro Sekunde zu pollen ist genau das vorgesehene Muster, es gibt kein Tageskontingent, kein Monatskontingent und kein Burst-Guthaben zu verwalten —, aber es ist eine Untergrenze, und eine Schleife, die zweimal innerhalb derselben Sekunde fragt, bekommt für die zweite Anfrage einen 429 statt einer schnelleren Antwort.
- Ein festes Intervall
- Eine Sekunde, jedes Mal, bis zur Frist. Vollkommen ausreichend für ein Warten, das in zehn Sekunden vorbei ist, und die richtige Grundeinstellung für einen einzelnen Test auf einem einzelnen Runner. Der einzige Fehler: Es fragt im selben Takt weiter, lange nachdem klar geworden ist, dass die Mail nicht kommt.
- Ein sich verbreiterndes Intervall
- Eine Sekunde, solange die Nachricht wahrscheinlich noch unterwegs ist, dann Verdopplung — zwei, vier, acht — mit einer Obergrenze. Das kostet ein wenig Latenz bei einer Nachricht, die spät ankommt, und spart die meisten Anfragen bei einem Warten, das ohnehin scheitern würde. Kappen Sie es: Ein Intervall, das sich ohne Obergrenze verdoppelt, verschläft die zweite Hälfte einer Zwei-Minuten-Frist.
- Jitter, addiert und nie subtrahiert
- Verteilen Sie die Durchläufe mit einem zufälligen Bruchteil, damit zwanzig Runner nicht mehr im selben Takt fragen. Das übliche Rezept — ein Zufallswert zwischen null und dem Intervall — ist hier falsch, weil die Hälfte seines Bereichs unter die Ein-Sekunden-Grenze fällt. Addieren Sie die Zufälligkeit stattdessen obendrauf: Das Intervall ist ein Minimum, und Jitter macht einen Durchlauf immer nur später, nie früher.
- Eine Pause, die nicht Sie bestimmen
- Wenn die Antwort ein
429ist, ist das Intervall genau das, wasRetry-Aftersagt, und der abgelehnte Durchlauf war kein Versuch. Zählen Sie ihn als einen, und eine gedrosselte Schleife verbringt ihre gesamte Frist damit, Ablehnungen zu sammeln, ohne das Postfach je gelesen zu haben.
Fünfzehn Sekunden bei einer Sekunde, dann Verdopplung bis zu einer Obergrenze von acht, mit Jitter obendrauf, passt fast jedes Warten in dieser Anleitung in sechs Zeilen:
def delay(attempt: int) -> float:
# One second while the message is probably still in flight, then
# wider. Never below a second: the list endpoint allows one call
# per second, per address, so jitter is added and never taken off.
step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
return step + random.uniform(0.0, step / 2)Der Exponent ist verschoben, sodass die Verbreiterung nach der festen Phase beginnt und nicht schon beim ersten Durchlauf. Ohne diese Verschiebung hat das Intervall bereits acht Sekunden erreicht, wenn eine langsame Anmeldemail eintrifft, und ein Warten, das zwölf Sekunden hätte dauern sollen, dauert zwanzig.
Nichts davon gilt für die erste Anfrage. Fragen Sie sofort, vor jedem Schlaf: Eine Nachricht, die schon im Postfach lag, als die Schleife startete — der Normalfall für alles, was ausgelöst wurde, bevor das Warten begann — sollte keine Sekunde Latenz kosten, bis sie bemerkt wird.
Eine Ablehnung lesen
Jede Antwort vom Listen-Endpunkt ist JSON, und die, die kein Postfach sind, teilen eine Form: einen error-Slug, der stabil ist und auf den man verzweigen sollte, und eine message, die Fließtext ist und jederzeit umformuliert werden kann. Eine Ablehnung wegen zu hoher Geschwindigkeit trägt außerdem einen Header:
HTTP/1.1 429 Too Many Requests
Retry-After: 1
Content-Type: application/json; charset=utf-8
{"error":"rate_limited","message":"one request per second, per address"}Retry-After ist in ganzen Sekunden angegeben, und es ist die echte Zahl — abgeleitet davon, wie viel vom Budget dieser Adresse tatsächlich noch übrig ist, nicht von einer Konstante in der Dokumentation. Genau so lange zu schlafen ist zugleich das Höflichste und das Schnellste: Ein kürzerer Schlaf wird wieder abgelehnt, ein längerer verschenkt Zeit. Hier ist alles, worauf eine Polling-Schleife treffen kann, und was jede Antwort wirklich von ihr verlangt.
| Was zurückkommt | Was es bedeutet | Was die Schleife tun sollte |
|---|---|---|
200 mit count: 0 | Das Postfach existiert und ist leer. Das ist die normale Antwort für den größten Teil eines Wartens. | Weiter warten. Das ist kein Fehler, und es wird auch keiner. |
429 — rate_limited | Zu schnell: eine zweite Listenanfrage innerhalb einer Sekunde für diese Adresse, oder mehr als 1,200 Anfragen in einer Minute aus dieser Quelle. | Schlafen Sie Retry-After Sekunden lang, dann fragen Sie erneut. Zählen Sie die Ablehnung nicht als Versuch. |
404 — unknown_domain | Der Teil nach dem @ wird hier nicht gehostet. Fast immer ein Tippfehler, oder eine Domain, deren MX-Eintrag nie hierher zeigte. | Aufhören. Kein noch so langes Warten repariert eine Domain. Geben Sie die Adresse aus, die man Ihnen übergeben hat. |
400 — invalid_address | Der Parameter address fehlt, ist länger als 320 Zeichen, oder hat nicht die Form name@domain. | Aufhören. Das ist ein Fehler beim Aufrufer, und es wird bei jedem Durchlauf derselbe Fehler sein. |
400 — bad_cursor | Der Wert von before ist überhaupt nicht wie eine Nachrichten-id geformt. Eine id, die wohlgeformt ist, aber abgelaufen ist, ist nicht dieser Fehler: Sie antwortet mit 200 und einer leeren Seite. | Hören Sie auf zu blättern und beginnen Sie wieder von der ersten Seite. |
404 — not_found, bei einer einzelnen Nachricht | Diese id ist nicht in diesem Postfach — oder war es, und ist inzwischen abgelaufen oder gelöscht worden. | Behandeln Sie es als verschwunden, nicht als verspätet. Eine id, die Sie vor Sekunden in einer Liste gesehen haben, kommt nicht zurück. |
500 — storage_failed | Auf unserer Seite ist beim Lesen des Postfachs etwas fehlgeschlagen. | Fragen Sie erneut, aber lassen Sie die Frist bestimmen, und fragen Sie nicht schneller als sonst. |
Zwei der sieben bedeuten Aufhören, und das sind die beiden, über die es sich lohnt, laut zu sein. Eine Schleife, die unknown_domain als „noch nicht“ behandelt, verbringt volle neunzig Sekunden damit, etwas zu beweisen, das ihr der Dienst schon in den ersten vierzig Millisekunden gesagt hat.
Welche Nachricht Ihnen gehört
Ein Postfach ist keine Warteschlange, und das Neueste darin ist nicht unbedingt das, worauf Sie warten. Auf einer öffentlichen Domain kann jeder, der die Adresse errät, dorthin senden; in einer Testsuite wird dieselbe Adresse oft zwischen Läufen wiederverwendet; und eine einzelne Anmeldung schickt häufig zwei Nachrichten — eine Willkommensmail und eine Bestätigung —, von denen nur eine den Code trägt. Die Lösung ist ein Wasserzeichen, und es muss genommen werden, bevor das geschieht, was die Mail auslöst.
- Bevor Sie das Formular absenden, listen Sie das Postfach mit
limit=1und merken Sie sich die id der neuesten Nachricht, oder gar nichts, wenn es leer ist. Diese id ist das Wasserzeichen. - Tun Sie, was zu tun ist — das Formular absenden, den Endpunkt aufrufen, den Knopf klicken.
- Pollen Sie die Liste. Nachrichten kommen mit der neuesten zuerst zurück, gehen Sie also von oben nach unten und halten Sie an, sobald Sie auf das Wasserzeichen treffen: Alles darunter ist älter als Ihre Aktion und kann ignoriert werden, ohne gelesen zu werden.
- Filtern Sie, was darüber steht, nach Absender, Betreff oder beidem. Ein Teilstring reicht meist, und es sollte der Teil sein, der nicht lokalisiert wird — ein Test, der auf „Bestätigen Sie Ihre E-Mail-Adresse“ passt, schlägt an dem Tag fehl, an dem das getestete Konto auf eine andere Sprache umgestellt wird.
- Erst dann, und nur dann, öffnen Sie sie. Die Liste trägt eine kurze
previewund nicht den Textkörper, und der Code, den Sie suchen, liegt sehr oft hinter deren Ende. Eine weitere Anfrage holt die ganze Nachricht, und sie wird gegen ein separates, weit größeres Budget als das der Liste gerechnet.
In einer Shell sehen diese beiden Abfragen so aus — zuerst das Wasserzeichen, dann der Poll:
curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=1"
curl -sG https://grabmail.io/api/v1/mailbox \
--data-urlencode "address=signup-42@grabmail.io" --data-urlencode "limit=25"Beide Anfragen nennen die Adresse vollständig, denn hier ist die Adresse das Postfach: Es gibt keine Session, keinen für Sie vorgehaltenen Cursor, und nichts an einem Aufruf, an das sich der nächste erinnert. Das ist auch der Grund, warum es unbedenklich ist, eine Adresse von zwei Stellen gleichzeitig zu beobachten — Lesen verbraucht nichts, sodass zwei Schleifen auf demselben Postfach beide jede Nachricht sehen und keine der anderen eine wegnehmen kann.
Genau einmal handeln
Ein Poll, der wiederholt wird, kann dieselbe Nachricht zweimal sehen, und das ist kein seltenes Ereignis: Der Server antwortet, die Verbindung bricht ab, bevor der Body Sie erreicht, Ihr HTTP-Client wiederholt die Anfrage, und die zweite Antwort enthält die Nachricht, die die erste schon trug. Wenn das, was Sie mit einer Nachricht tun, ein Linkklick, eine Zahlungsbestätigung oder ein Post in einen Kanal ist, ist es zweimal zu tun ein Bug mit Folgen außerhalb Ihres Prozesses.
- Behalten Sie die ids, die Sie schon bearbeitet haben
- Eine Menge von ids im Arbeitsspeicher reicht für ein Warten, das innerhalb einer einzigen Funktion lebt und stirbt. Für alles, was einen Neustart überstehen muss — ein Postfach, das von einem geplanten Job geleert wird, ein Agent, der einen Rückstand abarbeitet — muss es irgendwo aufgeschrieben werden, das den Neustart mit übersteht.
- Löschen ist idempotent
- Das Löschen einer Nachricht antwortet beim zweiten Mal genauso mit
200wie beim ersten, sodass ein wiederholtes Löschen nie wie ein Fehlschlag aussieht und nie einen Sonderfall braucht. Löschen Sie nachdem Sie gehandelt haben, nicht davor: Ein Absturz zwischen den beiden kostet Sie dann ein erneutes Lesen, was sich reparieren lässt, statt die Nachricht, was sich nicht reparieren lässt. - Die id hier ist nicht die Message-ID des Absenders
- Die id in der API ist unsere: Sie gilt nur innerhalb eines Postfachs und existiert nicht mehr, wenn die Nachricht abläuft. Der Header
Message-IDgehört dem Absender, er reist mit der Nachricht, und er ist derjenige, den Sie brauchen, wenn Sie dieselbe Mail über zwei Systeme hinweg abgleichen — die Anleitung zu den Headern sagt, wo man ihn findet.
Nichts davon wird für einen Test gebraucht, der auf einen Code wartet und das Postfach danach wegwirft. Alles davon wird gebraucht, sobald eine Schleife unbeaufsichtigt läuft, denn der Fehler, den es verhindert, sieht nicht wie ein Fehler aus: Er sieht aus, als würde die Arbeit zweimal, korrekt, erledigt.
Wenn das Warten dem Server gehört
Es gibt hier eine Stelle, an der Sie die Schleife nicht selbst schreiben, und die existiert für Aufrufer, die sich keine leisten können. Ein KI-Agent bezahlt für jeden Turn, den er mit Nachsehen verbringt, sodass ein Werkzeug, das neunmal „noch nichts“ antwortet, neun Turns aus nichts sind. Das wait_for_message des MCP-Servers hält die Anfrage stattdessen offen, pollt auf unserer Seite, und antwortet einmal — entweder mit der Nachricht, oder mit der schlichten Aussage, dass gewartet wurde und nichts kam.
$ curl -sX POST https://grabmail.io/mcp -H "Content-Type: application/json" -d '{
"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"wait_for_message","arguments":{
"address":"signup-42@grabmail.io","subject_contains":"code",
"timeout_seconds":25}}}'Vier Dinge daran lohnt es sich zu wissen, bevor Sie darauf aufbauen.
- Es wartet höchstens 25 Sekunden
timeout_secondskann um weniger bitten, aber nie um mehr. Die Obergrenze ist nicht willkürlich: Jeder Wartende ist ein Worker, der nichts tut außer zu schlafen, und eine Anfrage, die minutenlang offengehalten wird, stirbt lange vor ihrer Rückkehr an irgendeinem Proxy-Timeout.- Es filtert schon beim Hereinkommen
from_contains,subject_containsundsince_idsind dieselben drei Entscheidungen wie im Abschnitt oben, nur auf dem Server getroffen.since_idist das Wasserzeichen, und es zählt hier mehr als sonst irgendwo: Ohne es kehrt der Aufruf sofort zurück, mit allem, was schon im Postfach lag.- Ein Timeout ist eine Antwort, kein Fehler
- Wenn nichts eintrifft, liefert es
timed_outgesetzt, zusammen damit, wie lange es tatsächlich gewartet hat, und sagt ausdrücklich, dass ein erneuter Aufruf der Weg ist, weiter zu warten. Zwei oder drei Aufrufe sind ein normales Warten auf eine Anmeldemail: Das ist die Schleife, und es sind drei Turns statt neunzig. - Es gibt 8 Warteplätze, und keine Warteschlange
- Wenn alle davon belegt sind, kommt der Aufruf sofort zurück und sagt das, statt sich hinter sieben anderen Agenten in eine Schlange einzureihen. Das ist der richtige Fehlschlag: Ein Agent, dem „zu viele Wartevorgänge laufen bereits“ gesagt wird, kann das Postfach listen und weitermachen, während ein Agent, der in einer Warteschlange sitzt, nur sitzen kann.
Das Limit pro Adresse gilt auch hier innen weiter — unsere Schleife ist genauso rate-limitiert wie Ihre, sodass ein serverseitiges Warten kein Weg um die Untergrenze herum ist, nur ein Weg, nicht mehr in Turns dafür zu bezahlen. Für eine Testsuite lohnt sich das alles nicht: Ein Test ist bereits ein Prozess, der schlafen darf, und eine Schleife in der Sprache, in der der Test geschrieben ist, ist weit leichter zu debuggen als eine entfernte. Die MCP-Anleitung behandelt die übrigen Werkzeuge.
Die ganze Schleife, einmal
Alles von oben, in einer Datei: eine Frist von einer monotonen Uhr, ein Intervall, das sich mit einseitigem Jitter verbreitert, ein respektiertes Retry-After, das nicht als Versuch zählt, ein Wasserzeichen, das entscheidet, was neu ist, ein Filter auf den Betreff, und eine zusätzliche Anfrage, um die Nachricht zu holen, die die Liste nur andeutet.
import random
import time
import requests
API = "https://grabmail.io/api/v1"
ADDRESS = "signup-42@grabmail.io"
def delay(attempt: int) -> float:
# One second while the message is probably still in flight, then
# wider. Never below a second: the list endpoint allows one call
# per second, per address, so jitter is added and never taken off.
step = 1.0 if attempt < 15 else min(8.0, 2.0 ** (attempt - 14))
return step + random.uniform(0.0, step / 2)
def watermark(s):
# Read this BEFORE the form is submitted. Every id above it
# afterwards is mail that arrived because of what you did.
r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 1})
r.raise_for_status()
seen = r.json()["messages"]
return seen[0]["id"] if seen else None
def wait_for(s, subject, since, timeout=120.0):
deadline = time.monotonic() + timeout
attempt = 0
rejected = set()
while time.monotonic() < deadline:
r = s.get(f"{API}/mailbox", params={"address": ADDRESS, "limit": 25})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", "1")))
continue # refused, so it was not an attempt
if r.status_code == 200:
for m in r.json()["messages"]: # newest first
if m["id"] == since:
break # older than the watermark
if subject.lower() in m["subject"].lower():
full = s.get(f"{API}/message/{m['id']}",
params={"mailbox": ADDRESS})
full.raise_for_status()
return full.json()
rejected.add(m["subject"])
elif r.status_code < 500:
raise RuntimeError(r.json().get("error", r.status_code))
# a 5xx falls through: transient, and the deadline still governs
time.sleep(delay(attempt))
attempt += 1
raise TimeoutError(
f"nothing matching {subject!r} at {ADDRESS} in {timeout:.0f}s; "
f"saw {sorted(rejected) or 'nothing at all'}")Das sind absichtlich gut fünfzig Zeilen Standardbibliothek und ein HTTP-Client. Es gibt nichts zu installieren, nichts zu konfigurieren und kein Geheimnis darin — genau das ist der Punkt: Dieselbe Form wandert unverändert nach Node, in ein Shell-Skript, oder in was auch immer Ihr Testframework schon zum Stellen von Anfragen benutzt.
- Lesen Sie das Wasserzeichen vor der Aktion, die die Mail auslöst, niemals danach.
- Fragen Sie einmal sofort, und schlafen Sie erst danach. Schlafen Sie nie zuerst.
- Nehmen Sie die Frist von einer monotonen Uhr, und prüfen Sie sie am Anfang jedes Durchlaufs.
- Halten Sie das Intervall bei mindestens einer Sekunde pro Adresse, und fügen Sie Jitter nur nach oben hinzu.
- Schlafen Sie genau das, was
Retry-Aftersagt, und zählen Sie eine Ablehnung nicht als Versuch. - Verzweigen Sie auf den
error-Slug:unknown_domainundinvalid_addressbedeuten Aufhören, nicht Warten. - Gleichen Sie nach Absender oder Betreff ab, und hören Sie auf, die Liste zu durchlaufen, wenn Sie das Wasserzeichen erreichen.
- Öffnen Sie die Nachricht, bevor Sie sie parsen — die Liste trägt eine Vorschau, nicht den Textkörper.
- Schlagen Sie fehl mit der Adresse, dem Filter, dem Budget und den Betreffzeilen, die die Schleife verworfen hat.
Neun Regeln, und acht von ihnen existieren wegen eines Fehlschlags, den irgendjemand aus einem Protokoll rekonstruieren musste. Die eine, bei der es nicht um einen Fehlschlag geht, ist die zweite: einmal zu fragen, vor dem ersten Schlaf, ist das, was ein Warten auf eine bereits angekommene Nachricht vier Millisekunden statt einer Sekunde dauern lässt — was über eine Suite von zweihundert Tests drei Minuten Wanduhrzeit sind, die hinterher niemand erklären muss.
Fragen
Hat GrabMail einen Webhook?
Nein, und das ist keine Lücke, die noch gefüllt werden müsste. Der Dienst empfängt Mail und stellt sie ohne Schlüssel über HTTP zur Verfügung: Es gibt kein Konto hinter einer öffentlichen Adresse, an das sich ein Callback hängen ließe, und keine Warteschlange, die eine von Ihrem Endpunkt abgelehnte Zustellung aufhebt. Wenn Ihr Workflow wirklich nicht pollen kann, nennt die Vergleichsseite die Dienste, die einen anbieten.
Wie oft darf ich eine Adresse pollen?
Einmal pro Sekunde und Adresse — und das ist das vorgesehene Muster, nicht dessen Rand. Es gibt kein Tageskontingent, kein Monatskontingent und kein Burst-Guthaben zu verwalten. Zwanzig Postfächer, die von einem Runner aus einmal pro Sekunde gepollt werden, sind normale Nutzung; die einzige weitere Obergrenze sind 1,200 Anfragen pro Minute aus einer einzigen Quelle, was genau diese zwanzig sind und kein einundzwanzigstes.
Warum hat meine Schleife eine Nachricht aus einem früheren Testlauf zurückgegeben?
Weil sie den ersten Eintrag der Liste genommen hat, ohne zu fragen, wann er angekommen ist. Ein Postfach hält alles, was ihm 5 Tage lang zugesandt wurde, und eine wiederverwendete Adresse ist voll vom letzten Lauf. Lesen Sie die neueste id, bevor Sie die Mail auslösen, und ignorieren Sie alles von dieser id abwärts — oder löschen Sie den Inhalt des Postfachs zu Beginn des Tests, was eine Anfrage pro Nachricht ist und die Mehrdeutigkeit vollständig beseitigt.
Ist ein leeres Postfach ein 404?
Nein. Ein leeres Postfach ist absichtlich 200 mit count: 0 und einer leeren Liste, damit eine Polling-Schleife „noch nichts“ nie als Sonderfall behandeln muss. Ein 404 vom Listen-Endpunkt bedeutet, dass die Domain hier nicht gehostet wird; ein 404 bei einer einzelnen Nachricht bedeutet, dass diese id nicht in diesem Postfach ist, oder abgelaufen ist.
Wie lange sollte ich auf eine Bestätigungsmail warten?
Sechzig bis hundertzwanzig Sekunden in einem automatisierten Test, dreißig bis sechzig für eine Person, die vor einem Bildschirm wartet. Die meiste maschinell erzeugte Mail kommt innerhalb einstelliger Sekundenzahlen an; der lange Rest liegt an der Warteschlange des Absenders, nicht an der Zustellung. Wenn es regelmäßig knapp an Ihrer Frist vorbeigeht, ist eine längere Frist nicht die Antwort — dann stimmt etwas anderes nicht.
Können zwei Prozesse dieselbe Adresse gleichzeitig pollen?
Ja. Lesen verbraucht nichts, also sehen beide jede Nachricht, und keiner verbirgt Mail vor dem anderen. Sie teilen sich allerdings das Budget von einer Anfrage pro Sekunde für diese Adresse, sodass zwei Schleifen, die jede Sekunde fragen, jeweils etwa die Hälfte der Zeit abgelehnt werden: Geben Sie jeder zwei Sekunden, oder lassen Sie eine pollen und die Ergebnisse an die andere weitergeben.
Sollte ich pollen, oder das Wait-Werkzeug über MCP benutzen?
Pollen, wenn Sie einen Test oder ein Skript schreiben: Ein Prozess, der schlafen darf, sollte schlafen, und eine Schleife in Ihrer eigenen Sprache ist leichter zu debuggen als eine entfernte. Benutzen Sie wait_for_message, wenn der Aufrufer pro Turn bezahlt statt pro Sekunde, was in der Praxis einen KI-Agenten bedeutet. Es wartet bis zu 25 Sekunden pro Aufruf, filtert nach Absender und Betreff, und gibt einen schlichten Timeout zurück, bei dem Sie einfach erneut aufrufen können.


