Tüm yazılar
Pazarlama 6 dk okuma

Gönderdiğin E-Posta Ulaşıyor mu? SPF, DKIM, DMARC ve Liste Hijyeni

SPF, DKIM ve DMARC kayıtlarını doğru kurma, hizalama hatalarını giderme ve geri dönen adresleri bastırma listesine alma: Python ile teslim edilebilirlik denetimi.

#seo #analytics #automation

E-posta gönderim sağlığı, bir bültenin ya da sistem bildiriminin alıcının gelen kutusuna mı yoksa spam klasörüne mi düştüğünü belirleyen teknik etkenlerin toplamıdır. Küçük ve orta ölçekli işletmelerde konu çoğu zaman ancak bir alıcı "e-postanız gelmedi" dediğinde gündeme gelir. Oysa sipariş onayı, fatura bildirimi ya da parola sıfırlama e-postası ulaşmıyorsa otomasyonun geri kalanı çalışmış sayılmaz.

Sorunun büyük bölümü üç DNS kaydında ve bir listede toplanır. SPF hangi sunucuların alan adı adına gönderebileceğini söyler, DKIM her mesajı kriptografik olarak imzalar, DMARC bu ikisinin From adresiyle hizalanmasını ister ve uyumsuzlukta ne yapılacağını bildirir. Liste hijyeni ise geri dönen ve şikâyet eden adreslerin gönderim listesinden kalıcı olarak çıkarılmasıdır.

Bu yazıda dört parçayı sırayla kuruyoruz. İki kod örneği var: biri bir alan adının SPF, DKIM ve DMARC kayıtlarını DNS'ten çekip eksikleri raporlayan denetim betiği, diğeri geri dönen mesajları ayrıştırıp kalıcı bir bastırma listesine yazan ayıklayıcı. Son bölümde Türkiye'deki ticari ileti mevzuatının otomasyona nasıl yansıdığına bakıyoruz.

SPF ve DKIM: kim gönderebilir, mesaj yolda değişti mi

SPF (Sender Policy Framework), alan adının DNS'ine konan ve bu alan adı adına hangi sunucuların e-posta gönderebileceğini listeleyen bir TXT kaydıdır. DKIM (DomainKeys Identified Mail) ise gönderen sunucunun her mesajın başlığını ve gövdesini özel anahtarla imzalaması, alıcının da DNS'teki açık anahtarla bu imzayı doğrulamasıdır. SPF gönderenin kimliğini, DKIM mesajın bütünlüğünü kanıtlar; ikisi birbirini tamamlar.

SPF kaydı tek satırdır ve v=spf1 ile başlar. Kendi sunucunuz için ip4, ip6 ya da a mekanizması, kullandığınız gönderim servisi için servisin belgesinde yazan include ifadesi eklenir; satır -all ile biter. Yaygın hata birden fazla SPF kaydı bulundurmaktır. RFC 7208'e göre alan adında tek bir SPF kaydı olmalı ve kaydın çözümlenmesi sırasında yapılan DNS sorgusu sayısı 10'u aşmamalıdır. Her include kendi içinde yeni sorgular ürettiği için üç dört servis eklenince bu sınır fark edilmeden aşılır ve kayıt geçersiz sayılır.

DKIM için gönderim servisi bir seçici (selector) ve bir açık anahtar verir; anahtar secici._domainkey.alanadi adresine TXT kaydı olarak konur. Kendi sunucunuzdan gönderiyorsanız anahtar çiftini OpenDKIM ya da benzeri bir araç üretir. 2048 bit anahtar tercih edilir ve anahtar düzenli aralıklarla yenilenir. Yenileme sırasında eski ve yeni seçici bir süre birlikte yayında tutulur; böylece yolda olan mesajların imzası geçersiz kalmaz.

DMARC politikası ve hizalama

DMARC (Domain-based Message Authentication, Reporting and Conformance), alıcı sunucuya SPF ya da DKIM doğrulaması başarısız olan mesajlara ne yapacağını söyleyen ve sonuçları alan adı sahibine raporlayan DNS kaydıdır. Kayıt _dmarc.alanadi adresinde durur ve p=none, p=quarantine ya da p=reject politikalarından birini taşır. DMARC'ın SPF ve DKIM'den farkı, doğrulamanın okurun gördüğü From adresiyle hizalanmasını şart koşmasıdır.

