Tüm yazılar
Veri Otomasyonu 7 dk okuma

E-posta eklerini IMAP ile otomatik toplamak: gelen kutusundan veri hattına güvenli aktarım

IMAP ile e-posta eklerini otomatik toplama: imaplib ve email ile UID tabanlı seçim, SHA-256 tekilleştirme, gönderici kuralları, dosya doğrulama ve KVKK sınırları.

#python #imap #otomasyon #veri-isleme

E-posta eklerini otomatik toplama, tedarikçiden, bankadan ya da bir SaaS aracından her gün gelen CSV, Excel ve PDF dosyalarını gelen kutusundan elle indirmek yerine bir betiğin almasını ve veri hattına teslim etmesini sağlar. Küçük ve orta ölçekli işletmelerde veri akışının önemli bir bölümü hâlâ e-posta eki olarak dolaşır: stok listesi her sabah bir ekle gelir, banka ekstresi PDF olarak düşer, pazaryeri satış raporunu haftalık gönderir. Birisi bu ekleri indirip ortak klasöre taşımadıkça raporlar güncellenmez.

IMAP protokolü bu işi otomatikleştirmenin en taşınabilir yoludur. Hemen her posta sunucusu destekler, Python'un standart kütüphanesindeki imaplib ve email modülleri ek bağımlılık gerektirmez ve yetkilendirme tek bir servis hesabıyla çözülür. Zor olan bağlantı kurmak değil, aynı ekin iki kez işlenmesini, yanlış göndericiden gelen dosyanın hatta sızmasını ve yarım kalan bir çalıştırmanın mesajı kaybetmesini engellemektir.

Bu yazıda gelen kutusunu neden kırılgan bir kaynak saydığımı anlatıp güvenli bir toplama hattını adım adım kuruyorum: IMAP bağlantısı ve mesaj seçimi, eklerin ayrılması ve içerik özetiyle tekilleştirme, gönderici kuralları ve dosya doğrulama, güvenlik ve kişisel veri sınırları. Kod örnekleri yalnız standart kütüphane kullanır; Python 3.8 ve üzeri sürümlerde değişiklik gerektirmeden çalışır.

Gelen kutusu neden kırılgan bir veri kaynağıdır

Gelen kutusu, veri hattı açısından kırılgan bir kaynaktır çünkü içeriğini üreten taraf sizin kontrolünüzde değildir: gönderici dosya adını değiştirir, aynı raporu düzeltilmiş haliyle ikinci kez yollar, konu satırına tarih eklemeyi unutur ya da ek yerine indirme bağlantısı gönderir. Betik bu değişkenliği baştan varsaymalı ve her mesajı güvenilmez girdi gibi ele almalıdır.

Elle yapılan işte bu sorunları insan gözü sessizce çözer. Betik ise aynı durumlarda dört farklı biçimde kırılır:

  • Çift işleme: aynı ek iki mesajla gelir ve raporda satırlar ikiye katlanır.
  • Kayıp mesaj: betik mesajı okundu işaretler, sonra diske yazarken çöker; bir sonraki çalıştırma o mesajı bir daha görmez.
  • Sahte gönderici: benzer bir adresten gelen ek, filtre gevşekse doğrudan hatta girer.
  • Tehlikeli dosya adı: ../ içeren ya da çok uzun bir dosya adı, hedef klasörün dışına yazmaya ya da hataya yol açar.

Tasarım ilkesi tek cümleye sığar: mesajın durumunu değiştiren adım en sona gelir, tekilleştirme ise dosya adına değil içeriğin özetine dayanır. Bir çalıştırma yarım kalırsa sonraki çalıştırma aynı mesajı yeniden görür ve zaten kaydedilmiş eki özet tablosu sayesinde atlar. Bu yaklaşım, zamanlanmış iş tasarımındaki idempotency ilkesinin gelen kutusuna uyarlanmış halidir.

IMAP ile bağlanma ve mesaj seçimi

IMAP bağlantısı, posta sunucusuna TLS üzerinden açılan, bir klasör seçip içindeki mesajları ölçütlere göre arayan ve seçilenleri parça parça indiren oturumdur. Python'da imaplib.IMAP4_SSL sınıfı bu oturumu kurar. Arama sunucu tarafında çalışan IMAP SEARCH komutuyla yapılır, böylece bütün gelen kutusunu indirmek gerekmez ve hat büyük posta kutularında da hızlı kalır.

Mesajlara erişirken sıra numarası yerine UID kullanmak gerekir. Sıra numaraları oturum içinde bir mesaj silindiğinde kayar; UID ise klasör içinde kalıcıdır. Mesajı indirirken BODY.PEEK[] kullanmak da önemlidir: sıradan BODY[] isteği mesajı otomatik olarak okundu işaretler, bu da yarım kalan çalıştırmada mesajın kaybolmasına yol açar.

import email
import imaplib
import os

HOST = os.environ["IMAP_HOST"]
USER = os.environ["IMAP_USER"]
PASSWORD = os.environ["IMAP_PASSWORD"]


def baglan():
    """IMAP sunucusuna TLS ile bağlanır ve gelen kutusunu seçer."""
    imap = imaplib.IMAP4_SSL(HOST, 993)
    imap.login(USER, PASSWORD)
    imap.select("INBOX")
    return imap


def okunmamis_mesajlar(imap, gonderen):
    """Belirli bir göndericiden gelen okunmamış mesajların UID listesini döndürür."""
    durum, veri = imap.uid("search", None, "UNSEEN", "FROM", '"%s"' % gonderen)
    if durum != "OK":
        raise RuntimeError("IMAP arama başarısız: %s" % durum)
    return veri[0].split()


def mesaji_al(imap, uid):
    """UID ile tek mesajı indirir; PEEK sayesinde okundu işareti değişmez."""
    durum, veri = imap.uid("fetch", uid, "(BODY.PEEK[])")
    if durum != "OK":
        raise RuntimeError("IMAP fetch başarısız: %s" % uid)
    return email.message_from_bytes(veri[0][1])

Kimlik bilgileri ortam değişkeninden okunur; betiğin içine yazılmaz. FROM ölçütü sunucu tarafında uygulanır, bu yüzden on binlerce mesajlık bir kutuda bile yalnız ilgili göndericinin mesajları iner. UNSEEN ölçütü tek başına güvence değildir: biri kutuya web arayüzünden bakıp mesajı okundu yaptığında betik onu atlar. Bu nedenle bir sonraki bölümdeki özet tablosu asıl kayıt defteridir; okundu işareti yalnız sorgu yükünü azaltan bir ön filtredir.

Ekleri ayırma ve içerik özetiyle tekilleştirme

Ek ayırma, MIME yapısındaki parçaları gezip Content-Disposition değeri attachment olanları bulma, dosya adını güvenli bir biçime indirgeme ve içeriği diske yazma adımıdır. Tekilleştirme ise aynı içeriğin ikinci kez hatta girmesini engeller. Anahtar olarak dosya adı değil içeriğin SHA-256 özeti kullanılır, çünkü gönderici aynı adla farklı, farklı adla aynı dosyayı yollayabilir.

import hashlib
import pathlib
import re
import sqlite3
from email.header import decode_header, make_header

HEDEF = pathlib.Path("gelen")
IZINLI_UZANTI = {".csv", ".xlsx", ".pdf"}


def veritabani(yol="ekler.sqlite"):
    """Özet tablosunu açar; yoksa oluşturur. Tablo aynı zamanda denetim izidir."""
    db = sqlite3.connect(yol)
    db.execute(
        "CREATE TABLE IF NOT EXISTS ekler ("
        "sha256 TEXT PRIMARY KEY, mesaj_id TEXT, dosya TEXT, yol TEXT, "
        "alinma TEXT DEFAULT CURRENT_TIMESTAMP)"
    )
    return db


def guvenli_ad(ham):
    """MIME başlığındaki adı çözer, dizin bilgisini atar ve güvenli karakterlere indirger."""
    ad = str(make_header(decode_header(ham or "ek")))
    ad = pathlib.PurePosixPath(ad.replace("\\", "/")).name
    return re.sub(r"[^A-Za-z0-9._-]", "_", ad)[:120] or "ek"


