Deneyiminizi geliştirmek için çerezler kullanıyoruz. Bu siteyi ziyaret etmeye devam ederek çerez kullanımımızı kabul etmiş olursunuz.Gizlilik Politikası

EvilMailEvilMail
ÖzelliklerEvilMail ile yapabileceğiniz her şeyŞunlar için Geçici Mail…Discord, ChatGPT ve daha fazlası için geçici mailAğ AraçlarıDNS, WHOIS, port ve ağ araçlarıKarşılaştırEvilMail ile diğer geçici mail servislerinin karşılaştırması
APIREST API referansı ve rehberleriGeçici Mail APIHer dilde kopyala-yapıştır kod
UygulamalarFiyatlarBlog
HakkımızdaBiz kimiz ve neler geliştiriyoruzGüven ve GüvenlikVerilerinizi nasıl koruyoruzSSSSık sorulan soruların yanıtlarıİletişimEkibimizle iletişime geçin
GirişKayıt Ol
EvilMailEvilMail
UygulamalarFiyatlarBlog
GirişKayıt Ol
Ana SayfaBlogIMAP'ten Otomasyon Kurmak: Maili Çekmek Kolay, İki Kez İşlememek Zor
Geliştiriciler

IMAP'ten Otomasyon Kurmak: Maili Çekmek Kolay, İki Kez İşlememek Zor

IMAP kutusundan mail çekmek üç satır kod. Onu gece 03:00'te kimse uyanmadan çalışan, aynı maili iki kez işlemeyen bir servise çevirmek asıl mühendislik. UIDVALIDITY, idempotent tüketim, MIME ağacı ve IDLE + reconnect döngüsünü Node.js ve Python'da yan yana kuruyoruz.

EvilMail Team
2 Ağustos 2026
14 dk okuma
IMAP'ten Otomasyon Kurmak: Maili Çekmek Kolay, İki Kez İşlememek Zor
İçindekiler
IMAP gerçekte nasıl bir veri modeliBağlantı ve kimlik doğrulamaYeni mesajları güvenilir yakalamaMIME'ı doğru ayrıştırma — asıl acı buradaDedup: neden hem bayrak hem tabloGerçek zamanlı: IDLE ve reconnect döngüsüÜretim sertleştirme ve sınırlarÜretime çıkmadan hızlı kontrol listesi

Bir kutuya düşen mailleri otomatik işleyen bir servis kuruyorsun: gelen faturaları muhasebeye, başvuru maillerini bir kuyruğa, sipariş bildirimlerini bir webhook'a çeviriyorsun. En saf hali üç satır:

text
1. UNSEEN olanları çek
2. Ayrıştır (parse)
3. İşi yap, \Seen'e işaretle

Bu üç satır demoda kusursuz çalışır. Üretimde tam olarak üç yerden patlar: \Seen bayrağına güvenmek, UIDVALIDITY'yi yok saymak ve MIME gövdesini string olarak kesmeye çalışmak. Bağlanmak zaten kolay — asıl mühendislik durum takibi ve ayrıştırma. Bu yazı o üç patlamayı sırayla kapatıp, gece 03:00'te kimse uyanmadan çalışan idempotent bir tüketici kuruyor. Node.js (imapflow + mailparser) ve Python (IMAPClient + email) tarafını yan yana veriyorum.

IMAP gerçekte nasıl bir veri modeli

Bir mailbox'ı SELECT ettiğinde sunucu sana üç kritik sayı döner: UIDVALIDITY, UIDNEXT ve EXISTS. Buradaki en yaygın hata, mesajlara sequence number ile atıfta bulunmaktır. Sequence number oturum içinde kayar: 5 numaralı mesaj silinince eski 6 numara 5 olur. Bir sonraki turda "5'ten sonrasını al" dersen yanlış maili işlersin.

