Abonelikten Çıkma Bağlantısını Doğru Kurmak: CAN-SPAM, KVKK ve Tek-Tık Protokolü
Abonelikten çıkma bağlantısı bir CSS düğmesi değil, ABD CAN-SPAM, Türkiye İYS/6563 ve Gmail'in tek-tık kuralını aynı anda karşılaması gereken bir uç noktadır. GET tıklaması ile POST tek-tık arasındaki farkı, imzalı token tasarımını ve bastırma boru hattını kopyalanabilir mimariyle anlatıyoruz.
EvilMail Team4 Ağustos 202612 dk okuma
Bir abuse şikayeti düşer, log'a bakarsınız: kullanıcı üç ay önce "abonelikten çık" düğmesine basmış, ama istek hiçbir zaman işlenmemiş. Çünkü düğme, login isteyen bir preference center'a yönlendiriyordu; kullanıcı şifresini hatırlamadı, vazgeçti, sonra "spam" düğmesine bastı. CAN-SPAM'in 10 iş günü süresi çoktan geçti, İYS'nin 3 iş günü de öyle. Gmail Postmaster Tools'ta görünür spam oranınız 0,3%'ü aştı ve artık kampanyalarınız Promotions sekmesine bile değil, doğrudan Spam'e düşüyor.
Abonelikten çıkma bağlantısı, gönderim altyapısının en çok hafife alınan ama en pahalıya patlayan uç noktasıdır. Onu bir CSS düğmesi sanan ekipler, aslında iki ayrı hukuk rejimini ve iki RFC'yi aynı anda ihlal eder. Bu yazı o düğmeyi bir mühendislik uç noktası olarak ele alıyor: hangi HTTP fiili, hangi başlık, hangi süre, hangi imzalı token.
İki hukuk rejimi, tek buton
Aynı düğme iki farklı ülkenin mevzuatına cevap vermek zorunda ve bunlar örtüşse de aynı şey değil.
ABD — CAN-SPAM (15 U.S.C. 7704): Her ticari iletide görünür ve işleyen bir ret mekanizması olmalı. Ret ücretsiz olmalı; kullanıcıdan e-posta adresi ve ret tercihi dışında bilgi ya da login istenemez. Talep en geç 10 iş günü içinde işlenmeli. Kritik ama sık atlanan madde: unsubscribe bağlantısı, iletinin gönderilmesinden sonra en az 30 gün çalışır durumda kalmalı. FTC'nin enflasyona endeksli üst sınırı ihlal başına yaklaşık 53.088 dolardır ve her bir e-posta ayrı ihlal sayılır; milyonluk bir listede bozuk tek bir link, teorik ceza tabanını hızla milyar dolar mertebesine taşır.
Türkiye — 6563 sayılı Kanun + İYS + KVKK:
Burada iki farklı yükü ayırmak şart.
6563 (Elektronik Ticaretin Düzenlenmesi Hakkında Kanun) ve Ticari İletişim Yönetmeliği
iletiyi durdurmakla ilgilenir: her ticari elektronik iletide gönderici kimliği ve ret imkânı bulunmalı, ret ücretsiz olmalı ve
3 iş günü
içinde işlenmelidir. Onay ve ret yönetimi
İYS (İleti Yönetim Sistemi)
üzerinden yürütülür; alıcının ret durumu İYS'ye yansıtılmak zorundadır.
KVKK (6698)
ise veri tarafını kapsar: e-posta adresi kişisel veridir, işlenmesi açık rızaya dayanır ve ilgili kişinin silme/itiraz hakkı vardır.
Net bir ayrım tutun: İYS = iletiyi durdurma, KVKK = veriyi işleme/silme. Kullanıcı "çık" dediğinde 6563/İYS yükümlülüğünüz iletiyi kesmektir; bu, veriyi silme yükümlülüğü değildir. Silme, KVKK kapsamında ayrı ve ayrıca talep edilmesi gereken bir haktır. Bu ikisini karıştıran ekipler ya çok az yapar (ret'i işlemez) ya da çok fazla (kaydı siler ve bastırma yeteneğini kaybeder).
Gmail/Yahoo eşiği: artık opsiyonel değil
Şubat 2024'te yürürlüğe giren toplu gönderici kuralları, hukuki asgarinin üstüne teslimat tarafından dayatılan bir zorunluluk kattı. Günde tek bir alıcı sağlayıcıya 5.000+ ileti gönderiyorsanız:
Tek-tık abonelikten çıkma zorunlu (RFC 8058, aşağıda).
Ret talebi 2 gün içinde işlenmeli.
Kullanıcı görünür spam oranınız 0,3% altında kalmalı; hedef 0,1%'in altıdır.
SPF + DKIM + DMARC hizalı olmalı.
Bu üç eşik hukukla değil, teslimatla ilgilidir; ama pratikte hukuki asgariden daha sıkıdır. Gmail'in 2 günü, İYS'nin 3 iş gününden ve CAN-SPAM'in 10 iş gününden kısadır. Spam oranınızı Google Postmaster Tools'tan izlemiyorsanız kör uçuyorsunuz demektir.
List-Unsubscribe ve tek-tık: RFC 2369 + RFC 8058
İşin protokol katmanı iki başlığa dayanır.
List-Unsubscribe (RFC 2369) iki hedef taşımalı: bir https:// URL'si (POST için) ve bir mailto: adresi (RFC 8058 desteklemeyen istemciler için fallback). List-Unsubscribe-Post (RFC 8058) ise MUA'ya "bu bağlantı gerçekten tek tıkla, onaysız çalışır" der. Bu ikinci başlık olmadan Gmail düğmeyi tek-tık saymaz ve göstermez.
Kullanıcı Gmail'de düğmeye bastığında MUA şu isteği yapar:
http
POST /u/eyJzdWIiOjEyMywibGlzdCI6NCwiZXhwIjoxNzk5OTk5OTk5fQ.Xa9 HTTP/1.1
Host: evilmail.pro
Content-Type: application/x-www-form-urlencoded
List-Unsubscribe=One-Click
En sinsi hata: bu başlıkların DKIM imza kapsamına (h= listesine) girmemesi. Başlıklar imzalı değilse MUA onlara güvenmez ve tek-tık düğmesi hiç görünmez. DKIM h= alanınıza List-Unsubscribe:List-Unsubscribe-Post eklendiğinden emin olun. mailto: fallback'ini de silmeyin; Apple Mail'in eski sürümleri ve bazı kurumsal istemciler hâlâ POST'u değil mailto'yu kullanır.
GET tık vs POST tek-tık: güvenlik tarayıcıları tuzağı
Burası tüm mimarinin en kritik ayrımı ve ekiplerin en çok battığı yer.
MUA'nın tek-tık düğmesi bir HTTP POST'tur ve gövdesinde List-Unsubscribe=One-Click taşır. Bunu anında, onaysız işlemeniz gerekir; bir onay sayfası göstermek Gmail kuralının doğrudan ihlalidir.
E-posta gövdesindeki görünür "abonelikten çık" linki ise tarayıcıda bir HTTP GET'tir. Ve tuzak burada: Outlook SafeLinks, Barracuda, Mimecast, Proofpoint gibi güvenlik tarayıcıları, e-postadaki her linki kullanıcı görmeden önce prefetch GET ile açar. GET üzerinde doğrudan unsubscribe işlemi yaparsanız, hiç tıklamamış masum kullanıcıları toplu halde listeden düşürürsünüz. Sabah 200 kişilik listeniz, bir kurumsal tarayıcı geçtikten sonra 40 kişiye iner.
Çözüm katı ve basit: GET → tek butonlu onay sayfası göster. POST → gerçek işlemi yap. İkisi de login istemez. Prefetch yapan tarayıcı yalnızca onay sayfasının HTML'ini çeker, hiçbir şey değişmez; gerçek kullanıcı butona basınca tarayıcı POST atar ve işlem gerçekleşir.
Bir de idempotency: aynı token ikinci kez POST edildiğinde hata döndürmeyin, yine 200 dönün. MUA'lar retry yapar; ikinci istekte 500 görürse kullanıcıya "işlem başarısız" der ve o kişi spam düğmesine gider.
İmzalı token: login olmadan güvenli çıkış
Login isteyemezsiniz (CAN-SPAM ve İYS bunu yasaklar), ama URL'yi de savunmasız bırakamazsınız. /u/1023 gibi sıralı bir ID felakettir: saldırgan 1'den 100000'e kadar döngü kurup tüm listenizi düşürür (enumeration). E-posta adresini querystring'e koymak da ayrı bir hata; referer başlığı ve web sunucusu access log'u üzerinden KVKK kapsamında kişisel veri sızdırır.
Çözüm HMAC-SHA256 ile imzalı opak token'dır. Payload'da yalnızca abone ID, liste ID ve son kullanma zamanı taşınır; imza sunucudaki gizli anahtarla doğrulanır. Kimse geçerli bir token uyduramaz.
typescript
import { createHmac, timingSafeEqual } from "node:crypto";
function b64url(buf: Buffer): string {
return buf.toString("base64url");
}
export function signUnsubToken(sub: number, list: number, secret: string): string {
// exp: gönderimden 60 gün sonra (CAN-SPAM asgari 30 günün üstünde tut)
const exp = Math.floor(Date.now() / 1000) + 60 * 24 * 3600;
const body = b64url(Buffer.from(JSON.stringify({ sub, list, exp })));
const sig = b64url(createHmac("sha256", secret).update(body).digest());
return `${body}.${sig}`;
}
export function verifyUnsubToken(token: string, secret: string) {
const [body, sig] = token.split(".");
if (!body || !sig) return null;
const expected = b64url(createHmac("sha256", secret).update(body).digest());
const a = Buffer.from(sig), b = Buffer.from(expected);
if (a.length !== b.length || !timingSafeEqual(a, b)) return null; // constant-time
const { sub, list, exp } = JSON.parse(Buffer.from(body, "base64url").toString());
if (Math.floor(Date.now() / 1000) > exp) return null;
return { sub, list };
}
İki sayı önemli: exp'i en az 30 günün üzerinde tutun (CAN-SPAM linkin 30 gün çalışmasını istiyor); 60 gün makul bir öneri. Ve imza doğrulamasında mutlaka `timingSafeEqual` kullanın — normal === karşılaştırması timing attack'a açıktır.
İşleme boru hattı: silme değil, bastırma
Ret geldiğinde kaydı silmeyin. Silerseniz, aynı kişi altı ay sonra bir CSV import'uyla tekrar listenize girer ve yeniden e-posta almaya başlar; bu hem CAN-SPAM hem İYS ihlalidir. Bunun yerine kaydı bir bastırma listesine (suppression list) ekleyin. Gönderim öncesi her batch bu listeye karşı filtrelenir, böylece re-import bile alıcıyı koruyamaz.
İşlem idempotent olmalı ve her ret bir audit log satırı üretmeli: subscriber_id, timestamp, ip, user_agent, method (GET/POST), token_id. Bir gün "biz hiç çıkmadık" diyen bir müşteriye ya da bir regülatöre kanıt göstermeniz gerekecek.
SLA tasarımını en sıkı sürenin üstüne kurun. Üç rejim üç farklı süre veriyor — 2 gün (Gmail), 3 iş günü (İYS), 10 iş günü (CAN-SPAM) — ama hepsini karşılamanın tek yolu en sıkısına, yani Gmail'in 2 gününe göre kuyruk tasarlamaktır. Ret işlemini eşzamanlı yapın; asenkron push'ları (İYS, harici ESP senkronizasyonu) 2 günün çok altında bir kuyrukta koşturun.
Kısmi çıkış (preference center: "haftalık bülteni bırak ama işlem maillerini tut") sunabilirsiniz. Ama "hepsinden çık" seçeneği her zaman tek adımda ve login'siz erişilebilir olmalı. Preference center'ı zorunlu kılamazsınız.
Türkiye özeli: İYS entegrasyonu ve KVKK
İYS entegrasyonu iki yönlüdür. Gönderici olarak markanızı (VKN/marka) İYS'de kayıtlı tutarsınız; bir ret geldiğinde bunu İYS API'sine 3 iş günü içinde push etmeniz gerekir. Aynı zamanda İYS tarafındaki ret durumunu da senkron tutmalısınız — bir kullanıcı İYS'nin kendi arayüzünden çıkabilir ve bu sizin sisteminizde de ret olarak görünmelidir. Her ticari iletide gönderici kimliği ve ret imkânı görünür olmalı; İYS'de karşılığı olmayan bir gönderim baştan hatalıdır.
KVKK tarafında en sık atlanan nokta audit log'dur. Unsubscribe kaydınızda IP ve User-Agent tutuyorsanız — ki uyuşmazlıklar için tutmalısınız — bu kişisel veri işlemedir. Bir aydınlatma metniniz ve tanımlı bir saklama süreniz olmalı. Açık rıza geri çekilebilir bir şeydir; kullanıcının ret'i işlemeyi durdurma talebidir, silme talebi değildir. Bu ikisini ayrı akışlar olarak modelleyin.
Test ve doğrulama
Deploy etmeden önce şunları elle koşturun. Kendinize bir test kampanyası gönderip Gmail arayüzünde başlık yanında "Abonelikten çık" düğmesinin çıktığını görün — çıkmıyorsa büyük ihtimalle DKIM başlık kapsamı eksiktir.
Tek-tık POST'unu curl ile taklit edin ve idempotency'yi doğrulayın:
bash
curl -i -X POST 'https://evilmail.pro/u/eyJzdWIiOjEyMywibGlzdCI6NCwiZXhwIjoxNzk5OTk5OTk5fQ.Xa9' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'List-Unsubscribe=One-Click'
# Beklenen: HTTP/1.1 200 — ve ikinci kez çalıştırınca yine 200 (idempotent)
Güvenlik tarayıcısını taklit edin: aynı URL'ye düz bir curl -sI (GET) atın ve kullanıcının listeden düşmediğini, yalnızca onay sayfasının döndüğünü doğrulayın. Son olarak alınan test iletisinin Authentication-Results başlığında dkim=pass gördüğünüzden ve DKIM imzasının List-Unsubscribe başlığını kapsadığından emin olun. mail-tester.com ve RFC 8058 header doğrulayıcıları hızlı bir sağlık kontrolü verir; spam oranı için tek gerçek kaynak Google Postmaster Tools'tur.
Uygulama kontrol listesi
List-Unsubscribe hem https:// hem mailto: içeriyor mu
List-Unsubscribe-Post: List-Unsubscribe=One-Click mevcut mu
Her iki başlık DKIM h= imza kapsamında mı
POST onaysız/anında işliyor, GET yalnızca onay sayfası gösteriyor mu
Hiçbir yol login ya da ek bilgi istemiyor mu
Token HMAC-SHA256 imzalı, exp ≥ 30 gün, e-posta querystring'de değil