def ekleri_kaydet(mesaj, db):
    """Mesajdaki izinli ekleri diske yazar; aynı içerik ikinci kez gelirse atlar."""
    kaydedilen = []
    mesaj_id = (mesaj.get("Message-ID") or "").strip()
    for parca in mesaj.walk():
        if parca.get_content_disposition() != "attachment":
            continue
        ad = guvenli_ad(parca.get_filename())
        if pathlib.Path(ad).suffix.lower() not in IZINLI_UZANTI:
            continue
        icerik = parca.get_payload(decode=True)
        if not icerik:
            continue
        ozet = hashlib.sha256(icerik).hexdigest()
        if db.execute("SELECT 1 FROM ekler WHERE sha256 = ?", (ozet,)).fetchone():
            continue
        yol = HEDEF / ("%s_%s" % (ozet[:12], ad))
        yol.write_bytes(icerik)
        db.execute(
            "INSERT INTO ekler (sha256, mesaj_id, dosya, yol) VALUES (?, ?, ?, ?)",
            (ozet, mesaj_id, ad, str(yol)),
        )
        db.commit()
        kaydedilen.append(yol)
    return kaydedilen

Dosya adının önüne özetin ilk on iki karakteri eklenir; böylece gönderici her gün aynı adla rapor yollasa bile dosyalar üst üste yazılmaz ve hangi dosyanın hangi mesajdan geldiği tablo üzerinden izlenebilir. Özet kontrolü ile disk yazımı arasında başka bir çalıştırmanın girmesini istemiyorsanız hattı tek örnekli çalıştırmak yeterlidir; kilit deseni yukarıda bağlantı verdiğim yazıda anlatılıyor.

Ana döngü iki parçayı birleştirir ve mesajın durumunu en sonda değiştirir. Okundu işaretinin yanında mesajı bir arşiv klasörüne kopyalamak, gelen kutusunu temiz tutar ve bir sorun çıktığında ham mesaja geri dönmeyi mümkün kılar:

def calistir(gonderenler, arsiv="Islendi"):
    """Her gönderici için okunmamış mesajları işler; başarılı olanı işaretler ve arşivler."""
    HEDEF.mkdir(exist_ok=True)
    db = veritabani()
    imap = baglan()
    try:
        for gonderen in gonderenler:
            for uid in okunmamis_mesajlar(imap, gonderen):
                mesaj = mesaji_al(imap, uid)
                ekleri_kaydet(mesaj, db)
                imap.uid("store", uid, "+FLAGS", "\\Seen")
                imap.uid("copy", uid, arsiv)
    finally:
        imap.logout()
        db.close()

Arşiv klasörünün sunucuda önceden oluşturulmuş olması gerekir; Gmail gibi etiket tabanlı sunucularda kopyalama bir etiket ekler, klasör taşımaz. Mesajı gelen kutusundan tamamen kaldırmak isterseniz kopyanın ardından \Deleted bayrağı ve expunge çağrısı eklenir; ancak önce arşivin gerçekten yazıldığını doğrulamanızı öneririm.

Gönderici kuralları ve dosya doğrulama

Gönderici kuralları, hangi adresten gelen hangi ekin hangi hedef klasöre ve hangi şemaya gideceğini tanımlayan küçük bir yapılandırma tablosudur. Hat yalnız bu tabloda yer alan adresleri işler; tanımsız bir adresten gelen ek dikkate alınmaz ve günlüğe yazılır. Böylece yanlış yönlendirilmiş ya da kötü niyetli bir mesaj hatta veri sokamaz.

Her kural için şu alanları tutmak yeterlidir:

  • Gönderici adresi: tam adres; alan adı eşleşmesi tek başına yetmez.
  • Konu deseni: düzenli ifade; aynı göndericiden farklı raporlar geliyorsa ayırt eder.
  • İzinli uzantılar: kural başına ayrı liste; banka ekstresi için PDF, stok için CSV.
  • Hedef klasör ve şema: ekin düşeceği yer ve doğrulanacağı sütun tanımı.

Gönderici başlığının sahtelenebileceğini unutmayın: From alanı göndericinin beyanıdır, kanıt değildir. Posta sunucunuz gelen mesaja Authentication-Results başlığı ekliyorsa DKIM ve SPF sonucunun geçti olduğunu bu başlıktan kontrol edebilirsiniz; eklemiyorsa güvenlik sınırını gönderici adresine değil, ekin içeriğinin doğrulanmasına kurmak gerekir.

