API & otomatisasi

Menunggu email lewat kode: polling tanpa webhook

Webhook memberi tahu Anda begitu surat tiba. Tanpa itu, Anda harus bertanya sendiri — dan putaran yang bertanya itulah tempat test end-to-end jadi flaky, agen macet, dan script menabrak rate limit. Berikut yang harus dilakukan putaran itu dengan benar: sebuah tenggat dan bukan sebuah hitungan, sebuah interval yang melebar, sebuah aturan untuk memutuskan pesan mana yang milik Anda, dan satu-satunya tempat penantian bisa dikerjakan untuk Anda.

  • Menengah
  • 29 menit baca
Sebuah amplop biru melayang di samping sebuah stopwatch abu-abu dengan sebuah tanda panah melingkar biru yang mengitarinya

Push dan pull, serta biaya masing-masing

Cuma ada dua cara bagi kode Anda untuk tahu sebuah pesan sudah tiba. Entah pihak lain memberi tahu Anda, atau Anda bertanya. Semua yang lain — sebuah client library dengan waitFor di dalamnya, sebuah SDK yang “men-stream” sebuah kotak masuk, sebuah test helper yang blocking — adalah salah satu dari dua cara itu dengan mesinnya disembunyikan, dan ada gunanya tahu Anda sedang memegang yang mana sebelum harus men-debug-nya.

Empat susunan ini mencakup nyaris semua yang tersedia.

Webhook
Layanan ini membuat sebuah permintaan HTTP ke sebuah alamat milik Anda, setiap kali surat tiba. Ini penantian paling murah yang bisa ada — Anda tidak melakukan apa-apa sampai memang ada yang harus dilakukan — dan harganya adalah sebuah alamat di internet publik, sebuah listener yang aktif tepat pada saat suratnya tiba, sebuah rahasia bersama untuk membuktikan permintaan itu memang berasal dari mereka, dan jawaban Anda sendiri untuk apa yang terjadi kalau listener Anda sedang tidak ada di tempat.
Polling panjang
Anda membuat permintaannya dan server membiarkannya tetap terbuka sampai suratnya tiba atau timeout-nya habis. Ia tidak butuh apa pun dari Anda selain sebuah koneksi keluar, dan itu membebani server dengan satu worker per penunggu — itulah sebabnya setiap layanan yang menawarkannya membatasi baik lama penantiannya maupun jumlah penunggu yang bersamaan.
Polling biasa
Anda bertanya, berulang-ulang, dan setiap permintaan langsung dijawab dengan apa pun yang ada saat itu. Ini satu-satunya susunan yang berfungsi dari sebuah laptop di belakang router, dari sebuah runner CI tanpa rute masuk, dan dari sebuah agen yang berjalan di dalam sandbox milik orang lain — dan inilah keseluruhan topik panduan ini.
Protokol kotak surat
IMAP punya IDLE, yang sebetulnya sebuah polling panjang dengan topeng berbeda: koneksinya tetap terbuka dan server mengumumkan surat baru lewat situ. Ini benar-benar mirip push, dan ia butuh sebuah kotak surat dengan kredensial, sebuah client yang bisa menahan sebuah socket tetap terbuka dan menyambung ulang saat socket itu putus, serta sebuah server yang menghormati perintahnya — mesin yang sangat banyak untuk sebuah job yang cuma butuh satu pesan.
Layanan suratmembuat permintaanListener AndaURL publik, rahasia, aktifmemanggil alamat AndaKode Andamembuat permintaanKotak suratmenjawab apa pun yang adatanya sekali per detikSebuah webhook butuh sebuah alamat di internet publik. Sebuah polling cuma butuh sebuah putaran, dan tidak ada lagi yang lain.
Dua bentuk itu, dan hal yang sebenarnya menentukan pilihan di antara keduanya. Yang satu butuh sebuah alamat di internet publik; yang lain tidak butuh apa-apa selain kemampuan membuat permintaan keluar, satu-satunya dari kedua hal itu yang selalu dimiliki sebuah test runner.

Kalau ditaruh berdampingan, pilihannya ternyata lebih soal apa yang dituntut masing-masing dari mesin tempat kode Anda berjalan, daripada soal keelokan.