UID ise kalıcıdır — ta ki UIDVALIDITY değişene kadar. Otomasyonun tek doğru çapası UID'dir. Mantık şu: en son gördüğün UID'i kalıcı olarak sakla, her turda UID SEARCH UID last_uid+1:* ile yalnızca yenileri çek. Bütün kutuyu tarama, EXISTS sayısını sayma; sadece deltayı al.

UIDVALIDITY değişirse — kutu silinip yeniden oluşturulmuş, taşınmış ya da sunucu tarafında yeniden indekslenmişse — elindeki tüm UID cache'i çöp olur. Eski last_uid=5000, yeni dünyada bambaşka bir maile denk gelir. Kural net: her tur başında sakladığın uidvalidity ile sunucunun döndürdüğünü karşılaştır; eşleşmiyorsa cache'i sıfırla, last_uid=0 yap ve full resync'e gir.

\Seen bayrağına gelince: bu bayrak idempotency için felakettir. Kullanıcı telefonundaki mail uygulamasıyla kutuya bakarsa maili "okundu" yapar; senin işleyicin onu "işlenmiş" sanıp atlar. Ya da tam tersi. \Seen insan tüketiminin işaretçisidir, senin değil. Bunun yerine kendi özel keyword'ünü kullan: $Processed. Ve okuma yaparken mutlaka BODY.PEEK[] kullan — BODY[] maili \Seen yapar, BODY.PEEK[] yapmaz.

Bağlantı ve kimlik doğrulama

Üretimde daima 993/TLS (implicit TLS). 143 + STARTTLS hâlâ RFC'de var ama downgrade saldırı yüzeyi ve yanlış yapılandırma riski taşır; yeni bir servis kuruyorsan 143'e hiç bakma.

Bir şey ters gittiğinde en hızlı teşhis, protokolü elle konuşmaktır:

bash
openssl s_client -connect imap.gmail.com:993 -crlf
# bağlantı açılınca:
a LOGIN kullanici parola
a SELECT INBOX
# → * OK [UIDVALIDITY 1712345678] ...
# → * OK [UIDNEXT 5001] ...
a UID SEARCH UID 5000:*
# → * SEARCH 5000 5001 5002
a UID FETCH 5001 (BODY.PEEK[] FLAGS)

Bu çıktı, kütüphanenin arkasında ne olduğunu birebir gösterir; bir hatayı ayıklarken kütüphane katmanını atlayıp doğrudan buraya inmek çoğu zaman en kısa yoldur.

Node.js tarafında imapflow ile bağlantı ve mailbox açma:

javascript
import { ImapFlow } from 'imapflow';

const client = new ImapFlow({
  host: 'mail.evilmail.pro',
  port: 993,
  secure: true,
  auth: { user: '[email protected]', pass: process.env.IMAP_PASS },
  logger: false,
});

await client.connect();
const box = await client.mailboxOpen('INBOX');
// box.uidValidity (BigInt), box.uidNext, box.exists

Python tarafında IMAPClient:

python
from imapclient import IMAPClient

with IMAPClient('mail.evilmail.pro', port=993, ssl=True) as server:
    server.login('[email protected]', os.environ['IMAP_PASS'])
    status = server.folder_status('INBOX', ['UIDVALIDITY', 'UIDNEXT'])
    server.select_folder('INBOX')
    # status[b'UIDVALIDITY'], status[b'UIDNEXT']

Sağlayıcı gerçeği (2026): Gmail'de "less secure app" erişimi kapatıldı; app password hâlâ mümkün ama yalnızca 2FA açıksa ve Workspace yöneticisi izin veriyorsa. Ciddi otomasyonda XOAUTH2 fiili standart: access token 1 saatte biter, refresh token ile yenileme sorumluluğu tamamen sende. Microsoft 365'te temel kimlik doğrulama (basic auth) kapatıldı, IMAP için OAuth2 mecburi. Ama evilmail.pro gibi kendi Dovecot sunucunda düz kullanıcı/parola + 993/TLS hâlâ tümüyle geçerli — OAuth dansına girmeden robot hesabı açıp doğrudan bağlanırsın. Otomasyon kutusu için kendi altyapını kullanmanın en somut avantajı budur.

