Panduan30 Juli 20267 mnt baca

Ubah Email Apa Pun Menjadi Webhook dengan Cloudflare Email Routing

Alamat catch-all, handler email di Worker, dan sekitar enam puluh baris: setiap email ke domain Anda menjadi POST JSON ke pipeline HTTP yang sudah ada.

ALAMAT EMAIL HANYALAH URL MASUK LAINNYAsiapa punmengirim emailMXroutingparse + verifikasiPOSThandler Andaatau agen Andalogpercobaan ulang, log event, dan tombol Replay yang sama seperti sumber webhook lainnya
MX record masuk, HTTP POST keluar. Parsing dan verifikasi terjadi di antaranya.

Cloudflare Email Routing bisa menyerahkan setiap email yang dikirim ke domain Anda ke sebuah Worker. Gratis, tanpa mail server yang harus dijalankan, dan tanpa polling IMAP. Gabungkan aturan catch-all dengan handler email dan Anda mendapat sesuatu yang benar-benar berguna: alamat masuk tanpa batas di domain Anda, yang masing-masing mengubah email menjadi POST JSON ke pipeline HTTP apa pun yang sudah Anda jalankan.

Kami merilis persis ini untuk AnyHook (setiap endpoint kini juga sebuah alamat email), dan sebagian besar dari satu jam pengerjaannya habis untuk tiga keputusan desain, bukan untuk kode.

Penyiapan

  1. Aktifkan Email Routing di zone Anda. Cloudflare menambahkan MX record, dan aturan bernama yang sudah ada seperti hello@ tetap berjalan, karena aturan bernama didahulukan daripada catch-all
  2. Buat aturan catch-all dengan aksi Send to a Worker (kirim ke Worker)
  3. Ekspor handler email di samping handler fetch Anda:
export default {
  async fetch(request, env, ctx) { /* your existing worker */ },
  async email(message, env, ctx)  { /* new */ },
};

Handler menerima ForwardableEmailMessage: from, to, headers, rawSize, stream raw berisi pesan MIME lengkap, dan tiga kata kerja, forward(), reply(), dan setReject().

Keputusan 1: alamat adalah router Anda

Catch-all menerima email untuk setiap local part, jadi alamat itu sendiri bisa membawa informasi routing. Alamat kami menyandikan tenant dan App:

{user-slug}.{app-slug}@anyhook.net

Aturan parsing yang membuat ini aman adalah memecah pada titik terakhir, dan itu hanya aman karena slug disanitasi menjadi [a-z0-9-] saat dibuat. Slug tidak pernah bisa berisi titik, jadi pemecahannya tidak ambigu karena desain, bukan karena konvensi. Jika identifier Anda bisa berisi titik, pilih pemisah yang tidak mungkin ada di dalamnya, atau router ini salah merutekan.

export function parseInboxAddress(to: string) {
  const local = (to.split("@")[0] ?? "").toLowerCase();
  const dot = local.lastIndexOf(".");
  if (dot <= 0 || dot === local.length - 1) return null;

  const userSlug = local.slice(0, dot);
  const appSlug  = local.slice(dot + 1);
  if (!/^[a-z0-9-]+$/.test(userSlug) || !/^[a-z0-9-]+$/.test(appSlug)) return null;
  return { userSlug, appSlug };
}

Ubah ke huruf kecil lebih dulu; pengirim tidak mempertahankan kapitalisasi local part. Dan jadikan ini pure function: inilah bagian yang penuh percabangan, dan karena itu bagian yang bisa dites. Milik kami punya delapan tes (kapitalisasi, tanda hubung, titik di awal/akhir, +tag, garis bawah, path traversal, local part kosong). Kode perekat di sekitarnya tidak punya tes, karena perekatnya tidak bercabang.

Keputusan 2: tolak di SMTP, jangan pernah telan

Catch-all adalah magnet bagi setiap probe yang diarahkan ke domain Anda. Langkah yang menggoda adalah membuang diam-diam apa pun yang tidak bisa di-parse. Jangan, karena pengirim yang tidak menerima error akan menganggap emailnya terkirim. Untuk alamat yang salah ketik, itu berarti manusia yang menunggu balasan yang tidak pernah datang.

const parsed = parseInboxAddress(message.to);
if (!parsed) {
  message.setReject("Address must be {user}.{app}@example.com");
  return;
}

setReject memantulkan email di tahap SMTP, jadi server pengirim membuat pemberitahuan kegagalan yang semestinya. Sama halnya untuk email yang terlalu besar, dan periksa rawSize sebelum mem-buffer stream, bukan sesudahnya:

if (message.rawSize > MAX_BYTES) {
  message.setReject(`Message exceeds ${MAX_BYTES} bytes`);
  return;
}

Pesan masuk bisa berukuran hingga 25 MiB. Mengetahui pesan terlalu besar dengan memuatnya ke memori Worker adalah urutan yang salah.

CATCH-ALL ADALAH MAGNET UNTUK PROBEemail masukparse alamatperiksa rawSize duluoksatu webhooklogtidak bisa di-parse, atau lebih dari 25 MiBsetReject()bounce di SMTPserver pengirim membuat notifikasi gagal aslimembuangnya diam-diam memberi tahu seseorang bahwa email-nya terkirim, padahal tidak
Dua jalan keluar, tidak ada yang ketiga. Apa pun yang tidak bisa dirutekan dipantulkan di tempat yang bisa dilihat pengirim.

Keputusan 3: jangan bangun pipeline kedua

Handler email bisa saja mem-parse MIME, menerapkan kuota, menulis ke database, dan memasukkan pengiriman ke antrean, menduplikasi semua yang sudah dikerjakan jalur HTTP Anda. Hasilnya dua pipeline yang terus menjauh satu sama lain selamanya.

Ini jebakan yang sama dengan yang menjerat platform otomasi saat mesin workflow sekaligus menjadi ingress, dan itulah sebabnya run yang gagal di sana hanya meninggalkan event di catatan eksekusi run tersebut: Webhook n8n Tidak Berfungsi membahas mode kegagalan itu secara rinci.

Sebagai gantinya, kemas ulang pesan itu menjadi permintaan HTTP yang semestinya, lalu panggil handler fetch Anda sendiri:

export async function handleEmail(message, env, ctx) {
  const parsed = parseInboxAddress(message.to);
  if (!parsed) { message.setReject("…"); return; }

  const raw = await new Response(message.raw).text();
  const req = new Request(
    `https://in.example.com/${parsed.userSlug}/${parsed.appSlug}`,
    {
      method: "POST",
      headers: {
        "content-type": "application/json",
        "x-transport": "email",
      },
      body: JSON.stringify({
        type: "email.received",
        from: message.from,
        to: message.to,
        subject: message.headers.get("subject"),
        message_id: message.headers.get("message-id"),
        size: message.rawSize,
        raw,
      }),
    }
  );

  const res = await handler.fetch(req, env, ctx);
  if (!res.ok && res.status !== 202) {
    message.setReject(`Rejected (${res.status})`);
  }
}

Semua yang ada di downstream, yaitu autentikasi route, rate limit, kuota, batas payload, antrean, percobaan ulang (retry), dan logging, melewati jalur kode yang sama dengan webhook Anda. Satu pipeline, dua transport. Saat jalur HTTP mendapat fitur baru, email otomatis ikut mendapatkannya. Header x-transport: email adalah satu-satunya pembeda keduanya, untuk UI yang ingin menampilkan amplop, bukan payload.

Perhatikan empat baris terakhir: jika pipeline Anda sendiri menolak event-nya (App tidak dikenal, batas harian tercapai), teruskan penolakan itu sebagai penolakan SMTP juga. Pesan bounce adalah saluran pelaporan error Anda kepada pengirim.

Yang perlu diketahui sebelum mengandalkannya

  • Kirim seluruh string raw ke downstream dan parse di sana dengan library MIME sungguhan. Field praktis (subject, from) untuk routing dan tampilan; lampiran, bagian HTML, dan header DKIM ada di raw
  • Aturan bernama tetap berlaku. Catch-all hanya melihat email yang tidak diklaim aturan bernama mana pun, jadi forward hello@ yang sudah ada tidak tersentuh
  • Batas CPU paket Workers Free berlaku untuk handler email seperti Worker lainnya. Pola pengemasan ulang membuat handler tetap ramping, dan itu membantu

Jika Anda lebih suka tidak menjalankannya sendiri, pola persis inilah yang di-hosting AnyHook: satu panggilan API tanpa kunci mengembalikan endpoint yang sekaligus URL webhook dan alamat email. Keduanya mengisi satu log event yang bisa Anda baca lewat HTTP atau MCP.

Semua artikel30 Juli 2026 · 7 mnt

Berhenti kehilangan webhook.

Ganti satu URL. Dapatkan percobaan ulang, log event, dan putar ulang sekali klik. Atau lihat dulu: daftar dan jalankan latihan gangguan 30 detik, lalu saksikan event tetap selamat saat server Anda mati. Tidak perlu server.