Apa yang dibutuhkannya dari AndaWebhookPolling saja
Sebuah alamat tempat kode Anda bisa dihubungiYa: sebuah URL publik dengan sertifikat, yang bisa dirutekan dari internet.Tidak. Satu permintaan keluar itulah seluruh persyaratannya.
Sebuah rahasia untuk disimpan dan dirotasiYa: sebuah kunci tanda tangan, atau orang asing bisa mengirimkan sebuah pesan palsu kepada Anda.Tidak. Tidak ada apa pun untuk diverifikasi, karena tidak pernah ada yang datang tanpa diminta.
Sesuatu yang berjalan tepat saat surat tibaYa — dan kalau sedang mati, apakah Anda tetap menerima pesannya sama sekali bergantung pada kebijakan retry si pengirim, bukan pada keputusan Anda.Tidak. Tidak ada yang terlewat selagi Anda tidak melihat: kotak suratnya tetap menyimpannya selama 5 hari, apa pun yang terjadi.
Permintaan yang dibuat saat tidak ada suratTidak ada sama sekali. Itulah seluruh daya tariknya.Satu setiap interval — inilah biaya sungguhannya, dan inilah yang jadi bahasan sisa panduan ini.

Putaran yang ditulis semua orang lebih dulu

Panjangnya empat baris, ia berfungsi pada hari ia ditulis, dan setiap masalahnya muncul belakangan dan di tempat lain: di sebuah pipeline pukul tiga pagi, di sebuah agen yang sudah “berpikir” selama sebelas menit, di sebuah kotak surat yang menjawab 429 ke seorang kolega karena putaran Anda sedang menahan kuotanya.

putaran untuk memulai
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)

Ada lima hal yang salah darinya, dan cuma yang pertama yang jelas terlihat.

Ia tidak pernah menyerah
Tidak ada tenggat, jadi saat pesannya memang benar-benar tidak akan datang — formulirnya menolak alamatnya, antrean si pengirim tersendat, seseorang salah mengetik domainnya — putaran ini tidak gagal. Ia macet. Sebuah job yang macet lebih buruk daripada yang gagal, karena lognya berakhir tanpa pernah menyebutkan alasannya.
Ia menghitung percobaan dan menyebutnya detik
Bahkan dengan batas jumlah putaran, tiga puluh kali coba dengan jeda “satu detik” tidak pernah benar-benar tiga puluh detik: setiap putaran juga memakan waktu satu permintaan, dan sebuah permintaan yang butuh 400 ms mengubah tiga puluh detik Anda menjadi empat puluh dua. Tambahkan satu retry saja dan hitungannya berhenti jadi sekadar hitungan.
Setiap runner bertanya pada tick yang sama
Jalankan dua puluh job dari pipeline yang sama dan mereka akan melakukan polling secara serempak, karena semuanya mulai dalam selisih beberapa milidetik satu sama lain dan semuanya sleep tepat satu detik penuh yang sama. Puncaknya dua puluh kali lipat rata-ratanya, dan puncak itulah yang ditolak.
Ia mengambil pesan terbaru, bukan milik Anda
Entri pertama dalam daftarnya adalah apa pun yang berada di puncak kotak surat itu, yang pada sebuah alamat publik bisa jadi surat milik orang lain dan pada sebuah alamat yang dipakai ulang adalah surat minggu lalu. Sebuah putaran yang keluar begitu melihat pesan pertama akan dengan senang hati keluar sebelum pesan yang sebenarnya ditunggu itu tiba.
Ia memperlakukan setiap jawaban sebagai keberhasilan
Membaca daftar pesan dari sebuah 429 atau 404 memunculkan error tiga frame jauhnya dari apa pun yang menjelaskannya, dan membacanya dari sebuah 500 mungkin tidak memunculkan apa pun sama sekali. Kode statusnya adalah hal pertama yang harus dilihat, bukan yang terakhir.

Berhenti karena jam, bukan karena hitungan

