Di mana kodenya sebenarnya berada
Sebuah email verifikasi punya sampai tiga tempat kodenya bisa berada, dan tempat mana yang Anda baca menentukan segalanya setelah itu. JSON pesan dari API memberi Anda ketiganya sekaligus: subject, text (bagian teks polos, atau null) dan html (bagian HTML, atau null).
| Di mana | Bentuknya seperti apa | Cara membacanya |
|---|---|---|
Bagian teks polos (text) | Your code is 481920. It expires in 10 minutes. | Parse bagian ini duluan kalau ada. Tanpa markup, tidak ada yang perlu di-decode, dan kata-katanya stabil. |
Bagian HTML (html) | Kalimat yang sama di dalam sebuah tabel, sering dengan digit-digitnya di-styling satu per sel, dan setiap & ditulis sebagai entitas. | Ubah tag menjadi spasi, decode entitasnya, normalkan whitespace-nya, baru terapkan polanya. Jangan pernah meng-regex HTML mentah. |
| Baris subjek | 481920 is your verification code | Sebuah keberuntungan kalau pengirimnya melakukan ini: tidak ada isi pesan yang perlu di-parse sama sekali. Cocokkan pada subjeknya dan jadikan isi pesan sebagai cadangan. |
| Sebuah gambar | Kodenya digambar sebagai sebuah gambar, justru untuk mengalahkan skrip semacam ini. | Jarang terjadi, dan pertanda pengirimnya memang tidak menginginkan otomasi. Ubah template pengirimnya kalau itu milik Anda; tidak ada solusi jujur kalau bukan. |
Bagian teks polos adalah yang sebaiknya diutamakan, dan itu bagian yang biasanya dibuat otomatis oleh kebanyakan sistem templating dari HTML-nya — jadi biasanya memang ada. Kalau bagian itu null, bagian HTML jadi satu-satunya isi pesan, dan dua bagian berikutnya membahas cara membacanya dengan aman.
Patok polanya pada kata-kata Anda sendiri
Insting pertama adalah \d{6}. Pola ini cocok dengan kodenya, tapi juga cocok dengan tahun di footer, kode pos di blok alamat, enam digit terakhir sebuah nomor telepon, dan nomor pesanan yang muncul dua baris di atas kodenya. Yang mana pun muncul lebih dulu yang menang, dan pengujiannya akan mengetikkannya ke formulir dengan penuh percaya diri.
| Pola | Juga cocok dengan | Kesimpulan |
|---|---|---|
\d{6} | Tahun, kode pos, harga tanpa pemisah, nomor pesanan, nomor telepon, nomor resi. | Jangan pernah. Itu bukan sebuah pola, itu lempar koin. |
\b\d{6}\b | Semua yang di atas yang kebetulan persis enam digit dengan spasi di kedua sisinya — masih sebagian besarnya. | Nyaris tidak lebih baik. Batas kata tidak tahu apa itu sebuah kode. |
code is\D{0,12}(\d{6}) | Hanya enam digit yang mengikuti kata-kata yang template Anda taruh sebelum kodenya, dengan ruang untuk sebuah titik dua, sebuah spasi, atau sisa whitespace seukuran sebuah tag. | Ya. Pola ini hanya cocok dengan kodenya dan tidak ada yang lain, dan akan gagal pada hari seseorang mengubah kata-kata di emailnya — dan itu kegagalan yang justru ingin Anda ketahui. |
\D{0,12} adalah detail praktisnya: setelah tag-tagnya diganti dengan spasi, kata-kata dan digitnya bisa dipisahkan oleh sebuah titik dua, serangkaian spasi, atau sisa-sisa sebuah <strong> yang dulu ada di antara keduanya. Sampai selusin karakter non-digit menutupi semua kemungkinan itu tanpa membiarkan polanya meloncat ke angka lain.
Template yang memecah digit-digitnya
Sebuah desain populer menaruh setiap digit kodenya di kotaknya sendiri-sendiri, supaya enak dibaca di ponsel. Dalam HTML-nya itu berarti enam sel tabel, atau enam <span>, dan angkanya tidak akan pernah muncul sebagai enam karakter berurutan di mana pun dalam sumbernya:
<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>Sebuah regex pada HTML mentah tidak akan menemukan apa pun. Solusinya bukan regex yang lebih pintar; melainkan mengubah HTML-nya menjadi teks lebih dulu, dengan urutan yang tetap:
- Ganti setiap tag dengan sebuah spasi. Sebuah spasi, bukan kosong —
<td>4</td><td>8</td>harus menjadi4 8, bukan48yang menempel dengan apa pun yang mengikutinya. - Decode entitasnya.
&, ,'. Sebuah non-breaking space di antara dua digit bukanlah sebuah spasi bagi sebuah regex sampai ia di-decode. - Normalkan whitespace-nya, lalu cocokkan dengan digit yang boleh berspasi. Untuk desain berkotak-kotak itu, pakai
code is\D{0,12}(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)\s*(\d)lalu gabungkan grup-grupnya; untuk template biasa, pola polos dari bagian sebelumnya sudah cukup.
Helper-helper di bawah ini melakukan langkah satu dan dua untuk Anda dan mencari di kedua bagian sekaligus, jadi sebuah pengujian tidak perlu tahu desain mana yang dipakai template bulan ini.
Pesan terbaru tidak selalu yang benar
Setiap listing kotak surat di sini kembali dengan urutan terbaru lebih dulu, dan messages[0] adalah yang dibaca kebanyakan draf pertama. Ada tiga situasi yang membuat itu jadi pesan yang salah:
- Dua pesan dari satu aksi
- Pendaftaran mengirim sebuah email sambutan dan sebuah email kode, dengan urutan apa pun yang kebetulan lebih dulu keluar dari antrean pengirimnya. Separuh dari waktu, emailnya sambutan yang terbaru. Lakukan filter pada subjek, atau pada pengirim, sebelum mengambil apa pun.
- Sebuah pengiriman ulang
- Pengujiannya meminta kode itu dua kali — sekali karena kesalahan, sekali dengan sengaja — dan servernya hanya menerima yang paling baru. Pesan yang lebih lama masih ada di kotak surat, masih cocok dengan polanya, dan masih ter-parse menjadi enam digit yang sekarang sudah tidak berlaku.
- Sebuah run pengujian sebelumnya
- Hanya kalau alamatnya dipakai ulang, yang seharusnya tidak pernah terjadi. Alamat baru di setiap run membuat kasus ini mustahil terjadi; kalau Anda tidak bisa punya itu, snapshot di bawah ini adalah cadangannya.
Bentuk yang kukuh sama saja di runner mana pun: lihat apa yang ada di kotak surat sebelum Anda memicu emailnya, lalu terima hanya pesan yang belum ada sebelumnya dan cocok dengan subjek yang Anda harapkan.
// 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));Kode yang kedaluwarsa selama run berlangsung
Kebanyakan kode sekali pakai berlaku selama lima sampai lima belas menit. Itu terdengar lega sampai sebuah test suite mengantrekan dua puluh spec, yang masing-masing meminta kodenya di awal dan mengetikkannya di akhir. Tiga aturan berikut menjaga kodenya tetap hidup:
- Minta kodenya sedekat mungkin dengan saat dibutuhkan. Picu pengirimannya tepat sebelum penantian, bukan di sebuah langkah setup yang berjalan sementara pengujian lain masih mengantre.
- Jaga batas waktu penantian jauh di bawah masa berlaku kodenya. Batas waktu enam puluh detik untuk kode sepuluh menit masih menyisakan sembilan menit untuk mengetikkannya. Batas waktu sepuluh menit tidak menyisakan apa pun.
- Jangan pernah menyimpan sebuah kode untuk pengujian lain. Kode bersifat sekali pakai sekaligus berumur pendek; sebuah fixture bersama yang membagikannya adalah sebuah perlombaan antara dua pengujian untuk memperebutkan satu angka.
Extractor siap pakai
Tiga versi dari empat baris yang sama: kedua bagian digabung, tag menjadi spasi, entitas di-decode, whitespace dinormalkan, lalu polanya yang sudah dipatok. Ubah polanya agar cocok dengan kata-kata template Anda dan tidak ada lagi yang perlu disentuh.
Dari shell, dengan 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];
}JSON pesan yang dibaca ketiganya berasal dari GET /api/v1/message/{id}, didokumentasikan di referensi API; penantian yang memberi Anda id itu di awal ada di panduan pengujian dari ujung ke ujung, dan sebagai helper siap pakai untuk Playwright, Cypress, Python dan Node.js.
Sebelum Anda menganggapnya selesai
- Polanya dipatok pada kata-kata template Anda, dan disimpan di sebelah template itu.
- Kedua bagian dicari, sebagai teks: tag menjadi spasi, entitas di-decode, whitespace dinormalkan.
- Filter pada subjek atau pengirim, sehingga email sambutan tidak pernah menang atas email kode.
- Snapshot sebelum pengiriman ulang apa pun, dan hanya pesan baru yang diterima setelahnya.
- Batas waktu penantian jauh di bawah masa berlaku kodenya, dan pengirimannya dipicu tepat sebelum penantian.
- Pesan kegagalan yang menyebutkan id pesan dan subjek yang diperiksanya.
Itu mencakup semua cara sebuah extractor enam digit pernah tercatat lolos dengan angka yang salah. Seorang agen yang membaca email yang sama punya masalah yang sama dan satu alat lebih sedikit untuk mengatasinya, itulah sebabnya server MCP menyerahkan pesan lengkapnya, bukan sebuah tebakan — kotak masuk yang bisa dibaca agen AI membahas itu.
Pertanyaan
Sebaiknya saya membaca bagian teks atau bagian HTML?
Bagian teks kalau memang ada: itu stabil dan tidak ada yang perlu di-decode. Tetap cari di keduanya, seperti yang dilakukan helper-helpernya, supaya template yang hanya mengirim HTML tetap berfungsi dan template yang hanya mengirim teks tidak pernah tersandung HTML kosong.
Kode saya mengandung huruf. Apakah polanya berubah?
Hanya kelas karakternya: ([A-Z0-9]{6}), atau alfabet apa pun yang dipakai pengirimnya, tetap dipatok pada kata-kata sebelumnya. Tambahkan flag i kalau huruf besar-kecilnya tidak terjamin, dan hati-hati agar kelasnya tidak juga cocok dengan sebuah kata bahasa Inggris yang mengikuti patokannya.
Bagaimana dengan magic link, bukan kode?
Disiplin yang sama, pola yang berbeda: cocokkan URL-nya pada fragmen path yang Anda kenal — /confirm/, /auth/magic/ — bukan pada “tautan pertama”, karena sebuah email transaksional biasanya membawa lima tautan dan yang Anda inginkan jarang menjadi yang pertama. Decode & sebelum mengunjunginya.
Berapa lama sebuah pesan tersedia untuk dibaca?
5 hari setelah pesan itu tiba, dibaca ataupun tidak. Itu jauh lebih lama daripada masa berlaku kode mana pun, jadi sebuah pengujian tidak pernah perlu terburu-buru membacanya — hanya perlu terburu-buru mengetikkannya.
Bisakah saya mendapatkan kodenya tanpa melakukan polling ke kotak surat?
Lewat REST, tidak bisa: Anda melakukan polling sekali per detik dengan sebuah batas waktu, dan itulah ritme yang didokumentasikan dan tidak pernah dibatasi di luar itu. Lewat MCP ada tool wait_for_message yang menjaga panggilannya tetap terbuka sampai pesannya tiba, dan itulah bentuk yang dibutuhkan seorang agen AI.
Apakah saya butuh kunci API?
Tidak. Domain publiknya tidak butuh kunci, tidak butuh akun, dan tidak butuh header apa pun. Hanya kumpulan domain berbayar yang dijaga agar tidak masuk daftar blokir email sekali pakai yang memakai bearer token, dan kode ekstraksinya identik dalam kedua kasus.


