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 dev bu event type’ı desteklemiyor. Test için gerçekten mail göndermeniz ve wrangler tail ile 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.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir