Gelen e-postayı uygulamanıza sokmanın en hızlı yolu bir kutucuğu işaretlemektir: SendGrid Inbound Parse, Mailgun Routes, Postmark inbound. MX kaydınızı onlara verirsiniz, onlar da JSON'u endpoint'inize POST eder. Faturada görünmeyen maliyet şu: o servis, postanızı göndermeye çalışan dünyanın geri kalanına verilen SMTP cevabını da sizin adınıza veriyor. Parser'ınız iki saniye düştüğünde gönderen ne görüyor — 250 OK mu, 451 try again mı, yoksa 550 rejected mı? Bu tek cevap, o mailin size ulaşıp ulaşmayacağını belirler; ve onu üçüncü bir tarafa kiraladınız.
Gelen-posta boru hattının gerçek zorluğu MIME ayrıştırmak değildir; MIME çözülmüş bir problemdir. Zorluk, SMTP konuşmasının hangi anında hangi cevabı vereceğinize karar vermektir. Yanlış yerde 5xx dönerseniz gönderen kalıcı bounce üretir ve mail sonsuza dek kaybolur. Yanlış yerde 250 dönüp sonra webhook'u düşürürseniz gönderen "teslim edildi" görür ama mesaj sizde buharlaşır — sessiz kayıp, en kötü hata sınıfı. Kendi Postfix + parser + imzalı webhook zincirinizi kurunca üç şeyi geri alırsınız: teslimat semantiğinin kontrolü, kişisel verinin üçüncü tarafın sunucularından geçmemesi ve payload şemasında sınırsız özgürlük.
MX'i uygulamanıza değil, kabul eden bir MTA'ya yönlendirin
MX kaydına bir HTTP endpoint yazamazsınız çünkü MX bir SMTP konuşması bekler: EHLO, MAIL FROM, RCPT TO, DATA. Gönderen MTA'lar retry'larını RFC 5321 zamanlamasıyla yapar, TLS pazarlığı ister, backscatter ve spam ilk kapıda elenmelidir. Bunların hiçbiri HTTP'de yok. Araya bir MTA koyarsınız ve onun tek işi kabul katmanı olmaktır: mesajı diske alır, 250 döner, geri kalan her şeyi asenkron yapar.
DNS tarafı sıradan ama ihmal edilirse acıtır:
inbound.evilmail.pro. IN MX 10 mx1.evilmail.pro.
inbound.evilmail.pro. IN MX 20 mx2.evilmail.pro.
mx1.evilmail.pro. IN A 203.0.113.10mx1/mx2 için A kaydı yetmez — PTR (reverse DNS) de olmalı, yoksa Google ve Microsoft dahil birçok gönderen daha DATA'ya gelmeden reddeder. İkinci bir MX (20) yedeklilik verir: mx1 bakımdayken gönderenler mx2'ye düşer, mail kaybolmaz. MTA-STS ve TLSA opsiyoneldir ama inbound TLS'i zorunlu kılmak istiyorsanız değerlidir.
Postfix'ten parser'a teslim: pipe mı, LMTP mı
Postfix'in içeri aldığı mesajı parser'ınıza vermesinin iki yolu var. İlki pipe transport'u: her mesaj için bir process fork edilir ve mesaj stdin'den akıtılır.
master.cf:
inbound unix - n n - 10 pipe
flags=DRhu user=inbound argv=/opt/inbound/handle.py ${sender} ${recipient} ${size}main.cf ve transport eşlemesi:
message_size_limit = 26214400
transport_maps = hash:/etc/postfix/transport# /etc/postfix/transport → postmap ile derleyin
@evilmail.pro inbound:İşin can alıcı yeri burada: pipe process'inin exit-code'u doğrudan SMTP cevabına eşlenir. sysexits.h sözlüğü Postfix'in sözlüğüdür:
EX_OK(0) →250, mesaj teslim edildi kabul edilir ve kuyruktan silinirEX_TEMPFAIL(75) →4xx, Postfix mesajı kuyrukta tutar ve daha sonra tekrar denerEX_NOUSER(67) /EX_NOPERM(77) →5xx, gönderene kalıcı bounce üretilirEX_DATAERR(65) → kalıcı reddet
Bu tablo boru hattının anayasasıdır. Parser'ınız veritabanına yazamıyorsa 75 döner ve Postfix'in dayanıklı kuyruğu sizin için retry mekanizması olur — kendi retry kodunuzu yazmadan önce bedava kurtarma katmanı. Ama asla, uygulamanız geçici olarak yanıt vermiyor diye 67/77 dönmeyin; o kalıcı kayıptır.
Ölçekte pipe çöker. 100+ eşzamanlı teslimde her mesaj için Python yorumlayıcısını fork etmek CPU'yu yer ve postqueue -p şişer. Çözüm LMTP: parser'ı long-running bir servis olarak çalıştırıp Postfix'i içine teslim ettirin.
inbound unix - - n - - lmtptransport: @evilmail.pro lmtp:inet:127.0.0.1:24. Fork maliyeti sıfırdır, concurrency'yi servis içinde kontrol edersiniz ve DB bağlantı havuzu persistent kalır. LMTP'de de exit semantiği korunur: parser 250/4xx/5xx'i LMTP protokolü üzerinden doğrudan döner. Küçük hacimde pipe ile başlayın, ilk trafik dalgasında LMTP'ye geçin.
MIME'ı doğru ayrıştırmak
Naif yaklaşımın kırıldığı tek satır, Python'ın hangi policy'yi kullandığınızdır. email modülü hâlâ geriye dönük uyumluluk için compat32 policy'sini varsayılan tutabilir ve o policy RFC 2047 ile kodlanmış unicode header'ları — Türkçe konu satırlarını, gönderen adlarını — bozar. Modern policy.default bunları otomatik çözer.
import sys
from email import policy
from email.parser import BytesParser
msg = BytesParser(policy=policy.default).parse(sys.stdin.buffer)
subject = msg["subject"] # RFC2047 otomatik decode
body = msg.get_body(preferencelist=("plain", "html"))
text = body.get_content() if body else "" # charset otomatik çözülür
attachments = []
for part in msg.iter_attachments():
data = part.get_payload(decode=True) # bytes, base64/QP çözülmüş
if data is None:
continue
attachments.append({
"filename": part.get_filename(),
"content_type": part.get_content_type(),
"size": len(data),
"payload": data,
})get_content() charset'i kendisi çözer; get_payload(decode=True) base64 ve quoted-printable'ı sizin için açar. Charset None geldiğinde utf-8 fallback'ini errors="replace" ile yapın, aksi halde tek bozuk byte tüm mesajı düşürür. iter_attachments(), inline cid: referanslarını gövdeden ayırt eder — HTML gövdesinde gömülü görselleri ek olarak saymazsınız. Bellek koruması için message_size_limit'i Postfix tarafında 25 MiB'ye çektik; parser içinde de decode edilmiş toplam boyutu sınırlayın, açıldığında yüzlerce megabayta şişen bir arşiv LMTP servisini OOM'a sokmasın.
Webhook payload'ını imzalayın ve idempotent yapın
Uygulamanız /webhook endpoint'ine gelen POST'un gerçekten sizin parser'ınızdan geldiğini bilmeli. HMAC-SHA256 ile ham gövdeyi imzalayın:
import hmac, hashlib
sig = hmac.new(SECRET, body_bytes, hashlib.sha256).hexdigest()
headers = {
"X-EvilMail-Signature": f"sha256={sig}",
"X-EvilMail-Delivery": message_id,
"Content-Type": "application/json",
}Uygulama tarafında mutlaka hmac.compare_digest() kullanın — normal == string karşılaştırması timing saldırısına açıktır. İmzayı ham gövde bytes'ı üzerinden hesaplayın; JSON'u parse edip yeniden serialize ederseniz byte'lar değişir ve imza tutmaz.
İkinci zorunluluk idempotency. Postfix bir mesajı retry sırasında birden fazla kez teslim edebilir — parser 75 döndükten sonra tekrar denendiğinde webhook ikinci kez ateşlenir. Dedup anahtarı olarak sha256(message_id + recipient) kullanın; uygulamanız bu anahtarı daha önce gördüyse işlemeden 200 döner. Bu olmadan bir kullanıcı tek maili iki fatura, iki bildirim olarak görür.
Payload şeması sabit ve öngörülebilir olsun:
{
"message_id": "<[email protected]>",
"from": "[email protected]",
"to": "[email protected]",
"subject": "Fatura sorusu",
"date": "2026-07-04T10:22:11Z",
"text": "...",
"html": "...",
"headers": { "...": "..." },
"attachments": [
{ "filename": "fatura.pdf", "content_type": "application/pdf",
"size": 84213, "url": "https://s3.../24h-signed" }
],
"auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"spam_score": -1.2
}Ekleri inline base64 olarak gömmek payload'u şişirir ve 5 MB'lık PDF'i JSON içinde 6.67 MB'a çıkarır. Bunun yerine dosyayı geçici bir S3/MinIO bucket'ına koyup 24 saatlik imzalı URL verin. Uygulamanız gerçekten indirmek istediğinde çeker; çoğu zaman ek metadata'sı yeter.
Teslimat garantileri: accept-then-queue omurgası
Tek en önemli tasarım kararı: 250'i ne zaman dönersiniz? Cazip yanlış cevap "webhook 2xx döndükten sonra"dır — ama bu, gönderen MTA'yı sizin uygulamanızın latency'sine ve uptime'ına bağlar. Uygulamanız 30 saniye sürerse gönderen SMTP timeout'una girer ve mesajı tekrar dener; siz aynı maili üç kez alırsınız.
Doğru cevap: mesajı dayanıklı kuyruğa yazdıktan hemen sonra `250` dön, webhook'u asenkron dene. Birinci diyagramdaki mavi kutu — spool — 250'nin döndüğü yerdir. Postfix'in spool'u zaten fsync'lenmiş, güç kesintisine dayanıklı bir kuyruktur; ondan aldıktan sonra mesajı kaybetmezsiniz. Webhook başarısız olursa retry sizin iç kuyruğunuzun işidir, gönderenin değil.
Retry programı üstel geri çekilmeyle: 60s, 300s, 1800s, 7200s, 21600s, sonra dead-letter tablosu ve operatör alarmı. Webhook için 10s timeout uygulayın ve sadece 2xx'i başarı sayın. Kural nettir: 5xx = kalıcı fail, dead-letter'a git; 4xx ve timeout = retry. 3xx redirect'leri takip etmeyin — webhook endpoint'i yönlendirme yapmamalı.
Postfix'in kendi kuyruk ömrünü de ayarlayın ki mesajlar sonsuza dek asılı kalmasın:
maximal_queue_lifetime = 5d
bounce_queue_lifetime = 5dInbound kimlik doğrulama: SPF/DKIM/DMARC'ı payload'a taşıyın
Gelen mailin from başlığına güvenip aksiyon almak — "bu adres CEO'muzunki, ödemeyi onayla" — spoofing'e açık kapıdır. From: başlığı serbest metindir, kimse doğrulamaz. Doğrulanan şey SPF (zarf gönderen IP'si), DKIM (imza) ve DMARC (ikisinin hizası) sonuçlarıdır.
En temiz kurulum: opendkim veya rspamd'yi Postfix'in önünde çalıştırın; bunlar mesaja Authentication-Results: başlığı ekler. Parser bu başlığı authres ile ayrıştırıp sonucu payload'a koyar. Forwarding'e karşı ekstra güven istiyorsanız parser içinde dkimpy ile imzayı yeniden doğrulayabilirsiniz, ama pratikte MTA önündeki doğrulama yeterli ve daha hızlıdır.
Payload'a "auth": {"spf": "pass", "dkim": "pass", "dmarc": "pass"} koyun ve kararı uygulamaya bırakın. Altın kural: `dmarc != pass` ise uygulama otomatik aksiyon almasın — sadece kaydetsin. Bir insana veya moderasyon kuyruğuna düşsün. Forwarding senaryolarında (mailing list, alias) SPF kırılır ama DKIM ve varsa ARC zinciri korunur; bu yüzden bir mail forward edilmişse ARC başlıklarını da payload'a dahil edip uygulamanın "bu zincirin bir noktasında doğrulanmış" bilgisine erişmesini sağlayın.
Üretim kontrol listesi
- PTR + A + MX üçlüsünün ikisi de (
mx1,mx2) tam ve tutarlı — reverse DNS eksikse gönderenlerRCPT'te reddeder. - Parser
user=inboundunprivileged çalışır; systemd sandbox ile sıkın:NoNewPrivileges=yes,ProtectSystem=strict,PrivateTmp=yes. - Exit-code sözlüğü kodda sabit: DB hatası →
75, bilinmeyen alıcı →67, geçici hatada asla5xxyok. 250'yi yalnızca dayanıklı kuyruğa yazdıktan sonra dönün — webhook2xx'ini beklemeyin.
Bu zinciri kurduğunuzda gelen-posta artık üçüncü bir tarafın uptime'ına ve iyi niyetine bağlı bir kara kutu değil; SMTP cevabından webhook imzasına kadar her baytı sizin denetlediğiniz bir boru hattıdır. İlk trafik dalgası geldiğinde farkı, mail kaybetmeyerek anlarsınız.


