API, JavaScript'in gördüğü şekliyle
Sunucu tarafında kurulacak hiçbir şey ve karşı kimlik doğrulaması yapılacak hiçbir şey yoktur: genel bir alan adındaki bir posta kutusu, adresini bilen herkes tarafından, düz HTTPS üzerinden, JSON olarak okunabilir. Yüzeyin tamamı üç çağrıdır:
GET /api/v1/mailbox?address=…- Bir adreste bekleyen her şey, en yeniden başlayarak, bir özet listesi olarak. Boş bir posta kutusu,
count: 0ile200'dür — asla 404 değil.limit, tek bir yanıtı sınırlar (1-200, varsayılan 50) vebeforebunun ötesinde sayfalar. GET /api/v1/message/{id}?mailbox=…- Bir mesajın tamamı: gönderen, alıcı, konu, tarih, düz metin parçası, HTML parçası (ya da
null) ve her birinin hazır bir URL'si olan bir ek listesi. DELETE /api/v1/message/{id}?mailbox=…- Onu 5 gün içinde değil, şimdi kaldırır. İdempotenttir: iki kez silmek yine de
200yanıtı verir.
Aşağıdaki moduldeki tipler, tam olarak yanıt biçimleridir. Listeleme ayrıca bir alias taşır: aynı posta kutusuna teslimat yapan ama onu okumak için kullanılamayan, ayrı bir alan adındaki ikinci bir adres — gelen kutusunu açamamasını tercih ettiğinizde bir siteye vereceğiniz adres.
Modül
Tek bir dosya, tek bir sınıf, bağımlılık yok. Node'un sürüm 18'den beri birlikte getirdiği fetch ve crypto globallerinde çalışır, bu yüzden package.json'a eklenecek hiçbir şey yoktur. Bilinçli olarak sıradandır: bir son tarih döngüsü ve şimdiye kadar doğru olan tek yeniden deneme, bir 429'u uyuyarak geçirmek.
// grabmail.ts — a disposable inbox from Node 18+, Deno or Bun. No dependency, no key.
const API = 'https://grabmail.io/api/v1';
const DOMAIN = 'grabmail.io';
export type Summary = {
id: string; from: string; subject: string; date: string;
seen: boolean; attachments: number; expires_at: string;
};
export type Attachment = { filename: string; mime: string; size: number; url: string };
export type Message = {
id: string; from: string; to: string; subject: string; date: string;
text: string | null; html: string | null; attachments: Attachment[];
};
type Listing = { address: string; alias: string | null; count: number; next: string | null; messages: Summary[] };
type WaitOpts = { timeoutMs?: number; subjectContains?: string; fromContains?: string };
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
/** A mailbox nothing else is using. Nothing has to be created first. */
export function freshAddress(prefix = 'node'): string {
return `${prefix}-${crypto.randomUUID().slice(0, 8)}@${DOMAIN}`;
}
/** One GET, with the only retry that is ever right: waiting out a 429. */
async function get(url: string): Promise<Response> {
for (;;) {
const res = await fetch(url);
if (res.status === 429) {
await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000);
continue;
}
if (!res.ok) throw new Error(`${url} answered ${res.status}`);
return res;
}
}
export class Inbox {
constructor(readonly address: string = freshAddress()) {}
async list(limit = 50, before?: string): Promise<Listing> {
const q = new URLSearchParams({ address: this.address, limit: String(limit) });
if (before) q.set('before', before);
return (await get(`${API}/mailbox?${q}`)).json();
}
/** Block until a matching message arrives, then return it in full. */
async waitFor(opts: WaitOpts = {}): Promise<Message> {
const deadline = Date.now() + (opts.timeoutMs ?? 60_000);
while (Date.now() < deadline) {
const { messages } = await this.list();
const hit = messages.find(m =>
(!opts.subjectContains || m.subject.toLowerCase().includes(opts.subjectContains.toLowerCase())) &&
(!opts.fromContains || m.from.toLowerCase().includes(opts.fromContains.toLowerCase())));
if (hit) return this.read(hit.id);
await sleep(1000); // one read a second, never throttled
}
throw new Error(`no message for ${this.address} within the deadline`);
}
async read(id: string): Promise<Message> {
return (await get(`${API}/message/${id}?mailbox=${encodeURIComponent(this.address)}`)).json();
}
/** Optional and idempotent: everything expires on its own. */
async delete(id: string): Promise<void> {
await fetch(`${API}/message/${id}?mailbox=${encodeURIComponent(this.address)}`, { method: 'DELETE' });
}
/** The bytes of one attachment. Its URL already carries ?mailbox=. */
async download(a: Attachment): Promise<Response> {
return get(`https://grabmail.io${a.url}`);
}
}Kullanımı dört satırdır. Adresi yazdırın, bir adres istenen her yerde kullanın ve bekleyin:
import { Inbox } from './grabmail';
const inbox = new Inbox();
console.log('sign up with:', inbox.address);
const message = await inbox.waitFor({ subjectContains: 'code' });
console.log(message.subject);
console.log(message.text); // the plain-text part; message.html is the HTML part or nullDüz JavaScript, Deno ve Bun
Yukarıdaki TypeScript referanstır; sonraki bir bölümdeki ek akıtma dışında içinde Node'a özgü hiçbir şey yoktur. Çalıştığı diğer yerler için üç not:
- Düz JavaScript
- Tipleri çıkarın, aynı dosya olur. Aşağıdaki kısa sürüm, bir betiğin genellikle ihtiyaç duyduğu her şeydir — bir adres ve bir bekleme.
- Deno
- Olduğu gibi çalışır:
fetchvecrypto.randomUUID()globaldir ve betiğin yalnızca--allow-net=grabmail.io'ya ihtiyacı vardır. Bir ekiDeno.writeFile(path, new Uint8Array(await res.arrayBuffer()))ile kaydedin. - Bun
- TypeScript dahil, olduğu gibi çalışır. Bir eki,
Response'u doğrudan alanBun.write(path, res)ile kaydedin.
// grabmail.mjs — plain JavaScript, Node 18+: the same class without the types.
const API = 'https://grabmail.io/api/v1';
const sleep = ms => new Promise(r => setTimeout(r, ms));
export const freshAddress = (prefix = 'node') => `${prefix}-${crypto.randomUUID().slice(0, 8)}@grabmail.io`;
export async function waitFor(address, { timeoutMs = 60_000, subjectContains } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(`${API}/mailbox?address=${encodeURIComponent(address)}`);
if (res.status === 429) { await sleep(Number(res.headers.get('retry-after') ?? 1) * 1000); continue; }
if (!res.ok) throw new Error(`GET /mailbox answered ${res.status}`);
const { messages } = await res.json();
const hit = messages.find(m => !subjectContains || m.subject.toLowerCase().includes(subjectContains.toLowerCase()));
if (hit) return (await fetch(`${API}/message/${hit.id}?mailbox=${encodeURIComponent(address)}`)).json();
await sleep(1000);
}
throw new Error(`no message for ${address} within ${timeoutMs} ms`);
}Yoğun bir posta kutusu: before ile sayfalama
Bir listeleme en fazla 200 özet döndürür. Bundan fazlasını alan bir posta kutusu — örneğin kendi alan adınızdaki, bir günlük geri dönen postaları toplayan bir catch-all adresi — sayfa sayfa okunur: bir yanıtın next değerini bir sonraki isteğin before parametresi olarak geçirin ve next, null olduğunda durun. Bir async generator bunu bir for await haline getirir:
/** Every summary in the mailbox, newest first, however many pages it takes. */
export async function* allMessages(inbox: Inbox): AsyncGenerator<Summary> {
let before: string | undefined;
for (;;) {
const page = await inbox.list(200, before);
yield* page.messages;
if (!page.next) return;
before = page.next;
}
}
for await (const m of allMessages(inbox)) {
console.log(m.date, m.from, m.subject, 'expires', m.expires_at);
}İmleç, zaten sahip olduğunuz en eski mesajın kimliğidir, bu yüzden üstte yeni posta gelirken bile bir sayfa kararlı kalır. Bir gelen kutusunu bir betikten otomatikleştirme, imleci zamanlama ve saklama süresiyle birlikte daha ayrıntılı ele alır.
Diske akıtılan ekler
Her mesaj, eklerini bir dosya adı, beyan edilmiş bir tür, bayt cinsinden bir boyut ve bir URL ile listeler. URL zaten ?mailbox= parametresini taşır, bu yüzden olduğu gibi alınır. Yanıt, göndereninin dosyayı ne olarak etiketlediğinden bağımsız olarak her zaman bir Content-Disposition: attachment başlığıyla application/octet-stream'dır — gerçek tür, JSON'daki mime alanıdır. Arabelleğe almak yerine akıtın; üst sınır mesaj başına 5 MB boyutundadır ve yüzünü kaydeden bir betik hepsini bellekte tutmamalıdır.
import { createWriteStream } from 'node:fs';
import { mkdir } from 'node:fs/promises';
import { Readable } from 'node:stream';
import { pipeline } from 'node:stream/promises';
const message = await inbox.waitFor({ subjectContains: 'invoice' });
await mkdir(`downloads/${message.id}`, { recursive: true });
for (const a of message.attachments) {
console.log(a.filename, a.mime, a.size, 'bytes');
const res = await inbox.download(a);
await pipeline(Readable.fromWeb(res.body as any), createWriteStream(`downloads/${message.id}/${a.filename}`));
}Bir Vitest ya da Jest testinde
Test gövdesinin içindeki yeni bir Inbox, her teste kendi posta kutusunu verir; bu, bir posta testinin tek en önemli özelliğidir: hiçbir çalıştırma önceki bir çalıştırmanın mesajını asla okuyamaz ve paralel worker'lar birbirininkini hiçbir zaman okuyamaz. Ayıklama deseni, otomatik testlerde OTP kodları rehberinin ayrıntılı olarak açıkladığı nedenlerle, "altı rakam" yerine şablonun ifadesine sabitlenmiştir.
import { describe, it, expect } from 'vitest';
import { Inbox } from '../src/grabmail';
import { app } from '../src/app'; // whatever starts your server in-process
const CODE = /code is\D{0,12}(\d{6})/i; // anchored on YOUR template's wording
describe('sign-up', () => {
it('emails a code that confirms the account', async () => {
const inbox = new Inbox(); // a brand-new mailbox for this test only
await app.request('/signup', { method: 'POST', body: JSON.stringify({ email: inbox.address, password: 'hunter2hunter2' }) });
const message = await inbox.waitFor({ subjectContains: 'confirm' });
const code = `${message.text ?? ''} ${message.html ?? ''}`.match(CODE)?.[1];
expect(code).toBeDefined();
const res = await app.request('/confirm', { method: 'POST', body: JSON.stringify({ email: inbox.address, code }) });
expect(res.status).toBe(200);
}, 120_000); // above the 60 s mail deadline
});it'in üçüncü argümanı, altmış saniyelik posta son tarihinin üzerinde ayarlanmış test zaman aşımıdır; varsayılan beş saniye, posta düşmeden önce her testi bitirirdi. Aynı testin tarayıcı tarafından yürütülen bir sürümü için, Playwright rehberi bu sınıfı bir fixture içine sarar; her ikisini de bir CI runner'ında çalıştırmak bir giden trafik kuralı ve bir job zaman aşımı ekler; ikisi de GitHub Actions rehberinde.
Gözetimsiz ilk çalıştığında ortaya çıkan hatalar
Bunların hiçbiri bir dizüstü bilgisayarda bozulmaz. Hepsi bir salı gecesi zamanlanmış bir job'da bozulur.
| Belirti | Neden | Çözüm |
|---|---|---|
| Gönderen bozuk olsa bile her seferinde geçiyor | Her çalıştırmada aynı adres; ilk yoklama önceki çalıştırmanın mesajını buluyor. | Çalıştırma başına freshAddress(). Önemli olan budur. |
Günlükte 429, ardından bir çökme | Uyumayan bir döngü ya da tek bir adresi yoklayan iki betik. | Adres başına saniyede bir okuma; Retry-After'ı uyuyarak geçirin; betik başına bir adres. |
| Yavaş bir günde zaman aşımına uğruyor, yeniden denemede geçiyor | Bir son tarih yerine bir deneme sayısı, ya da göndereninin kuyruğundan daha kısa bir son tarih. | Bir Date.now() son tarihi, işlemsel bir posta için altmış saniye. |
/mailbox'tan 404 | Alan adı burada barındırılmıyor — bir yazım hatası ya da MX'i eksik kendi alan adınız. | Adresi kontrol edin; kendi alan adınız için, MX'in smtp.grabmail.io'i gösterdiğini kontrol edin. |
| Yanlış mesajı okuyor | Akış iki mesaj gönderdiğinde en yenisini aldı. | subjectContains ya da fromContains ile filtreleyin. |
Bir hafta çalışıyor, sonra bir mesajda 404 | 5 günden eski, saklanmış bir mesaj kimliği. | Hiçbir şey 5 günden fazla hayatta kalmaz. Önbelleğe almak yerine yeniden alın. |
Bitti demeden önce
- Çalıştırma başına, test başına ya da ajan başına taze bir adres — asla bir sabit değil.
- Gerçek zamanlı bir son tarih; saniyede bir okuma; uyuyarak geçirilen, asla fırlatılmayan
429. - Bir akış birden fazla mesaj gönderdiğinde konu ya da gönderen üzerinde bir filtre.
- Kendi ifadenize sabitlenmiş bir desenle önce ayrıştırılan metin parçası.
- Akıtılan, güvenilmeyen olarak ele alınan, mesaj kimliği altında kaydedilen ekler.
- Günler boyunca önbelleğe alınan bir mesaj kimliği yok; burada hiçbir şey 5 günden uzun yaşamaz.
İstemcinin tamamı bu kadar. Python'daki, requests ve httpx için aynı modül Python rehberinde; her durum koduyla birlikte istek ve yanıt biçimleri API referansında, ve istemciyi yazmak yerine üretmeyi tercih edecekler için bir OpenAPI 3.1 belgesi var.
Sorular
Bir API anahtarına ya da bir npm paketine ihtiyacım var mı?
İkisine de değil. Genel alan adları anahtar, hesap ya da başlık gerektirmez ve modül yalnızca Node 18 ve sonrasıyla gelen fetch'i kullanır. Yalnızca tek kullanımlık posta kara listelerinin dışında tutulan ücretli alan adı havuzu bir Authorization: Bearer başlığı kullanır ve bunun dışında kod onun için de aynıdır.
Tarayıcıda çalışır mı?
Aynı çağrılar bir sayfadan da çalışır, ama bir tarayıcı altmış saniyelik bir yoklama döngüsü için yanlış yerdir ve posta kutusu zaten geneldir — bunu sunucudan ya da test çalıştırıcısından okuyun. Bir web uygulamasını test ediyorsanız, Playwright rehberi yoklamayı ait olduğu yerde, test sürecinde tutar.
Bir süreç aynı anda kaç posta kutusunu yoklayabilir?
Rahatlıkla yirmi: adres başına sınır saniyede bir okumadır ve istemci başına üst sınır dakikada 1200 istektir, bu da saniyede bir kez yoklanan yirmi adres demektir. Yirmi waitFor çağrısı üzerinde bir Promise.all bunun içinde kalır; ötesinde, 429 dalı başarısız olmak yerine uyur.
Node'dan kendi alan adımı kullanabilir miyim?
Evet, DOMAIN sabiti dışında hiçbir değişiklik yapmadan. smtp.grabmail.io'i gösteren tek bir MX kaydı ve alan adı üzerindeki her adres, aynı modülün okuduğu bir posta kutusu haline gelir — kurulum burada. Uygulamanız genel tek kullanımlık alan adlarını reddettiğinde bu doğru yanıttır.
Betiğim onu kullanırken posta kutusu özel midir?
Hayır. Genel bir alan adında da kendi alan adınızda da, adresi bilen herkes onu okuyabilir. Birkaç saniye boyunca tek bir onay kodu tutan rastgele bir adres sorun değildir; gerçek müşteri postasını birine yönlendiren bir betik ise sorundur.
Bir betik yerine bir yapay zekâ ajanı için bir şey var mı?
Aynı origin'de, anahtarsız bir MCP sunucusu vardır; wait_for_message aracı, posta düşene kadar çağrıyı açık tutar — her yoklamanın ona token'a mal olduğu bir ajanın ihtiyaç duyduğu şekil budur. Bir yapay zekâ ajanının okuyabileceği bir gelen kutusu bunu ele alır.