Dosya doğrulama uzantıyla bitmez. Bir CSV dosyasının ilk satırındaki başlıkların beklenen sütun adlarıyla eşleştiği, bir Excel dosyasının gerçekten açılabildiği ve satır sayısının makul aralıkta olduğu kontrol edilir. Bu kontrolleri sütun tipleri ve zorunlu alanlarla birlikte tek bir şemada toplamak için veri sözleşmesi ve şema doğrulama yazısındaki yaklaşım doğrudan kullanılabilir: bozuk dosya hattın kapısında durur, karantina klasörüne taşınır ve ilgili kişiye bildirilir.

Güvenlik ve kişisel veri sınırları

Güvenlik sınırı, toplama betiğinin kullandığı posta hesabının yetkisini, kimlik bilgilerinin saklanma biçimini ve indirilen dosyaların nasıl ele alınacağını kapsar. Kişisel veri sınırı ise gelen kutusunun kendisinin kişisel veri barındıran bir ortam olduğunu kabul eder: gönderici adresi, imza bloğu ve mesaj gövdesi, 6698 sayılı Kişisel Verilerin Korunması Kanunu kapsamında işlenen veridir ve yalnız gerektiği kadarı saklanmalıdır.

Uygulamada şu kuralları öneriyorum:

  • Betik için ayrı bir servis posta hesabı açın; kişisel bir çalışanın kutusuna bağlanmayın. Raporların bu adrese yönlendirilmesini gönderici tarafında ayarlayın.
  • Parolayı ortam değişkeninden ya da bir sır kasasından okuyun; depoya ve günlük dosyalarına yazdırmayın.
  • Yalnız ekleri saklayın; mesaj gövdesini ve başlıkların tamamını diske yazmayın. Tabloda Message-ID ve gönderici adresi izlenebilirlik için yeterlidir.
  • Makro içerebilen biçimleri (.xlsm, .docm) izinli listeye almayın; indirilen dosyalara çalıştırma izni vermeyin ve ekleri açan kütüphaneleri güncel tutun.
  • Ekler için saklama süresi belirleyin ve süresi dolan dosyaları ile tablo kayıtlarını otomatik silin.

Sağlayıcı farkları da planlanmalıdır. Microsoft 365 ve Google Workspace, IMAP için temel parola doğrulamasını kısıtlamış durumda; kurumsal hesaplarda OAuth 2.0 akışı ya da yönetici onaylı uygulama parolası gerekir. Hangi yöntemin geçerli olduğunu sağlayıcının güncel belgelerinden doğrulayın ve imaplib tarafında authenticate çağrısıyla XOAUTH2 kullanmaya hazır olun. Kendi sunucunuzda Dovecot gibi bir IMAP servisi çalışıyorsa, betiğe özel bir kullanıcı ve yalnız okuma izni olan bir klasör yapısı en temiz çözümdür.

Sonuç: gelen kutusunu hatta bağlamak

E-posta ekleriyle beslenen bir veri hattı, dört ilkeye uyduğunda elle indirmeden daha güvenilir çalışır: mesajlar UID ile seçilir ve PEEK ile indirilir, tekilleştirme içerik özetiyle yapılır, mesajın durumu yalnız ek güvenle kaydedildikten sonra değişir ve yalnız tanımlı göndericilerden gelen, şemadan geçen dosyalar hatta girer. Geri kalan her şey bu dört ilkenin üzerine eklenen ayrıntıdır.

İlk adım olarak gelen kutunuza ekle gelen raporların listesini çıkarın: gönderici, sıklık, dosya biçimi ve bu dosyayı bekleyen rapor. Bu liste hem gönderici kurallarının ilk hali hem de otomasyonun size kazandıracağı zamanın kaba hesabıdır. Ardından tek bir gönderici için yukarıdaki üç fonksiyonu birleştirip birkaç gün elle yapılan işle yan yana çalıştırın; sonuçlar eşleştiğinde diğer göndericileri ekleyin.

Gelen kutunuzdan beslenen raporları otomatikleştirmek, mevcut bir hattı sağlamlaştırmak ya da gönderici kurallarını birlikte tasarlamak isterseniz iletişim bölümünden yazabilirsiniz.