Yeni mesajları güvenilir yakalama

Kalıcı durumun tamamı üç alandan ibaret: {mailbox, uidvalidity, last_uid}. Bunu Redis, SQLite ya da Postgres'te tut — nerede tuttuğun önemli değil, kaybolmaması önemli. Akış her turda şöyle işler:

  • 1.SELECT INBOX → sunucunun UIDVALIDITY'sini oku.
  • 2.Sakladığın uidvalidity ile karşılaştır. Eşleşmiyorsa: last_uid = 0, cache temizle (full resync).
  • 3.UID SEARCH UID {last_uid+1}:* ile yeni UID listesini al.
  • 4.Her mesajı BODY.PEEK[] ile çek, işle.
  • 5.İş başarıyla bittikten sonra UID STORE {uid} +FLAGS ($Processed) yaz ve last_uid'i ilerlet.

Bu sıralama bir at-least-once garantisi kurar. Kritik nokta: durumu işten *sonra* ilerletmek. İş yarıda patlarsa hiçbir şey yazma; bir sonraki tur aynı UID'i tekrar getirir ve tekrar dener. Bu yüzden işleyicinin kendisi idempotent olmalı — Message-ID dedup'a birazdan geliyoruz.

Bayrak değişimlerini de takip etmen gerekiyorsa — örneğin bir insan bir maili \Answered yaptığında tetiklenmek istiyorsan — RFC 7162 CONDSTORE/QRESYNC devreye girer. MODSEQ ile "şu MODSEQ'ten beri değişen bayraklar" sorgulanır ve tüm kutuyu taramadan flag delta'sı alınır. Çoğu "gelen maili işle" senaryosunda buna ihtiyacın yok; ama flag senkronizasyonu gerekiyorsa tek verimli yol budur.

Idempotent IMAP tüketici döngüsü Connect 993/TLS SELECT INBOX UIDVALIDITY oku UIDVALIDITY eşleşiyor? Full resync last_uid = 0 hayır UID SEARCH last_uid+1 : * evet BODY.PEEK[] FETCH MIME parse Message-ID görüldü mü? atla evet İş kuyruğu / webhook hayır +FLAGS $Processed last_uid ilerlet → SEARCH Hata → bayrak yazma; döngü tekrar dener (at-least-once)

MIME'ı doğru ayrıştırma — asıl acı burada

Ham mesajı (RFC 5322) elle string olarak kesmek en pahalı hatadır. Header'lar 78 karakterde katlanır (folding), Subject bir encoded-word olabilir (=?UTF-8?Q?Fatura_=23129?=), gövde iç içe geçmiş multipart'lardan oluşur. body.split('\n\n')[1] gibi bir şey yazan herkes eninde sonunda bir faturanın tutarını kaçırır.

Gerçek bir mailin yapısı bir ağaçtır:

Bir mailin MIME ağacı message/rfc822 multipart/mixed multipart/alternative text/plain quoted-printable · UTF-8 · gövde metni text/html base64 · zengin gövde application/pdf; name="fatura.pdf" base64 · ek walk() / simpleParser bu ağacı gezer — string olarak kesme.

Node.js'te bu ağacı elle gezmene gerek yok — mailparser'ın simpleParser'ı tüm işi yapar, encoded-word'leri çözer, charset'i normalize eder:

javascript
import { simpleParser } from 'mailparser';

const lock = await client.getMailboxLock('INBOX');
try {
  for await (const msg of client.fetch(`${lastUid + 1}:*`,
      { uid: true, source: true, flags: true })) {
    const mail = await simpleParser(msg.source);
    // mail.subject, mail.text, mail.html, mail.messageId
    // mail.attachments: [{ filename, contentType, content: Buffer, size }]
    await handle(mail, msg.uid);
  }
} finally {
  lock.release();
}