Hizalama (alignment) şu anlama gelir: SPF'in denetlediği zarf gönderen adresi (Return-Path) ile From alanındaki alan adı aynı olmalı; DKIM imzasındaki d= alanı da From ile eşleşmelidir. Bir gönderim servisi kendi alan adıyla imzalıyor ve Return-Path'i kendi alan adına ayarlıyorsa SPF ve DKIM tek tek geçer ama DMARC hizalaması başarısız olur. Çözüm, serviste özel dönüş adresi (custom return-path) ve kendi alan adınızla DKIM imzalamayı açmaktır.

Politika kademeli sıkılaştırılır:

  • Kayda p=none ile başlayın ve rua= etiketine toplu raporların gideceği bir adres yazın; bu aşamada hiçbir mesaj engellenmez, yalnız kimin adınıza gönderdiği görünür.
  • Raporlarda meşru tüm kaynaklar hizalı görünene kadar SPF ve DKIM'i düzeltin; unutulmuş bir CRM, muhasebe yazılımı ya da form aracı bu aşamada ortaya çıkar.
  • Ardından p=quarantine geçin; pct etiketiyle politikayı önce trafiğin küçük bir kesimine uygulayıp kademeli olarak genişletin.
  • Son aşama p=reject politikasıdır; alan adınızı taklit eden mesajlar artık alıcı sunucuda reddedilir.

Bu adımlar isteğe bağlı bir iyileştirme değildir. Google'ın ve Yahoo'nun toplu gönderici yönergelerine göre günde beş binin üzerinde mesaj gönderen alan adlarının DMARC kaydı, hizalı SPF ya da DKIM ve tek tıkla abonelikten çıkma başlığı bulundurması zorunludur; şartı sağlamayan gönderiler reddedilir ya da spam klasörüne yönlendirilir.

Aşağıdaki betik dnspython paketiyle üç kaydı çeker ve eksikleri listeler. Cevaplanmayan sorguyu kayıt yokluğundan ayırır; aksi halde geçici bir DNS kesintisi yanlış alarm üretir. DKIM seçicisini komut satırından alır, çünkü seçici adı DNS'ten keşfedilemez; gönderim servisinin verdiği adı yazmanız gerekir.

import sys

import dns.resolver
from dns.exception import DNSException


def txt_kayitlari(ad):
    """Kayıt yoksa boş liste, sorgu cevaplanmadıysa None döner; ikisi aynı şey değildir."""
    try:
        yanit = dns.resolver.resolve(ad, "TXT")
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
        return []
    except DNSException as hata:
        print("uyarı: %s sorgusu cevaplanmadı (%s)" % (ad, type(hata).__name__))
        return None
    return ["".join(p.decode() for p in r.strings) for r in yanit]


def denetle(alan, secici):
    bulgular = []
    kayitlar = txt_kayitlari(alan)
    spf = [k for k in (kayitlar or []) if k.lower().startswith("v=spf1")]
    if kayitlar is None:
        bulgular.append("SPF denetlenemedi: DNS sorgusu cevaplanmadı, yeniden deneyin")
    elif not spf:
        bulgular.append("SPF kaydı yok")
    elif len(spf) > 1:
        bulgular.append("birden fazla SPF kaydı var; alıcılar hepsini geçersiz sayar")
    elif not spf[0].rstrip().endswith(("-all", "~all")):
        bulgular.append("SPF kaydı -all ya da ~all ile bitmiyor: %s" % spf[0])

    dkim = txt_kayitlari("%s._domainkey.%s" % (secici, alan)) or []
    if not any("p=" in k for k in dkim):
        bulgular.append("DKIM açık anahtarı bulunamadı (seçici: %s)" % secici)

    dmarc = [k for k in (txt_kayitlari("_dmarc." + alan) or []) if k.lower().startswith("v=dmarc1")]
    if not dmarc:
        bulgular.append("DMARC kaydı yok")
    else:
        etiketler = dict(p.strip().split("=", 1) for p in dmarc[0].split(";") if "=" in p)
        if etiketler.get("p", "none").strip() == "none":
            bulgular.append("DMARC politikası none: raporlama açık ama koruma yok")
        if "rua" not in etiketler:
            bulgular.append("DMARC rua etiketi yok: toplu raporlar hiçbir yere gitmiyor")
    return bulgular