Ambil tenggatnya sekali saja, sebelum permintaan pertama, dari sebuah jam monotonik — yang tidak bisa melompat mundur saat mesinnya mengoreksi waktunya — dan bandingkan dengannya di awal setiap putaran. Semua yang lain dalam putarannya lalu bebas berubah tanpa mengubah berapa lama penantiannya berlangsung: Anda bisa melebarkan interval, mengulang sebuah penolakan, atau menambahkan filter kedua, dan sembilan puluh detik tetap sembilan puluh detik.

Seberapa lama itu cukup lama adalah pertanyaan soal si pengirim, bukan soal Anda. Surat yang dibuat sebuah mesin sebagai jawaban atas sebuah formulir biasanya terkirim dalam hitungan detik satu digit; sebuah antrean dengan backlog, sebuah penerima yang melakukan greylisting, atau sebuah batch per jam adalah orde besaran yang sama sekali berbeda, dan interval apa pun yang Anda pilih tidak akan membuatnya tiba lebih cepat.

Apa yang Anda tungguTenggat yang jujurYang harus dilakukan begitu tenggatnya lewat
Sebuah email pendaftaran atau verifikasi, di dalam sebuah test60 sampai 120 detikGagalkan testnya dan cetak alamatnya. Sembilan dari sepuluh kali kotak suratnya kosong karena formulirnya menolak alamat itu, dan alamat itulah hal pertama yang perlu dilihat siapa pun yang membaca lognya.
Sebuah reset kata sandi yang baru saja diminta seseorang30 sampai 60 detikBeri tahu mereka belum tiba dan tawarkan untuk mengirimnya lagi. Jangan biarkan mereka terus menatap layar yang diam saja: mereka toh akan minta yang kedua, dan sekarang ada dua kode.
Seorang agen yang menyelesaikan sebuah pendaftaran sendirianDua atau tiga penantian sisi server, jadi 50 sampai 75 detikKatakan itu dalam jawabannya. “Tidak ada email konfirmasi setelah satu menit” adalah sebuah hasil yang bisa ditindaklanjuti agennya; sebuah panggilan tool yang tidak pernah kembali bukan.
Sebuah newsletter, sebuah kuitansi, apa pun yang dikirim secara batchHitungan menit — atau jangan menunggu sama sekaliLakukan polling terjadwal saja dan biarkan prosesnya berakhir. Sesuatu yang duduk di sebuah socket selama sepuluh menit adalah sesuatu yang akan dimatikan oleh sebuah proxy, sebuah runner, atau sebuah batas container.

Tenggat itu juga tempat yang jujur untuk menaruh pesan error Anda. “Tidak ada yang cocok dengan ‘Confirm your email’ tiba di signup-42@grabmail.io dalam 90 s” menyebutkan alamatnya, filternya, dan tenggatnya, yaitu tiga dari empat hal yang dibutuhkan untuk mencari tahu apa yang terjadi. Yang keempat — apa yang memang tiba — juga layak dicetak: sebuah daftar subjek yang dilihat dan ditolak putarannya mengubah “ini cuma flaky” menjadi “subjeknya berubah” dalam sekali baca.

Seberapa sering bertanya, dan kapan melebarkannya

Batas bawahnya adalah apa pun yang diizinkan layanannya, dan di sini itu satu permintaan per detik, per alamat. Itu bukan sebuah larangan halus — polling sekali per detik memang pola yang dimaksudkan, tidak ada kuota harian, tidak ada kuota bulanan, dan tidak ada kredit burst untuk diurus — tapi itu tetap sebuah batas bawah, dan sebuah putaran yang bertanya dua kali dalam detik yang sama akan mendapat 429 untuk yang kedua, bukan jawaban yang lebih cepat.