Python'da stdlib email ile ağacı walk() ederek gezersin. Charset burada Türkçe için hayati: yanlış charset varsayımı ç/ş/ğ/ı/İ karakterlerini bozar. Doğru yol, parçanın kendi charset'ini okuyup errors='replace' ile decode etmek:

python
import email
from email.header import decode_header, make_header

raw = server.fetch(uids, ['BODY.PEEK[]'])
for uid, data in raw.items():
    m = email.message_from_bytes(data[b'BODY[]'])
    subject = str(make_header(decode_header(m['Subject'])))
    message_id = m['Message-ID']

    for part in m.walk():
        ctype = part.get_content_type()
        if ctype == 'text/plain' and not part.get_filename():
            cs = part.get_content_charset() or 'utf-8'
            body = part.get_payload(decode=True).decode(cs, errors='replace')
        elif part.get_filename():
            save_attachment(part)  # aşağıda

Ek yazarken üç şeye dikkat et: filename'i decode_header ile çöz, os.path.basename uygula ve bir whitelist ile path traversal'ı engelle, bir de üst boyut sınırı koy (örneğin 25 MB). Content-Disposition: attachment; filename="../../etc/cron.d/x" gönderen biri, kontrol etmezsen sunucuna dosya yazar.

python
import os

def save_attachment(part, out_dir='/var/spool/robot', max_bytes=25 * 1024 * 1024):
    name = str(make_header(decode_header(part.get_filename())))
    name = os.path.basename(name)          # path traversal kes
    payload = part.get_payload(decode=True)
    if payload is None or len(payload) > max_bytes:
        return
    path = os.path.join(out_dir, name)
    with open(path, 'wb') as f:
        f.write(payload)

Dedup: neden hem bayrak hem tablo

$Processed bayrağı tek başına yeterli değil, çünkü bayraklar oynak. Mail başka bir klasöre taşınırsa, kutu yeniden indekslenirse ya da bir insan yanlışlıkla bayrağı temizlerse aynı maili tekrar işlersin. Bu yüzden ikinci bir savunma hattı: her mailin Message-ID header'ını kalıcı bir processed_message_ids tablosunda tut. İşleyiciye girmeden önce bu tabloya bak; varsa atla. Bayrak "sunucu tarafı hızlı işaret", tablo "senin tarafında kesin gerçek". At-least-once teslimatı bir veri olarak kabul et ve işi iki kez çalışsa da zarar vermeyecek şekilde yaz: upsert, unique constraint, idempotency key.

Gerçek zamanlı: IDLE ve reconnect döngüsü

Polling ucuz ama gecikmeli. Anlık tepki istiyorsan RFC 2177 IDLE kullanırsın: IDLE komutunu gönderirsin, sunucu yeni mail geldiğinde * EXISTS push eder. Buradaki tuzak: sunucuların büyük çoğunluğu bir IDLE bağlantısını 29 dakikada sessizce düşürür. Sen fark etmezsin — soket açık görünür ama artık ölüdür. Kural: yaklaşık 28 dakikada bir DONE gönderip IDLE'ı yeniden kur. imapflow bunu içeride yönetir ama reconnect mantığını yine de sen yazmalısın.

Sağlam bir model IDLE + fallback polling hibrididir: IDLE dinlerken bir yandan 5 dakikada bir güvenlik amaçlı poll at. NAT/proxy arkasındaysan TCP keepalive'ı da aç. Bağlantı düştüğünde exponential backoff + jitter ile yeniden bağlan (1s, 2s, 4s… max 60s; jitter olmadan tüm worker'lar aynı anda vurur). Ve her reconnect'te SELECT sonrası UIDVALIDITY'yi tekrar doğrula — bağlantı koptuğu sürede kutu yeniden oluşturulmuş olabilir.

