Skip to content

21. İnternet işlevleri

Şimdi, TCP / IP (Transfer Control Protocol / Internet Protocol) programlama yapmamızı sağlayan Python'un internet işlevlerini ele alacağız.

Image

21.1. İnternet programlamasının temelleri

21.1.1. Genel Bilgiler

İki uzak makine A ve B arasındaki iletişimi ele alalım:

Image

A makinesindeki bir AppA uygulaması, İnternet'teki B makinesindeki bir AppB uygulamasıyla iletişim kurmak istediğinde, birkaç şeyi bilmesi gerekir:

  • IP adresi (İnternet Protokolü) veya B makinesinin adı;
  • AppB uygulamasının çalıştığı bağlantı noktası numarası. Nitekim B makinesi, İnternet üzerinde çalışan birçok uygulamayı destekleyebilir. Ağdan gelen bilgileri aldığında, bu bilgilerin hangi uygulamaya yönelik olduğunu bilmesi gerekir. B makinesindeki uygulamalar, iletişim bağlantı noktaları olarak da adlandırılan kapılar aracılığıyla ağa erişir. Bu bilgi, B makinesi tarafından alınan pakette bulunur ve böylece paket doğru uygulamaya iletilebilir;
  • makine B tarafından anlaşılan iletişim protokolleri. Çalışmamızda yalnızca TCP-IP protokollerini kullanacağız;
  • AppB uygulaması tarafından kabul edilen iletişim protokolü. Nitekim, A ve B makineleri birbirleriyle “iletişim kuracak”. Birbirlerine iletecekleri bilgiler, TCP-IP protokollerinde kapsüllenecektir. Bununla birlikte, zincirin sonunda AppB uygulaması, AppA uygulaması tarafından gönderilen bilgiyi aldığında, bu bilgiyi yorumlayabilmesi gerekir. Bu durum, A ve B adlı iki kişinin telefonla iletişim kurmasına benzer: diyalogları telefon aracılığıyla aktarılır. Konuşma, A'nın telefonunda sinyal biçiminde kodlanır, telefon hatları üzerinden taşınır ve B'nin telefonuna ulaşarak orada kod çözülür. B o zaman konuşmayı duyar. İşte burada diyalog protokolü kavramı devreye girer: A Fransızca konuşuyorsa ve B bu dili anlamıyorsa, A ve B anlamlı bir şekilde diyalog kuramazlar;

Bu nedenle, iletişim kuran iki uygulama, hangi iletişim türünü kullanacakları konusunda anlaşmalıdır. Örneğin, ftp hizmeti ile kurulan diyalog, pop hizmeti ile kurulan diyalogla aynı değildir: bu iki hizmet aynı komutları kabul etmez. Bu hizmetlerin diyalog protokolleri farklıdır;

21.1.2. TCP protokolünün özellikleri

Burada yalnızca TCP aktarım protokolünü kullanan ağ iletişimlerini inceleyeceğiz; bu protokolün başlıca özellikleri şunlardır:

  • veriyi göndermek isteyen işlem, öncelikle göndereceği bilgilerin alıcı işlemiyle bir bağlantı kurar. Bu bağlantı, gönderen makinenin bir bağlantı noktası ile alıcı makinenin bir bağlantı noktası arasında kurulur. İki bağlantı noktası arasında böylece sanal bir yol oluşturulur ve bu yol, bağlantıyı kuran sadece bu iki işleme ayrılır;
  • Kaynak işlem tarafından gönderilen tüm paketler bu sanal yolu takip eder ve gönderildikleri sırayla ulaşır;
  • gönderilen bilgi sürekli bir yapıya sahiptir. Gönderen süreç, kendi hızında bilgi gönderir. Bu bilgiler mutlaka hemen gönderilmez: TCP protokolü, gönderilmek üzere yeterli miktarda bilgi birikene kadar bekler. Bu bilgiler, TCP segmenti adı verilen bir yapıda depolanır. Bu segment dolduğunda, IP katmanına iletilir ve burada bir IP paketine kapsüllenir;
  • TCP protokolüyle gönderilen her segment numaralandırılır. Alıcı TCP protokolü, segmentleri sırayla aldığını doğrular. Düzgün bir şekilde alınan her segment için, gönderene bir alım onayı gönderir;
  • gönderici bunu aldığında, bunu gönderen işleme bildirir. Böylece gönderen işleme, bir segmentin başarıyla ulaştığını öğrenebilir;
  • eğer belirli bir süre geçtikten sonra, bir segmenti gönderen TCP protokolü bir onay almazsa, söz konusu segmenti yeniden gönderir ve böylece bilgi aktarım hizmetinin kalitesini garanti eder;
  • İletişim kuran iki işlem arasında kurulan sanal devre full-duplex'tir: bu, bilginin her iki yönde de aktarılabileceği anlamına gelir. Böylece, kaynak işlem bilgi göndermeye devam ederken bile hedef işlem onay mesajları gönderebilir. Bu, örneğin kaynak TCP protokolünün, onay mesajını beklemeden birden fazla segment göndermesine olanak tanır. Belirli bir süre sonra, n numaralı bir segmentin onayını almadığını fark ederse, segment gönderimini bu noktadan itibaren yeniden başlatır;

21.1.3. İstemci-sunucu ilişkisi

Genellikle İnternet üzerindeki iletişim asimetriktir: A makinesi, B makinesinden bir hizmet talep etmek için bir bağlantı başlatır; B makinesinin SB1 hizmetiyle bir bağlantı kurmak istediğini belirtir. B makinesi bunu kabul eder veya reddeder. Kabul ederse, A makinesi isteklerini SB1 hizmetine gönderebilir. Bu istekler, SB1 hizmeti tarafından anlaşılan iletişim protokolüne uymak zorundadır. Böylece, istemci makine olarak adlandırılan A makinesi ile sunucu makine olarak adlandırılan B makinesi arasında bir istek-yanıt diyaloğu kurulur. İki tarafın biri bağlantıyı sonlandıracaktır.

21.1.4. Bir istemcinin mimarisi

Bir sunucu uygulamasının hizmetlerini talep eden bir ağ programının mimarisi şu şekilde olacaktır:

ouvrir la connexion avec le service SB1 de la machine B
si réussite alors
    tant que ce n'est pas fini
        préparer une demande
        l'émettre vers la machine B
        attendre et récupérer la réponse
        la traiter
    fin tant que
finsi
fermer la connexion

21.1.5. Bir sunucunun mimarisi

Hizmet sunan bir programın mimarisi şu şekilde olacaktır:

1
2
3
4
5
ouvrir le service sur la machine locale
tant que le service est ouvert
    se mettre à l'écoute des demandes de connexion sur un port dit port d'écoute
    lorsqu'il y a une demande, la faire traiter par une autre tâche sur un autre port dit port de service
fin tant que

Sunucu programı, bir istemcinin ilk bağlantı talebini, hizmet almak amacıyla yaptığı sonraki taleplerden farklı şekilde işler. Program, hizmeti kendisi sağlamaz. Eğer bunu yapsaydı, hizmet süresince bağlantı taleplerini dinleyemezdi ve istemcilere hizmet verilemezdi. Sunucu şu şekilde çalışır: Dinleme bağlantı noktasında bir bağlantı isteği alındığında ve kabul edildiğinde, sunucu, istemcinin talep ettiği hizmeti sunmakla görevli bir görev oluşturur. Bu hizmet, sunucu makinesinin hizmet bağlantı noktası olarak adlandırılan başka bir bağlantı noktası üzerinden sunulur. Böylelikle aynı anda birden fazla istemciye hizmet verilebilir.

Bir hizmet görevi aşağıdaki yapıya sahip olacaktır:

1
2
3
4
5
6
tant que le service n'a pas été rendu totalement
    attendre une demande sur le port de service
    lorsqu'il y en a une, élaborer la réponse
    transmettre la réponse via le port de service
fin tant que
libérer le port de service

21.2. 'i keşfedin: İnternet iletişim protokolleri

21.2.1. Giriş

Bir istemci bir sunucuya bağlandığında, aralarında bir iletişim kurulur. Bu iletişimin niteliği, sunucunun iletişim protokolü olarak adlandırılan şeyi oluşturur. İnternette en yaygın olarak kullanılan protokoller arasında şunlar yer alır:

  • HTTP: HyperText Transfer Protokolü - bir web sunucusuyla (HTTP sunucusu) iletişim kurmak için kullanılan protokol;
  • SMTP: Simple Mail Transfer Protocol (Basit Posta Aktarım Protokolü) – e-posta gönderme sunucusuyla (SMTP sunucusu) iletişim kurmak için kullanılan protokol;
  • POP: Post Office Protocol (POST) - e-posta depolama sunucusuyla iletişim protokolü (POP sunucusu). Bu protokol, e-postaları göndermek yerine alınan e-postaları almak için kullanılır;
  • IMAP: Internet Message Access Protocol (İnternet Mesaj Erişim Protokolü) – bir e-posta depolama sunucusuyla iletişim kurmak için kullanılan protokol (IMAP sunucusu). Bu protokol, daha eski olan POP protokolünün yerini kademeli olarak almıştır;
  • FTP: Dosya Aktarım Protokolü (File Transfer Protocol) – bir dosya depolama sunucusuyla (FTP sunucusu) iletişim kurmak için kullanılan protokol;

Tüm bu protokoller, metin satırlı protokoller olma özelliğine sahiptir: istemci ve sunucu birbirleriyle metin satırları alışverişinde bulunur. Eğer şu işlemleri yapabilen bir istemci varsa:

  • TCP sunucusuyla bağlantı kurabilen;
  • sunucunun gönderdiği metin satırlarını konsolda görüntüleyebilen;
  • kullanıcının klavyeden girdiği metin satırlarını sunucuya gönderebilen bir istemci varsa;

o zaman, bu protokolün kurallarını bildiğimiz sürece, metin satırları protokolüne sahip bir TCP sunucusuyla iletişim kurabiliriz.

21.2.2. TCP Yardımcı Programları

Image

Bu belgeyle ilişkili kodlarda, iki adet TCP iletişim yardımcı programı bulunur:

  • [RawTcpClient], bir S sunucusunun P bağlantı noktasına bağlanmayı sağlar;
  • [RawTcpServer], P numaralı bağlantı noktasında istemcileri bekleyen bir sunucu oluşturmanıza olanak tanır;

Bunlar, kaynak kodları size verilen iki C# programıdır. Dolayısıyla bunları değiştirebilirsiniz.

TCP ve [RawTcpServer] sunucuları[RawTcpServeur port] sözdizimiyle çağrılır ve yerel makinenin (çalıştığınız bilgisayarın) [port] bağlantı noktasında TCP adlı bir hizmet oluşturur:

  • sunucu aynı anda birden fazla istemciye hizmet verebilir;
  • sunucu, kullanıcının klavyeden girdiği komutları yürütür. Bu komutlar şunlardır:
    • list: Sunucuya şu anda bağlı olan istemcileri listeler. Bunlar [id=x-nom=y] biçiminde görüntülenir. [id] alanı, istemcileri tanımlamak için kullanılır;
    • send x [texte]: x numaralı (id=x) müşteriye metin gönderir. Köşeli parantezler [] gönderilmez. Bunlar komutta gereklidir. Müşteriye gönderilen metni görsel olarak sınırlandırmaya yararlar;
    • close x: x numaralı müşteriyle olan bağlantıyı kapatır;
    • quit: tüm bağlantıları kapatır ve hizmeti durdurur;
  • müşteri tarafından sunucuya gönderilen satırlar konsolda görüntülenir;
  • tüm iletişim, [machine-port.txt] adlı bir metin dosyasına kaydedilir; burada
    • [machine], kodun çalıştığı makinenin adıdır;
    • [port], istemcinin isteklerine yanıt veren hizmet bağlantı noktasıdır;

TCP [RawTcpClient] istemcisi, [RawTcpClient serveur port] sözdizimiyle çağrılarak [serveur] sunucusunun [port] bağlantı noktasına bağlanır:

  • kullanıcının klavyeden girdiği satırlar sunucuya gönderilir;
  • sunucu tarafından gönderilen satırlar konsolda görüntülenir;
  • Tüm iletişimler, [serveur-port.txt] adlı bir metin dosyasına kaydedilir;

Bir örnek inceleyelim. PyCharm adlı iki terminal penceresi açıyoruz ve her birinde yardımcı programlar klasörüne gidiyoruz:

Image

Pencerelerden birinde, 100 numaralı bağlantı noktasında [RawTcpServer] sunucusunu başlatıyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user :
    1. satırda, yardımcı programlar klasöründeyiz;
    1. satırda, TCP sunucusunu 100 numaralı bağlantı noktasında başlatıyoruz;
  • 2-4. satırlar: Sunucu, bir TCP istemcisini bekler ve kullanıcının klavyeden girebileceği komutların bir listesini görüntüler;
    1. satırda, sunucu kullanıcının klavyeden gireceği bir komutu bekler;

Diğer komut penceresinde, TCP istemcisini başlatıyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
    1. satırda, yardımcı programlar klasörüne giriyoruz;
    1. satırda TCP istemcisini başlatıyoruz: ona yerel makinenin ([RawTcpClient] kodunun çalıştığı makine) 100 numaralı bağlantı noktasına bağlanmasını söylüyoruz;
    1. satırda, istemci sunucuya bağlanmayı başarmıştır. İstemcinin bilgileri belirtilmiştir: istemci, [DESKTOP-30FF5FB] makinesinde (bu örnekte yerel makine) bulunur ve sunucuyla iletişim kurmak için [51173] bağlantı noktasını kullanır:
    1. satırda, istemci kullanıcının klavyeden gireceği bir komutu bekliyor;

Sunucu penceresine geri dönelim. İçeriği değişmiştir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
    1. satırda bir istemci algılandı. Sunucu ona 1 numarasını verdi. Sunucu, uzak istemciyi (bilgisayar ve bağlantı noktası) doğru bir şekilde tanımladı;
    1. satırda, sunucu yeni bir istemci bekleme moduna geri dönüyor;

İstemci penceresine geri dönelim ve sunucuya bir komut gönderelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
    1. satır, sunucuya gönderilen komut;

Sunucu penceresine geri dönelim. İçeriği değişmiştir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
    1. satır, köşeli parantez içinde, sunucu tarafından alınan mesaj;

İstemciye bir yanıt gönderelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
send 1 [hello from server]
user :
    1. satır, istemci 1'e gönderilen yanıt. Yalnızca köşeli parantezlerin içindeki metin gönderilir, köşeli parantezlerin kendisi gönderilmez;

Müşteri penceresine geri dönelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
<-- [hello from server]
    1. satır, müşteri tarafından alınan yanıt. Alınan metin köşeli parantezlerin içindeki metindir;

Diğer komutları görmek için sunucu penceresine geri dönelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51173 connecté...
server : Attente d'un client...
client 1 : [hello from client]
send 1 [hello from server]
user : list
server : id=1-name=DESKTOP-30FF5FB-51173
user : close 1
server : Connexion client 1 fermée...
user : quit
server : fin du service
    1. satır, istemcilerin listesini istiyoruz;
    1. satır, yanıt;
    1. satır, 1 numaralı müşteri ile bağlantıyı sonlandırıyoruz;
    1. satır, sunucudan gelen onay;
    1. satır, sunucuyu kapatıyoruz;
    1. satır, sunucudan gelen onay;

İstemci penceresine geri dönelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 100
Client [DESKTOP-30FF5FB:51173] connecté au serveur [localhost-100]
Tapez vos commandes (quit pour arrêter) :
hello from client
<-- [hello from server]
Perte de la connexion avec le serveur...
    1. satır, istemci hizmetin sona erdiğini algıladı;

İki günlük dosyası oluşturuldu; biri sunucu, diğeri istemci için:

Image

  • [1] dosyasında sunucu günlükleri bulunur: dosya adı, [machine-port] biçiminde istemcinin adıdır. Bu sayede farklı istemciler için farklı günlük dosyaları elde edilebilir;
  • [2] dosyasında istemci günlükleri bulunur: dosya adı, [machine-port] biçiminde sunucunun adıdır;

Sunucu günlükleri şunlardır:


<-- [hello from client]
--> [hello from server]

İstemci günlükleri şunlardır:


--> [hello from client]
<-- [hello from server]

21.3. İnternetteki bir makinenin adını veya adresini (IP) bulmak

Image

İnternetteki makineler bir adresle (IP, IPv4 veya IPv6) ve çoğu zaman bir adla tanımlanır. Ancak sonuçta internet iletişim protokolleri tarafından yalnızca IP adresi kullanılır. Bu nedenle, adıyla tanımlanan bir makinenin IP adresini bilmek gerekir.

[ip-01.py] komut dosyası şu şekildedir:


# ithalatlar
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    # nom_machine: adresini almak istediğimiz makinenin adı IP
    try:
        # nom_machine-->IP adresi
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        # hata mesajı görüntülenir
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        # adres: IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        # hata mesajı görüntüleniyor
        print(f"names[{ip}]={erreur}")
        return


# ---------------------------------------- ana

# internet makineleri
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

# IP makinelerinin adresleri HOTES
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
# son
print("Terminé...")

Açıklamalar

    1. satır: [socket] modülü, internet soketlerinin yönetimi için gerekli işlevleri sağlar. [socket], elektrik prizi, ağ prizi anlamına gelir;
    1. satır: [get_ip_and_name] işlevi, bir makinenin internet adından şunları elde etmeyi sağlar:
    2. IP makinenin adresini;
    3. önceki IP adresinden elde edilen makinenin adını;
    1. satır: [socket.gethostbyname] işlevi, bu adlardan birinden bir makinenin IP adresini elde etmeyi sağlar (bir internet makinesinin bir ana adı ve takma adları olabilir);
    1. satır: Soketlerle ilgili işlevler, bir hata oluştuğunda [socket.error] istisnasını tetikler;
  • satır 19: [socket.gethostbyaddr] işlevi, bir makinenin IP adresinden adını elde etmeyi sağlar. 6. satırda geçirilen addan farklı bir ad elde edilebileceğini göreceğiz;
    1. satır: bir makine adları listesi. Son ad hatalıdır. [localhost] adı, üzerinde çalıştığınız ve komut dosyasını çalıştıran makineyi belirtir;
  • satır 33-35: Bu makinelerin IP değerleri görüntülenir;

Sonuçlar:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/ip/ip_01.py
-------------------------------------
ip[istia.univ-angers.fr]=193.49.144.41
names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
-------------------------------------
ip[www.univ-angers.fr]=193.49.144.41
names[193.49.144.41]=('ametys-fo-2.univ-angers.fr', [], ['193.49.144.41'])
-------------------------------------
ip[sergetahe.com]=87.98.154.146
names[87.98.154.146]=('cluster026.hosting.ovh.net', [], ['87.98.154.146'])
-------------------------------------
ip[localhost]=127.0.0.1
names[127.0.0.1]=('DESKTOP-30FF5FB', [], ['127.0.0.1'])
-------------------------------------
ip[xx]=[Errno 11001] getaddrinfo failed
Terminé...

Process finished with exit code 0

21.4. HTTP protokolü (HyperText Aktarım Protokolü)

21.4.1. Örnek 1

Image

Bir tarayıcı bir URL görüntülediğinde, o tarayıcı bir web sunucusunun, başka bir deyişle bir HTTP sunucusunun istemcisidir. İnisiyatif tarayıcıya aittir ve tarayıcı, sunucuya bir dizi komut göndererek işlemi başlatır. Bu ilk örnekte:

  • sunucu, [RawTcpServer] yardımcı programı olacaktır;
  • istemci ise bir tarayıcı olacaktır;

Öncelikle sunucuyu 100 numaralı bağlantı noktasında başlatıyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user :