Interval tetap
Satu detik, setiap kali, sampai tenggatnya. Sangat cukup untuk sebuah penantian yang akan berakhir dalam sepuluh detik, dan default yang tepat untuk satu test di satu runner. Satu-satunya kekurangannya, ia terus bertanya dengan laju yang sama jauh setelah jelas-jelas terlihat suratnya tidak akan datang.
Interval yang melebar
Satu detik selagi pesannya mungkin masih dalam perjalanan, lalu berlipat dua — dua, empat, delapan — dengan sebuah batas atas. Ini memakan sedikit latensi pada sebuah pesan yang tiba terlambat dan menghemat sebagian besar permintaan pada sebuah penantian yang toh akan gagal juga. Beri batas atas: sebuah interval yang berlipat dua tanpa batas atas akan menghabiskan separuh belakang dari tenggat dua menit itu dengan tertidur.
Jitter, ditambahkan dan tidak pernah dikurangkan
Renggangkan jarak antar putaran dengan sebuah pecahan acak supaya dua puluh runner berhenti bertanya pada tick yang sama. Resep yang biasa dipakai — sebuah nilai acak antara nol dan interval — salah di sini, karena separuh rentangnya jatuh di bawah batas bawah satu detik. Sebaliknya, tambahkan keacakannya di atas: intervalnya adalah sebuah minimum, dan jitter cuma pernah membuat sebuah putaran datang lebih lambat.
Sebuah jeda yang bukan Anda yang memilih
Saat jawabannya sebuah 429, intervalnya adalah apa pun yang dikatakan Retry-After, dan putaran yang ditolak itu bukan sebuah percobaan. Hitunglah itu sebagai satu percobaan dan sebuah putaran yang sedang dibatasi lajunya akan menghabiskan seluruh tenggatnya mengumpulkan penolakan tanpa pernah sekali pun membaca kotak suratnya.

Lima belas detik dengan interval satu detik, lalu berlipat dua sampai batas atas delapan, dengan jitter di atasnya, memuat nyaris setiap penantian dalam panduan ini ke dalam enam baris:

interval itu sendiri
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)

Eksponennya digeser supaya pelebarannya mulai setelah rentang tetap itu, bukan dari putaran pertama. Tanpa penggeseran itu, intervalnya sudah mencapai delapan detik pada saat sebuah email pendaftaran yang lambat tiba, dan sebuah penantian yang seharusnya cuma butuh dua belas detik jadi butuh dua puluh detik.

Semua ini tidak berlaku untuk permintaan pertama. Bertanyalah segera, sebelum sleep apa pun: sebuah pesan yang sudah ada di kotak surat saat putarannya mulai — kasus normal untuk apa pun yang dipicu sebelum penantiannya dimulai — seharusnya tidak memakan waktu satu detik pun untuk disadari.

Membaca sebuah penolakan

Setiap jawaban dari endpoint daftar itu berupa JSON, dan yang bukan sebuah kotak surat berbagi satu bentuk yang sama: sebuah slug error, yang stabil dan itulah yang jadi dasar percabangan, dan sebuah message, yang berupa prosa dan bisa diubah kata-katanya kapan saja. Sebuah penolakan karena terlalu cepat juga membawa sebuah header:

rupa sebuah penolakan
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 dinyatakan dalam detik penuh, dan itu angka yang sungguhan — diambil dari berapa lama kuota alamat ini sebenarnya masih tersisa, bukan dari sebuah konstanta di dokumentasi. Sleep tepat sebesar itu adalah hal paling santun dan paling cepat yang bisa dilakukan: sleep yang lebih singkat akan ditolak lagi, yang lebih panjang cuma membuang-buang waktu. Berikut semua yang bisa ditemui sebuah putaran polling, dan apa yang sebenarnya diminta setiap jawabannya.

Apa yang kembaliArtinya apaYang harus dilakukan putarannya
200 beserta count: 0Kotak suratnya ada dan kosong. Ini jawaban normal untuk sebagian besar durasi sebuah penantian.Tetap menunggu. Ini bukan sebuah error, dan tidak akan pernah jadi error.
429rate_limitedTerlalu cepat: sebuah permintaan daftar kedua dalam satu detik yang sama untuk alamat ini, atau lebih dari 1,200 permintaan dalam satu menit dari sumber ini.Sleep selama jumlah detik yang dikatakan Retry-After, lalu tanya lagi. Jangan hitung penolakan itu sebagai sebuah percobaan.
404unknown_domainBagian setelah @ itu tidak dihosting di sini. Nyaris selalu sebuah salah ketik, atau sebuah domain yang MX record-nya tidak pernah diarahkan ke sini.Berhenti. Menunggu berapa lama pun tidak akan memperbaiki sebuah domain. Cetak alamat yang Anda terima.
400invalid_addressParameter address-nya tidak ada, lebih panjang dari 320 karakter, atau tidak berbentuk name@domain.Berhenti. Ini kesalahan pada pemanggilnya, dan itu akan jadi kesalahan yang sama di setiap putaran.
400bad_cursorNilai before itu sama sekali tidak berbentuk seperti sebuah id pesan. Sebuah id yang bentuknya benar tapi sudah kedaluwarsa bukan error ini: itu menjawab 200 dengan sebuah halaman kosong.Berhenti melakukan paging dan mulai lagi dari halaman pertama.
404not_found, dari satu pesanId itu tidak ada di kotak surat itu — atau memang ada, dan sejak itu sudah kedaluwarsa atau terhapus.Anggap itu hilang, bukan terlambat. Sebuah id yang Anda lihat di sebuah daftar beberapa detik lalu tidak akan kembali.
500storage_failedAda sesuatu yang gagal di pihak kami saat membaca kotak suratnya.Tanya lagi, tapi biarkan tenggatnya yang mengatur dan jangan bertanya lebih cepat dari biasanya.