javascript
async function run() {
  for (;;) {
    try {
      await client.connect();
      const box = await client.mailboxOpen('INBOX');
      await resyncIfNeeded(box.uidValidity);   // UIDVALIDITY kontrolü
      await drainNewMessages();                // last_uid+1:*
      client.on('exists', () => drainNewMessages());
      await client.idle();                     // ~28dk'da otomatik yenilenir
    } catch (err) {
      await sleep(backoff() + jitter());       // exp backoff + jitter
    } finally {
      backoffReset();
    }
  }
}

Üretim sertleştirme ve sınırlar

  • Sağlayıcı kotaları: Gmail'de IMAP indirmesi hesap başına günde ~2500 MB, aynı hesaba eşzamanlı bağlantı ~15 ile sınırlı. Bu sınırlara toslarsan [OVERQUOTA] yersin; ekleri gerçekten indirmen gerekmiyorsa BODY.PEEK[HEADER] + BODYSTRUCTURE ile sadece gerekeni çek.
  • Tek tüketici: İki worker aynı kutuyu işlerse $Processed yarışı ve çift teslimat olur. Bir lider seçimi (Redis lock, advisory lock) ile kutu başına tek aktif tüketici garantile.
  • Gözlemlenebilirlik: last_uid gecikmesini, işlenmemiş UID sayısını ve IDLE bağlantısının yaşını metrik olarak yayınla. IDLE yaşı 28 dakikayı geçiyorsa alarm çal.
  • Güvenlik: Kimlik bilgilerini env/secret store'da tut, asla repoda değil. Ekleri sandbox'ta işle. Ve log'a mail gövdesi yazma — bir gün o log'da birinin kişisel verisi olur.

Üretime çıkmadan hızlı kontrol listesi

  • UIDVALIDITY'yi her turda doğrula; değiştiyse last_uid=0 full resync.
  • İdempotency işaretçisi olarak \Seen değil $Processed kullan.
  • Okumayı BODY.PEEK[] ile yap; BODY[] maili okundu işaretler.
  • IDLE bağlantısını ~28 dakikada bir yeniden kur; fallback polling ekle.
  • Message-ID dedup tablosu tut; bayrak kaybolabilir.
  • Ekleri boyut sınırı + basename + whitelist ile yaz.
  • Gmail/M365 için XOAUTH2 refresh'i sen yönet; kendi Dovecot'unda düz LOGIN yeterli.
  • Reconnect'te exponential backoff + jitter uygula, her seferinde UIDVALIDITY

Kutuyu değil, durumunu yönet.

Paylaş
𝕏 TwitterLinkedInReddit
ÖncekiIMAP FETCH, SEARCH ve ENVELOPE: Posta Kutusunu Programatik ve Verimli SorgulamaSonrakiDKIM'i Uygulamada mı MTA'da mı İmzalamalı: Mimari Kararın Tuzakları

İlgili Yazılar

Geliştiriciler

ManageSieve ile Sieve Script'lerini Uzaktan Yönetmek: Merkezi Dağıtım ve Sürümleme

11 dk okuma
Geliştiriciler

SMTP DSN ile Teslim ve Gecikme Bildirimlerini Programatik İşlemek

13 dk okuma
Geliştiriciler

CI/CD Hattında E-postaları Mailpit ile Yakalayıp Otomatik Test Etmek

11 dk okuma
EvilMailEvilMail

EvilMail — ücretsiz temp mail ve geçici e-posta servisi. Kayıt gerektirmez.

Ürün

ÖzelliklerFiyatlarAPITools

Kaynaklar

BlogFAQGlossaryKayıt OlHakkımızda

Yasal

Gizlilik PolitikasıKullanım KoşullarıGüven ve Güvenlikİletişim
© 2026 EvilMail. Tüm hakları saklıdır.
yeniden kontrol.