if __name__ == "__main__":
    alan, secici = sys.argv[1], sys.argv[2]
    sorunlar = denetle(alan, secici)
    if not sorunlar:
        print(alan, ": SPF, DKIM ve DMARC kayıtları yerinde")
    for s in sorunlar:
        print("-", s)
    sys.exit(1 if sorunlar else 0)

Betiği bir zamanlayıcıya bağlayıp haftada bir çalıştırmak, servis değişikliğinde ya da DNS düzenlemesinde bozulan kaydı alıcılardan önce yakalar. Çıkış kodu sıfırdan farklı olduğunda bildirim gönderen bir sarmalayıcı, denetimi bakım listesinden çıkarır.

Geri dönen adreslerin ayıklanması ve bastırma listesi

Bastırma listesi (suppression list), bir kez geri dönen, şikâyet eden ya da abonelikten çıkan adreslerin bir daha gönderim listesine girmemesini sağlayan kalıcı kayıttır. Geri dönme (bounce), alıcı sunucunun mesajı kabul etmeyip bir teslim durumu bildirimi (DSN) göndermesidir. Var olmayan adreslere gönderimi sürdürmek, alıcı sunucular nezdinde gönderen itibarını düşürür ve sonraki tüm mesajların spam klasörüne düşme olasılığını artırır.

İki tür geri dönme ayrı ele alınır. Kalıcı (hard) geri dönme 5.x.x durum koduyla gelir; adres yoktur ya da alan adı e-posta kabul etmiyordur, adres ilk seferde bastırma listesine girer. Geçici (soft) geri dönme 4.x.x kodu taşır; kutu dolu ya da sunucu geçici olarak erişilemez demektir, birkaç deneme sonra hâlâ dönüyorsa adres yine listeye alınır. Şikâyet geri bildirim döngüsünden (FBL) gelen adresler ise sorgusuz bastırılır.

Gönderim servisi kullanıyorsanız bu bilgiler webhook ya da API üzerinden gelir; kendi sunucunuzdan gönderiyorsanız DSN mesajlarını bir posta kutusundan okumanız gerekir. Her iki durumda da listeyi servisin panelinde bırakmak yerine kendi veri tabanınızda tutun: servis değiştiğinde liste sizinle gelir ve tüm gönderim kanalları aynı listeye bakar. Küçük bir ekip için bu iş SQLite tabanlı bir boru hattı ile çözülür; tablo adres, neden, durum kodu ve tarih sütunlarından oluşur.

Aşağıdaki betik standart kütüphaneyle bir DSN mesajını ayrıştırır, durum kodunu okur ve kalıcı geri dönmeyi bastırma listesine yazar. Aynı adres ikinci kez gelirse kayıt tekrarlanmaz; örnek DSN metni betiğin içinde durur, gerçek kullanımda posta kutusundan okunur.

import sqlite3
from email import message_from_string

VT = "bastirma.sqlite"


def bastirma_tablosu(baglanti):
    baglanti.execute(
        "CREATE TABLE IF NOT EXISTS bastirma ("
        "adres TEXT PRIMARY KEY, neden TEXT, durum_kodu TEXT, "
        "tarih TEXT DEFAULT CURRENT_TIMESTAMP)"
    )


def dsn_ayristir(ham):
    """Teslim durumu bildiriminden (RFC 3464) alıcıyı ve durum kodunu çıkarır."""
    ileti = message_from_string(ham)
    if ileti.get_content_type() != "multipart/report":
        return None
    for parca in ileti.walk():
        if parca.get_content_type() == "message/delivery-status":
            for alan in parca.get_payload():
                alici = alan.get("Final-Recipient", "")
                durum = alan.get("Status", "")
                if alici and durum:
                    return alici.split(";")[-1].strip().lower(), durum.strip()
    return None


