Cloudflare Workers ile E-posta Yönlendirme Sistemi Kurma
Klasik e-posta yönlendirme problemleri hep aynı yerde başlar: bir müşteri “[email protected]” adresine mail atıyor, bu mail kaybolup gidiyor ya da yanlış kişiye ulaşıyor. Sonra biri fark ediyor, sonra toplantı oluyor, sonra ticket açılıyor. Cloudflare Workers bu döngüyü kırmanın hem ucuz hem de son derece esnek bir yolunu sunuyor. Ben bu sistemi ilk kurduğumda “bu kadar basit olamaz” diye iki kez kontrol ettim, ama gerçekten bu kadar basit.
Neden Geleneksel E-posta Yönlendirme Yetmez?
Çoğu şirkette e-posta yönlendirme ya hosting panelinin forwarder özelliğiyle ya da bir mail sunucusunun alias mekanizmasıyla yapılır. Bu yöntemler işe yarar, ama esneklikleri sınırlıdır. Örneğin “Pazartesi ile Cuma arasında gelen mailler destek ekibine gitsin, hafta sonu ise nöbetçi kişiye gitsin” gibi bir kural koyamazsınız. Ya da “konu başlığında URGENT yazan mailler ayrıca SMS ile bildirilsin” diyemezsiniz.
Cloudflare Workers burada devreye giriyor. Cloudflare’in Email Routing özelliği ile Workers’ı birleştirdiğinizde, gelen her e-postayı bir JavaScript/TypeScript fonksiyonu içinde işleyebiliyorsunuz. Bu fonksiyon içinde istediğiniz mantığı kurabilirsiniz: header’lara bakabilirsiniz, konu başlığını parse edebilirsiniz, dışarıya HTTP isteği atabilirsiniz, Slack’e mesaj gönderebilirsiniz.
Temel Kavramlar ve Mimari
Sistemi kurmadan önce birkaç kavramı netleştirmek gerekiyor.
Email Routing Worker: Cloudflare’e gelen e-postayı karşılayan ve işleyen Worker fonksiyonu. Bu fonksiyon email event handler’ı üzerinden çalışır, normal HTTP Worker’larından farklıdır.
Forward Address: Mailin nihayetinde iletileceği hedef adres. Cloudflare bu adresi doğrulamanızı istiyor, yani rastgele bir adrese yönlendirme yapamazsınız, önceden verified olması gerekiyor.
Catch-all vs. Custom Address: Catch-all rule, alan adınıza gelen tüm mailleri yakalar. Custom address ise yalnızca belirli bir adresi hedefler. İkisini de Worker’a bağlayabilirsiniz.
Mimari şöyle işliyor: DNS MX kayıtları Cloudflare’e işaret ediyor, Cloudflare gelen maili alıyor, Worker’ı tetikliyor, Worker kendi mantığına göre maili iletiyor ya da reddediyor.
Cloudflare Email Routing Aktivasyonu
Önce Cloudflare panelinden domain’inizin Email Routing bölümüne gidin ve aktive edin. Bu adım otomatik olarak MX kayıtlarınızı günceller.
# Mevcut MX kayıtlarınızı kontrol edin
dig MX sizindomain.com
# Cloudflare aktivasyonu sonrası görmeniz gereken kayıtlar:
# sizindomain.com. 300 IN MX 13 route1.mx.cloudflare.net.
# sizindomain.com. 300 IN MX 7 route2.mx.cloudflare.net.
# sizindomain.com. 300 IN MX 31 route3.mx.cloudflare.net.
MX kayıtları yerleştikten sonra hedef e-posta adreslerini verified listesine eklemeniz gerekiyor. Cloudflare bu adreslere bir doğrulama maili atacak.
İlk Worker: Basit Yönlendirme
En basit senaryodan başlayalım. [email protected] adresine gelen her maili [email protected] adresine iletmek istiyorsunuz.
export default {
async email(message, env, ctx) {
const destinationAddress = "[email protected]";
// Mailin kim tarafından gönderildiğini loglayalım
console.log(`Mail alındı: ${message.from} -> ${message.to}`);
// Maili hedef adrese ilet
await message.forward(destinationAddress);
}
};
Bu kadar. Ama bu örnek gerçek dünyada pek işe yaramaz çünkü hiçbir mantık içermiyor. Şimdi daha ilginç senaryolara geçelim.
Konu Başlığına Göre Yönlendirme
Pratikte en çok kullandığım senaryo bu. Müşterilerden gelen “FATURA”, “DESTEK”, “SATIS” gibi anahtar kelimeler içeren mailler farklı departmanlara gidiyor.
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "";
const subjectLower = subject.toLowerCase();
// Konu başlığına göre yönlendirme kuralları
const routingRules = [
{ keywords: ["fatura", "invoice", "ödeme", "payment"], target: "[email protected]" },
{ keywords: ["destek", "support", "hata", "bug", "sorun"], target: "[email protected]" },
{ keywords: ["teklif", "quote", "satis", "satış", "fiyat"], target: "[email protected]" },
{ keywords: ["iade", "iptal", "cancel", "refund"], target: "[email protected]" }
];
let targetAddress = "[email protected]"; // varsayılan
for (const rule of routingRules) {
const matched = rule.keywords.some(keyword => subjectLower.includes(keyword));
if (matched) {
targetAddress = rule.target;
console.log(`Kural eşleşti: "${subject}" -> ${targetAddress}`);
break;
}
}
await message.forward(targetAddress);
}
};
Bu yapıda dikkat etmeniz gereken bir nokta var: Cloudflare’in verified adresleri listesinde olmayan bir adrese forward çağırırsanız Worker hata fırlatır. Tüm hedef adresleri önceden doğrulamanız şart.
Birden Fazla Adrese Eş Zamanlı İletme
Bazen bir maili tek kişiye değil, birden fazla kişiye aynı anda iletmek istersiniz. Örneğin kritik bir sistem bildirimi hem DevOps ekibine hem de yöneticiye gitmeli.
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "";
const from = message.from;
// Kritik alarm kontrolü
const isCritical = subject.toLowerCase().includes("critical") ||
subject.toLowerCase().includes("alarm") ||
subject.toLowerCase().includes("acil");
if (isCritical) {
// Promise.all ile eş zamanlı iletim
await Promise.all([
message.forward("[email protected]"),
message.forward("[email protected]"),
message.forward("nö[email protected]")
]);
console.log(`Kritik mail ${from} adresinden geldi, 3 kişiye iletildi`);
} else {
await message.forward("[email protected]");
}
}
};
Burada Promise.all kullanmak önemli. Sıralı await yaparsanız her forward işlemi diğerinin bitmesini bekler, bu da zaman kaybına yol açar. Paralel çalıştırın.
Spam ve Güvenlik Filtresi Ekleme
E-posta sistemlerinde en büyük sorunlardan biri spam ve phishing. Workers üzerinde basit bir filtre katmanı kurabilirsiniz.
export default {
async email(message, env, ctx) {
// Bilinen spam domain'lerini environment variable'dan oku
const blockedDomains = (env.BLOCKED_DOMAINS || "").split(",").map(d => d.trim());
const blockedKeywords = (env.BLOCKED_KEYWORDS || "").split(",").map(k => k.trim().toLowerCase());
const senderDomain = message.from.split("@")[1];
const subject = message.headers.get("subject") || "";
const subjectLower = subject.toLowerCase();
// Domain kontrolü
if (blockedDomains.includes(senderDomain)) {
console.log(`Engellendi: ${message.from} - Domain kara listede`);
message.setReject("Spam detected");
return;
}
// Konu başlığı anahtar kelime kontrolü
const hasBlockedKeyword = blockedKeywords.some(keyword =>
keyword.length > 0 && subjectLower.includes(keyword)
);
if (hasBlockedKeyword) {
console.log(`Engellendi: "${subject}" - Kara listedeki anahtar kelime`);
message.setReject("Message rejected");
return;
}
// SPF ve DKIM kontrolü (Cloudflare bunu otomatik yapar ama ek kontrol)
const spfStatus = message.headers.get("x-email-validation-spf");
if (spfStatus && spfStatus === "fail") {
console.log(`SPF başarısız: ${message.from}`);
message.setReject("SPF validation failed");
return;
}
await message.forward("[email protected]");
}
};
message.setReject() ile maili reddettiğinizde gönderene bir bounce mesajı gider. Bu davranış bazen istenmez, çünkü spam gönderenlere adresinizin aktif olduğunu söylemiş olursunuz. Duruma göre sadece return edip sessizce yutmayı da tercih edebilirsiniz.
Slack Entegrasyonu: Mail Gelince Bildirim
Bu özelliği özellikle küçük ekipler için çok faydalı buluyorum. Önemli mailler geldiğinde ekip kanalına otomatik bildirim atmak, herkesin haberdar olmasını sağlıyor.
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "(Konu yok)";
const from = message.from;
const to = message.to;
// Slack Webhook URL'sini environment variable'dan al
const slackWebhookUrl = env.SLACK_WEBHOOK_URL;
// Önce maili ilet
await message.forward("[email protected]");
// Sonra Slack'e bildirim gönder
if (slackWebhookUrl) {
const slackPayload = {
text: `*Yeni E-posta Alındı*`,
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `*Konu:* ${subject}n*Gönderen:* ${from}n*Alıcı:* ${to}`
}
}
]
};
// ctx.waitUntil ile Worker'ın kapanmasını beklemeden arka planda çalıştır
ctx.waitUntil(
fetch(slackWebhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(slackPayload)
}).catch(err => console.error("Slack bildirimi gönderilemedi:", err))
);
}
}
};
ctx.waitUntil() burada kritik bir rol oynuyor. Worker’ın ana işi maili iletmek, Slack bildirimi ise ikincil. waitUntil ile bu isteği Worker’ın yaşam döngüsü dışında arka planda tamamlatıyoruz. Eğer bunu kullanmazsanız, message.forward() döndükten hemen sonra Worker kapanabilir ve Slack isteği yarıda kalabilir.
Zaman Bazlı Yönlendirme
Mesai saatleri dışında gelen mailler nöbetçiye gitsin, mesai saatlerinde ise normal destek kuyruğuna. Bu senaryoyu çok sık görüyorum özellikle e-ticaret şirketlerinde.
export default {
async email(message, env, ctx) {
// Türkiye saati için UTC+3 offset
const now = new Date();
const turkeyOffset = 3 * 60; // dakika cinsinden
const turkeyTime = new Date(now.getTime() + turkeyOffset * 60 * 1000);
const hour = turkeyTime.getUTCHours();
const dayOfWeek = turkeyTime.getUTCDay(); // 0=Pazar, 6=Cumartesi
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
const isWorkingHours = hour >= 9 && hour < 18;
let targetAddress;
let routingReason;
if (isWeekend || !isWorkingHours) {
targetAddress = env.ONCALL_ADDRESS || "[email protected]";
routingReason = isWeekend ? "hafta sonu" : "mesai dışı";
} else {
targetAddress = env.SUPPORT_ADDRESS || "[email protected]";
routingReason = "mesai saati";
}
console.log(`Yönlendirme (${routingReason}): ${message.from} -> ${targetAddress}`);
await message.forward(targetAddress);
}
};
Burada bir tuzak var: JavaScript’in Date nesnesi UTC’de çalışır, dolayısıyla yaz saati uygulaması dönemlerinde Türkiye UTC+3 yerine UTC+3 olarak sabit kalır (Türkiye artık yaz saatine geçmiyor, bu yüzden sabit offset güvenli). Ama başka ülkeler için bu hesabı yapmak daha karmaşıklaşır.
wrangler ile Deployment
Worker’ı geliştirip deploy etmek için Cloudflare’in wrangler CLI aracını kullanıyoruz.
# wrangler kurulumu
npm install -g wrangler
# Cloudflare hesabınıza giriş
wrangler login
# Yeni email worker projesi oluşturma
mkdir email-router && cd email-router
wrangler init
# wrangler.toml dosyanız bu şekilde görünmeli:
cat wrangler.toml
name = "email-router"
main = "src/index.js"
compatibility_date = "2024-01-01"
[vars]
SUPPORT_ADDRESS = "[email protected]"
ONCALL_ADDRESS = "[email protected]"
# Hassas bilgileri environment variable olarak değil secret olarak ekleyin
# wrangler secret put SLACK_WEBHOOK_URL
# wrangler secret put BLOCKED_DOMAINS
# Secret'ları ekleyin (interaktif olarak değer girersiniz)
wrangler secret put SLACK_WEBHOOK_URL
wrangler secret put BLOCKED_DOMAINS
# Worker'ı deploy edin
wrangler deploy
# Logları canlı izlemek için
wrangler tail
wrangler tail komutu gerçek zamanlı log akışı sağlıyor. Worker’ı canlıya aldıktan sonra test maili gönderip logların nasıl aktığını izlemek, sorunları tespit etmek için çok işe yarıyor.
Cloudflare Dashboard’dan Worker’ı Email Routing’e Bağlama
Worker’ı deploy etmek yetmez, Email Routing kısmından bu Worker’ı etkinleştirmeniz gerekiyor.
# API üzerinden de yapabilirsiniz, ama panel daha pratik:
# Cloudflare Dashboard -> Email -> Email Routing -> Routing Rules
# "Catch-all address" veya özel bir adres için "Send to a Worker" seçin
# Açılan listeden email-router Worker'ınızı seçin
# Alternatif olarak Cloudflare API ile:
curl -X PUT
"https://api.cloudflare.com/client/v4/zones/ZONE_ID/email/routing/rules/catch_all"
-H "Authorization: Bearer YOUR_API_TOKEN"
-H "Content-Type: application/json"
-d '{
"actions": [{"type": "worker", "value": ["email-router"]}],
"enabled": true,
"matchers": [{"type": "all"}],
"name": "Catch-all to Worker"
}'
Gerçek Dünya: Composite Senaryo
Yukarıdaki tüm özellikleri birleştiren, production’da kullandığım bir yapıyı paylaşayım.
const ROUTING_RULES = [
{
name: "Fatura",
test: (subject, from) =>
/fatura|invoice|ödeme|payment/i.test(subject),
targets: ["[email protected]"],
slackChannel: "#muhasebe"
},
{
name: "Kritik Destek",
test: (subject, from) =>
/acil|urgent|critical|down|çöktü/i.test(subject),
targets: ["[email protected]", "[email protected]"],
slackChannel: "#acil-durum",
alwaysNotify: true
},
{
name: "Genel Destek",
test: (subject, from) =>
/destek|support|yardım|help/i.test(subject),
targets: ["[email protected]"],
slackChannel: "#destek"
}
];
async function notifySlack(webhookUrl, rule, message) {
const subject = message.headers.get("subject") || "";
return fetch(webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `[${rule.name}] *${subject}*nGönderen: ${message.from}`,
channel: rule.slackChannel
})
});
}
export default {
async email(message, env, ctx) {
const subject = message.headers.get("subject") || "";
const from = message.from;
// Spam kontrolü
const blockedDomains = (env.BLOCKED_DOMAINS || "").split(",");
const senderDomain = from.split("@")[1];
if (blockedDomains.includes(senderDomain)) {
message.setReject("Blocked");
return;
}
// Kural eşleştirme
let matchedRule = null;
for (const rule of ROUTING_RULES) {
if (rule.test(subject, from)) {
matchedRule = rule;
break;
}
}
const targets = matchedRule ? matchedRule.targets : [env.DEFAULT_ADDRESS];
// Paralel iletim
await Promise.all(targets.map(t => message.forward(t)));
// Slack bildirimi (arka planda)
if (matchedRule && env.SLACK_WEBHOOK_URL) {
ctx.waitUntil(
notifySlack(env.SLACK_WEBHOOK_URL, matchedRule, message)
.catch(e => console.error("Slack hatası:", e))
);
}
console.log(`İletildi: "${subject}" [${from}] -> ${targets.join(", ")} (Kural: ${matchedRule?.name || "varsayılan"})`);
}
};
Dikkat Edilmesi Gereken Noktalar
Birkaç pratik uyarı:
- Verified adres zorunluluğu:
message.forward()yalnızca Cloudflare’in doğrulanmış adres listesindeki hedeflere iletim yapabilir. Bunu atlamak için workaround yok.
- CPU limitleri: Free plan’da 10ms CPU süresi var, Paid plan’da 30ms. Karmaşık regex işlemleri ve uzun kural listeleri bu limiti zorlar. Kural sayısını makul tutun.
- Mail boyutu: Cloudflare büyük eklere sahip maillerde sorun çıkarabilir. 25MB üzeri mailler için alternatif düşünün.
- Rate limiting: Aynı adresten kısa sürede çok fazla mail gelirse Cloudflare’in kendi rate limitleriyle karşılaşabilirsiniz. Bu genellikle sorun değil ama izleyin.
- Test için gerçek mail gönderin: Workers’ta email handler’ı local’de test edemezsiniz.
wrangler devbu event type’ı desteklemiyor. Test için gerçekten mail göndermeniz vewrangler tailile logları izlemeniz gerekiyor.
Sonuç
Cloudflare Workers ile e-posta yönlendirme sistemi kurmak, hosting panellerinin sınırlı forwarder özelliklerinden çok daha fazlasını sunuyor. Kural tabanlı yönlendirme, spam filtreleme, çoklu alıcı desteği, Slack entegrasyonu ve zaman bazlı mantık, hepsini tek bir JavaScript dosyasında toplayabiliyorsunuz.
Özellikle küçük ve orta ölçekli ekipler için bu yaklaşım hem maliyet açısından hem de esneklik açısından çok anlamlı. Cloudflare’in free planı günde binlerce maili rahatlıkla karşılar, Paid plan’a geçme ihtiyacı genellikle doğmuyor.
Bu sistemi bir kez kurduğunuzda, “info@ adresine kim bakıyor” sorusu tarihe karışıyor. Artık her mail doğru yere, doğru zamanda, doğru kişilere ulaşıyor. Ve eğer bir şeyler değişirse, dashboard’a gitmeye gerek yok: sadece Worker kodunu güncelleyip deploy ediyorsunuz.