Dua dari tujuh itu berarti berhenti, dan itulah dua yang layak diributkan. Sebuah putaran yang memperlakukan unknown_domain sebagai “belum saatnya” akan menghabiskan sembilan puluh detik penuh untuk membuktikan sesuatu yang sudah diberitahukan layanannya dalam empat puluh milidetik pertama.

Pesan mana yang milik Anda

Sebuah kotak surat bukan sebuah antrean, dan hal terbaru di dalamnya tidak selalu hal yang Anda tunggu. Pada sebuah domain publik, siapa pun yang bisa menebak alamatnya bisa mengirim ke sana; dalam sebuah test suite, alamat yang sama sering dipakai ulang antar run; dan satu pendaftaran sering mengirim dua pesan — sebuah selamat datang dan sebuah konfirmasi — padahal cuma satu yang membawa kodenya. Solusinya adalah sebuah tanda air, dan itu harus diambil sebelum hal yang memicu suratnya.

  1. Sebelum mengirim formulirnya, daftar isi kotak surat itu dengan limit=1 dan simpan id pesan terbarunya, atau tidak ada apa-apa kalau kotaknya kosong. Id itulah tanda airnya.
  2. Lakukan hal itu — kirim formulirnya, panggil endpoint-nya, klik tombolnya.
  3. Polling daftarnya. Pesan kembali dengan urutan terbaru dulu, jadi telusuri dari atas ke bawah dan berhenti begitu Anda menemui tanda airnya: semua yang di bawahnya lebih tua daripada tindakan Anda dan bisa diabaikan tanpa perlu dibaca.
  4. Filter apa yang berada di atasnya berdasarkan pengirim, subjek, atau keduanya. Sebuah substring biasanya sudah cukup, dan itu sebaiknya bagian yang tidak akan dilokalkan — sebuah test yang mencocokkan “Confirm your email” akan gagal pada hari akun yang diuji dialihkan ke bahasa lain.
  5. Baru setelah itu, dan cuma setelah itu, buka pesannya. Daftarnya membawa sebuah preview pendek dan bukan bodinya, dan kode yang Anda cari sangat sering berada melewati akhir preview itu. Satu permintaan lagi mendapatkan keseluruhan pesannya, dan itu dibebankan ke kuota yang terpisah dan jauh lebih besar daripada kuota daftarnya.

Dalam sebuah shell, kedua pembacaan itu terlihat seperti ini — tanda airnya dulu, lalu pollingnya:

shell
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"

Kedua permintaan itu menyebutkan alamatnya secara lengkap, karena di sini alamat itu adalah kotak suratnya: tidak ada sesi, tidak ada cursor yang disimpan atas nama Anda, dan tidak ada apa pun dari satu panggilan yang diingat panggilan berikutnya. Itu juga sebabnya sebuah alamat aman untuk diawasi dari dua tempat sekaligus — membaca tidak menghabiskan apa pun, jadi dua putaran pada kotak surat yang sama akan sama-sama melihat setiap pesan dan tidak ada yang bisa mengambil satu pesan dari bawah hidung yang lain.

Bertindak persis sekali

