Wo der Code tatsächlich steht
Eine Bestätigungsmail hat bis zu drei Stellen, an denen der Code stehen kann, und welche davon Sie lesen sollten, entscheidet alles Weitere. Das Nachrichten-JSON von der API liefert alle drei auf einmal: subject, text (den Klartext-Teil, oder null) und html (den HTML-Teil, oder null).
| Wo | Wie es aussieht | Wie man es liest |
|---|---|---|
Der Klartext-Teil (text) | Your code is 481920. It expires in 10 minutes. | Parsen Sie das zuerst, wenn es existiert. Kein Markup, nichts zu dekodieren, und der Wortlaut ist stabil. |
Der HTML-Teil (html) | Derselbe Satz innerhalb einer Tabelle, oft mit den Ziffern gestylt, eine pro Zelle, und jedes & als Entity geschrieben. | Tags zu Leerzeichen machen, Entities dekodieren, Whitespace falten, dann das Muster anwenden. Nie Regex auf rohes HTML anwenden. |
| Die Betreffzeile | 481920 is your verification code | Ein Geschenk, wenn der Absender das macht: gar kein Body zu parsen. Matchen Sie im Betreff und weichen Sie auf den Body aus. |
| Ein Bild | Der Code als Bild gezeichnet, um genau diese Art von Skript auszuhebeln. | Selten, und ein Zeichen, dass der Absender keine Automatisierung möchte. Ändern Sie die Vorlage des Absenders, wenn sie Ihnen gehört; gibt es die nicht, gibt es dafür keinen ehrlichen Workaround. |
Der Klartext-Teil ist der, den man bevorzugen sollte, und er ist derjenige, den die meisten Templating-Systeme automatisch aus dem HTML erzeugen — er ist also meistens vorhanden. Ist er null, ist der HTML-Teil der einzige Body, und die nächsten beiden Abschnitte handeln davon, wie man ihn sicher liest.
Das Muster im eigenen Wortlaut verankern
Der erste Instinkt ist \d{6}. Das passt auf den Code, und es passt genauso auf die Jahreszahl im Footer, die Postleitzahl im Adressblock, die letzten sechs Ziffern einer Telefonnummer und die Bestellnummer, die zwei Zeilen über dem Code steht. Was auch immer zuerst kommt, gewinnt, und der Test tippt es mit vollem Vertrauen ins Formular.
| Muster | Passt auch auf | Urteil |
|---|---|---|
\d{6} | Jahreszahlen, Postleitzahlen, Preise ohne Trennzeichen, Bestellnummern, Telefonnummern, Sendungsnummern. | Niemals. Das ist kein Muster, das ist ein Münzwurf. |
\b\d{6}\b | Alles oben Genannte, das zufällig genau sechs Ziffern mit einem Leerzeichen auf beiden Seiten hat — immer noch das meiste davon. | Kaum besser. Wortgrenzen wissen nicht, was ein Code ist. |
code is\D{0,12}(\d{6}) | Nur die sechs Ziffern, die auf die Worte folgen, die Ihre Vorlage vor den Code setzt, mit Raum für einen Doppelpunkt, ein Leerzeichen oder so viel übrig gebliebenes Whitespace, wie ein Tag hinterlässt. | Ja. Es passt auf den Code und sonst nichts, und es schlägt an dem Tag fehl, an dem jemand die E-Mail umformuliert — und das ist ein Fehlschlag, von dem Sie erfahren wollen. |
Das \D{0,12} ist das praktische Detail: Nachdem Tags durch Leerzeichen ersetzt wurden, können die Worte und die Ziffern durch einen Doppelpunkt, eine Folge von Leerzeichen oder die Überreste eines <strong>, das früher dazwischen saß, getrennt sein. Bis zu ein Dutzend Nicht-Ziffern deckt all das ab, ohne dem Muster zu erlauben, zu einer anderen Zahl zu springen.
Vorlagen, die die Ziffern aufteilen
Ein beliebtes Design setzt jede Ziffer des Codes in eine eigene Box, damit es sich auf dem Handy gut liest. Im HTML sind das sechs Tabellenzellen oder sechs <span>s, und die Zahl taucht im Quelltext nirgends als sechs aufeinanderfolgende Zeichen auf:
<p>Your code is</p>
<table><tr>
<td class="digit">4</td><td class="digit">8</td><td class="digit">1</td>
<td class="digit">9</td><td class="digit">2</td><td class="digit">0</td>
</tr></table>Ein Regex auf dem rohen HTML findet nichts. Die Lösung ist kein cleverer Regex; sie besteht darin, das HTML zuerst in fester Reihenfolge in Text zu verwandeln:
- Ersetzen Sie jedes Tag durch ein Leerzeichen. Ein Leerzeichen, nicht nichts —
<td>4</td><td>8</td>muss zu4 8werden, nicht zu48, verklebt mit dem, was folgte. - Dekodieren Sie die Entities.
&, ,'. Ein geschütztes Leerzeichen zwischen zwei Ziffern ist für einen Regex kein Leerzeichen, solange es nicht dekodiert ist. - Falten Sie Whitespace, und matchen Sie dann so, dass zwischen den Ziffern Leerraum erlaubt ist. Für das Box-Design
code is\D{0,12}(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)und die Gruppen zusammenfügen; für eine normale Vorlage reicht das einfache Muster aus dem vorigen Abschnitt.
Die Helfer weiter unten erledigen die Schritte eins und zwei für Sie und durchsuchen beide Teile auf einmal, sodass ein Test nicht wissen muss, welches Design die Vorlage gerade diesen Monat verwendet.
Die neueste Nachricht ist nicht immer die richtige
Jede Postfach-Auflistung liefert hier zuerst die neueste zurück, und messages[0] ist das, was die meisten ersten Entwürfe lesen. Drei Situationen machen das zur falschen Nachricht:
- Zwei Nachrichten von einer Aktion
- Die Anmeldung verschickt eine Willkommensmail und eine Code-Mail, in welcher Reihenfolge auch immer sich die Warteschlange des Absenders leert. In der Hälfte der Fälle ist die Willkommensmail die neuere. Filtern Sie nach Betreff oder Absender, bevor Sie irgendetwas übernehmen.
- Ein erneuter Versand
- Der Test hat den Code zweimal angefragt — einmal aus Versehen, einmal absichtlich —, und der Server akzeptiert nur den neuesten. Die ältere Nachricht steckt noch im Postfach, passt immer noch auf das Muster und lässt sich immer noch in sechs Ziffern parsen, die inzwischen ungültig sind.
- Ein vorheriger Testlauf
- Nur, wenn die Adresse wiederverwendet wurde, was nie vorkommen sollte. Eine frische Adresse pro Lauf macht diesen Fall unmöglich; wenn das nicht geht, ist die Momentaufnahme weiter unten der Rückfallplan.
Die robuste Form ist in jedem Runner dieselbe: Schauen Sie nach, was vor dem Auslösen der Mail im Postfach liegt, und akzeptieren Sie danach nur eine Nachricht, die vorher noch nicht da war und zum erwarteten Betreff passt.
// Remember what is already there, THEN trigger the resend, THEN wait for something new.
const before = new Set((await listMailbox(address)).messages.map(m => m.id));
await page.getByRole('button', { name: 'Resend code' }).click();
const fresh = await waitFor(address, m => !before.has(m.id) && /code/i.test(m.subject));Codes, die während des Laufs ablaufen
Die meisten Einmalcodes sind fünf bis fünfzehn Minuten gültig. Das klingt großzügig, bis eine Testsuite zwanzig Specs in eine Warteschlange stellt, von denen jede ihren Code am Anfang angefordert und am Ende eingetippt hat. Drei Regeln halten den Code am Leben:
- Fordern Sie den Code so spät wie möglich an. Lösen Sie den Versand unmittelbar vor dem Warten aus, nicht in einem Setup-Schritt, der läuft, während andere Tests in der Warteschlange stehen.
- Halten Sie die Warte-Deadline deutlich unter der Lebensdauer des Codes. Eine Sechzig-Sekunden-Deadline für einen zehn Minuten gültigen Code lässt neun Minuten zum Eintippen. Eine Zehn-Minuten-Deadline lässt nichts übrig.
- Speichern Sie nie einen Code für einen anderen Test. Codes sind sowohl einmalig verwendbar als auch kurzlebig; eine gemeinsam genutzte Fixture, die einen ausgibt, ist ein Wettlauf zwischen zwei Tests um eine Zahl.
Fertige Extraktoren
Drei Versionen derselben vier Zeilen: beide Teile zusammengefügt, Tags zu Leerzeichen, Entities dekodiert, Whitespace gefaltet, dann das verankerte Muster. Ändern Sie das Muster passend zum Wortlaut Ihrer Vorlage, und sonst muss nichts angefasst werden.
Aus einer Shell, mit jq
curl -sG "https://grabmail.io/api/v1/message/$ID" --data-urlencode "mailbox=$ADDR" \
| jq -r '[.text, .html] | map(select(. != null)) | join(" ") | gsub("<[^>]*>"; " ")' \
| grep -oiE 'code is[^0-9]{0,12}[0-9]{6}' | grep -oE '[0-9]{6}' | head -1Python
import html
import re
TAGS = re.compile(r"<[^>]+>")
CODE = re.compile(r"code is\D{0,12}(\d{6})", re.I) # anchored on YOUR template's wording
def text_of(message: dict) -> str:
"""Both parts as plain text: tags out, entities decoded, whitespace folded."""
raw = f"{message.get('text') or ''}\n{message.get('html') or ''}"
return re.sub(r"\s+", " ", html.unescape(TAGS.sub(" ", raw)))
def code_from(message: dict, pattern: re.Pattern = CODE) -> str:
hit = pattern.search(text_of(message))
if not hit:
raise AssertionError(f"no code in message {message['id']!r} ({message['subject']!r})")
return hit.group(1)TypeScript
export type Message = { id: string; subject: string; text: string | null; html: string | null };
const TAGS = /<[^>]+>/g;
const ENTITIES: Record<string, string> = { '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", ' ': ' ' };
/** Both parts as plain text: tags out, the common entities decoded, whitespace folded. */
export const textOf = (m: Message): string =>
`${m.text ?? ''}\n${m.html ?? ''}`
.replace(TAGS, ' ')
.replace(/&(amp|lt|gt|quot|#39|nbsp);/g, e => ENTITIES[e])
.replace(/\s+/g, ' ');
/** Anchored on your own wording. A reworded template fails loudly. */
export function codeFrom(m: Message, pattern = /code is\D{0,12}(\d{6})/i): string {
const hit = textOf(m).match(pattern);
if (!hit) throw new Error(`no code in message ${m.id} ("${m.subject}")`);
return hit[1];
}Das Nachrichten-JSON, das diese lesen, kommt von GET /api/v1/message/{id}, dokumentiert in der API-Referenz; das Warten, mit dem man die ID überhaupt erst bekommt, steht in der Anleitung zum Ende-zu-Ende-Testen, und als fertige Helfer für Playwright, Cypress, Python und Node.js.
Bevor Sie es für fertig erklären
- Das Muster im Wortlaut Ihrer Vorlage verankert und direkt neben der Vorlage aufbewahrt.
- Beide Teile als Text durchsucht: Tags zu Leerzeichen, Entities dekodiert, Whitespace gefaltet.
- Ein Filter auf Betreff oder Absender, damit eine Willkommensmail nie gegen eine Code-Mail gewinnt.
- Eine Momentaufnahme vor jedem erneuten Versand, und danach werden nur neue Nachrichten akzeptiert.
- Die Warte-Deadline deutlich unter der Lebensdauer des Codes, und der Versand unmittelbar vor dem Warten ausgelöst.
- Eine Fehlermeldung, die die Nachrichten-ID und den Betreff nennt, in denen gesucht wurde.
Das deckt jede beobachtete Art ab, wie ein Sechs-Ziffern-Extraktor mit der falschen Zahl durchgekommen ist. Ein Agent, der dieselbe Mail liest, hat dieselben Probleme und ein Werkzeug weniger dafür, weshalb der MCP-Server ihm die ganze Nachricht in die Hand gibt statt einer Vermutung — ein Postfach, das ein KI-Agent lesen kann geht das durch.
Fragen
Sollte ich den Text-Teil oder den HTML-Teil lesen?
Den Text-Teil, wenn er existiert: Er ist stabil und hat nichts zu dekodieren. Durchsuchen Sie trotzdem beide, so wie es die Helfer tun, damit eine Vorlage, die nur HTML ausliefert, trotzdem funktioniert und eine Vorlage, die nur Text ausliefert, nie über leeres HTML stolpert.
Mein Code enthält Buchstaben. Ändert sich das Muster?
Nur die Zeichenklasse: ([A-Z0-9]{6}), oder welches Alphabet der Absender auch verwendet, weiterhin im vorangehenden Wortlaut verankert. Fügen Sie das Flag i hinzu, wenn die Groß-/Kleinschreibung nicht garantiert ist, und achten Sie darauf, dass die Klasse nicht auch auf ein Wort passt, das dem Anker folgt.
Was ist mit Magic Links statt Codes?
Dieselbe Disziplin, ein anderes Muster: Gleichen Sie die URL mit einem Pfad-Fragment ab, das Sie kennen — /confirm/, /auth/magic/ — statt mit „dem ersten Link“, denn eine transaktionale E-Mail trägt meist fünf Links, und der, den Sie wollen, ist selten der erste. Dekodieren Sie &, bevor Sie ihn aufrufen.
Wie lange ist eine Nachricht lesbar?
5 Tage nach ihrer Ankunft, egal ob sie gelesen wurde oder nicht. Das ist weit länger, als irgendein Code gültig bleibt, ein Test muss sich also beim Lesen nie beeilen — nur beim Eintippen.
Kann ich den Code bekommen, ohne das Postfach zu pollen?
Über REST nicht: Sie pollen einmal pro Sekunde mit einer Deadline, das ist der dokumentierte Rhythmus und wird nie gedrosselt. Über MCP gibt es ein Tool wait_for_message, das den Aufruf offen hält, bis die Nachricht landet, und genau das braucht ein KI-Agent.
Brauche ich einen API-Schlüssel?
Nein. Die öffentlichen Domains verlangen weder Schlüssel noch Konto noch Header. Nur der kostenpflichtige Pool von Domains, der von den Wegwerf-Mail-Sperrlisten ferngehalten wird, verwendet ein Bearer-Token, und der Extraktionscode ist so oder so identisch.