Ardından bir tarayıcıyla URL [http://localhost:100]'i talep ediyoruz, yani sorgulanan HTTP sunucusunun yerel makinenin 100 numaralı bağlantı noktasında çalıştığını belirtiyoruz:

Image

Sunucu penceresine geri dönelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpServer.exe 100
server : Serveur générique lancé sur le port 0.0.0.0:100
server : Attente d'un client...
server : Commandes disponibles : [list, send id [texte], close id, quit]
user : server : Client 1-DESKTOP-30FF5FB-51438 connecté...
server : Attente d'un client...
server : Client 2-DESKTOP-30FF5FB-51439 connecté...
server : Attente d'un client...
client 1 : [GET / HTTP/1.1]
client 1 : [Host: localhost:100]
client 1 : [Connection: keep-alive]
client 1 : [DNT: 1]
client 1 : [Upgrade-Insecure-Requests: 1]
client 1 : [User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36]
client 1 : [Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9]
client 1 : [Sec-Fetch-Site: none]
client 1 : [Sec-Fetch-Mode: navigate]
client 1 : [Sec-Fetch-User: ?1]
client 1 : [Sec-Fetch-Dest: document]
client 1 : [Accept-Encoding: gzip, deflate, br]
client 1 : [Accept-Language: fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7]
client 1 : []
server : Client 3-DESKTOP-30FF5FB-51441 connecté...
server : Attente d'un client...
    1. satır, bağlanan istemci;
  • 9-22. satırlar: müşterinin gönderdiği metin satırları dizisi:
      1. satır: Bu satırın biçimi [GET URL HTTP/1.1]'tir. URL / komutunu ister ve sunucudan HTTP 1.1 protokolünü kullanmasını talep eder;
      1. satır: Bu satırın biçimi [Host: serveur:port]'tir. [Host] komutunda büyük/küçük harf ayrımı yapılmaz. Burada, istemcinin 100 numaralı bağlantı noktasında çalışan yerel bir sunucuya sorgu gönderdiğini hatırlatmak isteriz;
      1. satır: [User-Agent] komutu, istemcinin kimliğini belirtir;
      1. satır: [Accept] komutu, istemci tarafından hangi belge türlerinin kabul edildiğini belirtir;
      1. satır: [Accept-Language] komutu, istenen belgelerin birden fazla dilde mevcut olması durumunda hangi dilde istenildiğini belirtir;
      1. satır: [Connection] komutu, istenen bağlantı modunu belirtir: [keep-alive], işlemler tamamlanana kadar bağlantının sürdürülmesi gerektiğini belirtir;
      1. satır: istemci komutlarını boş bir satırla sonlandırır;

Sunucuyu sonlandırarak bağlantıyı sonlandırıyoruz:


client 1 : []
server : Client 3-DESKTOP-30FF5FB-51441 connecté...
server : Attente d'un client...
quit
server : fin du service

21.4.2. Örnek 2

Artık bir tarayıcının URL'i talep etmek için gönderdiği komutları bildiğimize göre, URL'i TCP [RawTcpClient] istemcimizle talep edeceğiz. Laragon’un Apache sunucusu (|Installation de Laragon| paragrafı) web sunucumuz olacak.

Laragon'u ve ardından Apache web sunucusunu başlatalım:

Image

Image

Şimdi bir tarayıcı kullanarak URL ve [http://localhost:80] adreslerini ziyaret edelim. Burada sadece [localhost:80] sunucusunu belirtiriz, belge URL'i belirtmeyiz. Bu durumda, URL / adresi, yani web sunucusunun kök dizini istenir:

Image

  • [1]'e, yani istenen URL'e dönüştürülür. Başlangıçta [http://localhost:80] yazılmıştı ve tarayıcı (burada Firefox), bunu basitçe [localhost]'e dönüştürdü; çünkü protokol belirtilmediğinde [http] protokolü varsayılır ve bağlantı noktası belirtilmediğinde [80] bağlantı noktası varsayılır;
  • [2]'te, sorgulanan web sunucusunun / kök sayfası;

Şimdi, tarayıcı tarafından alınan metni inceleyelim:

Image

  • Alınan sayfaya sağ tıklayıp [2] seçeneğini seçiyoruz. Aşağıdaki kaynak kodu elde ediyoruz:

<!DOCTYPE html>
<html>
<head>
    <title>Laragon</title>

    <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">

    <style>
        html, body {
            height: 100%;
        }

        body {
            margin: 0;
            padding: 0;
            width: 100%;
            display: table;
            font-weight: 100;
            font-family: 'Karla';
        }

        .container {
            text-align: center;
            display: table-cell;
            vertical-align: middle;
        }

        .content {
            text-align: center;
            display: inline-block;
        }

        .title {
            font-size: 96px;
        }

        .opt {
            margin-top: 30px;
        }

            .opt a {
                text-decoration: none;
                font-size: 150%;
            }

        a:hover {
            color: red;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="content">
            <div class="title" title="Laragon">Laragon</div>

            <div class="info">
                <br />
                Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />
                PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
                Document Root: C:/MyPrograms/laragon/www<br />

            </div>
            <div class="opt">
                <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
            </div>
        </div>

    </div>
</body>
</html>

Şimdi URL ve [http://localhost:80] dosyalarını TCP istemcimizle işleyelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 80
Client [DESKTOP-30FF5FB:51541] connecté au serveur [localhost-80]
Tapez vos commandes (quit pour arrêter) :
    1. satırda, localhost sunucusunun 80 numaralı bağlantı noktasına bağlanıyoruz. Laragon web sunucusu burada çalışıyor;

Şimdi önceki paragrafta öğrendiğimiz komutları giriyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 80
Client [DESKTOP-30FF5FB:51544] connecté au serveur [localhost-80]
Tapez vos commandes (quit pour arrêter) :
GET / HTTP/1.1
Host: localhost:80

<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [Content-Type: text/html; charset=UTF-8]
<-- []
<-- [<!DOCTYPE html>]
<-- [<html>]
<-- [    <head>]
<-- [        <title>Laragon</title>]
<-- []
<-- [        <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">]
<-- []
<-- [        <style>]
<-- [            html, body {]
<-- [                height: 100%;]
<-- [            }]
<-- []
<-- [            body {]
<-- [                margin: 0;]
<-- [                padding: 0;]
<-- [                width: 100%;]
<-- [                display: table;]
<-- [                font-weight: 100;]
<-- [                font-family: 'Karla';]
<-- [            }]
<-- []
<-- [            .container {]
<-- [                text-align: center;]
<-- [                display: table-cell;]
<-- [                vertical-align: middle;]
<-- [            }]
<-- []
<-- [            .content {]
<-- [                text-align: center;]
<-- [                display: inline-block;]
<-- [            }]
<-- []
<-- [            .title {]
<-- [                font-size: 96px;]
<-- [            }]
<-- []
<-- [            .opt {]
<-- [                margin-top: 30px;]
<-- [            }]
<-- []
<-- [            .opt a {]
<-- [              text-decoration: none;]
<-- [              font-size: 150%;]
<-- [            }]
<-- [            ]
<-- [            a:hover {]
<-- [              color: red;]
<-- [            }]
<-- [        </style>]
<-- [    </head>]
<-- [    <body>]
<-- [        <div class="container">]
<-- [            <div class="content">]
<-- [                <div class="title" title="Laragon">Laragon</div>]
<-- [     ]
<-- [                <div class="info"><br />]
<-- [                      Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
<-- [                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [                      Document Root: C:/MyPrograms/laragon/www<br />]
<-- []
<-- [                </div>]
<-- [                <div class="opt">]
<-- [                  <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>]
<-- [                </div>]
<-- [            </div>]
<-- []
<-- [        </div>]
<-- [    </body>]
<-- [</html>]
Perte de la connexion avec le serveur...
    1. satırda, [GET] komutunu giriyoruz. Web sunucusunun / kök dizinini istiyoruz;
    1. satırda, [Host] komutu;
  • bunlar gerekli olan tek iki komuttur. Diğer komutlar için web sunucusu varsayılan değerleri kullanacaktır;
    1. satır, istemci komutlarını sonlandırması gereken boş satır;
    1. satırın altında, web sunucusunun yanıtı gelir;
  • 7-12. satırlar: sunucu yanıtının HTTP başlıkları;
    1. satır: HTTP başlıklarının sonunu belirten boş satır;
  • 14-82. satırlar: 4. satırda istenen HTML belgesi;

[localhost-80.txt] günlük dosyasını yüklüyoruz:

Image


--> [GET / HTTP/1.1]
--> [Host: localhost:80]
--> []
<-- [HTTP/1.1 200 OK]
<-- [Date: Sun, 05 Jul 2020 12:42:14 GMT]
<-- [Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19]
<-- [X-Powered-By: PHP/7.2.19]
<-- [Content-Length: 1776]
<-- [Content-Type: text/html; charset=UTF-8]
<-- []
<-- [<!DOCTYPE html>]
<-- [<html>]
<-- [    <head>]
<-- [        <title>Laragon</title>]
<-- []
<-- [        <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">]
<-- []
<-- [        <style>]
<-- [            html, body {]
<-- [                height: 100%;]
<-- [            }]
<-- []
<-- [            body {]
<-- [                margin: 0;]
<-- [                padding: 0;]
<-- [                width: 100%;]
<-- [                display: table;]
<-- [                font-weight: 100;]
<-- [                font-family: 'Karla';]
<-- [            }]
<-- []
<-- [            .container {]
<-- [                text-align: center;]
<-- [                display: table-cell;]
<-- [                vertical-align: middle;]
<-- [            }]
<-- []
<-- [            .content {]
<-- [                text-align: center;]
<-- [                display: inline-block;]
<-- [            }]
<-- []
<-- [            .title {]
<-- [                font-size: 96px;]
<-- [            }]
<-- []
<-- [            .opt {]
<-- [                margin-top: 30px;]
<-- [            }]
<-- []
<-- [            .opt a {]
<-- [              text-decoration: none;]
<-- [              font-size: 150%;]
<-- [            }]
<-- [            ]
<-- [            a:hover {]
<-- [              color: red;]
<-- [            }]
<-- [        </style>]
<-- [    </head>]
<-- [    <body>]
<-- [        <div class="container">]
<-- [            <div class="content">]
<-- [                <div class="title" title="Laragon">Laragon</div>]
<-- [     ]
<-- [                <div class="info"><br />]
<-- [                      Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />]
<-- [                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />]
<-- [                      Document Root: C:/MyPrograms/laragon/www<br />]
<-- []
<-- [                </div>]
<-- [                <div class="opt">]
<-- [                  <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>]
<-- [                </div>]
<-- [            </div>]
<-- []
<-- [        </div>]
<-- [    </body>]
<-- [</html>]
  • satır 11-79: HTML belgesi alındı. Önceki örnekte, Firefox da aynısını almıştı;

Artık bir TCP istemcisi programlamak için gerekli temellere sahibiz; bu istemci bir URL isteğinde bulunacaktır.

21.4.3. Örnek 3

Image

[http/01/main.py] betiği, HTTP dosyası tarafından yapılandırılan bir HTTP istemcisidir. Bu dosyanın içeriği şöyledir:


def configure():
    # sorgulanacak URLs
    urls = [
        # site: bağlanılacak sitenin adı
        # bağlantı noktası: web hizmetinin bağlantı noktası
        # GET: URL talep edildi
        # başlıklar: istekte gönderilecek HTTP başlıkları
        # endOfLine: gönderilen HTTP başlıklarında satır sonu işareti
        # encoding: sunucu yanıtının kodlaması
        # timeout: sunucudan yanıt için maksimum bekleme süresi
        {
            "site": "localhost",
            "port": 80,
            "GET": "/",
            "headers": {
                "Host": "localhost:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 0.5
        },
        {
            "site": "sergetahe.com",
            "port": 80,
            "GET": "/",
            "headers": {
                "Host": "sergetahe.com:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 5
        },
        {
            "site": "tahe.developpez.com",
            "port": 443,
            "GET": "/",
            "headers": {
                "Host": "tahe.developpez.com:443",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 2
        },
        {
            "site": "www.sergetahe.com",
            "port": 80,
            "GET": "/cours-tutoriels-de-programmation/",
            "headers": {
                "Host": "sergetahe.com:80",
                "User-Agent": "client Python",
                "Accept": "text/HTML",
                "Accept-Language": "fr"
            },
            "endOfLine": "\r\n",
            "encoding": "utf-8",
            "timeout": 5
        }
    ]
    # yapılandırma geri veriliyor
    return {
        "urls": urls
    }
  • Dosyanın içeriği, URL öğelerinden oluşan bir listedir; listenin her bir öğesi bir sözlük niteliğindedir. Bu sözlük, [site] anahtarıyla belirtilen siteye nasıl bağlanılacağını belirtir;
  • 4-10. satırlar: her sözlüğün anahtarlarının anlamları;

[http/01/main.py] komut dosyası şöyledir:


# ithalatlar
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # url["GET"] sitesinin urlURL URL'sini okur ve url[site].html dosyasına kaydeder
    # istemci/sunucu iletişimi, [url] sözlüğünde belirtilen HTTP protokolüne göre gerçekleşir
    # istisnaların yukarıya iletilmesine izin verilir

    sock = None
    html = None
    try:
        # [site]'e 80 numaralı bağlantı noktası üzerinden zaman aşımı ile bağlantı kurulur
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # bağlantı, iki yönlü bir iletişim akışını temsil eder
        # istemci (bu program) ile bağlanılan web sunucusu arasında
        # bu kanal, komut ve bilgi alışverişi için kullanılır
        # iletişim protokolü HTTP'tir

        # site.html dosyasının oluşturulması - dosya adı için uygun olmayan karakterler değiştirilir
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # istemci, sunucu ile HTTP diyaloğunu başlatacak
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        # Sunuculara bağlı olarak, istemcinin satırları \n veya \r\n ile bitmelidir
        end_of_line = url["endOfLine"]
        # istemci, URL yapılandırmasını istemek için GET komutunu gönderir["GET"]
        # sözdizimi GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # takip?
        if suivi:
            print(f"--> {commande}", end='')
        # komut sunucuya gönderiliyor
        sock.send(bytearray(commande, 'utf-8'))
        # başlıkların gönderilmesi HTTP
        for verb, value in url['headers'].items():
            # gönderilecek komut oluşturuluyor
            commande = f"{verb}: {value}{end_of_line}"
            # takip?
            if suivi:
                print(f"--> {commande}", end='')
            # komut sunucuya gönderilir
            sock.send(bytearray(commande, 'utf-8'))
        # web sunucusundan istenen belgeyi almak için HTTP [Connection: close] başlığı gönderiliyor
        # istenen belgeyi gönderdikten sonra bağlantıyı kapatması istenir
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # HTTP protokolünün başlıkları (headers) boş bir satırla bitmelidir
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # sunucu artık sock kanalı üzerinden yanıt verecektir. Tüm
        # verilerini gönderecek ve ardından kanalı kapatacaktır. Dolayısıyla istemci, sock kanalından gelen her şeyi okuyacaktır
        # kanal kapanana kadar gelen tüm verileri okur
        #
        # öncelikle sunucu tarafından gönderilen HTTP başlıkları okunur
        # bunlar da boş bir satırla sona erer
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # soketi sanki bir metin dosyasıymış gibi okunur
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # Bu dosya satır satır işlenir
        fini = False
        while not fini:
            # geçerli satır okunuyor
            ligne = file.readline().strip()
            # Boş olmayan bir satır var mı?
            if ligne:
                if suivi:
                    # başlığı görüntülenir HTTP
                    print(f"<-- {ligne}")
            else:
                # bu boş satırdı - başlıklar HTTP sona erdi
                fini = True
        # boş satırın ardından gelecek olan HTML belgesini okuyoruz
        # geçerli satır okunuyor
        ligne = file.readline()
        while ligne:
            # günlük dosyasına kayıt
            html.write(str(ligne))
            # sonraki satır
            ligne = file.readline()
            # sunucu bağlantıyı kapattığında döngü sona erer
    finally:
        # istemci bağlantıyı kapatır
        if sock:
            sock.close()
        # html dosyasının kapatılması
        if html:
            html.close()


# -------------------main

# uygulama yapılandırılıyor
import config
config = config.configure()

# yapılandırma dosyasından URL değerleri alınır
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # URL değerini [site] sitesinden okuma
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
# son
print("Terminé...")

Kod açıklamaları:

  • satır 108-109: [config.py] modülüne ait [config] sözlüğü alınır;
  • 111-122. satırlar: bu sözlük kullanılır;
    1. satır, 7: [get_url(url)] işlevi, url[site] web sitesinden bir belge ister ve bunu url[site].HTML metin dosyasına kaydeder. Varsayılan olarak, istemci/sunucu iletişimi konsolda günlüğe kaydedilir (izleme=True);
  • her şey bir [try / finally] dosyasında gerçekleştirilir (satır 14-96). [except] yorumu yoktur. İstisnalar çağıran koda iletilir ve bu kod tarafından durdurulup görüntülenir (satır 119-120);
  • 16-17. satırlar: web sunucusuna bağlantı kurulur. [socket.create_connection] işlevi üç parametre alır:
    • [param1]: ulaşılmak istenen internet sunucusunun adıdır;
    • [param2]: bağlanmak istediğimiz hizmetin bağlantı noktası numarasıdır;
    • [param3]: [socket.create_connection] bir soket döndürür ve [param3], varsa, oluşturulan soketin zaman aşım süresini belirtir. Zaman aşımı, soketin uzak makineden yanıt beklerken geçireceği maksimum bekleme süresidir;
  • 27-28. satırlar: Alınan HTML belgesinin depolanacağı [site.html] dosyasının oluşturulması;
  • satır 34-43: İstemcinin ilk komutu [GET URL HTTP/1.1] komutu olmalıdır;
  • satır 43: [sock.send] işlevi, istemcinin sunucuya veri göndermesini sağlar. Burada gönderilen metin satırının anlamı şudur: "Bağlı olduğum web sitesinin [URL] sayfasını (GET) istiyorum. HTTP protokolünün 1.1 sürümüyle çalışıyorum";
    1. satır: [sock.send(bytearray(commande, 'utf-8'))] komutu bir bayt dizisi (bytearray) gönderir. Bu dizi, [commande] dizesinin UTF-8 ile kodlanmış bir bayt dizisine dönüştürülmesiyle elde edilir;
  • 44-52. satırlar: HTTP [Host, User-Agent, Accept, Accept-Language…] protokolünün diğer satırları gönderilir. Bunların sırası önemli değildir;
  • 53-55. satırlar: Sunucudan, istenen belgeyi gönderdikten sonra bağlantısını kapatmasını istemek için HTTP [Connection: close] başlığı gönderilir. Varsayılan olarak sunucu bunu yapmaz. Bu nedenle, bunu sunucuya açıkça belirtmek gerekir. Bunun önemi, bu bağlantı kesilmesinin istemci tarafında algılanacak olmasıdır; bu sayede istemci, istenen belgenin tamamını aldığını anlayacaktır;
  • 56-57. satırlar: İstemcinin HTTP başlıklarını göndermeyi tamamladığını ve artık istenen belgeyi beklediğini belirtmek için sunucuya boş bir satır gönderilir;
  • 68-86. satırlar: Sunucu öncelikle, istenen belge hakkında çeşitli bilgiler veren bir dizi HTTP başlığı gönderecektir. Bu başlıklar boş bir satırla sona erer;
  • 69-73. satırlar: Sunucunun yanıtını satır satır okuyabilmek için [sock.makefile(encoding=encoding)] yöntemi kullanılır. İsteğe bağlı [encoding] parametresi, beklenen metnin kodlamasını belirtir. Bu işlemden sonra, sunucu tarafından gönderilen satır akışı klasik bir metin dosyası gibi okunabilir;
    1. satır: Sunucu tarafından gönderilen bir satır, [readline] yöntemi ile okunur. Satırın başındaki ve sonundaki boşluklar (boş karakterler, satır sonu işaretleri) temizlenir;
  • 81-83. satırlar: Satır boş değilse ve izleme talebi varsa, alınan satır konsolda görüntülenir;
  • 84-86. satırlar: Sunucu tarafından gönderilen HTTP başlıklarının sonunu belirten boş satır alınmışsa, 76. satırdaki döngü durdurulur;
  • 90-95. satırlar: Sunucunun yanıtındaki metin satırları, bir döngü kullanılarak satır satır okunabilir ve while metin dosyasına kaydedilebilir. Web sunucusu, talep edilen sayfanın tamamını gönderdiğinde, istemciyle olan bağlantısını kapatır. İstemci tarafında bu durum dosya sonu olarak algılanır ve 90-95. satırlardaki döngüden çıkılır;
  • 96-102. satırlar: hata olsun ya da olmasın, kod tarafından kullanılan tüm kaynaklar serbest bırakılır;

Sonuçlar:

Konsol aşağıdaki günlükleri görüntüler:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/http/01/main.py
-------------------------
localhost
-------------------------
Client : début de la communication avec le serveur [localhost]
--> GET / HTTP/1.1
--> Host: localhost:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [localhost]
<-- HTTP/1.1 200 OK
<-- Date: Sun, 05 Jul 2020 16:27:46 GMT
<-- Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
<-- X-Powered-By: PHP/7.2.19
<-- Content-Length: 1776
<-- Connection: close
<-- Content-Type: text/html; charset=UTF-8
-------------------------
sergetahe.com
-------------------------
Client : début de la communication avec le serveur [sergetahe.com]
--> GET / HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [sergetahe.com]
<-- HTTP/1.1 302 Found
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Content-Type: text/html; charset=UTF-8
<-- Transfer-Encoding: chunked
<-- Connection: close
<-- Server: Apache
<-- X-Powered-By: PHP/7.3
<-- Location: http://sergetahe.com:80/programlama-dersleri-ve-öğreticiler
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17106
-------------------------
tahe.developpez.com
-------------------------
Client : début de la communication avec le serveur [tahe.developpez.com]
--> GET / HTTP/1.1
--> Host: tahe.developpez.com:443
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [tahe.developpez.com]
<-- HTTP/1.1 400 Bad Request
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Server: Apache/2.4.38 (Debian)
<-- Content-Length: 453
<-- Connection: close
<-- Content-Type: text/html; charset=iso-8859-1
-------------------------
www.sergetahe.com
-------------------------
Client : début de la communication avec le serveur [www.sergetahe.com]
--> GET /cours-tutoriels-de-programmation/ HTTP/1.1
--> Host: sergetahe.com:80
--> User-Agent: client Python
--> Accept: text/HTML
--> Accept-Language: fr
Réponse du serveur [www.sergetahe.com]
<-- HTTP/1.1 301 Moved Permanently
<-- Date: Sun, 05 Jul 2020 16:27:45 GMT
<-- Content-Type: text/html; charset=iso-8859-1
<-- Content-Length: 263
<-- Connection: close
<-- Server: Apache
<-- Location: https://sergetahe.com/programlama-dersleri-ve-öğreticiler/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

Yorumlar

    1. satır: URL [http://localhost/] bulundu (kod 200);
    1. satır: URL [http://sergetahe.com/] bulunamadı (kod 302). Kod 302, istenen sayfanın URL'ten değiştiği anlamına gelir. Yeni adres, 36. satırdaki başlıkta belirtilmiştir;
    1. satır: [http://tahe.developpez.com] sunucusuna yapılan istek hatalıdır (kod 400);
    1. satır: URL [http://www.sergetahe.com/] bulunamadı (kod 301). Kod 301, istenen sayfanın URL adresinden kalıcı olarak değiştirildiğini belirtir. Yeni URL, 71. satırdaki HTTP [Location] başlığıyla belirtilmiştir;

Genel olarak, bir HTTP sunucusunun 3xx, 4xx ve 5xx kodları hata kodlarıdır.

Çalıştırma sonucunda şu dosyalar oluşturuldu:

Image

Alınan [output/localhost.HTML] dosyası şöyledir:


<!DOCTYPE html>
<html>
    <head>
        <title>Laragon</title>

        <link href="https://fonts.googleapis.com/css?family=Karla:400" rel="stylesheet" type="text/css">

        <style>
            html, body {
                height: 100%;
            }

            body {
                margin: 0;
                padding: 0;
                width: 100%;
                display: table;
                font-weight: 100;
                font-family: 'Karla';
            }

            .container {
                text-align: center;
                display: table-cell;
                vertical-align: middle;
            }

            .content {
                text-align: center;
                display: inline-block;
            }

            .title {
                font-size: 96px;
            }

            .opt {
                margin-top: 30px;
            }

            .opt a {
              text-decoration: none;
              font-size: 150%;
            }
            
            a:hover {
              color: red;
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="content">
                <div class="title" title="Laragon">Laragon</div>
     
                <div class="info"><br />
                      Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19<br />
                      PHP version: 7.2.19   <span><a title="phpinfo()" href="/?q=info">info</a></span><br />
                      Document Root: C:/MyPrograms/laragon/www<br />

                </div>
                <div class="opt">
                  <div><a title="Getting Started" href="https://laragon.org/docs">Getting Started</a></div>
                </div>
            </div>

        </div>
    </body>
</html>

Firefox tarayıcısıyla elde ettiğimiz belgeyle aynı belgeyi aldık.

Alınan [output/sergetahe_com.html] belgesi şöyledir:

Image

Çoğu HTTP sunucusu, kendilerine yöneltilen isteklerin yanıtlarını parçalar halinde gönderir. Gönderilen her parçanın başında, takip eden parçanın bayt sayısını belirten bir satır bulunur. Bu, istemcinin o parçayı almak için tam olarak bu bayt sayısını okumasını sağlar. Burada 0, takip eden parçanın sıfır bayt olduğunu gösterir. Hatırlanacağı üzere, sunucu [http://sergetahe.com/] belgesinin URL olarak değiştiğini belirtmişti. Dolayısıyla herhangi bir belge göndermedi.

[output/tahe_developpez_com.html] belgesi şöyledir:


<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>400 Bad Request</title>
</head><body>
<h1>Bad Request</h1>
<p>Your browser sent a request that this server could not understand.<br />
Reason: You're speaking plain HTTP to an SSL-enabled server port.<br />
 Instead use the HTTPS scheme to access this URL, please.<br />
</p>
<hr>
<address>Apache/2.4.38 (Debian) Server at 2eurocents.developpez.com Port 80</address>
</body></html>
  • 1-12. satırlar: istek hatalı olmasına rağmen (sonuçların 49. satırı) sunucu HTML belgesini göndermiştir. HTML belgesi, sunucunun hatanın nedenini belirtmesine olanak tanır. Bu neden 6. ve 7. satırlarda belirtilmiştir:
      1. satır: istemcimiz HTTP protokolünü kullanmıştır;
      1. satır: sunucu, HTTPS protokolüyle (S=güvenli) çalışmaktadır ve HTTP protokolünü kabul etmemektedir;

[output/www_sergetahe_com.html] belgesi şu şekildedir:


<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://sergetahe.com/cours-tutoriels-de-programmation/">here</a>.</p>
</body></html>

Burada da bir hata meydana gelmiştir (3. satır). Bununla birlikte, sunucu bu hatayı ayrıntılı olarak açıklayan bir HTML belgesi gönderir (1-7. satırlar).

21.4.4. Örnek 4

Önceki örnekler, HTTP istemcimizin yetersiz olduğunu göstermiştir. Şimdi, bahsedilen zorlukları (HTTPS protokolü, parçalar halinde gönderilen belge, yönlendirmeler vb.) yöneterek web belgelerini alabilen [curl] adlı bir aracı tanıtacağız: [curl] aracı Laragon ile birlikte kurulmuştur:

Image

PyCharm [1] terminalini açalım:

Image

  • [1]'te, PyCharm terminallerine erişim;
  • [2-3]'te, halihazırda aktif olan terminaller;
  • [4]'te, içinde bulunduğunuz klasör. Aşağıdaki adımlarda bunun önemi yoktur;

Terminalde şu komutu yazıyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>curl --help
Usage: curl [options...] <url>
     --abstract-unix-socket <path> Connect via abstract Unix domain socket
     --anyauth       Pick any authentication method
 -a, --append        Append to target file when uploading
     --basic         Use HTTP Basic Authentication
     --cacert <CA certificate> CA certificate to verify peer against

[curl –help] komutunun sonuç vermesi, [curl] komutunun terminalin PATH klasöründe olduğunu gösterir. Windows’ta PATH, kullanıcı yürütülebilir bir komut (burada [curl]) yazdığında taranan klasörlerin bütünüdür. PATH’in değeri şu şekilde öğrenilebilir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>echo %PATH%
C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts;C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\Program Files\Python38\Scripts\;C:\Program Files\Python38\;C:\windows\system32;C:\windows;C:\windows\System32\Wbem;C:\windows\System32\WindowsPowerShell\v1.0\;C:\windows\System32\OpenSSH\;C:\Program Files\Git\cmd;C:\Users\serge\AppData\Local\Microsoft\WindowsApps;;C:\Program Files\JetBrains\PyCharm Community Edition 2020.1.2\bin;
  1. satırda, PATH'in klasörleri noktalı virgülle ayrılmış olarak yer almaktadır. Bu listede Laragon ile ilgili herhangi bir klasör görünmemektedir. Biraz araştırma yapıldığında, [c:\windows\system32] klasöründe bir [curl] olduğu görülür. Daha önce yanıt veren dosya budur.

Laragon ile birlikte gelen [curl] aracını kullanmak istersek, şu şekilde ilerleyebiliriz:

Image

Image

  • [2]'te, Laragon terminali;
  • [3]'e, bu düğme yeni terminaller oluşturmanıza olanak tanır; her biri yukarıdaki pencerenin bir sekmesine yerleştirilir;
  • [4]'te, Laragon terminalinin PATH'i istenir;
  • PyCharm terminalinde elde edilenden çok farklı bir sonuç elde edilir. Bu PATH, Laragon kurulumu sırasında oluşturulan birçok klasörü içerir. [curl] aracını içeren klasör de bunlardan biridir:

Image

Daha sonra, istediğiniz terminali kullanabilirsiniz. Ancak, Laragon tarafından sağlanan bir aracı kullanmak istediğinizde, Laragon terminalinin tercih edilmesi gerektiğini unutmayın.

[curl --help] komutu, [curl]'in tüm yapılandırma seçeneklerini görüntüler. Bunların sayısı onlarcadır. Bunlardan çok azını kullanacağız. URL dosyasını istemek için [curl URL] komutunu yazmanız yeterlidir. Bu komut, istenen belgeyi konsolda görüntüler. Ayrıca, istemci ile sunucu arasındaki HTTP iletişimlerini görmek istersek, [curl --verbose URL] komutunu yazacağız. Son olarak, istenen HTML belgesini bir dosyaya kaydetmek için [curl --verbose --output fichier URL] komutunu yazacağız.

Bilgisayarımızın dosya sistemini karıştırmamak için başka bir konuma geçelim (burada bir Laragon terminali kullanıyorum):


λ cd \Temp\

C:\Temp
λ mkdir curl

C:\Temp
λ cd curl\

C:\Temp\curl
λ dir
 Le volume dans le lecteur C s’appelle Local Disk
 Le numéro de série du volume est B84C-D958

 Répertoire de C:\Temp\curl

05/07/2020  19:31    <DIR>          .
05/07/2020  19:31    <DIR>          ..
               0 fichier(s)                0 octets
               2 Rép(s)  892 388 098 048 octets libres                                          
    1. satırda, [c:\temp] klasörüne geçin. Bu klasör yoksa, yeni bir klasör oluşturabilir veya başka bir klasör seçebilirsiniz;
    1. satırda, [curl] adlı bir klasör oluşturuyoruz;
    1. satırda, bu klasöre gidin;
    1. satırda, klasörün içeriği listelenir. İçerik boştur (20. satır);

Laragon'daki Apache sunucusunun çalıştığından emin olun ve [curl] ile URL ve [http://localhost/]'i [curl –verbose –output localhost.html http://localhost/] komutuyla isteyin. Aşağıdaki sonuçlar elde edilir:


λ curl --verbose --output localhost.html http://localhost/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying ::1...
* TCP_NODELAY set
*   Trying 127.0.0.1...
* TCP_NODELAY set
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0* Connected to localhost (::1) port 80 (#0)
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0> GET / HTTP/1.1
> Host: localhost
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Sun, 05 Jul 2020 17:35:43 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
< X-Powered-By: PHP/7.2.19
< Content-Length: 1776
< Content-Type: text/html; charset=UTF-8
<
{ [1776 bytes data]
100  1776  100  1776    0     0   1062      0  0:00:01  0:00:01 --:--:--  1062
* Connection #0, localhost ana bilgisayarı olduğu gibi bırakıldı
  • 10-13. satırlar: [curl] tarafından [localhost] sunucusuna gönderilen satırlar. HTTP protokolü tanınmaktadır;
  • 14-20. satırlar: sunucu tarafından yanıt olarak gönderilen satırlar;
    1. satır: istenen belgenin başarıyla alındığını gösterir;

[localhost.html] dosyası istenen belgeyi içerir. Dosyayı bir metin düzenleyicisine yükleyerek bunu doğrulayabilirsiniz.

Şimdi URL ve [https://tahe.developpez.com:443/] dosyalarını isteyelim. Bu URL dosyasını elde etmek için, HTTP istemcisinin HTTPS dilini bilmesi gerekir. [curl] istemcisi bu dilini bilmektedir.

Konsol çıktıları şu şekildedir:


C:\Temp\curl
λ curl --verbose --output tahe.developpez.com.html https://tahe.developpez.com:443/
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 87.98.130.52...
* TCP_NODELAY set
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: C:\MyPrograms\laragon\bin\laragon\utils\curl-ca-bundle.crt
  CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [25 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [2563 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [264 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=*.developpez.com
*  start date: Jul  1 15:38:30 2020 GMT
*  expire date: Sep 29 15:38:30 2020 GMT
*  subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
} [5 bytes data]
> GET / HTTP/1.1
> Host: tahe.developpez.com
> User-Agent: curl/7.63.0
> Accept: */*
>
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [281 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [297 bytes data]
* old SSL session ID is stale, removing
{ [5 bytes data]
< HTTP/1.1 200 OK
< Date: Sun, 05 Jul 2020 17:39:53 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/html
<
{ [6 bytes data]
100   99k    0   99k    0     0  79343      0 --:--:--  0:00:01 --:--:-- 79343
* Connection #0, tahe.developpez.com ana bilgisayarına olduğu gibi bırakıldı
  • 10-39. satırlar: Bağlantıyı güvenli hale getirmek için istemci ile sunucu arasındaki iletişim; bağlantı şifrelenecektir;
  • 41-44. satırlar: [curl] istemcisi tarafından sunucuya gönderilen HTTP başlıkları;
    1. satır: İstenen belge başarıyla bulundu;
  • satır 57: belge parçalar halinde gönderiliyor;

[curl], hem HTTPS güvenli protokolünü hem de belgenin parçalar halinde gönderilmesini doğru bir şekilde yönetir. Gönderilen belge, [tahe.developpez.com.html] dosyasında bulunacaktır.

Şimdi de URL ve [http://sergetahe.com/cours-tutoriels-de-programmation] dosyalarını inceleyelim. Daha önce, bu URL dosyası için URL ve [http://sergetahe.com/cours-tutoriels-de-programmation/] dosyalarına (sonunda / işareti olan) bir yönlendirme olduğunu görmüştük.

Konsol çıktıları şu şekildedir:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/programlama-dersleri-ve-eğitimleri
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0*   Trying 87.98.154.146...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Sun, 05 Jul 2020 17:44:17 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/programlama-dersleri-ve-öğreticiler/
< Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [262 bytes data]
100   262  100   262    0     0   1858      0 --:--:-- --:--:-- --:--:--  1858
* Connection #0, sergetahe.com'i olduğu gibi bırakmak
* Issue another request to this URL: 'http://sergetahe.com/programlama-dersleri-ve-öğreticiler/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) ana sunucu sergetahe.com ile
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
< HTTP/1.1 301 Moved Permanently
< Date: Sun, 05 Jul 2020 17:44:17 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/programlama-dersleri-ve-öğreticiler/
< Set-Cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
< X-IPLB-Instance: 17095
<
* Ignoring the response-body
{ [263 bytes data]
100   263  100   263    0     0    764      0 --:--:-- --:--:-- --:--:--   764
* Connection #0, sergetahe.com ana sunucusuna olduğu gibi bırakıldı
* Issue another request to this URL: 'https://sergetahe.com/programlama-dersleri-ve-öğreticiler/'
*   Trying 87.98.154.146...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 443 (#1)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: C:\MyPrograms\laragon\bin\laragon\utils\curl-ca-bundle.crt
  CApath: none
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [512 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [102 bytes data]
* TLSv1.2 (IN), TLS handshake, Certificate (11):
{ [2572 bytes data]
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
{ [333 bytes data]
* TLSv1.2 (IN), TLS handshake, Server finished (14):
{ [4 bytes data]
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
} [70 bytes data]
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.2 (OUT), TLS handshake, Finished (20):
} [16 bytes data]
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0* TLSv1.2 (IN), TLS handshake, Finished (20):
{ [16 bytes data]
* SSL connection using TLSv1.2 / ECDHE-RSA-AES128-GCM-SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=sergetahe.com
*  start date: May 10 01:41:15 2020 GMT
*  expire date: Aug  8 01:41:15 2020 GMT
*  subjectAltName: host "sergetahe.com" matched cert's "sergetahe.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
} [5 bytes data]
* Using Stream ID: 1 (easy handle 0x2bee870)
} [5 bytes data]
> GET /cours-tutoriels-de-programmation/ HTTP/2
> Host: sergetahe.com
> User-Agent: curl/7.63.0
> Accept: */*
>
{ [5 bytes data]
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
} [5 bytes data]
  0     0    0     0    0     0      0      0 --:--:--  0:00:01 --:--:--     0< HTTP/2 200
< date: Sun, 05 Jul 2020 17:44:19 GMT
< content-type: text/html; charset=UTF-8
< server: Apache
< x-powered-by: PHP/7.3
< link: <https://sergetahe.com/programlama-dersleri-ve-öğreticiler/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/programlama-dersleri-ve-öğreticiler/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=2620178|XwIRd|XwIRd; path=/
<
{ [5 bytes data]
100 49634    0 49634    0     0  26040      0 --:--:--  0:00:01 --:--:-- 37830
* Connection #1, sergetahe.com’i olduğu gibi barındırmak için
    1. satır: Sunucu tarafından gönderilen yönlendirmeleri takip etmek istediğimizi belirtmek için [--location] seçeneği kullanılır;
    1. satır: sunucu, istenen belgenin URL olarak değiştiğini bildirir;
    1. satır: İstenen belgenin yeni URL adresini belirtir;
    1. satır: [curl], bu sefer yeni URL adresine yeni bir istek gönderir;
    1. satır: sunucu, URL'in değiştiğini tekrar bildirir;
    1. satır: Yeni URL, yönlendirilenle bir ayrıntı dışında tamamen aynıdır: protokol değişmiştir. Daha önce http iken (31. satır), artık HTTPS olmuştur (41. satır);
    1. satır: Yeni URL adresine yeni bir istek gönderilir. Bu istek şifrelenmiştir. Ayrıca, 53-91. satırlarda güvenlik kurulumuna ilişkin bir diyalog başlar;
    1. satır: Bu kez HTTP/2 protokolü kullanılarak yeni URL isteniyor;
    1. satır: belge bulundu;

İstenen belge, [sergetahe.com.html] dosyasında bulunacaktır.


C:\Temp\curl
λ dir
 Le volume dans le lecteur C s’appelle Local Disk
 Le numéro de série du volume est B84C-D958

 Répertoire de C:\Temp\curl

05/07/2020  19:44    <DIR>          .
05/07/2020  19:44    <DIR>          ..
05/07/2020  19:35             1 776 localhost.html
05/07/2020  19:44            49 634 sergetahe.com.html
05/07/2020  19:39           101 639 tahe.developpez.com.html
               3 fichier(s)          153 049 octets
               2 Rép(s)  892 385 628 160 octets libres

21.4.5. Örnek 5

Python'da, [pyccurl] adlı bir modül bulunur; bu modül, [curl] aracının yeteneklerini bir Python programında kullanmaya olanak tanır. Bu modülü kuruyoruz:

Image

Yeni bir [http/02/main.py] betiği yazacağız:

Image

[http/02/config] dosyası şu şekildedir:


def configure():
    # sorgulanacak URL listesi
    urls = [
        # site: bağlanılacak sunucu
        # timeout: sunucudan yanıt için maksimum bekleme süresi
        # hedef: istenecek URL
        # kodlama: sunucu yanıtının kodlaması
        {
            "site": "sergetahe.com",
            "timeout": 2000,
            "target": "http://sergetahe.com",
            "encoding": "utf-8"
        },
        {
            "site": "tahe.developpez.com",
            "timeout": 500,
            "target": "https://tahe.developpez.com",
            "encoding": "iso-8859-1"
        },
        {
            "site": "www.polytech-angers.fr",
            "timeout": 500,
            "target": "http://www.polytech-angers.fr",
            "encoding": "utf-8"
        },
        {
            "site": "localhost",
            "timeout": 500,
            "target": "http://localhost",
            "encoding": "utf-8"
        }
    ]
    # yapılandırma şu şekildedir
    return {
        ''urls': URL'ler
    }

Dosya, her biri aşağıdaki yapıya sahip bir dizi sözlük içerir:

  • site: bir web sunucusunun adı;
  • encoding: beklenen belgenin kodlama türü;
  • timeout: sunucudan yanıt için milisaniye cinsinden belirtilen maksimum bekleme süresi. Bu süre aşıldığında istemci bağlantısını kesecektir;
  • url: İstenen belgenin URL değeri;

[http/02/main.py] komut dosyasının kodu şöyledir:


# ithalatlar
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # URL url[url] adresini okur ve output/url['site'].html dosyasına kaydeder
    # eğer [suivi=True] ise, o zaman istemci/sunucu iletişiminin konsol izlemesi vardır
    # url[timeout], istemci çağrılarının zaman aşımı süresidir;
    # url [encoding], istenen belgenin kodlamasıdır

    # yapılandırma verileri alınır
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    # izleme
    print(f"Client : début de la communication avec le serveur [{server}]")

    # istisnaların yukarıya iletilmesine izin verilir
    html = None
    curl = None
    try:
        # Bir oturum başlatılıyor cURL
        curl = pycurl.Curl()
        # ikili akış
        flux = BytesIO()
        # curl seçenekleri
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: Alınan verilerin depolanacağı yer
            curl.WRITEDATA: flux,
            # ayrıntılı mod
            curl.VERBOSE: suivi,
            # yeni bağlantı - önbellek kullanılmaz
            curl.FRESH_CONNECT: True,
            # istek zaman aşımı (saniye cinsinden)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            # sertifikaların geçerliliğini kontrol etme SSL
            curl.SSL_VERIFYPEER: False,
            # yönlendirmeleri takip et
            curl.FOLLOWLOCATION: True
        }
        # curl ayarları
        for option, value in options.items():
            curl.setopt(option, value)
        # Bu şekilde ayarlanmış CURL isteğinin yürütülmesi
        curl.perform()
        # server.html dosyasının oluşturulması - dosya adı için sorun yaratan karakterlerin değiştirilmesi
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        # Alınan belgenin HTML dosyasına kaydedilmesi
        html.write(flux.getvalue().decode(encoding))
    finally:
        # kaynakların serbest bırakılması
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------ana
# uygulama yapılandırılıyor
import config
config = config.configure()

# yapılandırma dosyasından URL değerlerinin alınması
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        # [site] sitesinden URL okunuyor
        get_url(url)
    # BaseException hariç hata:
    #     print(f"Aşağıdaki hata oluştu: {hata}")
    finally:
        pass
# son
print("Terminé...")

Açıklamalar

    1. satır: [pycurl] modülünü içe aktarıyoruz;
    1. satır: Sunucudan alınan verileri ikili akışta depolamamızı sağlayacak [BytesIO] sınıfı içe aktarılır;
  • 70-72. satırlar: Uygulamanın yapılandırması alınır;
  • 75-85. satırlar: yapılandırmada bulunan URL listesi üzerinde döngü yapıyoruz;
    1. satır: Her bir URL için, [get_url] işlevi çağrılır; bu işlev, url[‘target’] zaman aşımı süresiyle urlURL adresinden URL dosyasını indirir;
    1. satır: [get_url] işlevi, sorgulanacak URL'in yapılandırmasını alır;
  • 16-19. satırlar: URL'in yapılandırması ayrı değişkenlere alınır;
    1. ve 61. satırlar: Tüm işlemler bir try / finally bloğu içinde gerçekleştirilir. Oluşan istisnalar durdurulmaz; bu istisnalar çağrı yapan koda iletilir ve çağrı yapan kod tarafından durdurulur;
    1. satır: [curl] oturumu hazırlanır. [pycurl.Curl()], sunucuyla işlemi gerçekleştirecek olan [curl] kaynağını döndürür;
    1. satır: Alınan verileri depolayacak ikili akışın örneği oluşturulur;
  • satır 32-48: [options] sözlüğü, sunucuya [curl] bağlantısını yapılandıracaktır. Bunların rolleri açıklamalarda belirtilmiştir;
  • satır 49-51: bağlantı seçenekleri [curl] kaynağına aktarılır;
    1. satır: Tanımlanan seçeneklerle URL'e bağlantı talebi. [curl.WRITEDATA: flux] seçeneği (36. satır) nedeniyle, [curl.perform()] işlevi alınan verileri [flux] dosyasına kaydedecektir;
  • 54-60. satırlar: Alınan HTML belgesini depolayacak olan HTML dosyası oluşturulur;
  • satır 60: [flux.getvalue()] ikili akışı, HTML dosyasında bir karakter dizisi olarak depolanacaktır. Bu dizinin kodlaması, [decode(encoding)] yönteminde belirtilmiştir. Bu nedenle, sunucu tarafından gönderilen belgenin kodlamasını bilmek gerekir. Hata yapılırsa, ikili akışın kod çözme işlemi başarısız olur. Kodlama, URL yapılandırma dosyasında belirtilmiştir (örneğin 12. satır). Sunucu bu bilgiyi HTTP başlıklarında gönderdiği için bu bilgiyi dinamik olarak yönetebilirdik. Bu daha iyi olurdu. Kodu basit tutmak için bunu yapmadık. Belgenin kodlama türünü öğrenmek için, bir tarayıcıyla istenen URL dosyasını çağırıp, tarayıcının hata ayıklama modunda (F12) gönderilen HTTP başlıklarına bakmak ya da belgenin kendisine bakmak yeterlidir; çünkü belge de kodlamayı belirtir:

Image

Image

  • 61-66. satırlar: ayrılan kaynaklar serbest bırakılır;

[main.py] komut dosyasını çalıştırdığımızda konsolda şu sonuçlar elde edilir:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/http/02/main.py
-------------------------
sergetahe.com
-------------------------
Client : début de la communication avec le serveur [sergetahe.com]
*   Trying 87.98.154.146:80...
* TCP_NODELAY set
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET / HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 302 Found
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=UTF-8
< Transfer-Encoding: chunked
< Server: Apache
< X-Powered-By: PHP/7.3
< Location: http://sergetahe.com/programlama-dersleri-ve-öğreticiler
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0, sergetahe.com sunucusuna olduğu gibi bırakıldı
* Issue another request to this URL: 'http://sergetahe.com/programlama-dersleri-ve-öğreticiler'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) ana sunucu sergetahe.com ile
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 262
< Server: Apache
< Location: http://sergetahe.com/programlama-dersleri-ve-öğreticiler/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0, sergetahe.com ana sunucusuna olduğu gibi bırakıldı
* Issue another request to this URL: 'http://sergetahe.com/programlama-dersleri-ve-öğreticiler/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) ana sunucu sergetahe.com ile
* Connected to sergetahe.com (87.98.154.146) port 80 (#0)
> GET /cours-tutoriels-de-programmation/ HTTP/1.1
Host: sergetahe.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:52 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 263
< Server: Apache
< Location: https://sergetahe.com/programlama-dersleri-ve-öğreticiler/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0, sergetahe.com ana sunucusuna olduğu gibi bırakıldı
* Issue another request to this URL: 'https://sergetahe.com/programlama-dersleri-ve-öğreticiler/'
*   Trying 87.98.154.146:443...
* TCP_NODELAY set
* ….
* Using Stream ID: 1 (easy handle 0x25eaec77010)
> GET /cours-tutoriels-de-programmation/ HTTP/2
Host: sergetahe.com
user-agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
accept: */*

* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 200 
< date: Mon, 06 Jul 2020 06:45:53 GMT
< content-type: text/html; charset=UTF-8
< server: Apache
< x-powered-by: PHP/7.3
< link: <https://sergetahe.com/programlama-dersleri-ve-öğreticiler/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/programlama-dersleri-ve-öğreticiler/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1, sergetahe.com'i olduğu gibi bırakmak için
-------------------------
tahe.developpez.com
-------------------------
Client : début de la communication avec le serveur [tahe.developpez.com]
*   Trying 87.98.130.52:443...
* TCP_NODELAY set
* Connected to tahe.developpez.com (87.98.130.52) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN, server accepted to use http/1.1
* Server certificate:
*  subject: CN=*.developpez.com
*  start date: Jul  1 15:38:30 2020 GMT
*  expire date: Sep 29 15:38:30 2020 GMT
*  subjectAltName: host "tahe.developpez.com" matched cert's "*.developpez.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
> GET / HTTP/1.1
Host: tahe.developpez.com
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* old SSL session ID is stale, removing
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:53 GMT
< Server: Apache/2.4.38 (Debian)
< X-Powered-By: PHP/5.3.29
< Vary: Accept-Encoding
< Transfer-Encoding: chunked
< Content-Type: text/html
< 
* Connection #0, tahe.developpez.com'i olduğu gibi bırakmak
-------------------------
www.polytech-angers.fr
-------------------------
Client : début de la communication avec le serveur [www.polytech-angers.fr]
*   Trying 193.49.144.41:80...
* TCP_NODELAY set
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET / HTTP/1.1
Host: www.polytech-angers.fr
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Location: http://www.polytech-angers.fr/fr/index.html
< Cache-Control: max-age=1
< Expires: Mon, 06 Jul 2020 06:45:55 GMT
< Content-Length: 339
< Content-Type: text/html; charset=iso-8859-1
< 
* Ignoring the response-body
* Connection #0, www.polytech-angers.fr'i barındırmak üzere olduğu gibi bırakıldı
* Issue another request to this URL: 'http://www.polytech-angers.fr/fr/index.html'
* Found bundle for host www.polytech-angers.fr: 0x25eacafb490 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) ana bilgisayar www.polytech-angers.fr ile
* Connected to www.polytech-angers.fr (193.49.144.41) port 80 (#0)
> GET /fr/index.html HTTP/1.1
Host: www.polytech-angers.fr
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Last-Modified: Mon, 06 Jul 2020 04:50:09 GMT
< ETag: "85be-5a9be9bfcf228"
< Accept-Ranges: bytes
< Content-Length: 34238
< Cache-Control: max-age=1
< Expires: Mon, 06 Jul 2020 06:45:55 GMT
< Vary: Accept-Encoding
< Content-Type: text/html; charset=UTF-8
< Content-Language: fr
< 
* Connection #0, www.polytech-angers.fr ana bilgisayarına olduğu gibi bırakıldı
-------------------------
localhost
-------------------------
Client : début de la communication avec le serveur [localhost]
*   Trying ::1:80...
* TCP_NODELAY set
* Connected to localhost (::1) port 80 (#0)
> GET / HTTP/1.1
Host: localhost
User-Agent: PycURL/7.43.0.5 libcurl/7.68.0 OpenSSL/1.1.1d zlib/1.2.11 c-ares/1.15.0 WinIDN libssh2/1.9.0 nghttp2/1.40.0
Accept: */*

* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Mon, 06 Jul 2020 06:45:54 GMT
< Server: Apache/2.4.35 (Win64) OpenSSL/1.1.1b PHP/7.2.19
< X-Powered-By: PHP/7.2.19
< Content-Length: 1776
< Content-Type: text/html; charset=UTF-8
< 
* Connection #0, localhost sunucusuna gönderildi ve değiştirilmedi
Terminé...

Process finished with exit code 0

Yorumlar

  • mavi renkli olanlar, sunucuya gönderilen http komutları;
  • yeşil renkli olanlar, istemci tarafından yanıt olarak alınan veriler;
  • [curl] aracıyla yapılan işlemlerle aynı veri alışverişi elde edilir;
      1. satır: URL [http://sergetahe.com/] isteniyor;
      1. satır: sunucu, sayfanın yerinin değiştiğini bildirir. 21. satır, yeni URL;
      1. satır: URL [http://sergetahe.com/cours-tutoriels-de-programmation] isteniyor;
    • satır 38: sunucu, sayfanın taşındığını bildirir. Satır 43, yeni URL;
    • satır 54: URL [http://sergetahe.com/cours-tutoriels-de-programmation/] isteniyor;
    • satır 60: sunucu, sayfanın taşındığını bildirir. Satır 65, yeni URL. Bu, [HTTPS] güvenli protokolünü kullanır;
    • 71-75. satırlar: sunucu ile güvenli protokol kurulur;
      1. satır: URL [https://sergetahe.com/cours-tutoriels-de-programmation/] isteniyor;
      1. satır: İstenen belge bulundu;

21.4.6. Sonuç

Bu bölümde, HTTP protokolünü inceledik ve web'den bir URL dosyasını indirebilen bir [http/02/main.py] betiği yazdık.

21.5. SMTP protokolü (Basit Posta Aktarım Protokolü)

21.5.1. Giriş

Image

Bu bölümde:

  • [Serveur B], kuracağımız yerel bir SMTP sunucusu olacaktır;
  • [Client A], çeşitli biçimlerdeki bir SMTP istemcisi olacaktır:
    • [RawTcpClient] istemcisi, SMTP protokolünü keşfetmek için;
    • [RawTcpClient] istemcisinin SMTP protokolünü yeniden canlandıran bir Python betiği;
    • her türlü e-postayı göndermek için [smtplib] modülünü kullanan bir Python betiği;

21.5.2. [gmail] adresinin oluşturulması

SMTP testlerimizi yapmak için, e-posta gönderebileceğimiz bir adrese ihtiyacımız olacak. Bunun için bir Gmail adresi [https://www.google.com/intl/fr/gmail/about/] oluşturacağız:

Image

Not: Oluşturduğunuz adrese birkaç e-posta gönderin. Oluşturduğunuz hesabın e-posta alabildiğinden emin olduktan sonra bir sonraki adıma geçin.

21.5.3. SMTP sunucusunun kurulumu

Testlerimiz için, hem e-posta göndermeye olanak tanıyan bir SMTP sunucusu hem de sunucuda depolanan e-postaları okumaya yarayan bir POP3 (Post Office Protocol) sunucusu ve yine sunucuda depolanan e-postaları okumaya yarayan, ancak bunun ötesine geçen bir IMAP (Internet Message Access Protocol) sunucusudur. Özellikle, sunucudaki e-postaların depolanmasını yönetmeye olanak tanır.

[hMailServer] e-posta sunucusu, URL ve [https://www.hmailserver.com/] sürümlerinde (Mayıs 2019) kullanılabilir.

Image

Kurulum sırasında sizden bazı bilgiler istenecektir:

Image

  • [1-2] adımında, hem e-posta sunucusunu hem de onu yönetmek için gerekli araçları seçin;
  • kurulum sırasında sizden yönetici şifresi istenecektir: bunu not edin, çünkü ileride ihtiyacınız olacak;

[hMailServer], bilgisayarın açılışında otomatik olarak başlatılan bir Windows hizmeti olarak kurulur. Manuel başlatma seçeneğini tercih etmeniz daha iyidir:

  • [3]'te, durum çubuğundaki giriş alanına [services] yazın;

Image

  • [4-8]'te, hizmeti [manuel] moduna alın (6) ve başlatın (7);

Başlatıldıktan sonra, [hMailServer] sunucusu yapılandırılmalıdır. Sunucu, [hMailServer Administrator] yönetim programı ile kurulmuştur:

Image

  • [2]'te, durum çubuğundaki giriş alanına [hmailserver] yazın;
  • [3]'te, yöneticiyi başlatın;
  • [4]'te, yöneticiyi [hMailServer] sunucusuna bağlayın;
  • [5]'te, [hMailServer]'in kurulumu sırasında girilen şifreyi yazın;

Şifrenizi unuttuysanız, şu adımları izleyin:

  • [hMailServer] sunucusunu durdurun;
  • <hmailserver>'ın sunucunun kurulum klasörü olduğu [<hmailserver>/bin/hmailserver.ini] dosyasını açın:

Image

  • [100] dosyasında, [AdministratorPassword] satırındaki şifreyi silin. Bu işlem sonucunda yönetici şifresi kalmayacaktır. Şifre istendiğinde sadece [Entrée] yazın;

ValidLanguages=english,swedish
[Security]
AdministratorPassword=
[Database]

Sunucu yapılandırmasına devam edelim:

Image

  • [1-2] satırına, henüz yoksa bir etki alanı ekleyin;

Image

  • [3]'te, yapacağımız testler için hemen hemen her şeyi girebiliriz. Gerçekte ise mevcut bir alan adını girmemiz gerekir;

Image

Bir kullanıcı hesabı oluşturacağız:

  • [Accounts] (7) üzerine sağ tıklayıp (8) seçeneğini seçerek yeni bir kullanıcı ekleyeceğiz;
  • [General] (9) sekmesinde, [guest] (10) adlı bir kullanıcıyı [guest] (11) şifresiyle tanımlıyoruz. Bu kullanıcının e-posta adresi [guest@localhost] (10) olacaktır;
  • [12]'te, [guest] kullanıcısı etkinleştirilmiştir;

Image

  • [13-14]'te, kullanıcı oluşturulur;

Image

  • [27]'te, SMTP hizmetinin bağlantı noktası;
  • [28]'te bu hizmet kimlik doğrulama gerektirmez;
  • [30]'te, SMTP sunucusunun istemcilerine göndereceği karşılama mesajını girin;

Image

Aynı işlemi POP3 sunucusu için de yapın:

Image

Aynı işlemi IMAP sunucusu için de tekrarlayalım:

Image

[hMailServer] sunucusunun varsayılan etki alanını (birden fazla olabilir) olarak belirtin:

Image

  • [37]'te, SMTP sunucusunun varsayılan etki alanının, [38]'te oluşturduğunuz etki alanı olduğunu belirtin;

Bu yapılandırmayı kaydettikten sonra, aşağıdaki şekilde test edebilirsiniz. Yardımcı programlar klasöründe bir PyCharm terminali açın:

Image

Ardından şu komutu girin:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25
Client [DESKTOP-30FF5FB:50170] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
    1. satır: [localhost] makinesinin 25 numaralı bağlantı noktasına bağlanıyoruz. Burada, [hMailServer] sunucusuna ait güvenli olmayan bir SMTP sunucusu çalışıyor;
    1. satır: Bir önceki 30. adımda yapılandırdığımız karşılama mesajını alıyoruz;

Demek ki SMTP sunucusu düzgün bir şekilde kurulmuş. [quit] komutunu girerek SMTP 25 sunucusuyla olan iletişimimizi sonlandırın.

Şimdi, güvenli posta aktarım hizmeti SMTP’in varsayılan bağlantı noktası olan 587 numaralı bağlantı noktası için de aynı işlemi yapalım:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 587
Client [DESKTOP-30FF5FB:50217] connecté au serveur [localhost-587]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
    1. satır, 587 numaralı bağlantı noktasında çalışan SMTP sunucusunun yanıtı;

Şimdi, POP3 posta aktarma hizmetinin varsayılan bağlantı noktası olan 110 numaralı bağlantı noktası için de aynısını yapalım:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 110
Client [DESKTOP-30FF5FB:50210] connecté au serveur [localhost-110]
Tapez vos commandes (quit pour arrêter) :
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
    1. satırda, POP3 sunucusundan karşılama mesajı aldık;

Şimdi, IMAP posta alma hizmetinin varsayılan bağlantı noktası olan 143 numaralı bağlantı noktası için de aynısını yapalım:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 143
Client [DESKTOP-30FF5FB:50212] connecté au serveur [localhost-143]
Tapez vos commandes (quit pour arrêter) :
<-- [* OK Bienvenue sur le serveur IMAP localhost.com]
    1. satırda, IMAP sunucusundan hoş geldiniz mesajı aldık;

21.5.4. E-posta okuyucusunun kurulumu

Göndereceğimiz e-postaları okumak için bir e-posta okuyucusuna ihtiyacımız var. Henüz bir okuyucusu olmayanlar için, [Thunderbird] okuyucusunun kurulum ve yapılandırmasını göstereceğiz:

  • [1]'te: [thunderbird]'i indirin ve kurun;

Image

  • henüz başlatılmamışsa [hMailServer] posta sunucusunu başlatın;
  • [2-3]: Thunderbird başlatıldıktan sonra, [guest@localhost] kullanıcısı için [hMailServer] posta sunucusunda bir e-posta hesabı oluşturacağız;

Image

Image

Image

  • [7-11]'te: [hMailServer] posta sunucusundaki e-postaları okumamızı sağlayacak olan POP3 sunucusu, [localhost] adresinde bulunur ve 110 numaralı bağlantı noktasında çalışır;
  • [12-16]: [hMailServer] posta sunucusundaki kullanıcılar adına e-posta göndermemizi sağlayacak olan SMTP sunucusu, [localhost] adresinde bulunur ve 25 numaralı bağlantı noktasında çalışır;
  • [18]: Bu yapılandırmanın geçerliliğini test edebiliriz;

Image

Image

  • [26] adresinde: SSL şifreleme olmadığı için Thunderbird, yapılandırmamızın riskler içerdiği konusunda bizi uyarıyor;
  • [28]'te: hesap oluşturuldu;

Oluşturulan hesabı test etmek için Thunderbird ile şunları yapacağız:

  • [guest@localhost.com] kullanıcısına bir e-posta göndereceğiz (SMTP protokolü);
  • bu kullanıcı tarafından alınan e-postayı okuyacağız (POP3 protokolü);

Image

  • [3]'te: gönderen;
  • [4]: alıcı;
  • [5]: e-postanın konusu;
  • [6]: e-postanın içeriği;
  • [7]: e-postayı göndermek için;

Image

  • [8-9]'te: [guest@localhost] kullanıcısının postası alınır;
  • [10-15]'te: alınan mesaj;

Ayrıca [pymailparlexemple@gmail.com] kullanıcısına da e-posta göndereceğiz. Alacağı e-postaları okuyabilmesi için Thunderbird'de ona bir hesap oluşturalım:

Image

Image

  • [4]'te: istediğinizi yazın;
  • [5] kısmına: adres [pymailparlexemple@gmail.com]'tir;
  • [6]'te: bu kullanıcıyı oluştururken ona verdiğiniz şifreyi girin;
  • [7]'te: bu yapılandırmayı onaylayın;

Image

  • [8]'te: Thunderbird, veritabanından aşağıdaki bilgileri aldı;
  • [9]'te: Posta okuma protokolü artık POP3 değil, IMAP'tir. İkisi arasındaki temel fark şudur: [POP3], okunan postayı posta okuyucusunun bulunduğu yerel makineye indirir ve uzak sunucudan silerken, [IMAP] postayı uzak sunucuda tutar;
  • [10]: SMTP sunucusunun kimliği;
  • [13]'te: IMAP ve SMTP sunucuları hakkında daha fazla bilgi almak için manuel yapılandırmaya geçilir;

Image

  • [14-17]'e: IMAP sunucusunun özellikleri;
  • [18-21]'e: SMTP sunucusunun özellikleri;
  • [22]'te: yapılandırma tamamlanır;

Image

  • [23-24]'te: yeni Thunderbird hesabı;
  • [26]'te: yeni bir mesaj yazıyoruz;

Image

  • [27]'te: gönderen [pymailparlexemple@gmail.com]'tir;
  • [28]'te: alıcı [pymailparlexemple@gmail.com]'tir;
  • [29-30]'te: mesaj;
  • [31]'te: göndermek için;

Image

  • [32]'te: farklı hesaplardan gelen postalar alınır;

Image

  • [33-36]'te: kullanıcı tarafından alınan e-postalar [pymailparlexemple@gmail.com]

Aynı şekilde şunu da oluşturuyoruz:

  • yeni bir Gmail hesabı [pymail2parlexemple@gmail.com];
  • aynı isimli kullanıcının mesajlarını almak için yeni bir Thunderbird hesabı [pymail2parlexemple@gmail.com]:

Image

Image

Artık SMTP, POP3 ve IMAP protokollerini incelemek için gerekli araçlara sahibiz. SMTP protokolüyle başlıyoruz.

21.5.5. SMTP protokolü

Image

[hMailServer] sunucusunun günlüklerini inceleyerek SMTP protokolünü keşfedeceğiz. Bunun için, [hmailServerAdministrator] komutuyla günlükleri etkinleştiriyoruz:

Image

Image

  • [2]'te günlükler etkinleştirilmiştir;
  • [3-5]'te: SMTP, POP3 ve IMAP protokolleri için günlükleri etkinleştiriyoruz;
  • [7]'te, günlükleri görüntülemek isteniyor;
  • [8]'te, günlük dosyasını herhangi bir metin düzenleyicisiyle açar;

Image

Aşağıdaki örnekte, istemci [Thunderbird] ve sunucu [hMailServer] olacaktır. Thunderbird'de, [guest@localhost.com] kullanıcısının kendisine bir mesaj göndermesini sağlayın:

Image

Günlük kayıtları şu şekilde olacaktır:


"SMTPD"    5828    22    "2020-07-07 10:02:54.263"    "127.0.0.1"    "SENT: 220 Bienvenue sur le serveur SMTP localhost.com"
"SMTPD"    21956    22    "2020-07-07 10:02:54.360"    "127.0.0.1"    "RECEIVED: EHLO [127.0.0.1]"
"SMTPD"    21956    22    "2020-07-07 10:02:54.362"    "127.0.0.1"    "SENT: 250-DESKTOP-30FF5FB[nl]250-SIZE 20480000[nl]250-AUTH LOGIN[nl]250 HELP"
"SMTPD"    5828    22    "2020-07-07 10:02:54.381"    "127.0.0.1"    "RECEIVED: MAIL FROM:<guest@localhost.com> SIZE=433"
"SMTPD"    5828    22    "2020-07-07 10:02:54.386"    "127.0.0.1"    "SENT: 250 OK"
"SMTPD"    21956    22    "2020-07-07 10:02:54.470"    "127.0.0.1"    "RECEIVED: RCPT TO:<guest@localhost.com>"
"SMTPD"    21956    22    "2020-07-07 10:02:54.473"    "127.0.0.1"    "SENT: 250 OK"
"SMTPD"    21956    22    "2020-07-07 10:02:54.478"    "127.0.0.1"    "RECEIVED: DATA"
"SMTPD"    21956    22    "2020-07-07 10:02:54.479"    "127.0.0.1"    "SENT: 354 OK, send."
"SMTPD"    21860    22    "2020-07-07 10:02:54.496"    "127.0.0.1"    "SENT: 250 Queued (0.016 seconds)"
"SMTPD"    21568    22    "2020-07-07 10:02:54.505"    "127.0.0.1"    "RECEIVED: QUIT"
"SMTPD"    21568    22    "2020-07-07 10:02:54.506"    "127.0.0.1"    "SENT: 221 goodbye"

Yukarıdaki satırlar, SMTP istemcisi (Thunderbird e-posta yöneticisi) ile SMTP sunucusu (hMailServer) arasında gerçekleşen iletişimi göstermektedir. [SENT] satırları, SMTP sunucusunun istemcisine ne gönderdiğini gösterir. [RECEIVED] satırları ise SMTP sunucusunun istemcisinden ne aldığını gösterir.

    1. satır: İstemcinin SMTP sunucusuna bağlanmasının hemen ardından, sunucu istemcisine hoş geldin mesajını gönderir;
    1. satır: Müşteri, kimliğini belirtmek için [EHLO] komutunu gönderir. Burada, IP [127.0.0.1] adresini verir; bu adres, [localhost] makinesini, yani SMTP istemcisini çalıştıran makineyi belirtir;
    1. satır: Sunucu, [250] şeklinde bir dizi yanıt gönderir. [nl], [newline]'i, yani \n karakterini ifade eder. Yanıtlar [250-] biçimindedir; ancak sonuncusu [250 ] biçimindedir. Böylelikle SMTP istemcisi, SMTP sunucusunun yanıtının sona erdiğini ve bir komut gönderebileceğini anlar. [250] komut dizisi, SMTP istemcisine kullanabileceği bir dizi komutu belirtmeyi amaçlıyordu;
    1. satır: SMTP istemcisi, mesajı kimin gönderdiğini belirten [MAIL FROM : adresse_mail_expéditeur] komutunu gönderir;
    1. satır: SMTP sunucusu, komutu anladığını belirten [250 OK] komutuyla yanıt verir;
    1. satır: SMTP istemcisi, alıcının adresini belirtmek için [RCPT TO : adresse_mail_destinataire] komutunu gönderir;
    1. satır: Yine SMTP sunucusu, komutu anladığını belirtir;
    1. satır: SMTP sunucusu, [DATA] komutunu gönderir. Bu, mesajın içeriğini göndereceği anlamına gelir;
    1. satır: SMTP sunucusu, [354 OK] yanıtıyla mesajı almaya hazır olduğunu belirtir. [send .] metni, SMTP istemcisinin mesajını yalnızca tek bir nokta içeren bir satırla bitirmesi gerektiğini belirtir;
  • ardından, SMTP istemcisinin mesajını gönderdiği görülmemektedir. Günlüklerde bu işlem gösterilmemektedir;
    1. satır: SMTP istemcisi, mesajın sonunu belirten noktayı gönderdi. SMTP sunucusu, mesajı kuyruğa (queued) aldığını bildirerek yanıt verdi;
  • SMTP istemcisi, bağlantıyı kapatacağını belirtmek için [QUIT] komutunu gönderir;
    1. satır: sunucu buna yanıt verir;

Artık SMTP protokolünün istemci/sunucu diyaloğunu bildiğimize göre, bunu [RawTcpClient] istemcimizle yeniden canlandırmaya çalışalım. PyCharm terminalini kullanıyoruz:

Image

Yeni bir örnek inceleyelim:

  • A istemcisi, [RawTcpClient] genel istemcisi olan TCP olacaktır;
  • sunucu B, [hMailServer] e-posta sunucusu olacaktır;
  • A istemcisi, B sunucusundan [guest@localhost.com] kullanıcısı tarafından kendisine gönderilen bir e-postayı dağıtmasını isteyecektir;
  • Gönderilen e-postanın alıcıya ulaştığını kontrol edeceğiz;

İstemciyi şu şekilde başlatıyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25 --quit bye
Client [DESKTOP-30FF5FB:53122] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
  • [1] satırında, SMTP'in [hMailServer] hizmetinin çalıştığı yerel makinenin 25 numaralı bağlantı noktasına bağlanıyoruz. [--quit bye] argümanı, kullanıcının [bye] komutunu girerek programdan çıkacağını belirtir. Bu argüman olmadan, programı sonlandırma komutu [quit]'tir. Ancak [quit], aynı zamanda SMTP protokolünün bir komutudur. Dolayısıyla bu belirsizliği ortadan kaldırmamız gerekir;
  • [2] satırında, istemci başarıyla bağlanmıştır;
  • [3] satırında, istemci klavyeden girilecek komutları bekliyor;
  • [4] satırında, sunucu müşteriye hoş geldin mesajını gönderir;

Diyaloğa şu şekilde devam ediyoruz:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 25
Client [DESKTOP-30FF5FB:53155] connecté au serveur [localhost-25]
Tapez vos commandes (quit pour arrêter) :
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
EHLO localhost
<-- [250-DESKTOP-30FF5FB]
<-- [250-SIZE 20480000]
<-- [250-AUTH LOGIN]
<-- [250 HELP]
MAIL FROM: guest@localhost.com
<-- [250 OK]
RCPT TO: guest@localhost.com
<-- [250 OK]
DATA
<-- [354 OK, send.]
from: guest@localhost.com
to: guest@localhost.com
subject: ceci est un test

ligne1
ligne2
.
<-- [250 Queued (37.824 seconds)]
QUIT
Fin de la connexion avec le serveur
  • [5] satırında, istemci [EHLO nom-de-la-machine-client] komutunu gönderir. Sunucu, [250-xx] (6) biçiminde bir dizi mesajla yanıt verir. [250] kodu, istemci tarafından gönderilen komutun başarılı olduğunu gösterir;
  • [10]'te, istemci mesajın gönderenini belirtir; bu örnekte [guest@localhost.com];
  • [11]'te sunucunun yanıtı yer alır;
  • [12]'te mesajın alıcısı belirtilir; burada alıcı, [guest@localhost.com] kullanıcısıdır;
  • [13]'te, sunucunun yanıtı;
  • [14]'te, [DATA] komutu sunucuya, istemcinin mesajın içeriğini göndereceğini bildirir;
  • [15]'te, sunucunun yanıtı;
  • [16-22]'te, istemci, yalnızca tek bir nokta içeren bir satırla biten bir metin satırları listesi göndermelidir. Mesaj, sırasıyla mesajın konusunu, göndereni ve alıcıyı tanımlamak için [Subject:, From:, To:] (16-18) satırlarını içerebilir;
  • [19]'te, önceki başlıkların ardından boş bir satır gelmelidir;
  • [20-21]'te, mesaj metni;
  • [22] satırında, mesajın sonunu belirten tek bir nokta yer almalıdır;
  • [23]'te, sunucu sadece tek bir nokta içeren satırı aldığında, mesajı kuyruğa alır;
  • [24]'te, istemci sunucuya işini bitirdiğini bildirir;
  • [25]'te, sunucunun istemciyle olan bağlantısını kapattığı görülür;

Şimdi Thunderbird ile [guest@localhost.com] kullanıcısının mesajı aldığını kontrol edelim:

Image

  • [1-6]'te, [guest@localhost.com] kullanıcısının mesajı başarıyla aldığını görüyoruz;

Son olarak, [RawTcpClient] adlı müşterimiz, SMTP [localhost] sunucusu üzerinden bir mesaj göndermeyi başardı. Şimdi aynı yöntemi kullanarak [pymailparlexemple@gmail.com]'e bir mesaj gönderelim:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe smtp.gmail.com 587
Client [DESKTOP-30FF5FB:53210] connecté au serveur [smtp.gmail.com-587]
Tapez vos commandes (quit pour arrêter) :
<-- [220 smtp.gmail.com ESMTP w13sm643278wrr.67 - gsmtp]
EHLO localhost
<-- [250-smtp.gmail.com at your service, [2a01:cb05:80e8:b500:3c4b:2203:91fa:9b00]]
<-- [250-SIZE 35882577]
<-- [250-8BITMIME]
<-- [250-STARTTLS]
<-- [250-ENHANCEDSTATUSCODES]
<-- [250-PIPELINING]
<-- [250-CHUNKING]
<-- [250 SMTPUTF8]
MAIL FROM: pymailparlexemple@gmail.com
<-- [530 5.7.0 Must issue a STARTTLS command first. w13sm643278wrr.67 - gsmtp]
QUIT
Fin de la connexion avec le serveur
    1. satır: 587 numaralı bağlantı noktasında çalışan Gmail'in SMTP sunucusunu kullanıyoruz;
    1. satır: SMTP sunucusu, nasıl yapılacağını bilmediğimiz bir güvenli bağlantı kurmamızı istediği için takıldık. Önceki örneğin aksine, [smtp.gmail.com] sunucusu (1. satır) kimlik doğrulama istiyor. Bu sunucu, yalnızca [gmail.com] etki alanında kayıtlı kullanıcıları istemci olarak kabul ediyor. Bu kimlik doğrulama işlemi güvenlidir ve şifreli bir bağlantı içinde gerçekleşir.

İlk örnek, Python'da temel bir SMTP istemcisi oluşturmak için gerekli temelleri sağladı. İkinci örnek ise, bazı SMTP sunucularının (aslında çoğunun) şifreli bir bağlantı üzerinden kimlik doğrulaması gerektirdiğini gösterdi.

21.5.6. [smtp/01] komut dosyaları: temel bir SMTP istemcisi

Daha önce SMTP protokolü hakkında öğrendiklerimizi Python'da yeniden oluşturacağız.

Image

[smtp/01/config] dosyası, uygulamayı şu şekilde yapılandırır:


def configure() -> dict:
    return {
        # açıklama: gönderilen e-postanın açıklaması
        # smtp-server: sunucu SMTP
        # smtp-port: SMTP sunucusunun bağlantı noktası
        # gönderen: gönderen
        # to: alıcı
        # konu: e-postanın konusu
        # mesaj: e-postanın içeriği
        "mails": [
            {
                "description": "mail to localhost via localhost",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost",
                # UTF-8 gönderiliyor
                "content-type": 'text/plain; charset="utf-8"',
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            },
            {
                "description": "mail to gmail via gmail",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymailparlexemple@gmail.com",
                "to": "pymailparlexemple@gmail.com",
                "subject": "to gmail via gmail",
                # UTF-8 gönderiliyor
                "Content-type": 'text/plain; charset="utf-8"',
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • 10-35. satırlar: gönderilecek e-postaların listesi. Her biri için aşağıdaki bilgiler belirtilir:
    • [description]: e-postayı açıklayan bir metin;
    • [smtp-server]: kullanılacak SMTP sunucusu;
    • [smtp-port]: hizmet bağlantı noktası;
    • [from]: e-postanın göndereni;
    • [to]: e-postanın alıcısı;
    • [subject]: e-postanın konusu;
    • [content-type]: e-postanın kodlaması;
    • [message]: e-postanın içeriği;

SMTP istemcisinin [01/main] kodu şöyledir:


# içe aktarmalar
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # gönderici adına smtpserver SMTP sunucusuna mesaj gönderiliyor
    # alıcıya. verbose=True ise, istemci-sunucu iletişimini izler

    # sistem hatalarının bildirilmesine izin verilir
    connexion = None
    try:
        # yerel makinenin adı (SMTP protokolü için gereklidir)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # smtpServer'in 25 numaralı bağlantı noktasında bir bağlantı açılır
        connexion = socket.create_connection((mail["smtp-server"], 25))

        # bağlantısı, müşteri (bu program) ile erişilen SMTP sunucusu arasında
        # istemci (bu program) ile bağlanılan SMTP sunucusu arasında
        # bu kanal, komut ve bilgi alışverişi için kullanılır

        # bağlantı kurulduktan sonra sunucu, okunan bir karşılama mesajı gönderir
        send_command(connexion, "", verbose, True)
        # ehlo komutu:
        send_command(connexion, f"EHLO {client}", verbose, True)
        # "mail from" komutu:
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # rcpt to komutu:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # data komutu
        send_command(connexion, "DATA", verbose, True)
        # gönderilecek mesajın hazırlanması
        # şu satırları içermelidir
        # From: gönderen
        # Kime: alıcı
        # boş satır
        # Mesaj
        # .
        data = f"{mail['message']}"
        # mesaj gönderiliyor
        send_command(connexion, data, verbose, False)
        # gönderme .
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # çıkış komutu
        send_command(connexion, "QUIT", verbose, True)
        # son
    finally:
        # bağlantıyı kapat
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # bağlantı kanalına komut gönder
    # verbose=True ise ayrıntılı mod
    # with_rclf=True ise, komuta rclf dizisini ekler

    # veriler
    rclf = "\r\n" if with_rclf else ""
    # komut boş değilse komut gönderilir
    if commande:
        # sistem hatalarının yukarıya iletilmesine izin verilir
        #
        # komut gönder
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # olası yankı
        if verbose:
            affiche(commande, 1)
        # 1000 karakterden kısa yanıtın okunması
        reponse = str(connexion.recv(1000), 'utf-8')
        # olası yanıt
        if verbose:
            affiche(reponse, 2)
        # hata kodunun alınması
        codeErreur = int(reponse[0:3])
        # sunucu tarafından gönderilen hata mı?
        if codeErreur >= 500:
            # hata ile bir istisna atılıyor
            raise BaseException(reponse[4:])
        # hatasız dönüş


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # değişimi ekrana mı görüntüler?
    # eğer sens=1 ise -->echange görüntülenir
    # eğer sens=2 ise, son iki rclf karakteri hariç <-- değişim mesajını görüntüler
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


# ana sayfa ----------------------------------------------------------------

# SMTP istemcisi (SendMail Aktarım Protokolü) mesaj göndermeye olanak tanır
# bilgiler, her sunucu için aşağıdaki bilgileri içeren bir yapılandırma dosyasından alınır

# açıklama: gönderilen e-postanın açıklaması
# smtp-server: SMTP sunucusu
# smtp-port: SMTP sunucusunun bağlantı noktası
# gönderen: gönderen
# to: alıcı
# konu: e-postanın konusu
# mesaj: e-postanın içeriği


# SMTP istemci-sunucu iletişim protokolü
# -> istemci, SMTP sunucusunun 25 numaralı bağlantı noktasına bağlanır
# <- sunucu, istemciye bir hoş geldin mesajı gönderir
# -> istemci EHLO komutunu gönderir: makinesinin adı
# <- sunucu OK yanıtını gönderir veya göndermez
# -> istemci "mail from: <gönderen>" komutunu gönderir
# <- sunucu OK yanıtını verir veya vermez
# -> istemci rcpt to: <alıcı> komutunu gönderir
# <- sunucu OK yanıtını verir veya vermez
# -> istemci data komutunu gönderir
# <- sunucu OK ile yanıt verir veya vermez
# -> istemci, mesajının tüm satırlarını gönderir ve tek bir karakter içeren bir satırla bitirir.
# <- sunucu OK yanıtını verir veya vermez
# -> istemci quit komutunu gönderir
# <- sunucu OK yanıtını verir veya vermez

# sunucunun yanıtları xxx metin biçimindedir; burada xxx, 3 basamaklı bir sayıdır. Herhangi bir xxx sayısı >=500
# bir hatayı bildirir. Yanıt, son satır hariç hepsi xxx- ile başlayan birden fazla satır içerebilir
# şu biçimdedir: xxx(boşluk)

# değiş tokuş edilen metin satırları, RC(#13) ve LF(#10) karakterleriyle bitmelidir

# uygulama yapılandırması
import config
config = config.configure()

# e-postalar tek tek işlenir
for mail in config['mails']:
    try:
        # günlükler
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # gönderilecek mesajın hazırlanması
        mail[
            "message"] = f"From: {mail['from']}\nTo: {mail['to']}\n" \
                         f"Subject: {mail['subject']}\n" \
                         f"Content-type: {mail['content-type']}" \
                         f"\n\n{mail['message']}"
        # mesajın ayrıntılı modda gönderilmesi
        sendmail(mail, True)
        # son
        print("Message envoyé...")
    except BaseException as erreur:
        # hatayı görüntüleme
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # sonraki e-posta

Açıklamalar

  • 134-136. satırlar: uygulama yapılandırılıyor;
  • 139-151. satırlar: yapılandırmada bulunan tüm e-postalar taratılır;
  • 141-143. satırlar: yapılacak işlemler görüntülenir;
  • 144-149. satırlar: gönderilecek mesaj tanımlanır. [message] mesajının başında [From, To, Subject, Content-type] başlıkları yer alır;
    1. satır: E-postanın gönderilmesi, iki parametre kabul eden [sendmail] işlevi tarafından gerçekleştirilir:
    2. [mail]: e-postanın gönderilmesi için gerekli bilgileri içeren sözlük;
    3. [verbose]: istemci/sunucu arasındaki iletişimin konsolda günlüğe kaydedilip kaydedilmeyeceğini belirten bir boole değeri;
  • 154-156. satırlar: [sendmail] işlevinden çıkan tüm istisnalar durdurulur. Bu istisnalar görüntülenir;
    1. satır: [mail], gönderilecek e-postayı tanımlayan sözlüktür;
    1. satır: SMTP protokolünde, istemci adını göndermelidir. Burada, istemci olarak kullanılacak yerel makinenin adı alınır;
    1. satır: Mesajın gönderileceği SMTP sunucusuna bağlantı kurulur;
  • 22-23. satırlar: SMTP sunucusuna bağlantı kurulduysa, bu sunucu bir hoş geldiniz mesajı gönderir ve bu mesaj burada okunur;
  • [sendmail] işlevi daha sonra bir SMTP istemcisinin göndermesi gereken çeşitli komutları gönderir:
    • 24-25. satırlar: EHLO komutu;
    • 26-27. satırlar: MAIL FROM komutu: ;
    • 28-29. satırlar: RCPT TO: komutu;
    • satır 30-31: DATA komutu;
    • satır 32-41: mesajın gönderilmesi (From, To, Subject, Content-type, metin);
    • 42-43. satırlar: sonlandırma işaretinin gönderilmesi;
    • satır 44-457: QUIT komutu, istemcinin SMTP sunucusuyla olan diyaloğunu sonlandırır;
  • [sendmail] komutunun yürütülmesi, tüm istisnaları çağıran koda iletilmesini sağlayan bir [try / finally] içinde gerçekleştirilir. Çağıran kodun tüm istisnaları durdurup görüntülediği bilinmektedir;
  • 48-50. satırlar: kaynakların serbest bırakılması;
    1. satır: [send_command] işlevi, istemciden gelen komutları SMTP sunucusuna göndermekle görevlidir. Bu işlev dört parametre kabul eder:
    2. [connexion]: İstemciyi sunucuya bağlayan bağlantı;
    3. [commande]: gönderilecek komut;
    4. [verbose]: TRUE ayarlanmışsa, istemci/sunucu arasındaki iletişim konsolda günlüğe kaydedilir;
    5. [with_rclf]: Eğer TRUE ise, komutu \r\n dizisiyle sonlandırarak gönderir. Bu, SMTP protokolündeki tüm komutlar için gereklidir, ancak [send_command] komutu da mesajı göndermek için kullanılır. Burada \r\n dizisi eklenmez;
    1. satır: komut, yalnızca boş değilse gönderilir;
  • 65-66. satırlar: komut, UTF-8 bayt dizisi biçiminde sunucuya gönderilir;
  • 70-71. satırlar: yanıtın tüm satırları okunur. Yanıtın 1000 karakterden az olduğu varsayılır. Yanıt birkaç satırdan oluşabilir. Her satır XXX-YYY biçimindedir; burada XXX sayısal bir koddur, ancak yanıtın son satırı XXX YYY biçimindedir (-) karakteri yoktur;
    1. satır: 1. satırdaki XXX hata kodunun okunması;
  • 78-80. satırlar: XXX sayısal kodu 500'den büyükse, sunucu bir hata döndürmüştür. Bu durumda bir istisna tetiklenir;

Sonuçlar

Komut dosyasının çalıştırılması sonucunda konsolda şu çıktılar görüntülenir:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/01/main.py
----------------------------------
Envoi du message [mail to localhost via localhost]
--> [EHLO DESKTOP-30FF5FB]
<-- [220 Bienvenue sur le serveur SMTP localhost.com]
--> [MAIL FROM: <guest@localhost.com>]
<-- [250-DESKTOP-30FF5FB
250-SIZE 20480000
250-AUTH LOGIN
250 HELP]
--> [RCPT TO: <guest@localhost.com>]
<-- [250 OK]
--> [DATA]
<-- [250 OK]
--> [From: guest@localhost.com
To: guest@localhost.com
Subject: to localhost via localhost
Content-type: text/plain; charset="utf-8"

aglaë séléné
va au marché
acheter des fleurs]
<-- [354 OK, send.]
--> [
.
]
<-- [250 Queued (0.000 seconds)]
--> [QUIT]
<-- [221 goodbye]
Message envoyé...
----------------------------------
Envoi du message [mail to gmail via gmail]
--> [EHLO DESKTOP-30FF5FB]
<-- [220 smtp.gmail.com ESMTP u1sm1364433wrb.78 - gsmtp]
--> [MAIL FROM: <pymailparlexemple@gmail.com>]
<-- [250-smtp.gmail.com at your service, [2a01:cb05:80e8:b500:3c4b:2203:91fa:9b00]
250-SIZE 35882577
250-8BITMIME
250-STARTTLS
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-CHUNKING
250 SMTPUTF8]
--> [RCPT TO: <pymailparlexemple@gmail.com>]
<-- [530 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp]
L'erreur suivante s'est produite : 5.7.0 Must issue a STARTTLS command first. u1sm1364433wrb.78 - gsmtp


Process finished with exit code 0
  • 3-30. satırlar: SMTP [hMailServer] sunucusunun [guest@localhost] adresine e-posta göndermek için kullanılması sorunsuz gerçekleşir;
  • 32-46. satırlar: SMTP ve [smtp.gmail.com] sunucularının [pymailparlexemple@gmail.com] adresine e-posta göndermek için kullanılması sorunsuz gerçekleşmiyor: 45. satırda, SMTP sunucusu bir hata mesajıyla birlikte 530 hata kodunu gönderiyor. Bu mesaj, SMTP istemcisinin önceden güvenli bir bağlantı üzerinden kimlik doğrulaması yapması gerektiğini belirtiyor. İstemcimiz bunu yapmadığı için reddediliyor;

Thunderbird'deki sonuçlar şu şekildedir:

Image

21.5.7. [smtp/02] komut dosyaları: [smtplib] kütüphanesi ile yazılmış bir SMTP bağlantısı

Image

Önceki istemcinin en az iki eksikliği vardır:

  1. sunucu talep ettiğinde güvenli bağlantı kullanamıyor;
  1. mesaja ek ekleyemiyor;

İlk eksikliği [smtp/02] betiğinde ele alacağız. Yeni betiğimizde [smtplib] Python modülünü kullanacağız.

[smtp/02/main] komut dosyası, aşağıdaki jSON ve [smtp/02/config] yapılandırma dosyalarını kullanacaktır:


def configure() -> dict:
    return {
        # açıklama: gönderilen e-postanın açıklaması
        # smtp-sunucusu: SMTP sunucusu
        # smtp-port: SMTP sunucusunun bağlantı noktası
        # gönderen: gönderen
        # to : alıcı
        # konu: e-postanın konusu
        # message : e-postanın içeriği
        "mails": [
            {
                "description": "mail to localhost via localhost avec smtplib",
                "smtp-server": "localhost",
                "smtp-port": "25",
                "from": "guest@localhost.com",
                "to": "guest@localhost.com",
                "subject": "to localhost via localhost avec smtplib",
                # aksanlı karakterler test ediliyor
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
            },
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # kimlik doğrulamalı SMTP
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

[smtp/01/config] dosyasında bulunan başlıkların aynısı burada da yer almaktadır; ancak SMTP sunucusu kimlik doğrulama istediğinde iki ek başlık daha eklenir:

  • 31. satır, [user]: bağlantıyı doğrulayan kullanıcının adı;
  • 32. satır, [password]: kullanıcının şifresi;

Bu iki alan, yalnızca bağlantı kurulan SMTP sunucusu kimlik doğrulama talep ederse görünür. Bu durumda kimlik doğrulama, güvenli bir bağlantı üzerinden gerçekleştirilir.

[smtp/02/main.py] komut dosyasının kodu şöyledir:


# içe aktarmalar
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # gönderici adına smtpserver SMTP sunucusuna mesaj gönderir
    # alıcıya. verbose=True ise, istemci-sunucu iletişimini izler

    # smtplib kütüphanesi kullanılır
    # istisnaların yukarıya iletilmesine izin verilir
    #
    # sunucu SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # ayrıntılı mod
    server.set_debuglevel(verbose)
    # güvenli bağlantı mı?
    if "user" in mail:
        # güvenli bağlantı
        server.starttls()
        # EHLO komut + kimlik doğrulama
        server.login(mail["user"], mail["password"])

   # Multipart mesajının oluşturulması - gönderilecek olan Multipart mesajı budur
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # mesaj gönderiliyor
    server.send_message(msg)
    # çıkılıyor
    server.quit()


# ana sayfa ----------------------------------------------------------------

# bilgiler, her sunucu için aşağıdaki bilgileri içeren bir yapılandırma dosyasından alınır

# açıklama: gönderilen e-postanın açıklaması
# smtp-sunucusu: sunucu SMTP
# smtp-port: SMTP sunucusunun bağlantı noktası
# gönderen: gönderen
# to: alıcı
# konu: e-postanın konusu
# content-type: e-postanın kodlaması
# mesaj: e-postanın içeriği


# uygulama yapılandırması
import config
config = config.configure()

# e-postalar tek tek işlenir
for mail in config['mails']:
    try:
        # günlükler
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # mesajın ayrıntılı modda gönderilmesi
        sendmail(mail, True)
        # son
        print("Message envoyé...")
    except BaseException as erreur:
        # hatayı gösterme
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # sonraki e-posta

Açıklamalar

  • 8-35. satırlar: Yalnızca [sendmail] işlevi kullanılır. Artık [smtplib] modülünü (2. satır) kullanacaktır;
  • 16. satır: SMTP sunucusuna bağlantı;
  • satır 18: [verbose=True] ise, istemci/sunucu arasındaki iletişim konsolda görüntülenecektir;
  • satır 20-24: SMTP sunucusu gerektiriyorsa, gerekli kimlik doğrulama yapılır;
  • 22. satır: Kimlik doğrulama, güvenli bir bağlantı üzerinden gerçekleştirilir;
  • 24. satır: kimlik doğrulama;
  • satır 26-33: mesajın gönderilmesi. Ardından [smtp/01/main] komut dosyası ile görülen diyalog gerçekleşecektir. Kimlik doğrulama yapılmışsa, bu işlem güvenli bir bağlantı üzerinden gerçekleşecektir;
  • satır 35: istemci/sunucu diyaloğu sonlandırılır;

[smtp/02/main] komut dosyasını çalıştırmadan önce, [pymailparlexemple@gmail.com] Gmail hesabının yapılandırmasını değiştirmeniz gerekir:

  • [pymailparlexemple@gmail.com] adlı Gmail hesabına giriş yapın;
  • aşağıdaki ayarları değiştirin: Image
  • [2] hesabında, daha az güvenli uygulamaların hesaba erişmesine izin verin;

Aynı işlemi ikinci Gmail hesabı [pymail2parlexemple@gmail.com] için de yapın.

Sonuçlar

[smtp/02/main] komut dosyasını çalıştırdığımızda, konsolda şu sonuçlar görüntülenir:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/smtp/02/main.py
----------------------------------
Envoi du message [mail to localhost via localhost avec smtplib]
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-DESKTOP-30FF5FB\r\n'
reply: b'250-SIZE 20480000\r\n'
reply: b'250-AUTH LOGIN\r\n'
reply: b'250 HELP\r\n'
reply: retcode (250); Msg: b'DESKTOP-30FF5FB\nSIZE 20480000\nAUTH LOGIN\nHELP'
send: 'mail FROM:<guest@localhost.com> size=310\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'rcpt TO:<guest@localhost.com>\r\n'
reply: b'250 OK\r\n'
reply: retcode (250); Msg: b'OK'
send: 'data\r\n'
reply: b'354 OK, send.\r\n'
reply: retcode (354); Msg: b'OK, send.'
data: (354, b'OK, send.')
send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: base64\r\nfrom: guest@localhost.com\r\nto: guest@localhost.com\r\ndate: Wed, 08 Jul 2020 08:35:39 +0200\r\nsubject: to localhost via localhost avec smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
reply: b'250 Queued (0.000 seconds)\r\n'
reply: retcode (250); Msg: b'Queued (0.000 seconds)'
data: (250, b'Queued (0.000 seconds)')
send: 'quit\r\n'
reply: b'221 goodbye\r\n'
reply: retcode (221); Msg: b'goodbye'
Message envoyé...
----------------------------------
Envoi du message [mail to gmail via gmail avec smtplib]
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
reply: b'250-SIZE 35882577\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-STARTTLS\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE 35882577\n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
send: 'STARTTLS\r\n'
reply: b'220 2.0.0 Ready to start TLS\r\n'
reply: retcode (220); Msg: b'2.0.0 Ready to start TLS'
send: 'ehlo [192.168.43.163]\r\n'
reply: b'250-smtp.gmail.com at your service, [37.172.118.130]\r\n'
reply: b'250-SIZE 35882577\r\n'
reply: b'250-8BITMIME\r\n'
reply: b'250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\r\n'
reply: b'250-ENHANCEDSTATUSCODES\r\n'
reply: b'250-PIPELINING\r\n'
reply: b'250-CHUNKING\r\n'
reply: b'250 SMTPUTF8\r\n'
reply: retcode (250); Msg: b'smtp.gmail.com at your service, [37.172.118.130]\nSIZE 35882577\n8BITMIME\nAUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH\nENHANCEDSTATUSCODES\nPIPELINING\nCHUNKING\nSMTPUTF8'
send: 'AUTH PLAIN AHB5bWFpbDJwYXJsZXhlbXBsZUBnbWFpbC5jb20AIzZwcklsaEQmQDFRWjNURw==\r\n'
reply: b'235 2.7.0 Accepted\r\n'
reply: retcode (235); Msg: b'2.7.0 Accepted'
send: 'mail FROM:<pymail2parlexemple@gmail.com> size=320\r\n'
reply: b'250 2.1.0 OK e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.1.0 OK e5sm4132618wrs.33 - gsmtp'
send: 'rcpt TO:<pymail2parlexemple@gmail.com>\r\n'
reply: b'250 2.1.5 OK e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.1.5 OK e5sm4132618wrs.33 - gsmtp'
send: 'data\r\n'
reply: b'354  Go ahead e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (354); Msg: b'Go ahead e5sm4132618wrs.33 - gsmtp'
data: (354, b'Go ahead e5sm4132618wrs.33 - gsmtp')
send: b'Content-Type: text/plain; charset="utf-8"\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: base64\r\nfrom: pymail2parlexemple@gmail.com\r\nto: pymail2parlexemple@gmail.com\r\ndate: Wed, 08 Jul 2020 08:35:40 +0200\r\nsubject: to gmail via gmail avec smtplib\r\n\r\nYWdsYcOrIHPDqWzDqW7DqQp2YSBhdSBtYXJjaMOpCmFjaGV0ZXIgZGVzIGZsZXVycw==\r\n.\r\n'
reply: b'250 2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (250); Msg: b'2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp'
data: (250, b'2.0.0 OK  1594190139 e5sm4132618wrs.33 - gsmtp')
send: 'quit\r\n'
Message envoyé...
reply: b'221 2.0.0 closing connection e5sm4132618wrs.33 - gsmtp\r\n'
reply: retcode (221); Msg: b'2.0.0 closing connection e5sm4132618wrs.33 - gsmtp'

Process finished with exit code 0
  • 40. satır: [smtplib] istemcisi, SMTP sunucusuyla şifreli bir bağlantı kurmak için diyalogu başlatır; bu, [smtp/main/01] komut dosyasında başaramadığımız bir işlemdi;
  • bunun dışında SMTP protokolünden bilinen komutlar yer almaktadır;

[pymail2parlexemple] kullanıcısının Gmail hesabına bakıldığında şunlar görülür:

Image

21.5.8. [smtp/03] komut dosyaları: ekli dosyaların yönetimi

Gönderilen e-postaya ek dosyalar eklenebilmesi için [smtp/02/main] komut dosyasını tamamlıyoruz.

Image

[smtp/03/main] komut dosyası, aşağıdaki [smtp/03/config] komut dosyası tarafından yapılandırılır:


import os


def configure() -> dict:
    # uygulama yapılandırması
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # açıklama: gönderilen e-postanın açıklaması
        # smtp-sunucusu: SMTP sunucusu
        # smtp-port: SMTP sunucusunun bağlantı noktası
        # gönderen: gönderen
        # to: alıcı
        # konu: e-postanın konusu
        # mesaj: e-postanın içeriği
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # kimlik doğrulamalı SMTP
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # burada, ekli dosyalar için mutlak yollar belirtilmelidir
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

[smtp/03/config] dosyası, daha önce kullanılan [smtp/02/config] dosyasından yalnızca, gönderilecek mesaja eklenecek dosyaların listesini belirten isteğe bağlı bir [attachments] listesinin (30-32. satırlar) varlığıyla farklılık gösterir.

[smtp/03/main] komut dosyası şu şekildedir:


# içe aktarmalar
import email
import mimetypes
import os
import smtplib
from email import encoders
from email.mime.audio import MIMEAudio
from email.mime.base import MIMEBase
from email.mime.image import MIMEImage
from email.mime.message import MIMEMessage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate



# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # mail[message] adresinden mail[smtp-server] SMTP sunucusuna mail[from] adına e-posta gönderiyor
    # adlı sunucuya mail[to] adına. verbose=True ise, istemci-sunucu arasındaki iletişimi izler

    # smtplib kütüphanesi kullanılır
    # istisnaların yukarıya iletilmesine izin verilir
    #
    # sunucu SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # ayrıntılı mod
    server.set_debuglevel(verbose)
    # güvenli bağlantı mı?
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # Multipart mesajı oluşturuluyor - gönderilecek mesaj budur
    # kaynak: https://docs.python.org/3.4/library/email-examples.html
    msg = MIMEMultipart()
    msg['From'] = mail["from"]
    msg['To'] = mail["to"]
    msg['Date'] = formatdate(localtime=True)
    msg['Subject'] = mail["subject"]
    # metin mesajı MIMEText biçiminde eklenir
    msg.attach(MIMEText(mail["message"]))
    # ekler taranır
    for path in mail["attachments"]:
        # yol, mutlak bir yol olmalıdır
        # ekli dosyanın türü tahmin edilir
        ctype, encoding = mimetypes.guess_type(path)
        # tahmin edilemedi ise
        if ctype is None or encoding is not None:
            # Tahmin yapılamadı veya dosya kodlanmış (sıkıştırılmış) ise, bu nedenle
            # genel bir "bag-of-bits" türü kullanılır.
            ctype = 'application/octet-stream'
        # türü maintype/subtype olarak ayrıştırılır
        maintype, subtype = ctype.split('/', 1)
        # farklı durumlar ele alınır
        if maintype == 'text':
            with open(path) as fp:
                # Not: karakter kümesinin hesaplanmasını ele almalıyız
                part = MIMEText(fp.read(), _subtype=subtype)
        elif maintype == 'image':
            with open(path, 'rb') as fp:
                part = MIMEImage(fp.read(), _subtype=subtype)
        elif maintype == 'audio':
            with open(path, 'rb') as fp:
                part = MIMEAudio(fp.read(), _subtype=subtype)
        # message / rfc822 türü durumları
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # diğer durumlar
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # Yükü Base64 kullanarak kodlayın
            encoders.encode_base64(part)
        # Dosya adı parametresini ayarlayın
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # Gönderilecek mesaja dosya eklenir
        msg.attach(part)
    # tüm ekler eklendi - mesajı bir karakter dizisi olarak gönderiyoruz
    server.send_message(msg)


# ana program ----------------------------------------------------------------

..

Açıklamalar

  • 18-32. satırlar: [sendmail] işlevi, eklerin olmadığı zamanki haliyle kalır;
  • 35. satır: Aşağıdaki kod, Python'un resmi belgelerinden alınmıştır;
  • 36. satır: Gönderilecek mesaj birkaç bölümden oluşacaktır: metin ve ekli dosyalar. Buna [Multipart] mesajı denir;
  • satır 37-40: [Multipart] mesajında, her e-postada bulunan olağan alanlar yer alır;
  • 42. satır: [Multipart] ve [msg] mesajlarının farklı bölümleri, [msg.attach] yöntemi (81. satır) ile mesaja eklenir. Ekli bölümler herhangi bir türde olabilir. Bunlar, MIME türü ile tanımlanır. Düz metnin MIME türü, [MIMEText] türüdür;
  • 44-81. satırlar: Gönderilecek mesajın tüm ekleri (81. satır) [msg Multipart] mesajına eklenecektir;
  • 44. satır: [path], eklenecek dosyanın mutlak yolunu temsil eder;
  • satır 47: eklenecek kısım için kullanılacak MIME türünü bulmak amacıyla, eklenecek dosyanın uzantısı (.docx, .php…) kullanılacaktır. [mimetypes.guess_type] yöntemi bu işi yapar. Bu yöntem iki bilgi döndürür:
    • [ctype]: dosyanın MIME türü;
    • [encoding]: dosyanın kodlamasına ilişkin bilgi;
  • 49-52. satırlar: Dosyanın MIME türünü belirleyemediğimiz durumda, bunun bir ikili dosya olduğu belirtilir (52. satır);
  • 54. satır: Bir dosyanın MIME türü, ana tür / ikincil tür olarak ayrılır; örneğin [application/pdf]. Bu iki öğe birbirinden ayrılır;
  • 56-76. satırlar: MIME ana türünün değerine göre farklı durumlar işlenir. Örneğin, PDF dosyasının [application/pdf] durumu söz konusu olduğunda, 70-76. satırlar yürütülür:
    • 56-59. satırlar: Eklenen dosyanın bir metin dosyası olduğu durum. Bu durumda, içeriği [fp.read] olan [MIMEText] türünde bir öğe oluşturulur;
    • 60-62. satırlar: Dosyanın bir resim içerdiği durum. Bu durumda, içeriği [fp.read] olan [MIMEImage] türünde bir öğe oluşturulur;
    • 63-65. satırlar: dosyanın bir ses dosyası olduğu durum. Bu durumda, içeriği [fp.read] olan [MIMEAudio] türünde bir öğe oluşturulur;
    • 66-69. satırlar: Dosyanın bir e-posta olduğu durum. Bu durumda, içeriği [email.message_from_bytes(fp.read())] olan [MIMEMessage] türünde bir öğe oluşturulur (69. satır). Önceki durumların aksine, MIME öğesinin içeriği ilgili dosyanın ikili içeriği iken, burada MIMEMessage öğesinin içeriği [email.message.Message] türündedir;
    • 70-76. satırlar: diğer durumlar. Buna örneğin, örneğimizdeki Word dosyaları ve PDF dosyası dahildir;
  • 72. satır: eklenecek dosya ikili modda (rb=read binary) açılır;
  • satır 74: [fp.read], ikili dosyanın tamamını okur;
  • 72-74. satırlar: [with open(…) as file] yapısı iki şey yapar:
    • dosyayı açar ve ona [file] tanımlayıcısını atar;
    • [with]'ten çıkıldığında, hata olsun ya da olmasın, [file] tanımlayıcısının kapatılmasını sağlar. Dolayısıyla bu, [try file=open(…)/ finally] yapısına bir alternatiftir;
  • 73. satır: Multipart mesajına eklenecek yeni bir [part] öğesi oluşturulur. Burada [MIMEBase] sınıfı kullanılır ve 54. satırda belirlenen [maintype, subtype] öğeleri yapıcıya aktarılır;
  • 74. satır: Multipart mesajına eklenecek öğenin bir içeriği olmalıdır. Bu içerik, [set_payload] yöntemi ile başlatılabilir;
  • 75-76. satırlar: Ekli dosyalar 7 bit kodlamaya tabi tutulmalıdır. Nitekim, geçmişte bazı SMTP sunucuları yalnızca 7 bit ile kodlanmış karakterleri destekliyordu. Burada ‘Base64’ adı verilen kodlama kullanılmaktadır;
  • 77. satır: Bu satırdan itibaren işlem, 56-76. satırlarda oluşturduğumuz tüm MIME türlerine uygulanır;
  • 79. satır: Multipart mesajına eklenecek öğenin, onu tanımlayan bir başlığı vardır. Burada, eklenen öğenin bir ek dosyaya karşılık geldiği belirtilir. Bu dosyanın adı, [add_header] yöntemine geçirilen üçüncü parametredir. Bu dosya adı, e-posta okuyucuları tarafından genellikle ekli dosyayı okuyucunun dosya sistemine bu adla kaydetmek için kullanılır. Şu ana kadar ekli dosyanın tam yoluyla çalışmıştık. Burada ise sadece dosya adını, yol bilgisi olmadan aktarıyoruz (satır 78);
  • satır 81: Dosyanın ikili içeriği [msg Multipart] mesajına eklenir;
  • satır 83: Mesajın tüm bölümleri [msg Multipart] dosyasına eklendiğinde, bu dosya gönderilir;

Sonuçlar

Daha önce sunulan [smtp/02/config] dosyasıyla [smtp/03/main] betiği çalıştırıldığında, [pymail2parlexemple@gmail.com] hesabı şunu alır:

Image

[4, 9-11]'te ekli dosyalar görülüyor.

Şimdi ekli bir e-posta örneğini inceleyelim. Yukarıdaki [3] dosyasına kaydedilen e-postayı alalım:

Image

E-postayı [smtp/03/attachments] klasörüne [mail attaché 1.eml] adıyla kaydediyoruz.

Şimdi [smtp/03/config] dosyasını şu şekilde değiştiriyoruz:


import os


def configure() -> dict:
    # uygulama yapılandırması
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # açıklama: gönderilen e-postanın açıklaması
        # smtp-sunucusu: SMTP sunucusu
        # smtp-port: SMTP sunucusunun bağlantı noktası
        # gönderen: gönderen
        # to: alıcı
        # konu: e-postanın konusu
        # mesaj: e-postanın içeriği
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # kimlik doğrulamalı SMTP
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # burada, ekli dosyalar için mutlak yollar belirtilmelidir
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • 33. satıra bir ek ekledik;

Şimdi [smtp/03/main] komut dosyasını yeniden çalıştırıyoruz. Bu işlem, [pymail2parlexemple@gmail.com] kullanıcısının posta kutusunda aşağıdaki sonucu verir:

Image

  • [1]'te, alınan e-posta;
  • [2]: mesajın metni;
  • [3]: ekli e-postanın metni;
  • [4] dosyasında: Thunderbird 5 ek buldu:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. Bu ek, kendisi de iki ek içeren bir e-postadır:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. POP3 protokolü

21.6.1. Giriş

Bir posta sunucusunda depolanan e-postaları okumak için iki protokol mevcuttur:

  • POP3 protokolü (Post Office Protocol), tarihsel olarak ilk protokol olmakla birlikte günümüzde pek kullanılmamaktadır;
  • IMAP protokolü (Internet Message Access Protocol), POP3 protokolünden daha yeni ve şu anda en yaygın olarak kullanılan protokoldür;

POP3 protokolünü incelemek için şu mimariyi kullanacağız:

Image

  • [Serveur B], duruma göre şunlardan biri olacaktır:
    • POP3 posta sunucusu tarafından uygulanan yerel bir POP3 sunucusu;
    • [gmail.com] e-posta yöneticisinin POP3 sunucusu olan [pop.gmail.com] sunucusu;
  • [Client A], çeşitli biçimlerdeki POP3 istemcisi olacaktır:
    • [RawTcpClient] istemcisi, POP3 protokolünü keşfetmek için;
    • [RawTcpClient] istemcisinin POP3 protokolünü yeniden canlandıran bir Python betiği;
    • POP3 sunucusu gerektirdiğinde, ekli dosyaları yönetmeye ve şifreli ve kimlik doğrulamalı bir bağlantı kullanmaya olanak tanıyan Python modüllerini kullanan bir Python betiği;

21.6.2. POP3 protokolünün incelenmesi

SMTP protokolünde yaptığımız gibi, POP3 protokolünü de [hMailServer] posta sunucusunun günlükleri yardımıyla inceleyeceğiz. Bunun için önce bu sunucuyu başlatmamız gerekiyor.

Thunderbird ile şunları yapacağız:

  • [guest@localhost.com] kullanıcısına bir e-posta göndereceğiz;
  • bu kullanıcının posta kutusunu okuyacağız;

Image

Image

Yukarıdaki [3-6]'te, [guest@localhost.com] kullanıcısı tarafından alınan mesaj yer almaktadır.

Şimdi [hMailServer] sunucusunun günlüklerini inceliyoruz. Bunun için [hMailServer Administrator] yönetim aracını kullanıyoruz:

Image

POP3 günlükleri şu şekildedir (bugünkü günlük dosyasının son satırları):


"POP3D"    35084    5    "2020-07-08 14:19:46.392"    "127.0.0.1"    "SENT: +OK Bienvenue sur le serveur POP3 localhost.com"
"POP3D"    34968    5    "2020-07-08 14:19:46.405"    "127.0.0.1"    "RECEIVED: CAPA"
"POP3D"    34968    5    "2020-07-08 14:19:46.407"    "127.0.0.1"    "SENT: +OK CAPA list follows[nl]USER[nl]UIDL[nl]TOP[nl]."
"POP3D"    35076    5    "2020-07-08 14:19:46.410"    "127.0.0.1"    "RECEIVED: USER guest"
"POP3D"    35076    5    "2020-07-08 14:19:46.411"    "127.0.0.1"    "SENT: +OK Send your password"
"POP3D"    34968    5    "2020-07-08 14:19:46.418"    "127.0.0.1"    "RECEIVED: PASS ***"
"POP3D"    34968    5    "2020-07-08 14:19:46.421"    "127.0.0.1"    "SENT: +OK Mailbox locked and ready"
"POP3D"    34968    5    "2020-07-08 14:19:46.423"    "127.0.0.1"    "RECEIVED: STAT"
"POP3D"    34968    5    "2020-07-08 14:19:46.423"    "127.0.0.1"    "SENT: +OK 1 612"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "RECEIVED: LIST"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "SENT: +OK 1 messages (612 octets)"
"POP3D"    34968    5    "2020-07-08 14:19:46.426"    "127.0.0.1"    "SENT: 1 612[nl]."
"POP3D"    35076    5    "2020-07-08 14:19:46.427"    "127.0.0.1"    "RECEIVED: UIDL"
"POP3D"    35076    5    "2020-07-08 14:19:46.428"    "127.0.0.1"    "SENT: +OK 1 messages (612 octets)[nl]1 42[nl]."
"POP3D"    34968    5    "2020-07-08 14:19:46.435"    "127.0.0.1"    "RECEIVED: RETR 1"
"POP3D"    34968    5    "2020-07-08 14:19:46.436"    "127.0.0.1"    "SENT: ."
"POP3D"    34924    5    "2020-07-08 14:19:46.459"    "127.0.0.1"    "RECEIVED: QUIT"
"POP3D"    34924    5    "2020-07-08 14:19:46.459"    "127.0.0.1"    "SENT: +OK POP3 server saying goodbye..."
  • 1. satır: POP3 sunucusu, yeni bağlanan istemciye (Thunderbird) bir hoş geldiniz mesajı gönderir;
  • 2. satır: İstemci, kullanabileceği komutların listesini istemek için [CAPA] (capabilities) komutunu gönderir;
  • 3. satır: Sunucu, [USER, UIDL, TOP] komutlarını kullanabileceğini bildirir. POP sunucusu, yanıtlarına [+OK] veya [-ERR] ile başlar; bu, istemcinin komutunu başarıyla veya başarısızlıkla yürüttüğünü belirtir;
  • 4. satır: İstemci, [USER guest] komutunu göndererek [guest] kullanıcısının posta kutusunu görüntülemek istediğini belirtir;
  • 5. satır: Sunucu, [+OK] yanıtını gönderir ve [guest] kullanıcısının şifresini ister;
  • 6. satır: İstemci, [guest] kullanıcısının şifresini göndermek için [PASS password] komutunu gönderir. Burada şifre açık metin halindedir, çünkü POP3 sunucusu güvenli bağlantı zorunluluğu getirmedi. Gmail’in POP3 sunucusunda durumun farklı olacağını göreceğiz;
  • 7. satır: Sunucu, kullanıcı adı ve şifre kombinasyonunu onayladı. [guest] kullanıcısının posta kutusunu kilitlediğini belirtiyor;
  • 8. satır: İstemci, posta kutusu hakkında bilgi isteyen [STAT] komutunu sunucuya gönderir;
  • 9. satır: Sunucu, 612 baytlık bir mesaj olduğunu bildirir. Genel olarak, N adet mesaj olduğunu belirtir ve bu mesajların toplam boyutunu verir;
  • 10. satır: İstemci, [LIST] komutunu gönderir. Bu komut, mesaj listesini ister;
  • satır 11: sunucu, mesaj listesini şu biçimde gönderir:
    • mesaj sayısını ve toplam boyutunu içeren bir özet satırı;
    • her mesaj için, mesaj numarasını ve boyutunu belirten bir satır;
  • 13. satır: Müşteri, mesajların kimlik numaralarıyla birlikte listesini talep eden [UIDL] komutunu gönderir. Nitekim, her mesaj e-posta hizmeti içinde benzersiz bir numara ile tanımlanır;
  • 14. satır: Sunucunun yanıtı. Burada, listedeki 1 numaralı mesajın kimlik numarasının 42 olduğu görülmektedir;
  • satır 15: İstemci, listedeki 1 numaralı mesajın kendisine aktarılmasını talep eden [RETR 1] komutunu gönderir;
  • 16. satır: POP3 sunucusu bunu gerçekleştirir;
  • 17. satır: İstemci, [QUIT] komutunu göndererek POP3 sunucusuyla bağlantısını keseceğini belirtir;
  • 18. satır: sunucu da istemciyle olan bağlantısını kesecek, ancak önce ona bir veda mesajı gönderir;

Şimdi, PyCharm penceresinde çalıştırılan [RawTcpClient] istemcisini kullanarak yukarıdaki diyalogun bazı unsurlarını yeniden canlandıracağız:

Image

Diyalog şu şekildedir:


(venv) C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\inet\utilitaires>RawTcpClient.exe localhost 110
Client [DESKTOP-30FF5FB:63762] connecté au serveur [localhost-110]
Tapez vos commandes (quit pour arrêter) :
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
USER guest
<-- [+OK Send your password]
PASS guest
<-- [+OK Mailbox locked and ready]
LIST
<-- [+OK 1 messages (612 octets)]
<-- [1 612]
<-- [.]
RETR 1
<-- [+OK 612 octets]
<-- [Return-Path: guest@localhost.com]
<-- [Received: from [127.0.0.1] (DESKTOP-30FF5FB [127.0.0.1])]
<-- [   by DESKTOP-30FF5FB with ESMTP]
<-- [   ; Wed, 8 Jul 2020 14:19:36 +0200]
<-- [To: guest@localhost.com]
<-- [From: "guest@localhost.com" <guest@localhost.com>]
<-- [Subject: protocole POP3]
<-- [Message-ID: <ca895136-25c5-411e-373a-a68cbd0eca51@localhost.com>]
<-- [Date: Wed, 8 Jul 2020 14:19:33 +0200]
<-- [User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101]
<-- [ Thunderbird/68.10.0]
<-- [MIME-Version: 1.0]
<-- [Content-Type: text/plain; charset=utf-8; format=flowed]
<-- [Content-Transfer-Encoding: 8bit]
<-- [Content-Language: fr]
<-- []
<-- [ceci est un test pour découvrir le protocole POP3]
<-- []
<-- [.]
QUIT
Fin de la connexion avec le serveur
  • 1. satır: [localhost] makinesinin 110 numaralı bağlantı noktasına bir bağlantı açılır. [hMailServer]'in POP3 hizmeti burada çalışır;
  • 5., 7., 9., 13. ve 34. satırlarda [USER, PASS, LIST, RETR, QUIT] komutlarını kullanıyoruz;
  • 4. satır: POP3 sunucusunun karşılama mesajı;
  • 5. satır: [guest] kullanıcısının posta kutusuna erişmek istediğimizi belirtiriz;
  • 7. satır: [guest] kullanıcısının şifresini açık metin olarak gönderiyoruz;
  • 9. satır: Posta kutusundaki mesajların listesi isteniyor;
  • 13. satır: 1 numaralı mesaj istenir;
  • 14-33. satırlar: POP3 sunucusu 1 numaralı mesajı gönderir;
  • 34. satır: oturum sonlandırılır;

Aşağıda, POP3 sunucusu tarafından kabul edilen bazı yaygın komutların özeti yer almaktadır:

  • [USER] komutu, e-posta kutusunu okumak istediğiniz kullanıcıyı tanımlamak için kullanılır;
  • [PASS] komutu, şifresini belirlemek için kullanılır;
  • [LIST] komutu, kullanıcının posta kutusunda bulunan mesajların listesini ister;
  • [RETR] komutu, numarası verilen mesajı görüntülemek için kullanılır;
  • [DELE] komutu, numarası verilen mesajın silinmesini ister;
  • [QUIT] komutu, sunucuya işlemin tamamlandığını bildirir;

Sunucunun yanıtı çeşitli şekillerde olabilir:

  • [+OK] ile başlayan tek bir satır, müşterinin önceki komutunun başarılı olduğunu belirtir;
  • [-ERR] ile başlayan tek bir satır, müşterinin önceki komutunun başarısız olduğunu belirtir;
  • birkaç satırdan oluşan yanıt; burada:
    • 1. satır [+OK] ile başlar;
    • son satır tek bir noktadan oluşur;

21.6.3. [pop3/01] komut dosyaları: temel bir POP3 istemcisi

Image

POP3 protokolü, SMTP protokolüyle aynı yapıya sahip olduğundan, [pop3/01/main.py] komut dosyası, [smtp/01/main.py] komut dosyasının bir uyarlamasıdır. Aşağıdaki [pop3/01/config.py] yapılandırma dosyasına sahip olacaktır:


def configure() -> dict:
    # e-postaların alındığı posta kutuları
    mailboxes = [
        # sunucu: POP3 sunucusu
        # bağlantı noktası: sunucunun bağlantı noktası POP3
        # kullanıcı: mesajlarını okumak istediğiniz kullanıcı
        # şifre: kullanıcının şifresi
        # maxmails: indirilecek maksimum e-posta sayısı
        # timeout: sunucudan yanıt alınması için maksimum bekleme süresi
        # encoding: alınan e-postaların kodlaması
        # delete: True ise, e-postalar posta kutusundan silinir
        # yerel olarak indirildikten sonra

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    # yapılandırma geri yüklenir
    return {
        "mailboxes": mailboxes
    }
  • 3-24. satırlar: kontrol edilecek posta kutularının listesi. Burada sadece bir tane var;
  • 4-12. satırlar: her bir posta kutusunu tanımlayan sözlük öğelerinin anlamları;
  • 15. satır: sorgulanan POP3 sunucusu, yerel [hMailServer] sunucusudur;
  • satır 17-18: [guest@localhost] kullanıcısının posta kutusunu okumak istiyoruz;
  • 19. satır: En fazla 10 e-posta okunacaktır;
  • 20. satır: İstemci, sunucudan yanıt gelene kadar en fazla 1 saniye bekleyecektir;
  • 21. satır: okunan mesajların kodlama türü;
  • 22. satır: İndirilen mesajlar silinmeyecek;

[pop3/01/main.py] komut dosyası şu şekildedir:


# ithalatlar
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # [mailbox] sözlüğüyle tanımlanan posta kutusunu okur
    # verbose=True ise, istemci-sunucu iletişimini izler



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # bağlantı kanalına komut gönderir
    # verbose=True ise ayrıntılı mod
    # with_rclf=True ise, alışverişe rclf dizisini ekler
    # yanıtın ilk satırını döndürür



# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    


# ana ----------------------------------------------------------------

# POP3 istemcisi (Post Office Protocol), bir posta kutusundaki mesajları okumaya olanak tanır
# POP3 istemci-sunucu iletişim protokolü
# -> istemci, SMTP sunucusunun 110 numaralı bağlantı noktasına bağlanır
# <- sunucu, istemciye bir hoş geldin mesajı gönderir
# -> istemci, USER kullanıcı komutunu gönderir
# <- sunucu OK yanıtını gönderir veya göndermez
# -> istemci PASS mot_de_passe komutunu gönderir
# <- sunucu OK yanıtını verir ya da vermez
# -> istemci LIST komutunu gönderir
# <- sunucu OK yanıtını verir ya da vermez
# -> istemci, her bir e-posta için RETR numaralı komutu gönderir
# <- sunucu OK yanıtını gönderir veya yanıt vermez. OK yanıtı gelirse, istenen e-postanın içeriği gönderilir
# -> sunucu e-postanın tüm satırlarını gönderir ve sonunda
# tek karakterini içeren bir satırla bitirir.
# -> istemci, bir e-postayı silmek için DELE numaralı komutu gönderir
# <- sunucu OK yanıtını verir veya vermez
# # -> istemci, sunucuyla olan diyaloğu sonlandırmak için QUIT komutunu gönderir
# <- sunucu OK yanıtını verir veya vermez
# sunucunun yanıtları +OK metin veya -ERR metin biçimindedir
# Yanıt birden fazla satırdan oluşabilir. Bu durumda son satır tek bir noktadan oluşur
# Alışverişi yapılan metin satırları, RC(#13) ve LF(#10) karakterleriyle bitmelidir
# 

# Uygulamanın yapılandırması alınır
import config
config = config.configure()

# posta kutuları tek tek işlenir
for mailbox in config['mailboxes']:
    try:
        # konsol görüntüleme
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # posta kutusunu ayrıntılı modda okuma
        readmails(mailbox, True)
        # son
        print("Lecture terminée...")
    except BaseException as erreur:
        # hatayı görüntüler
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

Yorumlar

Daha önce de belirttiğimiz gibi, [pop3/01/main.py], daha önce ele aldığımız [smtp/01/main.py] betiğinin bir uyarlamasıdır. Burada sadece temel farklılıklara değineceğiz:

  • 64. satır: [readmails] işlevi, bir posta kutusundaki e-postaları okumakla görevlidir. Bu posta kutusuna bağlanmak için gerekli bilgiler [mailbox] sözlüğünde yer almaktadır. İkinci parametre olan [True], burada istemci/sunucu iletişiminin izlenmesini talep eden [Verbose] parametresidir;

[readmails] işlevi şöyledir:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # [mailbox] sözlüğüyle tanımlanan posta kutusundaki e-postaları okur
    # verbose=True ise, istemci-sunucu iletişimini izler

    # posta kutusunun parametreleri ayrıştırılır
    # [mailbox] sözlüğünün geçerli olduğu varsayılır
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # sistem hatalarının bildirilmesine izin verilir
    connexion = None
    try:
        # [server]'in [port] numaralı bağlantı noktasında bir saniyelik zaman aşımı süresiyle bağlantı açılır
        connexion = socket.create_connection((server, port), timeout=timeout)

        # bağlantı, iki yönlü bir iletişim akışını temsil eder
        # istemci (bu program) ile bağlanılan POP3 sunucusu arasında
        # bu kanal, komut ve bilgi alışverişi için kullanılır

        # karşılama mesajını okuma
        send_command(mailbox, connexion, "", verbose, True)
        # USER komutu
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # PASS komutu
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # LIST komutu
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # mesaj sayısını öğrenmek için 1. satırın analizi
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # mesajlar üzerinde döngü
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # RETR komutu
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # DELE komutu
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # sonraki mesaj
            imessage += 1
        # komut QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # son
    finally:
        # bağlantı kapatılıyor
        if connexion:
            connexion.close()

Açıklamalar

  • 8-14. satırlar: incelenecek posta kutusunun yapılandırma bilgileri alınır;
  • 19-20. satırlar: POP3 sunucusuyla bağlantı kurulur;
  • 26-27. satırlar: sunucu tarafından gönderilen karşılama mesajı okunur;
  • 28-29. satırlar: E-postalarını almak istediğimiz kullanıcıyı tanımlamak için [USER] komutu gönderilir;
  • satır 30-31: Bu kullanıcının şifresini girmek için [PASS] komutu gönderilir;
  • satır 32-33: Bu kullanıcının posta kutusunda kaç tane e-posta olduğunu öğrenmek için [LIST] komutu gönderilir. [sendCommand] işlevi, sunucunun yanıtının ilk satırını döndürür. Sunucu bu satırda, posta kutusunda kaç tane mesaj olduğunu belirtir;
  • satır 34-36: yanıtın ilk satırından mesaj sayısını alıyoruz;
  • satır 39-46: Her bir mesaj üzerinde döngü yapılır. Her biri için iki komut verilir:
    • RETR i: i numaralı mesajı almak için (satır 40-41);
    • DELE i: yapılandırma, okunan mesajların sunucudan silinmesini gerektiriyorsa, mesajı silmek için (satır 43-44);
  • 47-48. satırlar: İşlemin tamamlandığını sunucuya bildirmek için [QUIT] komutu gönderilir;

[send_command] işlevi şöyledir:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # bağlantı kanalına komut gönder
    # verbose=True ise ayrıntılı mod
    # with_rclf=True ise, rclf dizisini değiş tokuşa ekler
    # yanıtın ilk satırını döndürür

    # satır sonu işareti
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # boş değilse komutu gönderir
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # olası yankı
        if verbose:
            affiche(commande, 1)
    # soketi bir metin dosyasıymış gibi okuma
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # bu dosya satır satır işlenir
    # 1. satırı okuma
    première_ligne = réponse = file.readline().strip()
    # ayrıntılı mod mu?
    if verbose:
        affiche(première_ligne, 2)
    # hata kodunun alınması
    code_erreur = réponse[0]
    if code_erreur == "-":
        # bir hata oluştu
        raise BaseException(réponse[5:])
    # birden fazla satırlık yanıtların özel durumu LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # yanıtın son satırı mı?
        dernière_ligne = False
        while not dernière_ligne:
            # sonraki satırı oku
            ligne_suivante = file.readline().strip()
            # ayrıntılı mod mu?
            if verbose:
                affiche(ligne_suivante, 2)
            # son satır mı?
            dernière_ligne = ligne_suivante == "."
    # bitti - ilk satırı döndürüyoruz
    return première_ligne

Açıklamalar

  • 13-18. satırlar: [command] komutu, yalnızca boş değilse POP3 sunucusuna gönderilir. Bu durum, istemci henüz herhangi bir komut göndermeden POP3 sunucusunun gönderdiği karşılama mesajını okumak için gereklidir;
  • 19-21. satırlar: Soket, sanki bir metin dosyasıymış gibi okunur. Bu, [readline] yöntemini (24. satır) kullanmamızı ve böylece mesajı satır satır okumamızı sağlayacaktır. Okunacak satırların kodlamasını belirtmek için [mailbox] sözlüğündeki [encoding] anahtarı kullanılır;
  • 24. satır: yanıtın ilk satırı okunur;
  • 28-32. satırlar: olası bir hata durumu yönetilir. Bu hatalar [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…] türündedir;
  • satır 32: hata mesajıyla birlikte bir istisna atılır;
  • satır 35: Yalnızca [list, retr] komutları birden fazla satırlık yanıtlara sahip olabilir;
  • satır 36-45: çok satırlı bir yanıt olması durumunda, son satır (satır 45) alınana kadar alınan tüm satırlar (satır 42-43) görüntülenir;
  • 46. satır: [LIST] komutunda, okunan ilk satır, posta kutusunda bulunan mesaj sayısını içerdiğinden bu satır döndürülür;

Sonuçlar

Önceki örneği ele alalım. Thunderbird ile, [guest@localhost] kullanıcısına aşağıdaki mesajı göndermiştik (hMailServer sunucusunun çalışıyor olması gerekir):

Image

Çalıştırıldığında şu sonuçlar elde edilir:


C:\Data\st-2020\dev\python\cours-2020\python3-flask-2020\venv\Scripts\python.exe C:/Data/st-2020/dev/python/cours-2020/python3-flask-2020/inet/pop3/01/main.py
----------------------------------
Lecture de la boîte mail POP3 guest@localhost:110
<-- [+OK Bienvenue sur le serveur POP3 localhost.com]
--> [USER guest]
<-- [+OK Send your password]
--> [PASS guest]
<-- [+OK Mailbox locked and ready]
--> [LIST]
<-- [+OK 1 messages (612 octets)]
<-- [1 612]
<-- [.]
--> [RETR 1]
<-- [+OK 612 octets]
<-- [Return-Path: guest@localhost.com]
<-- [Received: from [127.0.0.1] (DESKTOP-30FF5FB [127.0.0.1])]
<-- [by DESKTOP-30FF5FB with ESMTP]
<-- [; Wed, 8 Jul 2020 14:19:36 +0200]
<-- [To: guest@localhost.com]
<-- [From: "guest@localhost.com" <guest@localhost.com>]
<-- [Subject: protocole POP3]
<-- [Message-ID: <ca895136-25c5-411e-373a-a68cbd0eca51@localhost.com>]
<-- [Date: Wed, 8 Jul 2020 14:19:33 +0200]
<-- [User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101]
<-- [Thunderbird/68.10.0]
<-- [MIME-Version: 1.0]
<-- [Content-Type: text/plain; charset=utf-8; format=flowed]
<-- [Content-Transfer-Encoding: 8bit]
<-- [Content-Language: fr]
<-- []
<-- [ceci est un test pour découvrir le protocole POP3]
<-- []
<-- [.]
--> [QUIT]
<-- [+OK POP3 server saying goodbye...]
Lecture terminée...

Process finished with exit code 0
  • 15-31. satırlar: [guest@localhost]'e gönderilen mesaj doğru şekilde alınır.

Burada, bazı özellikleri eksik olan temel bir POP3 istemcisi ile karşı karşıyayız:

  1. güvenli bir POP3 sunucusuyla iletişim kurma yeteneği;
  2. mesaja ekli dosyaları okuma yeteneği;

Bu iki özelliği, bu sefer daha karmaşık olacak yeni bir komut dosyası ile hayata geçireceğiz.

21.6.4. [pop3/02] komut dosyaları: POP3 istemcisi ile [poplib] ve [email] modülleri

Ekli dosyaları yönetmeye ve güvenli sunucularla iletişim kurmaya olanak tanıyan bir POP3 istemcisi yazacağız. Ayrıca, mesajları ve ekli dosyalarını dosyalara kaydedeceğiz.

İki Python modülü kullanacağız:

  • [poplib]: POP3 protokolünü sağlayacak;
  • [email]: Alınan mesajları analiz etmemizi sağlayacak çok sayıda alt modülü bir araya getiren bir modüldür. Her mesaj, içinde aşağıdakileri bulabileceğimiz yapılandırılmış bir karakter dizisidir:
    • [From, To, Subject, Return-Path…] mesaj başlıkları;
    • metin ve gerekirse HTML biçimindeki mesaj;
    • ekli dosyalar;

Image

[inet/pop3/02/main] [1] betiği, [inet/pop3/02/config] [2] dosyası tarafından yapılandırılır ve [inet/shared/mail_parser] [3] modülünü kullanır.

[pop3/02/config] dosyası şu şekildedir:


import os


def configure() -> dict:
    # uygulama yapılandırması
    config = {
        # yönetilecek posta kutuları listesi
        "mailboxes": [
            # sunucu: POP3 sunucusu
            # bağlantı noktası: POP3 sunucusunun bağlantı noktası
            # kullanıcı: mesajlarını okumak istediğiniz kullanıcı
            # şifre: kullanıcının şifresi
            # maxmails: indirilecek maksimum e-posta sayısı
            # timeout: sunucudan yanıt alınması için maksimum bekleme süresi
            # delete: indirilen mesajların sunucudan silinmesi gerekiyorsa "true" olarak ayarlanır
            # ssl: e-postaların okunması güvenli bir bağlantı üzerinden yapılıyorsa "true"
            # output: indirilen mesajların saklanacağı klasör

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # komut dosyasının bulunduğu klasörün mutlak yolu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # syspath'e eklenecek klasörlerin mutlak yolları
    absolute_dependencies = [
        # yerel klasör
        f"{script_dir}/../../shared",
   ]

    # syspath yapılandırması
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # yapılandırmayı oluşturuyoruz
    return config

Bu dosya, kontrol edilecek posta kutularının listesini tanımlar ve uygulamanın Python yolunu belirler.

Burada tek bir posta kutusu bulunmaktadır:

  • 22-23. satırlar: e-postalarını okumak istediğimiz kullanıcı;
  • 20-21. satırlar: bu kullanıcının e-postalarını depolayan POP3 sunucusunun adı ve bağlantı noktası;
  • 24. satır: alınacak maksimum e-posta sayısı. Aslında, bu komut dosyasını kendi e-posta kutunuzda denerseniz, muhtemelen orada bulunan yüzlerce e-postayı almak istemeyeceksiniz;
  • 25. satır: Bir e-postayı okuduktan sonra silinip silinmeyeceğini belirten boole değeri (delete=True);
  • 26. satır: [ssl] özniteliğinin True olarak ayarlanması, 20-21. satırlarda tanımlanan POP3 sunucusunun şifreli bir bağlantı kullandığı anlamına gelir;
  • satır 27: sunucudan gelecek yanıtlar için saniye cinsinden belirtilen maksimum bekleme süresi;
  • 28. satır: Okunan e-postaların saklanacağı klasör. Eğer yoksa oluşturulacaktır. Burada göreceli bir ad kullanılmıştır. Çalıştırıldığında, bu ad, komut dosyasını çalıştırdığınız klasöre göre belirlenir. [Pycharm] ile bu klasör, [pop3/02] komut dosyasının bulunduğu klasör olacaktır;

[pop3/02/main] komut dosyası şu şekildedir:


# ithalatlar
import email
import os
import poplib
import shutil


# bir posta kutusunun okunması
def readmails(mailbox: dict, verbose: bool):
    # [mailbox] sözlüğüyle tanımlanan e-posta kutusunu okur
    # verbose=True ise, istemci-sunucu iletişimini izler


# ana program ----------------------------------------------------------------
#   POP3 istemcisi (Post Office Protocol), e-postaları okumaya olanak tanır

# uygulamanın yapılandırması alınır
import config
config = config.configure()

# posta kutularını tek tek işleme
for mailbox in config['mailboxes']:
    try:
        # konsol ekranı
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # posta kutusunu ayrıntılı modda okuma
        readmails(mailbox, True)
        # son
        print("Lecture terminée...")
    except BaseException as erreur:
        # hatayı görüntüler
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • 17-36. satırlar: Komut dosyasının [main] kısmı, [pop3/01] komut dosyasınınkine benzerdir;

[readmails] işlevi şöyledir:


# bir posta kutusunu okuma
def readmails(mailbox: dict, verbose: bool):
    # [mailbox] sözlüğünde tanımlanan posta kutusunu okur
    # verbose=True ise, istemci-sunucu iletişimini izler

    # mail_parser'i içe aktarır
    from mail_parser import save_message

    # posta kutusunun parametreleri ayrıştırılır
    # [mailbox] sözlüğünün geçerli olduğu varsayılır
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    ssl = mailbox['ssl']
    timeout = mailbox['timeout']
    output = mailbox['output']

    # sistem hatalarının bildirilmesine izin verilir
    pop3 = None
    try:
        # Varsa depolama klasörleri oluşturulur
        if not os.path.isdir(output):
            os.mkdir(output)
        # kullanıcı
        dir2 = f"{output}/{user}"
        # [dir2] klasörü varsa silinir, ardından yeniden oluşturulur
        if os.path.isdir(dir2):
            # silme
            shutil.rmtree(dir2)
        # oluşturma
        os.mkdir(dir2)
        # [server]'in [port] bağlantı noktasında bir bağlantı açılır
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # bağlantı, iki yönlü bir iletişim akışını temsil eder
        # istemci (bu program) ile bağlanılan POP3 sunucusu arasında
        # bu kanal, komut ve bilgi alışverişi için kullanılır

        # ayrıntılı mod
        pop3.set_debuglevel(2 if verbose else 0)
        # karşılama mesajını okuma
        pop3.getwelcome(    )
        # USER komutu
        réponse = pop3.user(user)
        # PASS komutu
        réponse = pop3.pass_(password)
        # LIST komutu
        liste = pop3.list()
        # e-postalar listede[1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # geçerli öğe
            élément = éléments[imail]
            # öğesi, dizeye dönüştürülen bir bayt listesidir
            desc = élément.decode()
            # boşluklarla ayrılmış bir dize var
            # 1. öğe mesaj numarasıdır
            num = desc.split()[0]
            # mesaj alınır
            message = pop3.retr(int(num))
            # mesajın satırları message içinde bulunur [1]
            str_message = ""
            for ligne in message[1]:
                # satır, string olarak kod çözülen bir bayt dizisidir
                str_message += f"{ligne.decode()}\r\n"
            # mesajın dosyası
            dir3 = f"{dir2}/message_{num}"
            # Klasör yoksa, oluşturulur
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # konu email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # bir e-posta daha
            imail += 1
            # maksimum sınıra ulaşıldı mı?
            fini = imail == maxmails or imail == nb_mails

        # komut QUIT
        pop3.quit()
    finally:
        # bağlantı kapatılıyor
        if pop3:
            pop3.close()

Açıklamalar

  • 6-7. satırlar: 80. satırda kullanılan [mail_parser.save_message] işlevi içe aktarılır;
  • fonksiyon kodu bir try (22. satır)/finally (88. satır) bloğu içine alınmıştır. Böylece tüm istisnalar ana koda iletilir ve burada durdurulup görüntülenir;
  • 11-18. satırlar: Posta kutusunun yapılandırma bilgileri alınır;
  • 23-33. satırlar: tüm mesajlar, yapılandırmada [output/user], [output] ve [user] olarak tanımlanan klasöre kaydedilecektir. Böylece sırasıyla [output] ve ardından [output/user] klasörleri oluşturulur. Sonuncusunu oluşturmak için önce 31. satırdan silinir. [shutil], içe aktarılması gereken bir modüldür. [shutil.rmtree(dir)], [dir] klasörünü ve içindeki tüm içeriği siler;
  • sistem dosyaları üzerindeki tüm işlemler için, yine içe aktarılması gereken [os] modülü kullanılır;
  • 34-38. satırlar: POP3 sunucusuyla bir bağlantı açılır. Sunucu güvenli ise [poplib.POP3_SSL] sınıfı, aksi takdirde [poplib.POP3] sınıfı kullanılır. 35. satırda kullanılan [ssl] özniteliği, posta kutusunun yapılandırmasından gelir;
  • 45. satır: Günlük kaydı seviyesi belirlenir:
    • 0: günlük yok;
    • 1: POP3 istemcisi tarafından gönderilen komutlar günlüğe kaydedilir;
    • 2: ayrıntılı günlükler. Ayrıca POP3 istemcisinin ne aldığını da görüyoruz;
  • satır 47: Bağlantı kurulduktan sonra, POP3 sunucusu bir hoş geldiniz mesajı gönderir. Bu mesajı okuruz;
  • 48-49. satırlar: POP3 protokolünün USER komutu;
  • satır 50-51: POP3 protokolünün PASS komutu;
  • 52-53. satırlar: POP3 protokolünün LIST komutu. Yanıt bir tuple'dır (response, ['mesg_num octets'…], bayt), örneğin liste=(b'+OK 3 mesaj (3859 bayt)', [b'1 584', b'2 550', b'3 2725'], 22). Tuple'ın ilk iki elemanının bayt olduğu görülmektedir (b önekli). liste[1], her elemanının iki bilgiyi içeren bir bayt dizisi olduğu bir dizidir: mesaj numarası ve bayt cinsinden boyutu;
  • 56. satır: Yukarıdakilerden, posta kutusundaki mesaj sayısının [taxpayers[slice(10,12)]] ile elde edilebileceği sonucuna varılır;
  • satır 59-84: Her bir mesaj üzerinde döngü yapılır. Tüm mesajlar okunduğunda veya yapılandırmada belirlenen maksimum e-posta sayısına ulaşıldığında döngü sonlandırılır;
  • satır 61: liste[1] dizisinin geçerli elemanı, yani b'1 584' gibi bir bayt dizisi;
  • 63. satır: bayt dizisinden bir karakter dizisine dönüştürülür. Artık '1 584' karakter dizisine sahibiz;
  • satır 66: mesaj numarası alınır, burada '1' karakter dizisi;
  • 68. satır: POP3 RETR num komutu gönderilir. Şu türde bir yanıt alınır:

[message=(b'+OK 584 octets', [b'Return-Path: guest@localhost', b'Received: from [127.0.0.1] (localhost [127.0.0.1])', b'\tby DESKTOP-528I5CU with ESMTPA', b'\t; Tue, 17 Mar 2020 09:41:50 +0100', b'To: guest@localhost', b'From: "guest@localhost" <guest@localhost>', b'Subject: test', b'Message-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>', b'Date: Tue, 17 Mar 2020 09:41:48 +0100', b'User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101', b' Thunderbird/68.6.0', b'MIME-Version: 1.0', b'Content-Type: text/plain; charset=utf-8; format=flowed', b'Content-Transfer-Encoding: 8bit', b'Content-Language: fr', b'', b'h\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.', b''], 614)]
  • (devam)
    • message, üç elemandan oluşan bir tuple'dır;
    • message[1], satırlardan oluşan bir dizidir. Her satır bir bayt dizisidir (b önekli). Tam mesaj, bu satırlar kümesinden oluşur;
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language], mesajın başlıklarıdır. Her biri alınan mesaj hakkında bilgi verir. Bu bilgiler, mesajın gövdesini (message[1] dizisinin sondan bir önceki elemanı) elde etmemizi sağlayacaktır;
  • 71-73. satırlar: Mesajın tüm satırlarından oluşan [strMessage] dizesi oluşturulur. Artık mesajı bir karakter dizisi biçiminde elimizde tutuyoruz. Bu mesaj, başka mesajların yanı sıra ekleri de içerebilir. Çünkü ekler de bir karakter dizisi biçiminde eklenir. Dolayısıyla unutulmaması gereken bir nokta, bir e-postanın başlangıçta bir karakter dizisi olduğudur ve ekleri, olası diğer kapsüllenmiş mesajları ve elbette gönderenin yazdığı mesaj gövdesini çıkarmak için bu karakter dizisini analiz etmek gerekir;
  • 74-78. satırlar: Mesaj gövdesini ve ekleri [dir3] klasörüne yerleştireceğiz;
  • 79-80. satırlar: Mesajın analizini [save_message] işlevine devredeceğiz:
    • 1. parametre, mesaj içeriğinin saklanacağı klasör olan [dir3]'tir;
    • ikinci parametre ise [email.message.Message] türündedir. Bu nesne, mesajın çeşitli bölümlerini (gövde, ekler) ve tüm başlıklarını almak için gerekli yöntemlere sahiptir. Bu nesneyi kullanabilmek için [email] modülünü içe aktarmak gerekir. [email.message_from_string] işlevi, mesajın karakter dizisinden bir [email.message.Message] nesnesi oluşturmaya olanak tanır;

[save_message] işlevi, [mail_parser] modülünün bir parçasıdır:

Image

[mail_parser] modülü, [readmails] işlevinin 6-7. satırlarına içe aktarılmıştır;

[mail_parser.py]'te [save_message] işlevi şu şekildedir:


# içe aktarmalar
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os


# email.message.Message türünde bir mesajın yedeklenmesi
# bu işlev özyinelemeli olarak çağrılabilir
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # çıktı: mesajların kaydedildiği klasör
    # email_message: yedeklenecek mesaj
    # irfc822: ekli e-postaların numaralandırılmasında kullanılan sıra numarası
    #
    # mesajın gövdesi
    part = email_message
    # [From, To, Subject] başlıkları, çok parçalı bölümlerden birinde
    # ya da [text/*] bölümünde ([multipart] bölümü bulunmadığında)
    keys = part.keys()
    # "From" başlıklarında yer almalıdır; aksi takdirde bölümde aranan başlıklar bulunmaz
    if "From" in keys:
        # bazı başlıkları alıyoruz
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        # başlıkları bir metin dosyasına kaydedilir
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # dosyaya yazma
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # dosyanın türü [part]
    main_type = part.get_content_maintype()

Açıklamalar

  • 12. satır: işlev en fazla üç parametre alır:
  • [output]: Mesajın kaydedileceği klasör (2. parametre);
  • [email_message]: [email.message.Message] türünde bir mesaj. Bu tür, yapılandırılmış bir türdür. E-postanın metnini ve tüm ekli dosyaları içerir ve çeşitli öğelerini almak için yöntemler sunar;
  • [irfc822]: Bu parametre, [email_message] içinde kapsüllenmiş e-postaları numaralandırmak için kullanılır;
  • 18. satır: [email_message] nesnesi, [part] içine yerleştirilir. [email.message.Message] türü, aynı zamanda [email.message.Message] türüne sahip olan [part] parçalarını (mesaj gövdesi, ekler, kapsüllenmiş e-postalar) içerir. Her bir [part] parçası alt parçalara sahip olabilir. Dolayısıyla [email.message.Message] türü, [email.message.Message] türündeki öğelerden oluşan bir ağaçtır:
    • [part.ismultipart()], [part] parçası alt parçalar içeriyorsa [True] değerine eşittir. Bu alt parçalar o durumda [part.get_payload()] aracılığıyla erişilebilir;
    • [part.ismultipart()] değeri [False] olduğunda, ilk mesaj ağacının bir yaprağına ulaşıldığı anlamına gelir: bu, aşağıdakilerden biri olabilir:
      • normal metin biçiminde mesaj gövdesi;
      • HTML biçiminde bir metin olarak mesaj gövdesi;
      • bir ek ([part.ismultipart()]'in [True]'e eşit olduğu kapsüllenmiş mesajlar hariç);
  • [email.message.Message] parametresinin ağaç yapısı nedeniyle, [save_message] işlevi özyinelemeli olarak çağrılacaktır. Ağacın yapraklarına ulaşıldığında, yani [part.ismultipart()]'in [False]'e eşit olduğu bir [part] bölümü bulunduğunda özyineleme sona erer;
  • 21. satır: Şu anda analiz edilen mesajın anahtarlarını (veya başlıklarını) görmek istiyoruz (rekürsiflik nedeniyle bu, ilk mesajın bir alt parçası olabilir);
  • 23-35. satırlar: başlıkları kaydetmek istiyoruz:
    • [From]: mesajın göndereni;
    • [To]: mesajın alıcısı;
    • [Subject]: mesajın konusu;
    • [Return-Path]: yanıt vermek istendiğinde yanıtlanması gereken alıcı. Aslında bu bilgi her zaman [From]'te bulunmaz;
    • [User-Agent]: POP3 sunucusuyla iletişim kuran istemci;
    • [Date]: e-postanın gönderilme tarihi;
  • 23. satır: Bir mesajın yalnızca bir bölümü bu başlıkları içerir. Diğer bölümler için 23-35. satırlardaki kod göz ardı edilecektir;
  • 25-30. satırlar: altı başlıktan oluşan bir liste oluşturulur;
  • 25. satır: İlk başlığı inceleyelim:
    • [part.get(key)], [key] anahtarıyla ilişkili başlığı elde etmemizi sağlar;
    • bu başlık kodlanmış olabilir. Kodlama utf-8 değilse, başlık kodunu çözüp [decode_header] işlevini kullanarak utf-8 olarak yeniden kodlarız;
    • ilk başlık [From: pymail2lexemple@gmail.com] biçiminde olacaktır;
  • 31-35. satırlar: başlıklar [output/headers.txt] dosyasına kaydedilir;

[decode_header] işlevi şu şekildedir (yine [mail_parser.py] içinde):


# başlıkların kodunun çözülmesi
def decode_header(header: object) -> str:
    # başlık kodunun çözülmesi
    header = email.header.decode_header(f"{header}")
    # sonuç bir dizidir - burada sadece (başlık, kodlama) türünde bir eleman olacaktır
    # eğer encoding==None ise, o zaman header bir karakter dizisidir
    # aksi takdirde, encoding ile kodlanmış bir bayt listesidir
    header, encoding = header[0]
    if not encoding:
        # kodlama yoksa
        return header
    else:
        # kodlama varsa, kod çözülür
        return header.decode(encoding)

Açıklamalar

  • 4. satır: başlık kodunu çözüyoruz:
    • [email.header] modülünü içe aktarmak gerekir;
    • [(header1,encoding1) , (header2, encoding2)…] tuple'larından oluşan bir liste elde edilir;
    • [From, To, Subject, Return-Path, Date] başlıkları için listede tek bir öğe bulunur;
    • 8. satır: Tek başlığı ve kodlamasını alıyoruz:
      • eğer [encoding==None] ise, [header] başlığı bir karakter dizisi biçimindedir;
      • aksi takdirde, [header], kodlanmış başlığı temsil eden bir bayt dizisidir;
  • 10-11. satırlar: kodlama yoksa, başlık olduğu gibi verilir;
  • 12-14. satırlar: kodlama varsa, alınan bayt dizisini bir karakter dizisine dönüştürür ve bunu döndürür;

[save_message] işlevine geri dönelim:


# email.message.Message türündeki bir mesajın kaydedilmesi
# bu işlev özyinelemeli olarak çağrılabilir
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # çıktı: mesajların kaydedildiği klasör
    # email_message: kaydedilecek mesaj
    # irfc822: ekli e-postaların numaralandırılmasında kullanılan sıra numarası
    #
    # mesajın gövdesi
    part = email_message
    # [From, To, Subject] başlıkları, çok parçalı bölümlerden birinde
    # ya da [multipart] bölümü bulunmadığında [text/*] bölümünde
    keys = part.keys()
    # "From" başlıklarında yer almalıdır; aksi takdirde bölümde aranan başlıklar bulunmaz
    if "From" in keys:
        # bazı başlıkları alıyoruz
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        # başlıkları bir metin dosyasına kaydedilir
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # dosyaya yazma
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # mesajın türü [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # mesajın türü text/plain ise
    if type_of_part == "text/plain":
        # metin mesajı
        save_textmessage(output, part, 0)

    # mesajın türü text/html ise
    elif type_of_part == "text/html":
        # mesaj HTML
        save_textmessage(output, part, 1)

    # mesaj bölümlerden oluşan bir kapsayıcıysa
    elif part.is_multipart():
        
    else:
        
    # diğer bölümler göz ardı edilir (text/plain, text/html ve attachment olmayanlar)
    # irfc822'nin geçerli değerini verir (output klasöründe sıralanan ekli e-postaların numaralandırılması)
    return irfc822

Açıklamalar

  • satır 1-26: ilk mesajın başlıklarını işledik;
  • 28-31. satırlar: [email.message.Message] türündeki bir mesajın bölümleri bir ana türe ve bir alt türe sahiptir. Bunları alıyoruz;
  • satır 32-35: İşlenen bölümün türü [text/plain] ise, ilk mesaj ağacının bir yaprağına ulaşılmış demektir. Bu, gönderenin mesajına yazdığı metindir;
  • 35. satır: Bu metin bir dosyaya yazılır:
    • 1. parametre [output], metnin kaydedileceği klasördür;
    • ikinci parametre, kaydedilecek metni içeren mesajın kısmıdır;
    • üçüncü parametre, normal bir metni kaydetmek için 0, HTML türü bir metni kaydetmek için 1 değerindedir;
  • 37-40. satırlar: Eğer bölümün türü [text/html] ise, o zaman yine ilk mesaj ağacının bir yaprağına ulaşmış oluruz. Bu, gönderenin mesajında yazdığı metindir, ancak bu sefer HTML biçimindedir. Tüm posta yöneticileri bu biçimi desteklemez;

[save_textmessage] işlevi şu şekildedir:


# metin mesajının kaydedilmesi
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # başlıklar
    headers = []
    # mesajın karakter kümesi
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # içeriğin kodlama modu
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # 8 bit modu sorun yarattı
    if content_transfer_encoding == "8bit":
        # e-postadaki mesaj alınır
        msg = part.get_payload()
    else:
        # e-postadaki mesaj alınır
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # metin türlerine göre
    filename = None
    if type_of_text == 0:
        # başlıkların kaydedilmesi
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # dosyaya yazma
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # içerik için metin dosyası
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # içerik için HTML dosyası
        filename = f"{output}/mail.html"
    # mesajın kaydedilmesi
    with codecs.open(filename, "w", "utf-8") as file:
        # dosyaya yazma
        file.write(msg)

Açıklamalar

  • Başlıklar gibi, mesaj metni de kodlanabilir. İki tür kodlama olabilir:
    • metnin ilk kodlaması (utf-8, iso-8859-1…). Bu, mesajı gönderen e-posta istemcisi tarafından kullanılan kodlamadır. Alınan mesajın [Content-Type] başlığıyla bilinir;
    • mesajın gönderilebilmesi için metnin geçirdiği ikinci bir kodlama. Bu, alınan mesajın [Transfer-Content-Encoding] başlığıyla bilinir;
  • 6. satır: metnin ilk kodlaması;
  • 11. satır: Metnin alıcıya aktarılması için uygulanan ikinci kodlama;
  • 9. ve 13. satırlar: Bu iki bilgi, [headers] listesine eklenir. Bu bilgiler, mesajın belirli başlıklarını kaydeden [headers.txt] dosyasındaki bilgilere eklenecektir;
  • 20. satır: [email.contentmanager.raw_data_manager.get_content], mesajı ilk kodlamasıyla elde etmemizi sağlar. Kodlama 2'den kurtulmuş oluruz. Yalnızca [email.contentmanager.raw_data_manager] nesnesi, iki tür [Transfer-Content-Encoding]'i yönetir:
    • [quoted-printable];
    • [base64];

Diğerlerini yok sayar. Oysa örneğin Thunderbird, "8bit" olarak adlandırılan [Transfer-Content-Encoding] kodlamasını kullanır. Bu kodlama yok sayılır ve aksanlı karakterler içeren mesajlar bozulur. Mesaj, [part.get_payload()] yöntemi ile elde edilebilir (15-17. satırlar);

  • 21. satır: Bu aşamaya geldiğimizde, mesaj aktarım kodlamasından arındırılmış, yani gönderen tarafından yazıldığı haliyle karşımıza çıkar;
  • satır 22-37: burada bir metin mesajını kaydetmemiz gereken durum söz konusudur;
    • satır 24-28: 9. ve 13. satırlarda oluşturulan iki başlığı [headers.txt] dosyasına kaydediyoruz. Bu dosya zaten mevcut ve başlıklar içeriyor. Bu nedenle, bu dosyayı açmak için "a" modu (satır 25) kullanılır. "a", "append" (ekle) anlamına gelir ve yeni başlıklar, [headers.txt] dosyasının mevcut içeriğine (dosyanın sonuna) eklenir;
    • 30. satır: metin mesajının kaydedileceği dosyanın adı;
    • satır 33: HTML mesajının kaydedileceği dosyanın adı;
    • satır 34-37: utf-8 metni bir dosyaya kaydedilir;

[save_message] işlevine geri dönelim:


# email.message.Message türünde bir mesajın kaydedilmesi
# bu işlev özyinelemeli olarak çağrılabilir
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # çıktı: mesajların kaydedildiği klasör
    # email_message: kaydedilecek mesaj
    # irfc822: ekli e-postaların numaralandırılmasında kullanılan sıra numarası
    #
    # mesajın gövdesi
    part = email_message
    # [From, To, Subject] başlıkları, çok parçalı bölümlerden birinde
    # ya da [multipart] bölümü bulunmadığında [text/*] bölümünde
    keys = part.keys()
    # "From" başlıklarında yer almalıdır; aksi takdirde bölümde aranan başlıklar bulunmaz
    if "From" in keys:
        # bazı başlıkları alıyoruz
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        # başlıkları bir metin dosyasına kaydedilir
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # dosyaya yazma
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # mesajın türü [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # mesajın türü text/plain ise
    if type_of_part == "text/plain":
        # metin mesajı
        save_textmessage(output, part, 0)

    # mesajın türü text/html ise
    elif type_of_part == "text/html":
        # mesaj HTML
        save_textmessage(output, part, 1)

    # mesaj bölümlerden oluşan bir kapsayıcıysa
    elif part.is_multipart():
        # ekli e-postanın özel durumu
        if type_of_part == "message/rfc822":
            # ekli e-posta için yeni bir output2 klasörü oluşturma
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # irfc822 mesajının alt bölümlerinin output2 klasörüne kaydedilmesi
            for subpart in part.get_payload():
                # yeni irfc822 klasöründe 0'dan yeniden başlar
                save_message(output2, subpart, 0)

        else:
            # ekli bir e-posta söz konusu değil
            # alt bölümleri mevcut output klasörüne kaydediyor
            # irfc822, her message/rfc822 alt bölümü için artırılmalıdır
            for subpart in part.get_payload():
                # save_message, irfc822'nin son değerini verir
                # subpart="message/rfc822" ise irfc822'nin son değeri 1 artırılır, aksi takdirde artırılmaz
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # diğer durumlar (text/plain değil, text/html değil, multipart değil)
        # ek var mı?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # diğer bölümler göz ardı edilir (text/plain değil, text/html değil, attachment değil)
    # irfc822'nin mevcut değerini verir (output klasöründe sıralanan ekli e-postaların numaralandırılması)
    return irfc822

Açıklamalar

  • satır 33-40: İlk mesaj ağacının bir ucundaki bir mesaj için olası iki durumu ele aldık (alt bölüm yok). Ele almamız gereken iki durum daha var:
    • 43-62. satırlar: analiz edilen bölümün kendisinin alt bölümler içerdiği durum (part.ismultipart()==True);
    • 63-68. satırlar: Kalan durumlar için, yalnızca analiz edilen bölümün bir ek olduğu durumu ele alıyoruz;

Bu son durumu ele alıyoruz. Yine başlangıçtaki mesajın bir ucundayız (alt bölüm yok). Bu türden iki durumla daha önce karşılaştık: text/plain ve text/html türleri. Şimdi ekli dosya durumunu ele alıyoruz.

  • 66. satır: Ek, [Content-Disposition] anahtarıyla tanımlanır;
  • 67. satır: Bu anahtar mevcutsa ve [attachment] dizesiyle başlıyorsa, mesajın ekli bir parçasıyla karşı karşıyayız;
  • 68. satır: ek, [output] klasörüne kaydedilir;

[save_attachment] işlevi şu şekildedir:


# ek dosyanın kaydedilmesi
def save_attachment(output: str, part: email.message.Message):
    # ekli dosyanın adı
    filename = os.path.basename(part.get_filename())

    # dosya adı kodlanabilir
    # örneğin =?utf-8?Q?Dersler-Öğreticiler-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # ekli dosya kaydedilir
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • 4. satır: [part] bir ekse, ekli dosyanın adı [part.get_filename] ile elde edilir. Yalnızca dosya adı saklanır, yolu saklanmaz;
  • 8. satır: Dosya adları genellikle mesaj başlıklarıyla aynı şekilde kodlanmıştır. Bu nedenle, kodlamayı çözmek için [decode_header] işlevi kullanılır;
  • 11. satır: Ekli dosyanın içeriği, şimdilik dosyanın orijinal içeriğinin metin olarak kodlanması (genellikle base64) sonucu ortaya çıkan bir karakter dizisidir. Bu orijinal içeriği elde etmek için [part.get_payload(decode=True)] işlevi kullanılır. [decode=True] parametresi, ekli dosyanın içeriğinin kodunun çözülmesi gerektiğini belirtir. Böylece bir bayt dizisi elde edilir;
  • 10. satır: Bu bayt dizisi, [output/filename] dosyasına kaydedilir. Dosyanın "wb" açılış modu, "write binary" anlamına gelir;

[save_message] işlevinin koduna geri dönelim:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    # çıktı: mesajların kaydedildiği klasör
    # email_message: kaydedilecek mesaj
    # irfc822: ekli e-postaların numaralandırma dizisindeki sıralı numara
    #
    # mesajın gövdesi
    part = email_message
    # [From, To, Subject] başlıkları, çok parçalı bölümlerden birinde
    # ya da [multipart] bölümü bulunmadığında [text/*] bölümünde
    keys = part.keys()
    # "From" başlıklarında yer almalıdır; aksi takdirde bölümde aranan başlıklar bulunmaz
    if "From" in keys:
        # bazı başlıkları alıyoruz
        headers = [f"From: {decode_header(part.get('From'))}",
                   f"To: {decode_header(part.get('To'))}",
                   f"Subject: {decode_header(part.get('Subject'))}",
                   f"Return-Path: {decode_header(part.get('Return-Path'))}",
                   f"User-Agent: {decode_header(part.get('User-Agent'))}",
                   f"Date: {decode_header(part.get('Date'))}"]
        # başlıkları bir metin dosyasına kaydedilir
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # dosyaya yazma
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # mesajın türü [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # mesajın türü text/plain ise
    if type_of_part == "text/plain":
        # metin mesajı
        save_textmessage(output, part, 0)

    # mesajın türü text/html ise
    elif type_of_part == "text/html":
        # mesaj HTML
        save_textmessage(output, part, 1)

    # mesaj bölümlerden oluşan bir kapsayıcıysa
    elif part.is_multipart():
        # ekli e-postanın özel durumu
        if type_of_part == "message/rfc822":
            # ekli e-posta için yeni bir output2 klasörü oluşturma
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # irfc822 mesajının alt bölümlerinin output2'ye kaydedilmesi
            for subpart in part.get_payload():
                # yeni irfc822 klasöründe 0'dan yeniden başlar
                save_message(output2, subpart, 0)

        else:
            # ekli bir e-posta söz konusu değil
            # alt bölümleri mevcut output klasörüne kaydedilir
            # irfc822, her message/rfc822 alt bölümü için artırılmalıdır
            for subpart in part.get_payload():
                # save_message, irfc822'nin son değerini verir
                # subpart="message/rfc822" ise 1 artırılır, aksi takdirde artırılmaz
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # diğer durumlar (text/plain değil, text/html değil, multipart değil)
        # ek var mı?
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # diğer bölümler göz ardı edilir (text/plain değil, text/html değil, attachment değil)
    # irfc822'nin mevcut değerini verir (output klasöründe sıralanan ekli e-postaların numaralandırılması)
    return irfc822

Yorumlar

  • Başlangıçtaki mesaj ağacının uçlarını ele aldık: [text/plain, text/html et Content-Disposition=attachment;…] bölümleri. Şimdi, analiz edilen bölümün bir bölümler kaptarı olduğu, yani [part.is_multipart()==True] alt bölümlerini içerdiği durumu ele almamız gerekiyor (41. satır). Mesaj ağacının sonlarına ulaşmak için bu alt bölümleri analiz etmek gerekir;
  • 43. satır: Analiz edilen parçanın türü [message/rfc822] olduğunda özel bir işlem uygulanır. Bu, bir e-postanın türüdür. Yani, bir e-postanın ekinde başka bir e-posta bulunduğu durumdur;

Kod şu şekildedir:


    # mesaj bir bölümler kümesi ise
    elif part.is_multipart():
        # ekli e-postanın özel durumu
        if type_of_part == "message/rfc822":
            # ekli e-posta için yeni bir output2 klasörü oluşturma
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            # irfc822 mesajının alt bölümlerinin output2 klasörüne kaydedilmesi
            for subpart in part.get_payload():
                # yeni irfc822 klasöründe 0'dan yeniden başlar
                save_message(output2, subpart, 0)

        else:
            # ekli bir e-posta söz konusu değil
            # alt bölümler mevcut output klasörüne kaydedilir
            # irfc822, her message/rfc822 alt bölümü için artırılmalıdır
            for subpart in part.get_payload():
                # save_message, irfc822'nin son değerini verir
                # subpart="message/rfc822" ise irfc822 değeri 1 artırılır, aksi takdirde artırılmaz
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • [message/rfc822] parçası ile diğer çok parçalı parçalar arasındaki fark, yedekleme klasörünün değişmesidir;
    • 6-8. satırlar: [message/rfc822] parçası için, kaydetme klasörü 7. satırdaki [output/rfc822_x] klasörü olur; burada x, ekli e-postanın numarasıdır (birinci için 1, ikinci için 2…);
    • 21. satır: Diğer çok parçalı bölümler için, yedekleme klasörü ilk mesajın [output] klasörü olarak kalır. Klasör değiştirilmez;
  • satır 10-12: Her alt bölüm, [save_message] işlevine yapılan özyinelemeli bir çağrı ile kaydedilir. Üçüncü parametre, [subpart] içinde kapsüllenmiş e-postaların numaralandırma indeksidir. Başlangıçta bu indeks 0 değerindedir;
  • 21. satır: 12. satırla aynı açıklama geçerlidir, ancak [irfc822] işlevinin 3. parametresinin değeri değişir. 18-21. satırlardaki döngüde birden fazla e-posta kapsüllenmişse, bunlar […/rfc822-1…/rfc822_2…] klasörlerine yerleştirilmelidir. Dolayısıyla, [save_message] işlevinin 3. parametresi sırasıyla 1, 2, 3… değerlerini almalıdır. Bunu sağlamak için, [save_message], [irfc822]’in değerini (satır 21) ayarlar.

Bir örnek verelim ve 18. satırdaki alt bölümlerin listesinin [subpart1, subpart2, subpart3, subpart4, subpart5] olduğunu ve [subpart1, subpart3, subpart5]'in ekli e-postalar olduğunu varsayalım; [subpart2]'in bir text/plain bölümü, [subpart4]'in bir ek olduğunu ve [irfc822=0] mesajında henüz ekli bir e-postaya rastlanmadığını varsayalım. Bu durumda:

  • (devamı)
    • [subpart1], 21. satırda kaydedilir: [saveMessage] işlevi irfc822=0 ile yürütülür;
    • [subpart1] ekli bir e-postadır, bu nedenle irfc822 değeri 1'e geçer (kodun 6. satırı). [output/irfc822_1] adlı bir klasör oluşturulur. Dolayısıyla [saveMessage(ouput,subpart1,0)] tarafından döndürülen değer 1'dir (23. satır);
    • [subpart2], 21. satırda kaydedilir: [saveMessage] işlevi, irfc822=1 olarak çalıştırılır;
    • [subpart2], e-posta eki değildir. Dolayısıyla irfc822 değeri 1 olarak kalır. Bu değer, 21. satırda alınır;
    • [subpart3], 21. satırda kaydedilir: [save_message] işlevi, irfc822=1 olarak çalıştırılır;
    • [subpart3], eki olan bir e-postadır; bu nedenle irfc822 değeri 2'ye geçer (kodun 6. satırı). [output/irfc822_2] adlı bir klasör oluşturulur. Dolayısıyla [save_message(ouput,subpart1,1)] tarafından döndürülen değer 2'dir (21. satır);
    • [subpart4], 21. satırda kaydedilir: [save_message] işlevi, irfc822=2 olarak çalıştırılır;
    • [subpart4], e-posta eki değildir. Dolayısıyla irfc822 değeri 2 olarak kalır. Bu, 21. satırda alınan değerdir;
    • [subpart5], 21. satırda kaydedilir: [save_message] işlevi, irfc822=2 olarak çalıştırılır;
    • [subpart5], ekli bir e-postadır; bu nedenle irfc822 değeri 3’e geçer (kodun 6. satırı). [output/irfc822_3] adlı bir klasör oluşturulur. Dolayısıyla, [save_message(ouput,subpart1,2)] tarafından döndürülen değer 3’tür (21. satır);

Çalıştırma örnekleri

[Gmail, Outlook, em Client, Thunderbird] adresinden [pymail2parlexemple@gmail.com] adresine 4 e-posta gönderiyoruz

  • [Gmail]: [https://mail.google.com/];
  • [Outlook]: [https://outlook.live.com/owa/];
  • [em Client]: [https://www.emclient.com/];
  • [Mozilla Thunderbird] : [https://www.thunderbird.net/fr/] ;

Tüm e-postaların konusu [hélène va au marché], metni ise [acheter des légumes] olacaktır. Aksanlı karakterlerin nasıl alınacağını test etmek istiyoruz.

Bu e-postaları, aşağıdaki [pop3/02/config] dosyasıyla yapılandırılmış [pop3/02/main] komut dosyası ile okuyoruz:


import os


def configure() -> dict:
    # uygulama yapılandırması
    config = {
        # yönetilecek posta kutularının listesi
        "mailboxes": [
            # sunucu: POP3 sunucusu
            # bağlantı noktası: POP3 sunucusunun bağlantı noktası
            # kullanıcı: mesajlarını okumak istediğiniz kullanıcı
            # şifre: kullanıcının şifresi
            # maxmails: indirilecek maksimum e-posta sayısı
            # timeout: sunucudan yanıt alınması için maksimum bekleme süresi
            # delete: indirilen mesajların sunucudan silinmesi gerekiyorsa "true" olarak ayarlanır
            # ssl: e-postaların okunması güvenli bir bağlantı üzerinden yapılıyorsa "true"
            # output: indirilen mesajların saklanacağı klasör

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # komut dosyasının bulunduğu klasörün mutlak yolu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # syspath'e eklenecek klasörlerin mutlak yolları
    absolute_dependencies = [
        # yerel klasör
        f"{script_dir}/../../shared",
    ]

    # syspath yapılandırması
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # yapılandırmayı oluşturuyoruz
    return config

Sonuç şu şekildedir:

Image

1 numaralı mesaj, Thunderbird tarafından gönderilen mesajdır:

Image

  • [5]'te, Thunderbird [3], [8bit] türünde bir [Transfer-Content-Encoding] kullanıyor;
  • [4]'te: mesaj, UTF-8 olarak kodlanmıştır;

2 numaralı mesaj, em Client tarafından gönderilen mesajdır:

Image

Image

[em Client]'in metinleri utf-8 [4] olarak kodladığı ve bunları [quoted-printable] [5]'e aktardığı dikkat çekmektedir. Ayrıca mesajın bir kopyasını HTML ve [7-8] formatlarında da göndermiştir. Burada test edilen tüm e-posta istemcileri bunu yapabilir. Bu, bir yapılandırma ayarıdır.

3 numaralı mesaj, Gmail tarafından gönderilen mesajdır:

Image

Gmail'in metinleri utf-8 [3] olarak kodladığı ve bunları [quoted-printable] [4] olarak ilettiği dikkat çekmektedir. [6] formatında, mesajın HTML versiyonu.

4 numaralı mesaj, Outlook tarafından gönderilen mesajdır:

Image

Outlook'un metinleri iso-8859-1 [3] olarak kodladığı ve bunları [quoted-printable] ve [4]'e aktardığı dikkat çekmektedir.

Yukarıdaki örnekler iki şeyi göstermektedir:

  • [pop3/02] istemcimiz düzgün çalışmıştır;
  • e-posta istemcileri, e-posta gönderme konusunda farklı yöntemler kullanır;

Şimdi de ekli dosyalara bakalım. Thunderbird ile [pymail2parlexemple@gmail.com] kullanıcısının posta kutusunu boşaltıyoruz. Ardından [smtp/03/main] komut dosyasını kullanarak aşağıdaki [smtp/03/config] yapılandırmasıyla bir e-posta gönderiyoruz:


import os


def configure() -> dict:
    # uygulama yapılandırması
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # açıklama: gönderilen e-postanın açıklaması
        # smtp-sunucusu: SMTP sunucusu
        # smtp-port: SMTP sunucusunun bağlantı noktası
        # gönderen: gönderen
        # to: alıcı
        # konu: e-postanın konusu
        # message : e-postanın içeriği
        "mails": [
            {
                "description": "mail to gmail via gmail avec smtplib",
                "smtp-server": "smtp.gmail.com",
                "smtp-port": "587",
                "from": "pymail2parlexemple@gmail.com",
                "to": "pymail2parlexemple@gmail.com",
                "subject": "to gmail via gmail avec smtplib",
                # aksanlı karakterleri test ediyoruz
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # kimlik doğrulamalı SMTP
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # burada, ekli dosyalar için mutlak yollar belirtilmelidir
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • 31-33. satırlar: E-postaya ek olarak:
  • bir Word dosyası;
  • bir PDF dosyası;
  • aynı iki dosyayı ek olarak içeren bir e-posta;

E-posta gönderildikten sonra, [pop3/02] komut dosyasını çalıştırarak [pymail2parlexemple@gmail.com] kullanıcısının e-posta kutusunu okuyoruz. Sonuçlar şu şekildedir:

Image

  • [1]'te: iki ek dosyası olan mesaj;
  • [2]: e-postanın kendisi ve ona ekli iki dosya;

Sonuç

[mail_parser.py] modülü oldukça karmaşıktır. Bunun nedeni, e-postaların kendisinin karmaşıklığıdır. Bu modülü, IMAP protokolü için yeniden kullanacağız.

21.7. IMAP protokolü

21.7.1. Giriş

Bir e-posta sunucusunda depolanan e-postaları okumak için iki protokol mevcuttur:

  • POP3 protokolü (Post Office Protocol); tarihsel olarak ilk protokol olmakla birlikte günümüzde pek kullanılmamaktadır;
  • IMAP protokolü (Internet Message Access Protocol), POP3'ten daha yeni ve şu anda en yaygın olarak kullanılan protokoldür;

IMAP protokolünü incelemek için şu mimariyi kullanacağız:

Image

  • [Serveur B], duruma göre şunlardan biri olacaktır:
    • [hMailServer] posta sunucusu tarafından uygulanan yerel bir IMAP sunucusu;
    • [imap.gmail.com:993] sunucusu, [Gmail] e-posta yöneticisinin IMAP sunucusudur;
  • [Client A], ekleri yönetmeye olanak tanıyan Python modüllerini kullanan ve IMAP sunucusu gerektirdiğinde şifreli ve kimlik doğrulamalı bir bağlantı kullanan bir Python betiği olacaktır;

IMAP protokolü, POP3 protokolünün ötesine geçer:

  • e-postalar IMAP sunucusunda saklanır ve klasörler halinde düzenlenebilir;
  • IMAP istemcisi, bu klasörlerin oluşturulması, değiştirilmesi ve silinmesi için komutlar gönderebilir;

Thunderbird ile bir örneğe bakalım. Aşağıdaki mimaride:

Image

  • Thunderbird, istemci A'dır;
  • [imap.gmail.com], B sunucusudur (Gmail);

Thunderbird ile [pymail2parlexemple@gmail.com] kullanıcısının e-postalarında bir klasör oluşturalım:

Image

  • [1-6]'te, [dossier1] klasörünü oluşturuyoruz;

Image

  • dosyasını [7-8] dosyasına dönüştürdükten sonra, [Courrier entrant] klasöründeki tüm dosyaları (fare ile) [dossier1] klasörüne taşıyoruz;

Şimdi Gmail web sitesine giriş yapalım ve [pymail2parlexemple@gmail.com] kullanıcısı olarak oturum açalım:

Image

  • [2-3]'te gelen kutusu boş;
  • [1] hesabında, [dossier1] klasörü oluşturulmuştur;

Image

  • [4-6]'te: [dossier1] klasörüne taşınan e-postalar;

Karşımızda şu mimari bulunmaktadır:

Image

  • Müşteri A, Thunderbird uygulamasıdır;
  • C İstemcisi, Gmail web uygulamasıdır;
  • Sunucu B, Gmail'in IMAP sunucusudur;

Kullanıcının klasör yapısı, IMAP sunucusu tarafından yönetilir. Ardından tüm IMAP istemcileri bu sunucuyla senkronize olarak kullanıcıya hesabındaki klasörleri gösterir. Burada Thunderbird, aşağıdakileri yapmak için birkaç komut göndermiştir:

  • [dossier1] klasörünü oluşturmak;
  • bu klasöre mesajları aktarmak;

21.7.2. [imap/main] komut dosyası: IMAP istemcisi ile [imaplib] modülü

Image

[imap/main] komut dosyası, aşağıdaki [imap/config] komut dosyası tarafından yapılandırılır:


import os


def configure() -> dict:
    # uygulama yapılandırması
    config = {
        # yönetilecek posta kutuları listesi
        "mailboxes": [
            # sunucu: IMAP sunucusu
            # bağlantı noktası: IMAP sunucusunun bağlantı noktası
            # kullanıcı: mesajlarını okumak istediğiniz kullanıcı
            # şifre: kullanıcının şifresi
            # maxmails: indirilecek maksimum e-posta sayısı
            # timeout: sunucudan yanıt alınması için maksimum bekleme süresi
            # delete: indirilen mesajların sunucudan silinmesi gerekiyorsa "true" olarak ayarlanır
            # ssl: e-postaların okunması güvenli bir bağlantı üzerinden yapılıyorsa "true"
            # output: indirilen mesajların saklanacağı klasör

            {
                "server": "imap.gmail.com",
                "port": "993",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # komut dosyasının bulunduğu klasörün mutlak yolu
    script_dir = os.path.dirname(os.path.abspath(__file__))

    # syspath'e eklenecek klasörlerin mutlak yolları
    absolute_dependencies = [
        # yerel klasör
        f"{script_dir}/../shared",
    ]

    # syspath yapılandırması
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    # yapılandırmayı oluşturuyoruz
    return config

Açıklamalar

  • 8-29. satırlar: [mailboxes] anahtarı, kontrol edilecek posta kutuları listesiyle ilişkilendirilmiştir;
  • 20. satır: IMAP sunucusu;
  • 21. satır: hizmet bağlantı noktası;
  • 22-23. satırlar: e-postalarını okumak istediğiniz kullanıcı;
  • 24. satır: Okunmak istenen e-postaların maksimum sayısı;
  • 25. satır: IMAP sunucusuyla güvenli bir bağlantı kurulması gerekip gerekmediğini belirtir (True) veya gerekmediğini belirtir (False);
  • satır 26: sunucudan yanıt gelmesi için maksimum bekleme süresi;
  • 27. satır: okunan e-postaların kaydedileceği klasör;

[imap/main] komut dosyası şu şekildedir:


# ithalatlar
import email
import imaplib
import os
import shutil


# -----------------------------------------------------------------------

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#   e-postaları okumaya olanak tanıyan IMAP istemcisi

# uygulamanın yapılandırması alınır
import config
config = config.configure()

# posta kutularını tek tek işleme
for mailbox in config['mailboxes']:
    try:
        # konsol ekranı
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # posta kutusunu okuma
        readmails(mailbox)
        # son
        print("Lecture terminée...")
    # BaseException hariç hata:
    #     # hata görüntülenir
    #     print(f"Aşağıdaki hata oluştu: {hata}")
    finally:
        pass

Açıklamalar

  • 14-36. satırlar: |pop3/02/main| komut dosyasında daha önce karşılaştığımız yaklaşımı burada da görüyoruz;

[readmails] işlevi şöyledir:


def readmails(mailbox: dict):
    # istisnaların yukarıya iletilmesine izin verilir
    #
    # e-posta ayrıştırıcı modülü
    from mail_parser import save_message

    # yapılandırma bilgilerini alıyoruz
    output = mailbox['output']
    user = mailbox['user']
    password = mailbox['password']
    timeout = mailbox['timeout']
    server = mailbox['server']
    port = int(mailbox['port'])
    maxmails = mailbox['maxmails']
    ssl = mailbox['ssl']
    #
    # başlıyoruz
    imap_resource = None
    try:
        # Varsa depolama klasörleri oluşturuluyor
        if not os.path.isdir(output):
            os.mkdir(output)
        # kullanıcı
        dir2 = f"{output}/{user}"
        # [dir2] klasörü varsa siliniyor, ardından yeniden oluşturuluyor
        if os.path.isdir(dir2):
            # silme
            shutil.rmtree(dir2)
        # oluşturma
        os.mkdir(dir2)
        # IMAP sunucusuna bağlanma
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # istemci iletişim zaman aşımı
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # kimlik doğrulama
        imap_resource.login(user, password)
        # INBOX klasörü seçiliyor (gelen postalar)
        imap_resource.select('INBOX')
        # bu klasördeki tüm mesajlar alınır: kriter ALL
        # özel bir kodlama yok: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0], tüm mesajların numaralarını boşlukla ayıran bir bayt dizisidir
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        # e-postalar tek tek okunur
        while not fini:
            # num, ikili sistemde bir mesaj numarasıdır
            num = nums[imail]
            # print(f"{num} numaralı mesaj")

            # num numaralı mesajı alıyoruz
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"type={typ2}, data={data2}")

            # data, tuple'lar içeren bir listedir; burada tek bir tuple vardır
            # data[0], tuple'dır; dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX ise tuple'ın ikinci elemanıdır
            # dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX, mesajın tüm satırlarını temsil eden bir bayt dizisi içerir
            # "mesaj" derken, mesaj metni + tüm ek dosyalar kastedilmektedir

            # mesaj, email.message.Message türü olarak alınır
            message = email.message_from_bytes(data2[0][1])
            # mesajın klasörü
            dir3 = f"{dir2}/message_{int(num)}"
            # Klasör yoksa, oluşturulur
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # mesaj kaydedilir
            save_message(dir3, message)
            # sonraki mesaj
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # posta kutusuyla bağlantı kesilir
            imap_resource.close()
            # IMAP sunucusundan oturum kapatılır
            imap_resource.logout()

Yorumlar

  • 7-15. satırlar: Yapılandırma öğeleri alınır;
  • 19. ve 79. satırlar: Kod, bir try / finally bloğu ile kontrol edilmektedir. Dolayısıyla istisnalar yakalanmamaktadır (except cümlesi bulunmamaktadır); bu istisnalar, çağıran koda iletilir ve orada durdurulup görüntülenir;
  • 23-30. satırlar: E-postaların kaydedileceği klasör oluşturulur;
  • 31-35. satırlar: IMAP sunucusuna bağlanılır. Kullanılan sınıf, IMAP sunucusunun güvenli (IMAP4_SSL) olup olmadığına göre farklılık gösterir;
  • satır 36-38: İstemci/sunucu iletişimi için zaman aşımı süresi belirlenir;
  • 39-40. satırlar: IMAP sunucusunda kimlik doğrulaması yapılır;
  • satır 41-42: IMAP adlı bir kullanıcının e-posta kutusunun klasörlere ayrılabileceğini görmüştük. [INBOX] klasörü gelen postalar için olan klasördür. [dossier1] klasörünü seçmek için [imapResource.select('dossier1')] yazılır;
  • 43-45. satırlar: [INBOX] klasöründe bulunan tüm mesajların listesi isteniyor:
    • [imapResource.search]'in ilk parametresi bir kodlama türüdür. [None], "kodlama filtresi yok" anlamına gelir;
    • ikinci parametre bir kriterdir. Bunu ifade etmenin farklı yolları vardır. [ALL] kriteri, klasördeki tüm mesajların istenildiğini ifade eder;

[imapResource.search]'in sonucu şuna benzer:


typ=OK, data=[b'1 2']

[data], elde edilen mesaj numaralarını içeren bir listedir. Bu numaralar ikili sistemdedir. Yukarıda, [INBOX] klasöründe iki mesaj bulunmuştur;

  • 49. satır: mesaj numaraları alınır. Yukarıda, ikili olarak kodlanmış numaralardan oluşan [b'1' b'2'] listesi elde edilecektir;
  • 53-78. satırlar: [INBOX] klasöründeki mesajları okumak için döngüye girilir;
  • 54-55. satırlar: mesaj numarası;
  • 58-59. satırlar: [num] numaralı mesaj, IMAP sunucusundan istenir;
    • 1. parametre, istenen mesajın numarasıdır;
    • ikinci parametre, "(part1)(part2)…" biçiminde bir dizedir; burada [parti], mesajın bir bölümünün adıdır. Bu konuyu derinlemesine incelememedim. Ad (RFC822) e-postanın tamamını ifade eder;

Şu şekilde bir çıktı alınıyor:


type=OK, data=[(b'1 (RFC822 {614}', b'Return-Path: guest@localhost\r\nReceived: from [127.0.0.1] (localhost [127.0.0.1])\r\n\tby DESKTOP-528I5CU with ESMTPA\r\n\t; Tue, 17 Mar 2020 09:41:50 +0100\r\nTo: guest@localhost\r\nFrom: "guest@localhost" <guest@localhost>\r\nSubject: test\r\nMessage-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>\r\nDate: Tue, 17 Mar 2020 09:41:48 +0100\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101\r\n Thunderbird/68.6.0\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\nContent-Language: fr\r\n\r\nh\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.\r\n\r\n'), b')']

Burada [data] öğesi, tek öğeli bir listedir ve bu tek öğe, üç öğeden oluşan bir tuple'dır:


data = [
    (b'1 (RFC822 {614}',
     b'Return-Path: guest@localhost\r\nReceived: from [127.0.0.1] (localhost [127.0.0.1])\r\n\tby DESKTOP-528I5CU with ESMTPA\r\n\t; Tue, 17 Mar 2020 09:41:50 +0100\r\nTo: guest@localhost\r\nFrom: "guest@localhost" <guest@localhost>\r\nSubject: test\r\nMessage-ID: <2572d0f0-5b7c-2c31-5a70-c628293d5709@localhost>\r\nDate: Tue, 17 Mar 2020 09:41:48 +0100\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:68.0) Gecko/20100101\r\n Thunderbird/68.6.0\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8; format=flowed\r\nContent-Transfer-Encoding: 8bit\r\nContent-Language: fr\r\n\r\nh\xc3\xa9l\xc3\xa8ne est all\xc3\xa9e au march\xc3\xa9 acheter des l\xc3\xa9gumes.\r\n\r\n'),
    b')'
]

Bu tuple'ın ikinci elemanı, istenen mesajın tamamını temsil eden bir ikili dizidir. Yukarıda, [mail_parser] modülünün incelenmesi sırasında daha önce tanıtılmış elemanları görebiliriz.

data[0], iki elemanlı bir tuple'ı temsil eder. data[0][1], mesajın satırlarını ikili biçimde temsil eder.

  • 68. satır: [response.text[39:]] işlevi, mesaj satırlarından [email.message.Message] türünde bir nesne oluşturur. [email.message.Message] türü, daha önce yazdığımız [mail_parser] modülünün parametre türüdür;
  • 69-73. satırlar: [num] numaralı mesajın yedekleme klasörünü oluşturuyoruz;
  • 75. satır: 5. satırdaki [mail_parser] modülünün [save_message] işlevini çağırıyoruz. Bu işlev, |pop3/02/main| başlığı altında açıklanmıştır;
  • 76-78. satırlar: Bir sonraki mesajı işlemek için döngüye geri dönülür;
  • satır 79-84: hata olsun ya da olmasın:
    • satır 82: sorgulanan dosya ile bağlantı sonlandırılır;
    • satır 84: IMAP sunucusundan bağlantı kesilir;

Elde edilen sonuçlar, [pop3/02/main] komut dosyasıyla elde edilen sonuçlarla aynıdır. Aynı [mail_parser] e-posta ayrıştırıcısı kullanıldığı için bu normaldir.