Sebuah polling yang diulang bisa melihat pesan yang sama dua kali, dan itu bukan kejadian yang jarang: server menjawab, koneksinya putus sebelum bodinya sampai ke Anda, HTTP client Anda mencoba ulang, dan jawaban kedua berisi pesan yang sudah dibawa jawaban pertama. Kalau apa yang Anda lakukan dengan sebuah pesan adalah mengklik sebuah tautan, mengonfirmasi sebuah pembayaran, atau memposting ke sebuah channel, melakukannya dua kali adalah sebuah bug dengan konsekuensi di luar proses Anda.

Simpan id yang sudah Anda tangani
Sekumpulan id di memori sudah cukup untuk sebuah penantian yang lahir dan mati di dalam satu fungsi saja. Untuk apa pun yang harus bertahan lewat sebuah restart — sebuah kotak surat yang dikosongkan sebuah job terjadwal, seorang agen yang menyelesaikan sebuah backlog — itu harus dituliskan di suatu tempat yang ikut bertahan bersamanya.
Menghapus itu idempotent
Menghapus sebuah pesan menjawab 200 baik pada kali kedua maupun kali pertama, jadi sebuah penghapusan yang diulang tidak pernah terlihat seperti kegagalan dan tidak pernah butuh penanganan khusus. Hapus setelah Anda bertindak, bukan sebelumnya: sebuah crash di antara keduanya lalu cuma membebani Anda dengan satu pembacaan ulang, yang masih bisa diperbaiki, bukan kehilangan pesannya, yang tidak bisa.
Id di sini bukan Message-ID milik pengirim
Id dalam API itu milik kami: lingkupnya satu kotak surat, dan itu berhenti ada begitu pesannya kedaluwarsa. Header Message-ID adalah milik pengirim, ia ikut berjalan bersama pesannya, dan itulah yang Anda perlukan kalau mencocokkan surat yang sama lintas dua sistem — panduan tentang header menyebutkan di mana menemukannya.

Semua ini tidak dibutuhkan untuk sebuah test yang cuma menunggu satu kode lalu membuang kotak suratnya. Semua ini dibutuhkan begitu sebuah putaran berjalan tanpa diawasi, karena kegagalan yang dicegahnya tidak terlihat seperti kegagalan: itu terlihat seperti pekerjaan yang dikerjakan, dua kali, dengan benar.

Saat menunggu menjadi tugas server

Ada satu tempat di sini di mana Anda tidak perlu menulis putarannya sendiri, dan itu ada untuk pemanggil yang tidak sanggup menanggung biayanya. Seorang agen AI membayar setiap giliran yang dihabiskannya untuk memeriksa, jadi sebuah tool yang menjawab “belum ada” sembilan kali adalah sembilan giliran tanpa hasil. wait_for_message milik server MCP justru membiarkan permintaannya tetap terbuka, melakukan polling di sisi kami, dan menjawab sekali saja — entah dengan pesannya, atau dengan sebuah pernyataan sederhana bahwa ia sudah menunggu dan tidak ada yang datang.

satu panggilan, menunggu hingga 25 detik
$ 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}}}'

Ada empat hal tentangnya yang layak diketahui sebelum Anda membangun sesuatu di atasnya.

Ia menunggu paling lama 25 detik
timeout_seconds bisa minta yang lebih singkat dan tidak pernah bisa minta yang lebih lama. Batas atasnya bukan sembarangan: setiap penunggu adalah sebuah worker yang tidak melakukan apa pun selain sleep, dan sebuah permintaan yang dibiarkan terbuka selama beberapa menit adalah sebuah permintaan yang mati kena timeout proxy milik orang lain jauh sebelum ia sempat kembali.
Ia memfilter sejak permintaannya masuk
from_contains, subject_contains, dan since_id adalah tiga keputusan yang sama seperti pada bagian di atas, cuma dilakukan di server. since_id adalah tanda airnya, dan itu lebih penting di sini daripada di tempat lain mana pun: tanpanya, panggilannya langsung kembali dengan apa pun yang sudah ada di kotak surat itu.
Sebuah timeout adalah sebuah jawaban, bukan sebuah error
Kalau tidak ada yang datang, ia mengembalikan timed_out yang terisi, beserta berapa lama ia sebenarnya menunggu, dan mengatakan dengan jelas bahwa memanggil lagi adalah cara Anda terus menunggu. Dua atau tiga panggilan adalah penantian normal untuk sebuah email pendaftaran: itulah putarannya, dan itu tiga giliran, bukan sembilan puluh.
Ada 8 tempat menunggu, dan tidak ada antrean
Kalau semuanya sedang sibuk, panggilannya langsung kembali dan mengatakan itu, daripada ikut berdiri dalam antrean di belakang tujuh agen lain. Ini kegagalan yang tepat: seorang agen yang diberi tahu “terlalu banyak penantian yang berjalan” bisa langsung mendaftar isi kotak surat dan melanjutkan, sementara seorang agen yang duduk dalam antrean cuma bisa duduk saja.

