Bir cron job düşünün: gece 03:00'te a FETCH 1:* (RFC822) çalıştırıyor, 50.000 mesajın tamamını ham olarak indiriyor, 6 GB trafik yakıyor, gelen kutusunun her mesajını \Seen yapıyor ve Dovecot'un index'ini uzun bir okuma kilidinde tutuyor. Sabah kullanıcı gelen kutusunu açıyor, hepsi okunmuş görünüyor. "Neden hepsi okundu?" ticket'ı açılıyor. Tek suçlu, IMAP'in verdiği araçları kullanmamak.
IMAP4rev1 (RFC 3501) ve halefi IMAP4rev2 (RFC 9051) tam da bu problemi çözmek için tasarlandı. Protokol size üç ayrı kaldıraç verir: sunucu tarafında filtreleme (SEARCH), gövdeye dokunmadan meta veri (ENVELOPE ve BODYSTRUCTURE) ve tek bir MIME parçasını byte aralığıyla çekme (BODY.PEEK[part]<offset.length>). Marifet bunları doğru sırada zincirlemekte: önce daralt, sonra sadece gerekeni, sadece bir kez indir. Mail istemcisi, senkronizasyon servisi ya da deliverability aracı yazıyorsanız IMAP sihir değil, byte cinsinden ölçülebilir bir protokoldür — yeter ki sözleşmesini okuyun.
Sequence number vs UID: ilk ve en kritik karar
Bir posta kutusundaki her mesajın iki adresi vardır. Sequence number, kutudaki sıralı pozisyondur (1, 2, 3...) ve bir mesaj EXPUNGE ile silindiğinde kayar — 5 numaralı mesaj silinince eski 6 numara 5 olur. UID ise UIDVALIDITY değeri sabit kaldığı sürece kalıcıdır; mesaj hiçbir zaman başka bir UID almaz, silinen UID tekrar kullanılmaz.
Sonuç net: ciddi hiçbir istemci sequence number ile durum saklamaz. a FETCH 5 ... komutunuz ile sunucunun onu işlemesi arasında bir EXPUNGE gelirse yanlış mesajı çekersiniz. Bu yüzden buradaki her komut UID öneki taşır. UIDVALIDITY değişirse — ki bu, kutunun silinip yeniden yaratıldığı anlamına gelir — tüm client cache'iniz anında geçersizdir; UID'ler artık başka mesajlara işaret eder. Bu değeri SELECT/EXAMINE yanıtından okuyup cache anahtarınıza gömün.
Oturum, SELECT vs EXAMINE ve iki adres uzayı
Port 993 implicit TLS'tir (bağlantı anında şifreli), 143 STARTTLS ile yükseltilir. Ham bir oturumu openssl s_client ile açalım; protokolü çıplak görmek herhangi bir kütüphaneden daha öğreticidir:
openssl s_client -connect imap.evilmail.pro:993 -crlf
a1 LOGIN [email protected] parola
a2 EXAMINE INBOX
* 50000 EXISTS
* OK [UIDVALIDITY 1720051200] UIDs valid
* OK [UIDNEXT 4021] Predicted next UID
* OK [HIGHESTMODSEQ 90210] Highest
a2 OK [READ-ONLY] EXAMINE completedEn kritik tercih SELECT yerine EXAMINE. SELECT INBOX kutuyu yazılabilir açar: \Seen bayrağı setlenebilir, EXPUNGE üretilebilir. EXAMINE INBOX ise salt-okunur açar — hiçbir bayrağa dokunulmaz, hiçbir şey silinmez. Tarama, analiz, indexleme veya deliverability denetimi yapıyorsanız doğru cevap her zaman EXAMINE'dir; kutuyu değiştirme niyetiniz yoksa yazılabilir açmanın hiçbir gerekçesi yok.
Yanıttaki üç satırı saklayın: UIDVALIDITY cache geçerliliği için, UIDNEXT yeni mesaj tespiti için (bir sonraki mesaj bu UID'yi alacak), HIGHESTMODSEQ ise birazdan göreceğimiz artımlı senkronizasyon için.
ENVELOPE: mesaj gövdesine dokunmadan başlık özeti
Bir mesajın kim/kime/ne zaman/hangi konu bilgisini almak için gövdeyi indirmeye gerek yok. ENVELOPE, sunucunun kendi index'inden okuduğu, parse edilmiş bir başlık özetidir:
a3 UID FETCH 4012 (ENVELOPE)Dönen yapı tam 10 alanlı, sıra-bağımlı, pozisyonel bir parantezli listedir — isimli JSON değil. Alanlar her zaman şu sırada gelir: date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, message-id. Bir alan yoksa değeri NIL'dir (boş string ya da atom değil, kelimenin tam anlamıyla NIL). Örneğin sender başlığı mesajda yoksa sunucu from değerini oraya kopyalar; bcc neredeyse her zaman NIL döner.
ENVELOPE'un sınırı, sadece o 10 alanı taşımasıdır. Deliverability işi yapıyorsanız Authentication-Results, DKIM-Signature, List-Unsubscribe ya da Received zinciri gerekir — bunlar ENVELOPE'ta yoktur. O durumda BODY.PEEK[HEADER.FIELDS (...)] ile tam istediğiniz başlıkları çekin, ne fazlası ne eksiği.
SEARCH: filtrelemeyi sunucuya bırak
Ağa hiç mesaj indirmeden 50.000'i 40'a düşürmenin yolu SEARCH'tür. Kriterler boşlukla ayrılır ve AND'lenir; OR ve NOT prefix operatörlerdir:
a4 UID SEARCH SINCE 1-Jul-2026 UNSEEN FROM "billing@" NOT SUBJECT "digest"
* SEARCH 4012 4015 4018 4019
a4 OK Search completedTemel kriterler: tarih (SINCE/BEFORE/ON, INTERNALDATE tabanlı — yani mesajın sunucuya ulaşma zamanı, Date: başlığı değil; Date: başlığına bakmak için SENTSINCE), boyut (LARGER/SMALLER <byte>), bayrak (SEEN/UNSEEN/FLAGGED/ANSWERED/DELETED/DRAFT), başlık (HEADER Message-ID "<...>") ve tam metin (TEXT/BODY, sunucunun FTS desteğine bağlı — Dovecot'ta genelde bir arama eklentisi gerekir). Tarih formatı 01-Jul-2026
İki tuzak. Birincisi: 200.000 sonuçlu bir arama, 200.000 UID'lik dev bir satır döndürür. ESEARCH (RFC 4731) bunu çözer:
a5 UID SEARCH RETURN (COUNT MIN MAX) SINCE 1-Jul-2026
* ESEARCH (TAG "a5") UID COUNT 200000 MIN 4012 MAX 51988Koca dizi yerine tek tamsayı. Sayfalama ya da sayaç için idealdir. İkinci tuzak Türkçe'ye özgü: ASCII olmayan bir konuyu (SUBJECT "ödeme") aramadan önce charset belirtin — a UID SEARCH CHARSET UTF-8 SUBJECT "ödeme". Charset belirtmezseniz ya da sunucu desteklemiyorsa ya BAD alırsınız ya da eşleşmez; bu davranışı CAPABILITY üzerinden test edin.
FETCH'i verimli kullanmak: PEEK, BODYSTRUCTURE, kısmi çekme
Daralttıktan sonra, çekme aşamasının üç demir kuralı var.
Kural 1 — her zaman `BODY.PEEK[...]`. BODY[...] yan etki olarak \Seen bayrağını setler; BODY.PEEK[...] setlemez. Analiz ya da tarama yaparken kullanıcının gelen kutusunu okunmuş yapmak istemezsiniz. EXAMINE zaten yazmayı engeller, ama alışkanlık olarak her zaman PEEK yazın.
Kural 2 — gövdeden önce `BODYSTRUCTURE` al. MIME ağacını indirmeden öğrenirsiniz: hangi parça text/plain, hangisi text/html, ekler nerede, parça numaraları ne (1, 2.1, 1.2). Böylece 4 MB'lık PDF eki yerine sadece 1 numaralı text/plain parçasını hedefleyebilirsiniz.
Kural 3 — büyük parçaları byte aralığıyla çek. <offset.length> sözdizimiyle bir parçanın yalnızca bir dilimini alırsınız.
Bunları tek round-trip meta veri + hedefli çekme olarak zincirleyin:
a6 UID FETCH 4012:4020 (UID FLAGS INTERNALDATE RFC822.SIZE ENVELOPE BODYSTRUCTURE)
a7 UID FETCH 4012 BODY.PEEK[1]<0.8192>a6 bir aralık için tüm meta veriyi tek komutta getirir. Aralık sözdizimi esnektir: 4000:4020 (aralık), 4000:* (4000'den sona), 1,3,5:9 (karışık). a7 ise yalnızca ilk parçanın ilk 8 KB'ını çeker — bir önizleme ya da spam sınıflandırması için fazlasıyla yeterli, \Seen'e dokunmadan. Round-trip gecikmesini gizlemek için pipelining kullanın: birden çok tag'i yanıtları beklemeden art arda yollayın, sunucu sırayla yanıtlar.
Rakamlarla: 50.000 mesaj × ~120 KB ortalama = ~6 GB tam indirme. Aynı kutu ENVELOPE + FLAGS + BODYSTRUCTURE ile ~30–60 MB meta veri, yani yaklaşık 100:1 azalma. SEARCH ile önce daralttıysanız gerçekten indirdiğiniz gövde sayısı yüzlerce değil, onlarcadır.
Artımlı senkronizasyon: CONDSTORE / QRESYNC ile MODSEQ
İlk senkronizasyondan sonra her seferinde kutuyu baştan taramak israftır. CONDSTORE ve QRESYNC (RFC 7162) tam bunun içindir. Her mesajın bir MODSEQ değeri vardır; kutunun HIGHESTMODSEQ'ini SELECT yanıtından saklar ve bir sonraki sefer yalnızca o değerden beri değişenleri istersiniz:
a8 ENABLE QRESYNC
a9 EXAMINE INBOX (QRESYNC 1720051200 90210)
* VANISHED (EARLIER) 4001,4005:4009
* 5 FETCH (UID 4015 FLAGS (\Seen \Answered) MODSEQ (90455))
a10 UID FETCH 1:* (FLAGS) (CHANGEDSINCE 90210 VANISHED)a9'daki QRESYNC (UIDVALIDITY HIGHESTMODSEQ) parametresiyle sunucu size iki şeyi tek atışta verir: son senkrondan beri silinen UID'ler (VANISHED (EARLIER)) ve bayrağı değişen mesajlar. Client tarafında tam diff hesaplamanıza gerek kalmaz. Maliyet kutu boyutuyla değil değişiklik sayısıyla orantılıdır — O(değişiklik). Bir mobil istemcide bu, her açılışta 6 GB yerine birkaç KB fark eder; pil ve mobil veri planı doğrudan bu tercihe bağlıdır. CAPABILITY yanıtında QRESYNC/CONDSTORE yoksa, UIDNEXT tabanlı "yeni mesaj var mı" kontrolü artı periyodik bayrak taramasıyla fallback yaparsınız.
imapflow ile üretim kodu (Node/TS)
evilmail'in tarama altyapısı imapflow üzerine kurulu. Ham komutları elle yazmak yerine kütüphane pipelining, literal handling ve QRESYNC'i sizin için yönetir. Kritik nokta, EXAMINE karşılığı olan readOnly bayrağı ve bir tip tuzağı:
import { ImapFlow } from 'imapflow';
const client = new ImapFlow({
host: 'imap.evilmail.pro', port: 993, secure: true,
auth: { user, pass },
});
await client.connect();
// readOnly: true -> EXAMINE, \Seen bayrağı asla setlenmez
const lock = await client.getMailboxLock('INBOX', { readOnly: true });
try {
const uids = await client.search({ since: new Date('2026-07-01'), seen: false }, { uid: true });
for await (const msg of client.fetch(uids, {
uid: true, envelope: true, flags: true, bodyStructure: true,
}, { uid: true })) {
// TUZAK: msg.envelope ve msg.flags undefined olabilir
const subject = msg.envelope?.subject ?? '(konu yok)';
const isSeen = msg.flags?.has('\\Seen') ?? false;
}
// sadece text/plain parçasını stream olarak çek
const { content } = await client.download('4012', '1', { uid: true });
} finally {
lock.release();
await client.logout();
}imapflow'un tip tanımlarında msg.envelope ve msg.flags opsiyoneldir — bazı sunucu yanıtlarında gerçekten gelmeyebilirler. Optional chaining (?.) kullanmadan msg.envelope.subject yazarsanız TypeScript uyarır; derlemeyi zorlarsanız runtime'da undefined patlaması yersiniz. imapflow ile üretimde en sık düşülen tuzak budur.
Pre-flight kontrol listesi
- `EXAMINE` kullan (imapflow'da
readOnly: true). Kutuyu değiştirmiyorsan yazılabilir açma. - UID'ye kilitlen. Sequence number'ı asla state olarak saklama; her komuta
UIDöneki koy. - `SEARCH`'ü `RETURN (COUNT MIN MAX)` ile ölç. Dev UID dizisi çekmeden önce boyutu öğren.
- `BODYSTRUCTURE`'ı gövdeden önce çek. MIME ağacını gör, sadece istediğin parça numarasını hedefle.
- Her zaman `BODY.PEEK[...]`.
BODY[...]sessizce\Seensetler. - Büyük parçaları `<offset.length>` ile dilimle. Önizleme için ilk 8 KB genelde yeter.
- Artımlı git: `CHANGEDSINCE` / `QRESYNC`. İlk senkrondan sonra tam tarama yasak.
- `UIDVALIDITY` değişince cache'i tamamen at.
Bu dokuz madde, "kolay" FETCH 1:* RFC822 ile ölçülebilir bir tarama motoru arasındaki tüm farktır. IMAP sihir değil; sözleşmesini okuyup doğru sırada çağırdığınızda, 50.000 mesajlık bir kutuyu tek TCP oturumunda, \Seen'e dokunmadan, tam indirmenin onda biri trafiğiyle tararsınız.


