SMTP DSN ile Teslim ve Gecikme Bildirimlerini Programatik İşlemek
İşlemsel e-posta gönderiyorsun, uygulaman "gönderildi" diyor ama hangisinin gerçekten kutuya düştüğünü bilmiyorsun. DSN bu boşluğu kapatmak için var olan tek standart mekanizma — ama vahşi doğada yarı bozuk çalışıyor. ENVID korelasyonu, Postfix üretimi ve çalışan bir Python bounce hattı ile mühendis gerçekçiliğiyle ele alıyoruz.
EvilMail Team3 Ağustos 202613 dk okuma
50 bin işlemsel e-posta gönderdin. Uygulamanın logu her satır için 250 2.0.0 Ok, queued as ... yazıyor ve dashboard "gönderildi" diyor. Ama bu cümlenin tek anlamı şu: mesajı bir sonraki hop kabul etti. Alıcının gerçekten INBOX'ına düşüp düşmediği, yoksa iki hop sonra 550 User unknown ile mi çarptığı hakkında elinde hiçbir şey yok. Return-path sessiz, çünkü bounce'lar From adresine değil zarftaki MAIL FROM adresine dönüyor ve muhtemelen kimse o kutuyu okumuyor.
SMTP'nin bu uçurumu kapatmak için standartlaştırdığı tek in-band sinyal DSN — Delivery Status Notification. Ama beklentiyi baştan doğru kurmak lazım: DSN "teslim onayı alırım" hayali değil. Bazı parçaları büyük sağlayıcılarda pratikte ölü, bazı parçaları ise üretim kalitesinde güvenilir. İş, hangisinin hangisi olduğunu bilip yalnız güvenilir kısımları bir suppression/retry hattına bağlamakta.
DSN ne söyler, ne söylemez
DSN aslında iki RFC'nin birleşimi. RFC 3461 SMTP servis eklentisini tanımlar — yani gönderirken NOTIFY, RET, ENVID
SMTP DSN Rehberi: Teslim ve Bounce Bildirimlerini Programatik İşleme — EvilMail Blog
,
ORCPT
parametrelerini nasıl talep edeceğini. RFC 3464 ise geri dönen raporun MIME formatını, o meşhur
message/delivery-status
gövdesini tanımlar. Bir de RFC 3462/6522 (
multipart/report
sarmalayıcısı) ve RFC 3463 (enhanced status kodları, yani
5.1.1
gibi
class.subject.detail
üçlüleri) devreye girer.
DSN'in raporladığı Action değerleri beştir: failed, delayed, delivered, relayed, expanded. Buradaki en kritik dürüst uyarıyı hemen vereyim: `NOTIFY=SUCCESS` büyük sağlayıcılarda pratikte ölü. Gmail ve Microsoft 365, gizlilik ve anti-abuse gerekçesiyle başarı DSN'lerini büyük ölçüde onurlandırmaz. Success DSN gelmemesini "teslim edilmedi" diye yorumlarsan üretimde yanarsın. delivered ile relayed arasındaki ince farka da dikkat: relayed, mesajın DSN desteklemeyen bir sisteme aktarıldığı anlamına gelir, teslim garantisi vermez.
Gerçekten güvenebileceğin iki şey var: failed (FAILURE) ve delayed (DELAY). Kalıcı bir hata varsa neredeyse her uyumlu MTA sana bir bounce DSN döner. Bunlar da döngü önleme için <> null return-path ile gelir ve From başlığına değil zarf MAIL FROM adresine döner. Yani ayrık, okunabilir bir bounce kutusu kurmadıysan bu sinyalleri hiç görmezsin.
SMTP oturumunda DSN talep etmek
DSN müzakere edilen bir yetenektir. Alıcı MTA EHLO yanıtında 250-DSN satırını yayınlamıyorsa parametrelerini görmezden gelir ya da oturumu reddeder. Önce yeteneği kontrol et, sonra MAIL FROM üzerine RET ve ENVID, RCPT TO üzerine NOTIFY ve ORCPT ekle.
EHLO relay.evilmail.pro
250-mx.example.com
250-DSN
250 SIZE 52428800
MAIL FROM:<[email protected]> RET=HDRS ENVID=EVM-8842-a91f
250 2.1.0 Ok
RCPT TO:<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE ORCPT=rfc822;[email protected]
250 2.1.5 Ok
RET=HDRS sadece orijinal başlıkları geri döndür der (RET=FULL tüm gövdeyi döndürür — 40KB'lık bir e-postayı bounce'ta geri almak istemezsin). ENVID=EVM-8842-a91f ise korelasyonun bel kemiği: her giden mesaj için ürettiğin benzersiz bir kimlik. Alıcı MTA bunu DSN'de Original-Envelope-Id olarak aynen geri yankılamak zorunda. Bu id'yi göndermeden önce DB'ne yaz; DSN döndüğünde hangi mesaj/alıcı/kampanya olduğunu tek JOIN ile çözersin.
Python'da smtplib düşük seviyeli mail()/rcpt() metotlarıyla bu parametreleri geçmene izin verir (yüksek seviyeli sendmail() vermez):
ORCPT, forwarding zincirleri boyunca korunması gereken orijinal alıcıdır — alias'lı adreslerde final recipient değişse bile hangi adrese yazmaya çalıştığını bilirsin.
Dönen DSN'in anatomisi
Geri dönen bounce, Content-Type: multipart/report; report-type=delivery-status olan üç parçalı bir MIME mesajıdır:
`text/plain` — insan için açıklama ("your message couldn't be delivered..."). Parser bunu görmezden gelir.
`message/delivery-status` — makine okunur çekirdek. Bakılacak yer burası.
`message/rfc822` veya `text/rfc822-headers` — orijinal mesaj/başlıklar. ENVID kaybolduysa buradaki Message-ID fallback'in olur.
İkinci parça iki alt bloğa bölünür: mesaj başına alanlar (Reporting-MTA, Original-Envelope-Id, Arrival-Date) ve alıcı başına alanlar. Tipik bir kalıcı hata bloğu şöyle görünür:
Status alanı RFC 3463 enhanced status kodudur ve sınıflandırmanın kalbidir: 2.x.x başarı, 4.x.x geçici (retry edilebilir), 5.x.x kalıcı. Pratikte en sık gördüklerin: 5.1.1 bilinmeyen kullanıcı, 5.2.2 mailbox dolu, 5.7.1 policy reddi/yetkisiz gönderim, 4.4.7 mesaj kuyrukta beklerken süresi doldu, 4.7.x geçici reputation/policy. Diagnostic-Code ise uzak MTA'nın ham SMTP yanıtını taşır — enhanced kod belirsizse gerçek hikayeyi burada okursun. DELAY bildirimlerinde ayrıca Last-Attempt-Date ve Will-Retry-Until alanları gelir; bunlar mesajın hâlâ kuyrukta olduğunu, henüz vazgeçilmediğini söyler.
Postfix tarafında DSN üretimi
Kendi altyapını işletiyorsan Postfix NOTIFY, RET, ENVID ve ORCPT parametrelerini kutudan çıktığı hâliyle onurlandırır — özel bir şey yapman gerekmez. Asıl dikkat edeceğin ayar gecikme bildirimleri. Postfix'te delay_warning_time varsayılan olarak 0, yani DELAY DSN'leri kapalı. Bir mesaj kuyrukta takıldığında haber almak istiyorsan aç:
delay_warning_time = 4h, dört saatten uzun kuyrukta kalan mesajlar için gönderene bir "hâlâ deniyorum" DSN'i tetikler. notify_classes ise postmaster'a hangi olayların kopyalanacağını belirler; 2bounce (bounce'ın kendisinin bounce etmesi) altyapı sorunlarını erken yakalamak için değerlidir. bounce_template_file ile kendi ürettiğin bounce'ların metnini özelleştirebilirsin, ama asıl önemli nokta şu: kendi ürettiğin DSN'ler backscatter kaynağı olmasın. Bir mesajı önce kabul edip sonra bounce üretmek, sahte gönderene spam yağdırmak demektir. Reddi mümkün olduğunca SMTP oturumu sırasında (RCPT TO aşamasında 550 ile) yap, kabul-sonrası bounce'a mecbur kalma.
DSN'leri programatik işlemek
Bounce kutusuna düşen ham mesajı Python'un email modülüyle ayrıştırmak düz iş. Anahtar, walk() ile message/delivery-status parçasını bulup alt bloklarını dolaşmak:
python
import email
from email import policy
def parse_dsn(raw: bytes) -> list[dict]:
m = email.message_from_bytes(raw, policy=policy.default)
results, envid = [], None
for part in m.walk():
if part.get_content_type() == "message/delivery-status":
for block in part.get_payload(): # per-message + per-recipient
if block.get("Original-Envelope-Id"):
envid = block.get("Original-Envelope-Id")
action = block.get("Action")
if action: # sadece per-recipient bloklar
results.append({
"envid": envid,
"recipient": block.get("Final-Recipient", "").split(";")[-1].strip(),
"action": action,
"status": block.get("Status"),
"diagnostic": block.get("Diagnostic-Code"),
})
return results
def classify(rec: dict) -> str:
status = (rec.get("status") or "")
if status.startswith("5"):
return "hard_bounce" # suppression list'e yaz, bir daha gönderme
if status.startswith("4") or rec["action"] == "delayed":
return "soft_bounce" # retry sayacı + pencere
if rec["action"] in ("delivered", "relayed"):
return "delivered_stat" # istatistik; teslim KANITI değil
return "unknown"
Sınıflandırma eşikleri net: 5.x → hard bounce → adresi kalıcı suppression list'e yaz ve o adrese bir daha gönderme (aksi halde reputation'ını yakarsın). 4.x/DELAY → soft bounce → retry sayacını artır; makul bir tavan koy, örneğin 72 saatlik pencerede en fazla 4 deneme, sonra bırak. delivered/relayed → sadece metrik olarak say, teslim kanıtı sayma.
ENVID ile DB eşleştirmesi ideal yoldur. Ama bazı MTA'lar ENVID'i korumaz; o durumda üçüncü MIME parçasındaki (text/rfc822-headers) orijinal Message-ID'ye düş ve mesajı onunla bul. Bu yüzden gönderirken hem ENVID'i hem de Message-ID'yi DB'ne yazmalısın.
Return-path tasarımı ve korelasyon
İki korelasyon stratejisi var. VERP (Variable Envelope Return Path), alıcıyı doğrudan return-path'e gömer: [email protected]. ENVID'e göre avantajı, DSN gövdesi bozuk gelse bile hangi alıcı olduğunu zarftan okuyabilmen. Dezavantajı, tek gönderime birden fazla alıcı sığdıramaman. Yüksek hacimde çoğu ekip VERP + ENVID'i birlikte kullanır.
Her hâlükârda ayrık bir bounce domaini şart — bounces.evilmail.pro gibi. Bu domaine gelen postayı ya Postfix transport'unda bir pipe ile parser'a besle ya da ayrı bir mailbox'a düşürüp IMAP ile periyodik ingest et. Kritik olan, bu kutunun From başlığındaki adresten farklı olması: DSN'ler zarf MAIL FROM adresine döner, oradan okumazsan sinyali kaybedersin.
Son bir ayrım: DSN'i akrabalarıyla karıştırma. MDN (message/disposition-notification, RFC 8098) okundu bilgisidir — kullanıcının açıp açmadığı, teslimatla ilgisi yok. ARF (report-type=feedback-report, RFC 5965) ise spam şikayeti geri bildirim döngüsüdür. Üçü de multipart/report sarmalayıcısıyla gelir ama report-type farklıdır. Boru hattının başında report-type'a bakıp üçünü ayrı kanallara yönlendir; ARF'i suppression'a bağlaman gerekir, MDN'i genelde sadece istatistiğe.
Üretimde tuzaklar ve checklist
Sahada tekrar tekrar can yakan noktalar: success DSN'e teslim kanıtı diye güvenmek; toplu/newsletter trafiğinde NOTIFY'ı açık bırakıp backscatter üretmek (bunun için NOTIFY=NEVER kullan); aynı 4.x hatasını kör tekrarla retry storm'una dönüştürmek; catch-all bounce kutusunun binlerce raporla taşıp ingest'i tıkaması.
Test için swaks gönderim ucunu doğrulamaya yarar ama DSN parametreleri için birinci sınıf bayrağı yoktur; NOTIFY/RET/ENVID geçmen gerekiyorsa smtplib ya da elle bir oturum kullan:
bash
swaks --server mx.example.com --ehlo relay.evilmail.pro \
--from [email protected] --to [email protected]
# swaks DSN parametrelerini set etmez; NOTIFY/RET/ENVID icin smtplib ya da elle oturum kullan
Üretime almadan önce şunları doğrula:
ENVID her mesajda benzersiz ve gönderim anında DB'ye yazılıyor; Message-ID de fallback olarak saklanıyor.
Ayrık bounce domaini/return-path kurulu ve okunuyor (pipe veya IMAP ingest).
Sadece FAILURE + DELAY'e güven; delivered/relayed/success'i metrik say, kanıt sayma.
Suppression otomasyonu: 5.x gelen adres anında kalıcı listeye, bir daha gönderim yok.
Retry tavanı: 4.x/DELAY için sayaç + pencere (ör. 4 deneme / 72h), sonra bırak.
DSN / ARF / MDN ayrımı: report-type'a göre üç ayrı kanal.
Backscatter yok: reddi mümkünse RCPT aşamasında yap; toplu trafikte NOTIFY=NEVER.