Batas per alamat itu masih berlaku di dalamnya — putaran kami pun dibatasi lajunya persis seperti punya Anda, jadi sebuah penantian sisi server bukan cara untuk memutar balik batas bawahnya, cuma cara untuk berhenti membayarnya dengan giliran. Untuk sebuah test suite, semua ini tidak sepadan dengan repotnya: sebuah test memang sudah berupa proses yang boleh sleep, dan sebuah putaran dalam bahasa tempat test itu ditulis jauh lebih mudah di-debug daripada yang jarak jauh. Panduan MCP membahas sisa tool-toolnya.

Keseluruhan putarannya, sekali saja

Semua yang di atas, dalam satu file: sebuah tenggat dari sebuah jam monotonik, sebuah interval yang melebar dengan jitter satu sisi, Retry-After yang dihormati dan tidak dihitung sebagai percobaan, sebuah tanda air untuk memutuskan mana yang baru, sebuah filter pada subjeknya, dan satu permintaan ekstra untuk mengambil pesan yang cuma di-preview daftarnya.

penantian yang kukuh
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'}")

Ini sengaja dibuat sekitar lima puluh baris standard library dan satu HTTP client. Tidak ada yang perlu diinstal, tidak ada yang perlu dikonfigurasi, dan tidak ada rahasia di mana pun di dalamnya — itulah intinya: bentuk yang sama berpindah tanpa berubah ke Node, ke sebuah shell script, atau ke apa pun yang sudah dipakai test framework Anda untuk membuat permintaan.

  1. Baca tanda airnya sebelum tindakan yang memicu suratnya, jangan pernah sesudahnya.
  2. Tanya sekali segera, dan baru setelah itu sleep. Jangan pernah sleep dulu.
  3. Ambil tenggatnya dari sebuah jam monotonik, dan uji itu di awal setiap putaran.
  4. Jaga intervalnya tetap di angka satu detik per alamat atau lebih, dan tambahkan jitter cuma ke arah atas.
  5. Sleep tepat sebesar yang dikatakan Retry-After, dan jangan hitung sebuah penolakan sebagai sebuah percobaan.
  6. Bercabanglah berdasarkan slug error: unknown_domain dan invalid_address berarti berhenti, bukan menunggu.
  7. Cocokkan berdasarkan pengirim atau subjek, dan berhenti menyusuri daftarnya begitu Anda mencapai tanda airnya.
  8. Buka pesannya sebelum Anda mem-parsing-nya — daftarnya membawa sebuah preview, bukan bodinya.
  9. Gagalkan dengan menyertakan alamatnya, filternya, tenggatnya, dan subjek-subjek yang ditolak putarannya.

Sembilan aturan, dan delapan di antaranya ada karena sebuah kegagalan yang harus disusun ulang seseorang dari sebuah log. Yang bukan soal kegagalan adalah yang kedua: bertanya sekali sebelum sleep pertama itulah yang membuat sebuah penantian untuk pesan yang sebenarnya sudah tiba cuma butuh empat milidetik, bukan satu detik penuh — yang, di seluruh suite berisi dua ratus test, jadi tiga menit waktu sungguhan yang tidak perlu dijelaskan siapa pun sesudahnya.

Pertanyaan

Apakah GrabMail punya webhook?