def isle(baglanti, ham):
    sonuc = dsn_ayristir(ham)
    if sonuc is None:
        return "dsn değil"
    adres, durum = sonuc
    if durum.startswith("5."):
        imlec = baglanti.execute(
            "INSERT OR IGNORE INTO bastirma (adres, neden, durum_kodu) VALUES (?, ?, ?)",
            (adres, "kalici geri donme", durum),
        )
        baglanti.commit()
        return ("bastirildi: " if imlec.rowcount == 1 else "zaten listede: ") + adres
    return "gecici, yeniden denenecek: " + adres


ORNEK_DSN = """From: MAILER-DAEMON@ornek.net
To: bounce@sirket.example
Subject: Undelivered Mail Returned to Sender
MIME-Version: 1.0
Content-Type: multipart/report; report-type=delivery-status; boundary="sinir"

--sinir
Content-Type: text/plain; charset=utf-8

Teslim edilemedi.
--sinir
Content-Type: message/delivery-status

Reporting-MTA: dns; mx.ornek.net

Final-Recipient: rfc822; olmayan@ornek.net
Action: failed
Status: 5.1.1
--sinir--
"""


if __name__ == "__main__":
    vt = sqlite3.connect(VT)
    bastirma_tablosu(vt)
    print(isle(vt, ORNEK_DSN))
    print(isle(vt, ORNEK_DSN))
    print(vt.execute("SELECT adres, durum_kodu FROM bastirma").fetchall())

Gönderim listesi her kampanyadan önce bu tabloyla birleştirilir ve eşleşen adresler çıkarılır. Adımı n8n iş akışı içinde gönderim düğümünün önüne konan bir süzgeç olarak kurmak, elle atlanmasını engeller.

Türkiye'de ticari ileti kuralları ve otomasyona yansıması

Ticari elektronik ileti, Türkiye'de 6563 sayılı Elektronik Ticaretin Düzenlenmesi Hakkında Kanun'a göre alıcının önceden onayı olmadan gönderilemez ve onaylar İleti Yönetim Sistemi (İYS) üzerinden kayıt altına alınır. Bülten ve kampanya e-postaları bu kapsama girer; sipariş onayı, fatura ve parola sıfırlama gibi işlem bildirimleri ise onaya bağlı değildir. Mevzuat, teslim edilebilirlik kurallarıyla aynı yöne bakar: istenmeyen mesaj hem hukuki risk hem itibar kaybıdır.

Otomasyon tarafında bunun anlamı şudur: gönderim listesindeki her adresin onay kaynağı ve tarihi kayıtlıdır, abonelikten çıkma isteği anında bastırma listesine işlenir ve İYS'ye yansıtılır, işlem bildirimleriyle pazarlama mesajları ayrı gönderim akışlarında tutulur. KVKK açısından e-posta adresi kişisel veridir; bastırma listesi de dahil tüm kayıtlar saklama süresi ve erişim kısıtıyla korunur, işleme amacı dışındaki alanlar hiç toplanmaz.

Sonuç: kayıtları kur, listeyi temiz tut

E-posta gönderim sağlığı bir kez kurulup unutulan bir ayar değil, DNS kayıtları ve bastırma listesinden oluşan yaşayan bir düzendir. SPF kimin gönderebileceğini, DKIM mesajın bütünlüğünü, DMARC ikisinin From adresiyle hizasını denetler; bastırma listesi ise itibarı zedeleyen adreslerin listeye geri sızmasını engeller. Dört parça birlikte çalışmadığında en iyi yazılmış bülten bile okura ulaşmaz.

İlk adım olarak denetim betiğini kendi alan adınızda çalıştırın ve DMARC kaydını p=none ile raporlama adresi vererek yayına alın. Bir hafta boyunca gelen raporlar adınıza gönderen her kaynağı gösterecek; hizalama tamamlanınca politikayı sıkılaştırın ve bastırma listesini gönderim akışının önüne koyun.

Gönderim altyapınızı bu düzene taşımak ve bastırma listesini mevcut otomasyonunuza bağlamak için iletişim bölümünden yazabilirsiniz.