IMAP IDLE'ın İçi: Gerçek Zamanlı Push'un Protokol Mekaniği
Polling her 60 saniyede bir sunucuyu döven bir israftır. IMAP IDLE bu döngüyü tersine çevirir ama bir push protokolü değildir. Ham telnet diyaloğu, 29 dakika kuralı, NAT ölümleri ve NOTIFY/QRESYNC/APNS/JMAP'in devreye girdiği nokta — bir mail altyapısı mühendisinin gözünden.
EvilMail Team1 Ağustos 202612 dk okuma
Bir istemcinin yeni postayı fark etmesi için elinde iki yol var: ya durup sorar, ya da sunucunun konuşmasını bekler. Sorma yolu, yani polling, şöyle görünür: her 60 saniyede bir SELECT veya STATUS at, sayıya bak, değişmişse çek. Tek kullanıcıda zararsız. 10.000 kullanıcıda saniyede yaklaşık 166 gereksiz komut demek — çoğu "hayır, değişen bir şey yok" cevabıyla dönen boş sorgular. Mobilde durum daha kötü: her sorgu telefonun radyosunu uykudan uyandırır, pili yakar, hücresel bağlantıyı sızdırır.
IMAP IDLE'ın tek işi bu döngüyü çöpe atmaktır. RFC 2177 (1997, topu topu üç sayfalık bir doküman) istemciye şunu der: "Sor demeyi bırak. Bekle. Kutuda bir şey değişince ben sana söylerim." İstemci bir komut verir, sunucu bağlantıyı açık tutar ve kutu durumu değiştiğinde etiketsiz (untagged) yanıtları kendisi iter.
Ama net olalım: IDLE bir "push protokolü" değildir. APNS gibi bir bildirim ağı, JMAP gibi bir olay akışı değildir. IDLE, tek bir seçili kutuya kilitlenmiş, kırılgan ve ölçeklenmesi pahalı bir bekleme durumudur. Doğru çalıştırmak için tam olarak nasıl işlediğini ve nerede sessizce koptuğunu bilmek gerekir. Ve IDLE'ı kullanmadan önce sunucunun CAPABILITY yanıtında IDLE görmen şart — yoksa komut hata döner.
IDLE el sıkışması: ham diyalog
Gerçek çıktıya bakalım. TLS üstünden 993 portuna elle bağlanıp oturumu adım adım sürelim:
IMAP IDLE ve Push Bildirimleri: Protokol Mekaniği | evilmail.pro — EvilMail Blog
a1 LOGIN [email protected] gizli-parola
a1 OK [CAPABILITY IMAP4rev1 IDLE CONDSTORE QRESYNC ...] Logged in
a2 SELECT INBOX
* 3 EXISTS
* 0 RECENT
* OK [UIDVALIDITY 1699887654] UIDs valid
* OK [UIDNEXT 4] Predicted next UID
a2 OK [READ-WRITE] Select completed
a3 IDLE
+ idling
+ idling bir *continuation* yanıtıdır — sunucu "ağzımdan çıkanı dinle, artık ben konuşacağım" der. Bu noktadan sonra istemci hiçbir komut gönderemez. Tek gönderebileceği şey DONE'dır. Şimdi başka bir oturumdan bu kutuya bir mail düşsün:
* 4 EXISTS
* 1 RECENT
Sunucu kendiliğinden konuştu. Dikkat: * 4 EXISTS sadece "kutuda artık 4 mesaj var" der. Hangi mesaj olduğunu, UID'ini, göndereni söylemez. Bu yüzden EXISTS'i gören istemcinin bir sonraki işi IDLE'dan çıkıp UID FETCH ile o yeni mesajı çekmektir. Kullanıcı bir maili okunmuş işaretlerse bayrak değişimi gelir:
* 2 FETCH (FLAGS (\Seen))
Başka bir cihazdan bir mesaj silinirse:
* 3 EXPUNGE
Akışı durdurmak için istemci etiketsiz, düz metin bir satır yollar — tag yok, tırnak yok, sadece dört harf:
DONE
a3 OK IDLE terminated
Ve döngü yeniden başlar: a4 IDLE → + idling → bekle. IDLE bir "aç ve unut" durumu değil, sürekli yeniden kurulması gereken bir döngüdür.
29 dakika kuralı ve bağlantının sessiz ölümü
RFC 3501 sunucuya, 30 dakika hareketsizlikten sonra bağlantıyı kapatma iznini açıkça verir. Bu yüzden IDLE'da bekleyen bir istemci en geç 29 dakikada bir döngüyü yenilemek zorundadır: DONE yolla, a OK al, hemen yeni bir IDLE aç. Bunu yapmazsan sunucu bir gün seni sessizce düşürür ve bir daha posta bildirimi almazsın.
Asıl katil ise 30 dakikayı beklemez. Ev ve mobil ağlardaki NAT tabloları ile güvenlik duvarları, üzerinden veri akmayan bir TCP oturumunu çoğunlukla 5-15 dakika içinde tablolarından siler. Kötüsü, ne istemci ne sunucu bundan haberdar olur. Bağlantı yarı-açık hale gelir — istemci hâlâ dinlediğini sanır, sunucu paket gönderse de FIN/RST geri dönmez, iki taraf da ölü bir tünele bakar. Yeni mail düşer, sunucu * EXISTS iter, paket NAT'ta düşer, istemci hiçbir şey görmez.
Dovecot bunun çözümünü imap_idle_notify_interval ile sunar (varsayılan 2 dakika). Bu aralıkta sunucu, hiçbir değişiklik olmasa bile periyodik olarak * OK Still here satırını iter. Bu iki işi birden yapar: tünelden veri aktığı için NAT eşlemesini canlı tutar ve yarı-açık TCP'yi erken tespit etmeye yardım eder. Buna ek olarak çekirdek seviyesinde TCP keepalive de devrede olmalı. İstemci tarafındaki ders açık: gelen * OK satırlarını sessizce yut, onları "değişiklik" sanma; ama gelmemeye başlarlarsa bağlantının öldüğünü anla ve yeniden kur.
IDLE'ın kör noktası: tek kutu, tek bağlantı
IDLE'ın en sinsi kısıtı burada. IDLE yalnızca o an SELECT edilmiş kutuyu izler. INBOX'ı izlerken aynı anda "Arşiv", "Gönderilenler" ve üç etiket klasörünü de izlemek istiyorsan tek çözüm var: her kutu için ayrı bir TCP bağlantısı açıp her birinde ayrı bir IDLE döngüsü sürmek. Yani INBOX + 5 klasör = kullanıcı başına 6 kalıcı bağlantı.
Ölçek matematiğini çıkaralım. 10.000 kullanıcı × ortalama 3 cihaz × izlenen 2 kutu = 60.000 kalıcı TCP bağlantısı. Her biri sunucuda bir dosya tanıtıcısı (fd), genellikle bir imap process ve kendi bellek payıdır. Darboğaz CPU değil — IDLE'da bekleyen bir bağlantı neredeyse hiç işlemci harcamaz. Darboğaz boşta duran bağlantıların birikmesidir: ulimit -n ile belirlenen fd limiti, service imap { process_limit }, login process sayısı ve toplam RAM. IDLE bu yüzden "hafif ama ölçeklenmesi pahalı" diye anılır — tek tek ucuz, toplamda pahalı.
IDLE'ın ötesi: ne zaman neye geçmeli
On binlerce kullanıcıda tek-kutu-tek-bağlantı modeli duvara toslar. Devreye giren alternatifleri ne zaman seçeceğinle birlikte sıralayalım:
NOTIFY (RFC 5465): IDLE'ın tek-kutu kısıtını kaldırır. Tek bir bağlantıda birden çok kutuyu ve daha granüler olayları (yeni mesaj, bayrak, taşıma) izleyebilirsin. Kâğıt üzerinde IDLE'ın halefi; pratikte sunucu ve istemci desteği hâlâ seyrek, o yüzden CAPABILITY'de görmeden güvenme.
CONDSTORE / QRESYNC (RFC 7162): Bunlar IDLE'ın yerine geçmez, onu tamamlar. Her mesaja bir MODSEQ verir; bağlantı koptuktan sonra tekrar bağlanınca tüm kutuyu baştan senkronlamak yerine SELECT ... (QRESYNC ...) ile sadece son gördüğün MODSEQ'ten beri değişen deltayı çekersin. Reconnect maliyetini uçurumdan indirir.
Apple XAPPLEPUSHSERVICE + APNS: Oyunu tamamen değiştirir. Sunucu, kutu değişikliğini Apple'ın push ağına iter; iOS Mail hiçbir kalıcı IMAP bağlantısı tutmaz. Cihaz uykuda, radyo kapalı, pil dinç — bildirim geldiğinde uyanıp çeker. Dovecot bunu push notification eklentisiyle destekler.
JMAP (RFC 8620 core + RFC 8621 mail): Bu işi protokol seviyesinde çözer. HTTP üzerinde çalışır, değişiklikleri EventSource (SSE) veya WebPush ile iter, delta senkronu Email/changes ile yapar. Yeni bir istemci sıfırdan yazıyorsan doğru yön budur; IDLE'ın tüm bağlantı ve NAT dertlerini baştan yok eder.
evilmail.pro bağlamında pratik desen şu: temp-email tarafında gelen postayı saniyeler içinde arayüze yansıtmak için sunucu içinde tek bir IDLE dinleyici çalıştırıp, oradan tarayıcıya SSE/WebSocket köprüsü kurmak en sağlamı. Binlerce tarayıcıya IMAP bağlantısı açtırmak yerine bir dinleyici IMAP'ı konuşur, ön yüz sadece hafif bir olay akışı tüketir.
Dovecot ayarları ve dinleyici iskeleti
Sunucu tarafındaki kritik parametreler:
# /etc/dovecot/conf.d/20-imap.conf
protocol imap {
imap_idle_notify_interval = 2 mins # * OK Still here → NAT/TCP keepalive
mail_max_userip_connections = 20 # cihaz x kutu fan-out için başparmak kuralı
}
service imap {
process_limit = 4096 # eşzamanlı IDLE bağlantı tavanı
}
# fd limitini bağlantı sayısına göre boyutlandır
# ulimit -n → 60k bağlantı hedefliyorsan 65535 az gelir
Sunucu içi dinleyicinin görevi basit: EXISTS'i yakala, yeni UID'i FETCH ile çek, ön yüze it. imapflow ile iskelet:
javascript
import { ImapFlow } from 'imapflow';
const client = new ImapFlow({
host: 'mail.evilmail.pro', port: 993, secure: true,
auth: { user, pass }
});
await client.connect();
await client.mailboxOpen('INBOX');
// EXISTS geldiğinde tetiklenir — sadece "artık N mesaj var" der
client.on('exists', async (data) => {
// yeni gelen aralığı çek (data.count = yeni toplam)
for await (const msg of client.fetch(
`${data.prevCount + 1}:*`,
{ uid: true, envelope: true, flags: true }
)) {
// imapflow: envelope/flags undefined olabilir → optional chaining şart
pushToFrontend({
uid: msg.uid,
from: msg.envelope?.from?.[0]?.address ?? null,
subject: msg.envelope?.subject ?? '(konusuz)',
seen: msg.flags?.has('\\Seen') ?? false
});
}
});
// imapflow IDLE döngüsünü kendi yönetir; sen sadece dinle
await client.idle();
client.idle() çağrısı imapflow'un IDLE'ı açıp süresi dolmadan yenilemesini, * OK satırlarını yutmasını ve kopmada yeniden bağlanmasını üstlenir — yine de production'da bunun etrafına kendi reconnect/backoff mantığını sarmak akıllıca. Python tarafında aynı işi IMAPClient kütüphanesinin idle() desteğiyle yaparsın; mantık birebir aynıdır: EXISTS'i yakala, UID FETCH at, delta'yı ilet.
Pratik kontrol listesi
IDLE'ı 29 dakika sınırına dayanma — güvenli tarafta ~10 dakikada bir DONE + yeni IDLE ile yenile.
* OK Still here ve keepalive satırlarını sessizce yut; onları değişiklik sanma, ama gelmeyi bırakırlarsa bağlantıyı ölü kabul et.
DONE sonrası makul sürede OK gelmezse bağlantıyı kapat ve yeniden kur; yarı-açık tünelde beklemeye devam etme.
Her izlenen kutu için ayrı bağlantı bütçesi ayır; fan-out'u (kullanıcı × cihaz × kutu) önceden hesapla.
ulimit -n, process_limit ve mail_max_userip_connections'ı hedef bağlantı sayısına göre önceden boyutlandır.
Reconnect'te tam SELECT yerine QRESYNC ile sadece delta çek; MODSEQ'ini kalıcı sakla.
Mobil istemci için mümkünse kalıcı bağlantıyı bırak, APNS ya da JMAP push'a devret.
CAPABILITY'de IDLE, NOTIFY, QRESYNC desteğini çalışma zamanında kontrol et — varsayma; sunucular arasında farklıdır.
IDLE 1997'de doğdu ve hâlâ ayakta, çünkü küçük ölçekte kusursuz çalışıyor. Ama ölçek büyüdükçe onun bir bekleme durumu olduğunu, gerçek bir push kanalı olmadığını hatırlamak gerekiyor. Bağlantıları saymaya başladığın gün, cevap NOTIFY, QRESYNC ve push-offload'da yatıyor.