Tidak, dan itu bukan sebuah kekurangan yang menunggu diisi. Layanan ini menerima surat dan menyediakannya lewat HTTP tanpa kunci apa pun: tidak ada akun di balik sebuah alamat publik untuk menempelkan sebuah callback, dan tidak ada antrean untuk menahan sebuah pengiriman yang ditolak endpoint Anda. Kalau alur kerja Anda benar-benar tidak bisa melakukan polling, halaman perbandingan menyebutkan layanan-layanan yang memang menawarkannya.

Seberapa sering saya boleh melakukan polling pada sebuah alamat?

Sekali per detik, per alamat — dan itu memang pola yang dimaksudkan, bukan batas ekstremnya. Tidak ada kuota harian, tidak ada kuota bulanan, dan tidak ada kredit burst untuk diurus. Dua puluh kotak surat yang di-polling sekali per detik dari satu runner adalah pemakaian biasa; satu-satunya batas lain adalah 1,200 permintaan per menit dari satu sumber, yang persis pas untuk kedua puluh kotak surat itu dan tidak menyisakan tempat untuk yang kedua puluh satu.

Kenapa putaran saya mengembalikan sebuah pesan dari run test sebelumnya?

Karena putaran itu mengambil entri pertama dalam daftarnya tanpa bertanya kapan pesan itu tiba. Sebuah kotak surat menyimpan apa pun yang dikirim ke sana selama 5 hari, dan sebuah alamat yang dipakai ulang penuh dengan sisa run sebelumnya. Baca id terbaru sebelum Anda memicu suratnya dan abaikan semua yang berada di bawah id itu — atau hapus isi kotak suratnya di awal test, yang cuma satu permintaan per pesan dan menghilangkan ambiguitasnya sama sekali.

Apakah kotak surat yang kosong itu sebuah 404?

Tidak. Sebuah kotak surat yang kosong adalah 200 dengan count: 0 dan sebuah daftar kosong, dengan sengaja, supaya sebuah putaran polling tidak pernah perlu menangani “belum ada apa-apa” sebagai kasus khusus. Sebuah 404 dari endpoint daftar berarti domainnya tidak dihosting di sini; sebuah 404 dari satu pesan berarti id itu tidak ada di kotak surat itu, atau sudah kedaluwarsa.

Berapa lama saya harus menunggu sebuah email verifikasi?

60 sampai 120 detik dalam sebuah test otomatis, 30 sampai 60 detik untuk seseorang yang menunggu di depan layar. Kebanyakan surat yang dibuat mesin tiba dalam hitungan detik satu digit; ekor panjangnya adalah soal antrean si pengirim, bukan soal pengirimannya. Kalau itu rutin mendekati tenggat Anda, tenggat yang lebih panjang bukan jawabannya — ada yang salah di tempat lain.

Bisakah dua proses melakukan polling pada alamat yang sama secara bersamaan?

Bisa. Membaca tidak menghabiskan apa pun, jadi keduanya melihat setiap pesan dan tidak ada yang menyembunyikan surat dari yang lain. Meski begitu, keduanya berbagi kuota satu-permintaan-per-detik untuk alamat itu, jadi dua putaran yang bertanya setiap detik masing-masing akan ditolak kira-kira separuh waktu: beri masing-masing dua detik, atau biarkan satu yang melakukan polling dan membagikan hasilnya ke yang lain.

Sebaiknya saya melakukan polling, atau memakai tool menunggu lewat MCP?

Lakukan polling, kalau Anda menulis sebuah test atau sebuah script: sebuah proses yang boleh sleep sebaiknya sleep, dan sebuah putaran dalam bahasa Anda sendiri lebih mudah di-debug daripada yang jarak jauh. Pakai wait_for_message kalau pemanggilnya membayar per giliran, bukan per detik, yang dalam praktiknya berarti seorang agen AI. Ia menunggu paling lama 25 detik per panggilan, memfilter berdasarkan pengirim dan subjek, dan mengembalikan sebuah timeout biasa yang bisa Anda panggil lagi begitu saja.

Coba selagi masih segar

Alamat hanya perlu satu klik, tanpa akun dan tanpa kartu. Semua yang ada di panduan ini langsung berfungsi dengannya.

Selamat datang kembali

Kotak surat dan domain Anda, di satu tempat.