Skip to content

21. تابع‌های اینترنت

اکنون به توابع اینترنتی پایتون می‌پردازیم که به ما امکان برنامه‌نویسی TCP / IP (پروتکل کنترل انتقال / پروتکل اینترنت) را می‌دهند.

Image

21.1. مبانی برنامه‌نویسی اینترنت

21.1.1. عمومی

بیایید ارتباط بین دو ماشین دور، A و B را در نظر بگیریم:

Image

زمانی که یک برنامه AppA روی ماشین A بخواهد با یک برنامه AppB روی ماشین B در اینترنت ارتباط برقرار کند، باید چندین چیز را بداند:

  • آدرس IP IP یا نام میزبان ماشین B؛
  • شماره پورت مورد استفاده توسط برنامه AppB. این به این دلیل است که ماشین B ممکن است میزبان برنامه‌های متعددی باشد که در اینترنت در حال اجرا هستند. هنگامی که اطلاعاتی را از شبکه دریافت می‌کند، باید بداند آن اطلاعات برای کدام برنامه در نظر گرفته شده است. برنامه‌های روی ماشین B از طریق رابط‌هایی که به نام پورت‌های ارتباطی نیز شناخته می‌شوند، به شبکه دسترسی پیدا می‌کنند. این اطلاعات در داخل بسته دریافتی توسط ماشین B قرار دارد تا بتواند به برنامه صحیح تحویل داده شود؛
  • پروتکل‌های ارتباطی قابل فهم برای ماشین B. در مطالعه ما، تنها از پروتکل‌های TCP-IP استفاده خواهیم کرد؛
  • پروتکل گفتگوی پذیرفته‌شده توسط برنامه AppB. در واقع، ماشین‌های A و B با یکدیگر «صحبت» خواهند کرد. آنچه می‌گویند در پروتکل‌های TCP-IP قرار داده خواهد شد. با این حال، هنگامی که در سوی دیگر این زنجیره، اپلیکیشن AppB اطلاعاتی را که توسط اپلیکیشن AppA ارسال شده است دریافت می‌کند، باید قادر به تفسیر آن باشد. این مشابه وضعیتی است که در آن دو نفر، الف و ب، از طریق تلفن با یکدیگر ارتباط برقرار می‌کنند: مکالمه آنها توسط تلفن منتقل می‌شود. گفتار توسط تلفن الف به صورت سیگنال رمزگذاری شده، از طریق خطوط تلفن منتقل شده و به تلفن ب می‌رسد تا رمزگشایی شود. سپس شخص B صدای گفتار را می‌شنود. اینجاست که مفهوم پروتکل گفتگوی دوطرفه مطرح می‌شود: اگر A به فرانسوی صحبت کند و B آن زبان را نفهمد، A و B قادر به برقراری یک گفتگوی معنادار نخواهند بود؛

بنابراین، دو برنامه در حال ارتباط باید بر سر نوع دیالوگی که استفاده خواهند کرد، توافق کنند. به عنوان مثال، مکالمه با سرویس ftp با مکالمه با سرویس pop یکسان نیست: این دو سرویس دستورات یکسانی را قبول نمی‌کنند. آنها پروتکل مکالمه متفاوتی دارند؛

21.1.2. ویژگی‌های پروتکل TCP

در اینجا، ما فقط ارتباطات شبکه‌ای را که از پروتکل حمل‌ونقل TCP استفاده می‌کنند، بررسی خواهیم کرد که ویژگی‌های اصلی آن به شرح زیر است:

  • فرآیندی که مایل به ارسال داده است، ابتدا یک اتصال با فرایندی که قرار است اطلاعات مورد نظر را دریافت کند، برقرار می‌کند. این اتصال بین یک پورت در ماشین فرستنده و یک پورت در ماشین گیرنده برقرار می‌شود. بدین ترتیب یک مسیر مجازی بین دو پورت ایجاد می‌شود که منحصراً برای دو فرایندی که اتصال را برقرار کرده‌اند، رزرو خواهد شد؛
  • تمام بسته‌های ارسالی توسط فرآیند مبدأ از این مسیر مجازی پیروی کرده و به ترتیبی که ارسال شده‌اند، می‌رسند؛
  • اطلاعات منتقل‌شده پیوسته است. فرآیند ارسال اطلاعات با سرعت خود اطلاعات را ارسال می‌کند. این اطلاعات لزوماً بلافاصله ارسال نمی‌شوند: پروتکل TCP منتظر می‌ماند تا اطلاعات کافی برای ارسال داشته باشد. این اطلاعات در ساختاری به نام بخش TCP ذخیره می‌شوند. به محض پر شدن این بخش، به لایه IP منتقل می‌شود، جایی که در یک بسته IP جاسازی خواهد شد؛
  • هر بخش ارسال‌شده توسط پروتکل TCP شماره‌گذاری می‌شود. پروتکل گیرنده TCP بررسی می‌کند که بخش‌ها را به ترتیب دریافت می‌کند. برای هر بخش که به درستی دریافت می‌شود، یک تأیید به فرستنده ارسال می‌کند؛
  • هنگامی که فرستنده این را دریافت می‌کند، فرآیند ارسال را مطلع می‌سازد. بنابراین فرآیند ارسال می‌تواند تأیید کند که یک بخش با موفقیت تحویل داده شده است؛
  • اگر پروتکل TCP که یک سگمنت را ارسال کرده است، پس از گذشت مدت زمان معینی، تأیید دریافت را دریافت نکند، آن سگمنت را مجدداً ارسال می‌کند و بدین ترتیب کیفیت سرویس تحویل اطلاعات را تضمین می‌نماید؛
  • دورسوی مجازی ایجاد شده بین دو فرآیند ارتباطی full-duplex است: این بدان معناست که اطلاعات می‌تواند در هر دو جهت جریان یابد. بدین ترتیب، فرآیند مقصد می‌تواند حتی زمانی که فرآیند مبدأ به ارسال اطلاعات ادامه می‌دهد، تأییدها را ارسال کند. این امر به پروتکل مبدأ TCP اجازه می‌دهد تا چندین بخش را بدون انتظار برای تأیید ارسال کند. اگر پس از گذشت مدت زمان معینی متوجه شود که برای یک سگمنت خاص با شماره n تأیید دریافت نکرده است، ارسال سگمنت‌ها را از آن نقطه از سر خواهد گرفت؛

21.1.3. رابطهٔ مشتری-سرور

ارتباطات در اینترنت اغلب ناهم‌زمان (asymmetric) است: ماشین A یک اتصال را برای درخواست یک سرویس از ماشین B آغاز می‌کند و مشخص می‌کند که می‌خواهد یک اتصال با سرویس SB1 روی ماشین B برقرار کند. ماشین B یا آن را می‌پذیرد یا رد می‌کند. اگر درخواست پذیرفته شود، ماشین A می‌تواند درخواست‌های خود را به سرویس SB1 ارسال کند. این درخواست‌ها باید مطابق با پروتکل گفتگویی باشند که سرویس SB1 آن را درک می‌کند. بدین ترتیب یک دیالوگ درخواست-پاسخ بین ماشین A، که به عنوان ماشین کلاینت شناخته می‌شود، و ماشین B، که به عنوان ماشین سرور شناخته می‌شود، برقرار می‌گردد. یکی از این دو طرف اتصال را قطع خواهد کرد.

21.1.4. معماری کلاینت

معماری یک برنامه شبکه‌ای که خدمات یک برنامه سرور را درخواست می‌کند به شرح زیر خواهد بود:

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. معماری سرور

معماری یک برنامه ارائه‌دهنده خدمات به شرح زیر خواهد بود:

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

برنامه سرور، درخواست اتصال اولیه یک کلاینت را به طور متفاوتی از درخواست‌های بعدی او برای یک سرویس، مدیریت می‌کند. این برنامه خود سرویس را ارائه نمی‌دهد. اگر این کار را انجام می‌داد، در حین ارائه سرویس دیگر منتظر درخواست‌های اتصال نمی‌ماند و در نتیجه به کلاینت‌ها خدماتی ارائه نمی‌شد. این برنامه به شیوه‌ای متفاوت عمل می‌کند: به محض دریافت و پذیرش درخواست اتصال در پورت گوش‌دادن، سرور وظیفه‌ای را ایجاد می‌کند که مسئول ارائه خدمات درخواستی توسط کلاینت است. این سرویس در پورت دیگری روی ماشین سرور، که به پورت سرویس معروف است، ارائه می‌شود. این امر امکان ارائه خدمات همزمان به چندین کلاینت را فراهم می‌کند.

یک وظیفه سرویس ساختار زیر را خواهد داشت:

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. پروتکل‌های ارتباطی اینترنت را کشف کنید

21.2.1. مقدمه

هنگامی که یک کلاینت به یک سرور متصل می‌شود، یک ارتباط دوطرفه بین آنها برقرار می‌گردد. ماهیت این ارتباط، چیزی را تشکیل می‌دهد که به آن پروتکل ارتباطی سرور گفته می‌شود. از جمله رایج‌ترین پروتکل‌ها در اینترنت می‌توان به موارد زیر اشاره کرد:

  • HTTP: پروتکل انتقال HyperText – پروتکل ارتباط با یک سرور وب (سرور HTTP
  • SMTP: پروتکل انتقال سادهٔ ایمیل – پروتکل ارتباط با یک سرور ارسال ایمیل (سرور SMTP
  • POP: پروتکل دفتر پستی – پروتکل برای ارتباط با یک سرور ذخیره‌سازی ایمیل (سرور POP). این پروتکل برای بازیابی ایمیل‌های دریافتی استفاده می‌شود، نه برای ارسال آنها؛
  • IMAP: پروتکل دسترسی به پیام اینترنت – پروتکل ارتباط با یک سرور ذخیره‌سازی ایمیل (سرور IMAP). این پروتکل به تدریج جایگزین پروتکل قدیمی‌تر POP شده است؛
  • FTP: پروتکل انتقال فایل – پروتکل ارتباط با یک سرور ذخیره‌سازی فایل (سرور FTP);

تمام این پروتکل‌ها مبتنی بر متن هستند: کلاینت و سرور خطوط متنی را مبادله می‌کنند. اگر یک کلاینت قادر به انجام موارد زیر باشد:

  • برقراری ارتباط با یک سرور TCP؛
  • نمایش خطوط متنی که سرور برای آن ارسال می‌کند، روی کنسول؛
  • ارسال خطوط متنی به سرور که کاربر روی صفحه‌کلید تایپ می‌کند؛

بنابراین ما می‌توانیم با استفاده از یک پروتکل مبتنی بر متن با سرور TCP ارتباط برقرار کنیم، مشروط بر اینکه قواعد آن پروتکل را بدانیم.

21.2.2. خدمات TCP

Image

کد مرتبط با این سند شامل دو ابزار ارتباطی TCP است:

  • [RawTcpClient] به شما امکان می‌دهد تا به پورت P روی سرور S متصل شوید؛
  • [RawTcpServer] به شما امکان می‌دهد سرور ایجاد کنید که روی پورت P منتظر مشتریان باشد؛

این‌ها دو برنامه C# هستند که کدهای منبع آن‌ها ارائه شده است. بنابراین می‌توانید آن‌ها را تغییر دهید.

سرور TCP [RawTcpServer] استبا استفاده از دستوری به شکل [RawTcpServeur port] فراخوانی می‌شود تا سرویس TCP را روی پورت [port] ماشین محلی (کامپیوتری که روی آن کار می‌کنید) ایجاد کند:

  • سرور می‌تواند به طور همزمان به چندین مشتری خدمت‌رسانی کند؛
  • سرور دستورات وارد شده توسط کاربر از طریق صفحه‌کلید را اجرا می‌کند. این دستورات به شرح زیر هستند:
    • list: فهرست کلاینت‌های متصل به سرور را نمایش می‌دهد. این موارد در قالب [id=x-nom=y] نمایش داده می‌شوند. فیلد [id] برای شناسایی کلاینت‌ها استفاده می‌شود؛
    • send x [texte]: متنی را به کلاینت شماره x (شناسه=x) ارسال می‌کند. کروشه‌های مربعی [] ارسال نمی‌شوند. آن‌ها در دستور لازم هستند و برای تفکیک بصری متن ارسالی به کلاینت استفاده می‌شوند؛
    • close x: اتصال با مشتری شماره x را قطع می‌کند؛
    • quit: تمام اتصالات را می‌بندد و سرویس را متوقف می‌کند؛
  • خطوط ارسال‌شده توسط کلاینت به سرور در کنسول نمایش داده می‌شوند؛
  • تمام ارتباطات در یک فایل متنی به نام [machine-port.txt] ثبت می‌شود، که در آن
    • [machine] نام ماشینی است که کد روی آن در حال اجراست؛
    • [port] پورت سرویس است که به درخواست‌های کلاینت پاسخ می‌دهد؛

کلاینت TCP [RawTcpClient] با استفاده از دستوری به شکل [RawTcpClient serveur port] برای اتصال به پورت [port] روی سرور [serveur] فراخوانی می‌شود:

  • خطوط تایپ‌شده توسط کاربر روی صفحه‌کلید به سرور ارسال می‌شوند؛
  • خطوط ارسال‌شده توسط سرور روی کنسول نمایش داده می‌شوند؛
  • تمام ارتباطات در یک فایل متنی به نام [serveur-port.txt] ثبت می‌شود؛

بیایید یک مثال را بررسی کنیم. دو پنجره ترمینال PyCharm را باز کنید و در هر یک از آن‌ها به پوشه utilities بروید:

Image

در یکی از پنجره‌ها، سرور [RawTcpServer] را روی پورت ۱۰۰ راه‌اندازی کنید:


(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 :
  • خط ۱: ما در پوشهٔ utilities هستیم؛
  • خط ۱: ما سرور TCP را روی پورت ۱۰۰ راه‌اندازی می‌کنیم؛
  • خطوط ۲–۴: سرور منتظر یک کلاینت (TCP) است و فهرستی از دستورات را نمایش می‌دهد که کاربر می‌تواند در صفحه‌کلید تایپ کند؛
  • خط ۵: سرور منتظر وارد شدن یک فرمان توسط کاربر از طریق صفحه‌کلید است؛

در پنجرهٔ دستور دیگر، کلاینت TCP را راه‌اندازی می‌کنیم:


(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) :
  • خط ۱: اکنون در پوشهٔ utilities هستیم؛
  • در خط ۱، کلاینت TCP را راه‌اندازی می‌کنیم: به آن دستور می‌دهیم تا به پورت ۱۰۰ روی ماشین محلی (ماشینی که کد [RawTcpClient] روی آن در حال اجرا است) متصل شود؛
  • خط ۲: کلاینت با موفقیت به سرور متصل شده است. ما جزئیات کلاینت را مشخص می‌کنیم: این کلاینت روی ماشین [DESKTOP-30FF5FB] (در این مثال، ماشین محلی) قرار دارد و از پورت [51173] برای ارتباط با سرور استفاده می‌کند:
  • خط ۳: کلاینت در انتظار دستوری است که توسط کاربر از طریق صفحه‌کلید وارد می‌شود؛

بیایید به پنجرهٔ سرور بازگردیم. محتوای آن تغییر کرده است:


(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...
  • خط ۵: یک کلاینت شناسایی شده است. سرور شماره ۱ را به آن اختصاص داده است. سرور کلاینت راه دور را به درستی شناسایی کرده است (ماشین و پورت)؛
  • خط ۶: سرور اکنون منتظر یک کلاینت جدید است؛

بیایید به پنجرهٔ کلاینت بازگردیم و یک دستور به سرور ارسال کنیم:


(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
  • خط ۴، فرمان ارسال‌شده به سرور؛

بیایید به پنجرهٔ سرور بازگردیم. محتوای آن تغییر کرده است:


(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]
  • خط ۷، در کروشه‌ها، پیامی که توسط سرور دریافت شده است؛

بیایید پاسخی برای مشتری ارسال کنیم:


(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 :
  • خط ۸، پاسخی که به کلاینت ۱ ارسال می‌شود. فقط متن داخل کروشه‌ها ارسال می‌شود، نه خود کروشه‌ها؛

بیایید به پنجرهٔ کلاینت بازگردیم:


(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]
  • خط ۵، پاسخی که توسط کلاینت دریافت شده است. متن دریافتی همان متنی است که بین کروشه‌ها قرار دارد؛

بیایید به پنجرهٔ سرور بازگردیم تا چند دستور دیگر را ببینیم:


(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
  • خط ۹، ما لیست کلاینت‌ها را درخواست می‌کنیم؛
  • خط ۱۰، پاسخ؛
  • خط ۱۱، ما اتصال با مشتری شماره ۱ را قطع می‌کنیم؛
  • خط ۱۲، تأیید سرور؛
  • خط ۱۳، سرور را خاموش می‌کنیم؛
  • خط ۱۴، تأیید سرور؛

بیایید به پنجرهٔ کلاینت بازگردیم:


(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...
  • خط ۶، کلاینت پایان سرویس را تشخیص داده است؛

دو فایل لاگ ایجاد شده‌اند، یکی برای سرور و دیگری برای کلاینت:

Image

  • در [1]، لاگ‌های سرور: نام فایل، نام کلاینت در قالب [machine-port] است. این امکان را می‌دهد که برای کلاینت‌های مختلف، فایل‌های لاگ متفاوتی داشته باشیم؛
  • در [2]، لاگ‌های کلاینت: نام فایل نام سرور است در قالب [machine-port];

لاگ‌های سرور به شرح زیر است:


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

لاگ‌های کلاینت به شرح زیر است:


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

21.3. نام یا آدرس IP یک ماشین در اینترنت را به دست آور

Image

ماشین‌ها در اینترنت با یک آدرس (IP, IPv4 یا IPv6) و، معمولاً، با یک نام شناسایی می‌شوند. با این حال، در نهایت تنها آدرس IP توسط پروتکل‌های ارتباطی اینترنت استفاده می‌شود. بنابراین لازم است آدرس IP یک ماشین را که با نامش شناسایی شده است، بدانیم.

اسکریپت [ip-01.py] به شرح زیر است:


# واردات‌ها
import socket


# ------------------------------------------------
def get_ip_and_name(nom_machine: str):
    #nom_machine: نام ماشینی که آدرس آن مورد نیاز است IP
    try:
        #nom_machine-->آدرس IP
        ip = socket.gethostbyname(nom_machine)
        print(f"ip[{nom_machine}]={ip}")
    except socket.error as erreur:
        #یک خطا نمایش داده می‌شود
        print(f"ip[{nom_machine}]={erreur}")
        return

    try:
        #آدرس IP --> nom_machine
        names = socket.gethostbyaddr(ip)
        print(f"names[{ip}]={names}")
    except socket.error as erreur:
        # یک خطا نمایش داده می‌شود
        print(f"names[{ip}]={erreur}")
        return


# ---------------------------------------- اصلی

# ماشین‌های اینترنتی
hosts = ["istia.univ-angers.fr", "www.univ-angers.fr", "sergetahe.com", "localhost", "xx"]

#به IP ماشین‌های HOTES می‌پردازد
for host in hosts:
    print("-------------------------------------")
    get_ip_and_name(host)
#پایان
print("Terminé...")

توضیحات

  • خط ۲: ماژول [socket] توابع مورد نیاز برای مدیریت سوکت‌های اینترنت را فراهم می‌کند. [socket] به یک پریز برق یا یک سوکت شبکه اشاره دارد؛
  • خط ۶: تابع [get_ip_and_name] به شما امکان می‌دهد موارد زیر را از نام اینترنتی یک ماشین به‌دست آورید:
    • آدرس ماشین (IP);
    • نام ماشین که از آدرس قبلی IP استخراج شده است؛
  • خط ۱۰: تابع [socket.gethostbyname] آدرس IP یک ماشین را از یکی از نام‌های آن بازیابی می‌کند (یک ماشین اینترنتی ممکن است یک نام اصلی و نام‌های مستعار داشته باشد)؛
  • خط ۱۲: توابع ساکت به محض وقوع خطا، استثنای [socket.error] را پرتاب می‌کنند؛
  • خط ۱۹: تابع [socket.gethostbyaddr] نام یک ماشین را از آدرس آن IP بازیابی می‌کند. خواهیم دید که ممکن است نام متفاوتی از آنچه در خط ۶ ارسال شده است بازگردانده شود؛
  • خط ۳۰: فهرستی از نام‌های ماشین‌ها. نام آخر نادرست است. نام [localhost] به ماشینی اشاره دارد که روی آن کار می‌کنید و اسکریپت را اجرا می‌کند؛
  • خطوط ۳۳–۳۵: مقادیر IP برای این ماشین‌ها نمایش داده می‌شوند؛

نتایج:


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 (پروتکل انتقال HyperText)

21.4.1. مثال ۱

Image

وقتی یک مرورگر یک URL را نمایش می‌دهد، به‌عنوان کلاینت یک وب‌سرور عمل می‌کند، یا به عبارت دیگر، یک سرور HTTP. این مرورگر ابتکار عمل را در دست می‌گیرد و با ارسال تعدادی دستور به سرور شروع می‌کند. برای این مثال اول:

  • سرور ابزار [RawTcpServer] خواهد بود؛
  • کلاینت یک مرورگر وب خواهد بود؛

ابتدا، سرور را روی پورت ۱۰۰ راه‌اندازی می‌کنیم:


(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 :

سپس، با استفاده از یک مرورگر وب، ما URL و [http://localhost:100] را درخواست می‌کنیم؛ به عبارت دیگر، ما مشخص می‌کنیم که سرور HTTP که در حال پرس‌وجو است، روی پورت ۱۰۰ ماشین محلی در حال اجرا است:

Image

بیایید به پنجره سرور بازگردیم:


(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...
  • خط ۵، کلاینتی که متصل شد؛
  • خطوط ۹ تا ۲۲: سری خطوط متنی که ارسال کرده است:
    • خط ۹: این خط در قالب [GET URL HTTP/1.1] است. این خط URL را درخواست می‌کند و به سرور دستور می‌دهد که از پروتکل HTTP نسخه ۱.۱ استفاده کند؛
    • خط ۱۰: این خط دارای فرمت [Host: serveur:port] است. حالت دستور [Host] بی‌ربط است. باید توجه داشت که کلاینت در حال پرس‌وجو از یک سرور محلی است که روی پورت ۱۰۰ در حال اجرا است؛
    • خط ۱۴: دستور [User-Agent] مشتری را شناسایی می‌کند؛
    • خط ۱۵: دستور [Accept] مشخص می‌کند که کدام نوع اسناد توسط کلاینت پذیرفته می‌شوند؛
    • خط ۲۱: فرمان [Accept-Language] زبان ارائه‌ی اسناد درخواستی را مشخص می‌کند، در صورتی که به زبان‌های متعددی موجود باشند؛
    • خط ۱۱: فرمان [Connection] حالت اتصال مورد نظر را مشخص می‌کند: [keep-alive] نشان می‌دهد که اتصال باید تا تکمیل تبادل داده‌ها حفظ شود؛
    • خط ۲۲: کلاینت دستورات خود را با یک خط خالی پایان می‌دهد؛

ما با خاموش کردن سرور، اتصال را قطع می‌کنیم:


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

21.4.2. مثال ۲

اکنون که دستورات ارسالی توسط یک مرورگر برای درخواست یک URL را می‌دانیم، این URL را با استفاده از کلاینت TCP [RawTcpClient] خود درخواست خواهیم کرد. سرور آپاچی در Laragon (بخش |نصب Laragon|) وب‌سرور ما خواهد بود.

بیایید لارگون و سپس وب‌سرور آپاچی را راه‌اندازی کنیم:

Image

Image

اکنون، با استفاده از یک مرورگر وب، صفحات URL و [http://localhost:80] را درخواست کنیم. در اینجا، ما فقط سرور [localhost:80] را مشخص می‌کنیم و هیچ سند URL را مشخص نمی‌کنیم. در این حالت، URL / درخواست می‌شود، یعنی ریشه وب‌سرور:

Image

  • به [1]، URL درخواستی. ما در ابتدا [http://localhost:80] را تایپ کردیم و مرورگر (در این مورد فایرفاکس) آن را به [localhost] تبدیل کرد، زیرا وقتی پروتکلی مشخص نمی‌شود، پروتکل [http] به‌طور ضمنی در نظر گرفته می‌شود، و وقتی پورت مشخص نمی‌شود، پورت [80] به‌طور ضمنی در نظر گرفته می‌شود؛
  • به [2]، صفحهٔ ریشهٔ / وب‌سرور مورد پرسش؛

اکنون، بیایید متن دریافت‌شده توسط مرورگر را مشاهده کنیم:

Image

  • روی صفحه دریافت‌شده کلیک راست کرده و گزینه [2] را انتخاب کنید. کد منبع زیر نمایش داده می‌شود:

<!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>

اکنون با استفاده از کلاینت خود TCP، URL و [http://localhost:80] را درخواست کنیم:


(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) :
  • در خط ۱، ما به پورت ۸۰ سرور localhost متصل می‌شویم. این همان پورت است که سرور وب Laragon در آن اجرا می‌شود؛

اکنون دستورات کشف‌شده در پاراگراف قبلی را وارد می‌کنیم:


(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...
  • خط ۴، دستور [GET]. ما درخواست دایرکتوری ریشه / وب‌سرور را می‌دهیم؛
  • خط ۵، فرمان [Host]؛
  • این‌ها تنها دو دستور ضروری هستند. برای سایر دستورات، سرور وب از مقادیر پیش‌فرض استفاده خواهد کرد؛
  • خط ۶، خط خالی که باید دستورات کلاینت را خاتمه دهد؛
  • پس از خط ۶، پاسخ وب‌سرور می‌آید؛
  • خطوط ۷–۱۲: سربرگ‌های HTTP پاسخ سرور؛
  • خط ۱۳: خط خالی که پایان هدرهای HTTP را نشان می‌دهد؛
  • خطوط 14–82: سند HTML درخواست‌شده در خط 4؛

ما در حال بارگذاری فایل لاگ [localhost-80.txt] هستیم:

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>]
  • خطوط ۱۱–۷۹: سند HTML دریافت شده است. در مثال قبلی، فایرفاکس همان سند را دریافت کرده بود؛

اکنون ما اصول اولیه را برای برنامه‌نویسی یک کلاینت TCP داریم که یک URL را درخواست خواهد کرد.

21.4.3. مثال ۳

Image

اسکریپت [http/01/main.py] یک کلاینت HTTP است که توسط فایل [config.py] پیکربندی شده است. محتویات این فایل به شرح زیر است:


def configure():
    # URLs برای پرس‌وجو
    urls = [
        # site: نام سایتی که باید به آن متصل شد
        # پورت: پورت سرویس وب
        # GET: URL درخواست شده
        # سربرگ‌ها: HTTP سربرگ‌هایی که باید در درخواست ارسال شوند
        # endOfLine: کاراکتر پایان خط در سربرگ‌های HTTP ارسال شده
        # رمزگذاری: رمزگذاری پاسخ سرور
        # timeout: حداکثر زمان انتظار برای پاسخ سرور
        {
            "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
        }
    ]
    #پیکربندی بازگردانده می‌شود
    return {
        "urls": urls
    }
  • محتویات فایل فهرستی از ورودی‌های URL است که هر کدام یک فرهنگ لغت هستند. این فرهنگ لغت نحوه اتصال به سایتی را که با کلید [site] مشخص شده است، تعیین می‌کند؛
  • خطوط ۴–۱۰: معنای کلیدها در هر فرهنگ لغت؛

اسکریپت [http/01/main.py] به شرح زیر است:


#وارد می‌کند
import codecs
import socket


# -----------------------------------------------------------------------
def get_url(url: dict, suivi: bool = True):
    # URL را از سایت ["GET"] می‌خواند و آن را در فایل [site].html ذخیره می‌کند
    # گفتگوی کلاینت/سرور مطابق پروتکل HTTP مشخص‌شده در فرهنگ لغت [url] انجام می‌شود
    # اجازه داده می‌شود استثناها propagate شوند

    sock = None
    html = None
    try:
        # اتصال به [site] روی پورت ۸۰ با زمان‌بندی پایان اتصال
        site = url['site']
        sock = socket.create_connection((site, int(url['port'])), float(url['timeout']))

        # ارتباط نشان‌دهنده جریان ارتباط دوطرفه است
        # بین کلاینت (این برنامه) و وب‌سروری که با آن تماس گرفته می‌شود
        #این کانال برای تبادل دستورات و اطلاعات استفاده می‌شود
        #پروتکل ارتباطی HTTP است

        # ایجاد فایل site.html – کاراکترهای مشکل‌ساز با نام فایل جایگزین می‌شوند
        site2 = site.replace("/", "_")
        site2 = site2.replace(".", "_")
        html_filename = f'{site2}.html'
        html = codecs.open(f"output/{html_filename}", "w", "utf-8")

        # کلاینت دیالوگ HTTP را با سرور آغاز خواهد کرد
        if suivi:
            print(f"Client : début de la communication avec le serveur [{site}]")

        #بسته به سرور، خطوط کلاینت باید با \n یا \r\n پایان یابند
        end_of_line = url["endOfLine"]
        # کلاینت فرمان GET را برای درخواست پیکربندی URL ارسال می‌کند ["GET"]
        # سینتکس GET URL HTTP/1.1
        commande = f"GET {url['GET']} HTTP/1.1{end_of_line}"
        # ردیابی؟
        if suivi:
            print(f"--> {commande}", end='')
        # دستور به سرور ارسال می‌شود
        sock.send(bytearray(commande, 'utf-8'))
        #ارسال سربرگ‌ها HTTP
        for verb, value in url['headers'].items():
            # ساخت دستورالعمل برای ارسال
            commande = f"{verb}: {value}{end_of_line}"
            #پیگیری؟
            if suivi:
                print(f"--> {commande}", end='')
            # دستور به سرور ارسال می‌شود
            sock.send(bytearray(commande, 'utf-8'))
        # سربرگ HTTP [Connection: close] ارسال می‌شود تا از سرور وب بپرسد
        # برای بستن اتصال پس از ارسال سند درخواستی
        sock.send(bytearray(f"Connection: close{end_of_line}", 'utf-8'))
        # سربرگ‌های پروتکل HTTP باید با یک خط خالی پایان یابند
        sock.send(bytearray(end_of_line, 'utf-8'))
        #
        # سرور اکنون از طریق کانال ساکت پاسخ خواهد داد. آن همه را ارسال خواهد کرد
        #داده‌های آن را می‌خواند و سپس کانال را می‌بندد. بنابراین کلاینت همه چیز را که از طریق sock می‌رسد می‌خواند
        #تا زمانی که کانال بسته شود
        #
        #ابتدا سرآیندهای HTTP ارسال‌شده توسط سرور خوانده می‌شوند
        # این‌ها نیز با یک خط خالی پایان می‌یابند
        if suivi:
            print(f"Réponse du serveur [{site}]")

        # ساکت را طوری می‌خواند که گویی یک فایل متنی است
        encoding = f"{url['encoding']}" if url['encoding'] else None
        if encoding:
            file = sock.makefile(encoding=encoding)
        else:
            file = sock.makefile()
        # ما این فایل را خط به خط پردازش می‌کنیم
        fini = False
        while not fini:
            #خواندن خط فعلی
            ligne = file.readline().strip()
            #آیا خط خالی نیست؟
            if ligne:
                if suivi:
                    # سربرگ HTTP را نمایش دهید
                    print(f"<-- {ligne}")
            else:
                # آن خط خالی بود – سربرگ‌ها HTTP پایان یافته‌اند
                fini = True
        #در حال خواندن سند HTML است که پس از خط خالی می‌آید
        #خواندن خط جاری
        ligne = file.readline()
        while ligne:
            # در فایل گزارش ثبت شد
            html.write(str(ligne))
            # خط بعدی
            ligne = file.readline()
            #حلقه زمانی که سرور اتصال را قطع می‌کند پایان می‌یابد
    finally:
        # کلاینت اتصال را قطع می‌کند
        if sock:
            sock.close()
        # بستن فایل HTML
        if html:
            html.close()


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

# راه‌اندازی برنامه
import config
config = config.configure()

#بازیابی URL از فایل پیکربندی
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        #خواندن URL از سایت [site]
        get_url(url)
    except BaseException as erreur:
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
#پایان
print("Terminé...")

توضیحات کد:

  • خطوط ۱۰۸–۱۰۹: دیکشنری [config] از ماژول [config.py] بازیابی می‌شود؛
  • خطوط 111–122: این فرهنگ لغت استفاده می‌شود؛
  • خطوط ۱۱۸ و ۷: تابع [get_url(url)] یک سند را از وب‌سایت url[site] بازیابی کرده و آن را در فایل متنی url[site].HTML ذخیره می‌کند. به‌طور پیش‌فرض، تبادل‌های کلاینت/سرور در کنسول ثبت می‌شوند (tracking=True);
  • همه چیز در یک [try / finally] (خطوط 14–96) مدیریت می‌شود. هیچ عبارت [except] وجود ندارد. استثناها به کد فراخوانی منتقل می‌شوند، که آنها را دریافت و نمایش می‌دهد (خطوط 119–120);
  • خطوط 16–17: باز کردن یک اتصال به سرور وب. تابع [socket.create_connection] سه پارامتر می‌گیرد:
    • [param1]: نام میزبان اینترنتی است که می‌خواهیم به آن دسترسی پیدا کنیم؛
    • [param2]: شماره پورت سرویسی است که می‌خواهید به آن متصل شوید؛
    • [param3]: [socket.create_connection] یک ساکت (socket) را بازمی‌گرداند، و [param3]، در صورت وجود، تایم‌اوت (timeout) برای ساکت ایجاد شده را مشخص می‌کند. تایم‌اوت، حداکثر مدتی است که ساکت در انتظار پاسخ از ماشین راه دور باقی می‌ماند؛
  • خطوط ۲۷–۲۸: ایجاد فایل [site.html] که سند دریافتی HTML در آن ذخیره خواهد شد؛
  • خطوط ۳۴–۴۳: اولین فرمان کلاینت باید فرمان [GET URL HTTP/1.1] باشد؛
  • خط ۴۳: تابع [sock.send] به کلاینت اجازه می‌دهد داده‌ها را به سرور ارسال کند. در اینجا، خط متنی که ارسال شده است معنای زیر را دارد: «من می‌خواهم (GET) صفحه [URL] را از وب‌سایتی که به آن متصل هستم دریافت کنم.» من از پروتکل HTTP، نسخه ۱.۱ استفاده می‌کنم؛
  • خط ۴۳: دستور [sock.send(bytearray(commande, 'utf-8'))] یک آرایه بایت ارسال می‌کند. این آرایه با تبدیل رشته [commande] به دنباله‌ای از بایت‌های رمزگذاری‌شده در UTF-8 به دست می‌آید؛
  • خطوط ۴۴–۵۲: خطوط باقی‌مانده پروتکل HTTP [Host, User-Agent, Accept, Accept-Language…] ارسال می‌شوند. ترتیب آن‌ها اهمیتی ندارد؛
  • خطوط ۵۳–۵۵: هدر HTTP [Connection: close] ارسال می‌شود تا به سرور دستور دهد که پس از ارسال سند درخواستی، اتصال را ببندد. به طور پیش‌فرض، این کار انجام نمی‌شود. بنابراین لازم است صراحتاً از آن خواسته شود این کار را انجام دهد. مزیت آن این است که این قطع ارتباط در سمت کلاینت تشخیص داده می‌شود و اینگونه کلاینت متوجه می‌شود که کل سند درخواستی را دریافت کرده است؛
  • خطوط ۵۶–۵۷: یک خط خالی برای سرور ارسال می‌شود تا نشان دهد که کلاینت ارسال هدرهای HTTP خود را به پایان رسانده و اکنون در انتظار سند درخواستی است؛
  • خطوط ۶۸–۸۶: سرور ابتدا مجموعه‌ای از سربرگ‌های HTTP را ارسال می‌کند که جزئیات مختلفی در مورد سند درخواستی ارائه می‌دهند. این سربرگ‌ها با یک خط خالی پایان می‌یابند؛
  • خطوط ۶۹–۷۳: برای خواندن پاسخ سرور به صورت خط به خط، از متد [sock.makefile(encoding=encoding)] استفاده می‌شود. پارامتر اختیاری [encoding] رمزگذاری متن مورد انتظار را مشخص می‌کند. پس از این عملیات، جریان خطوط ارسال‌شده توسط سرور را می‌توان به عنوان یک فایل متنی استاندارد خواند؛
  • خط ۷۸: یک خط ارسال‌شده توسط سرور با استفاده از متد [readline] خوانده می‌شود. فاصله‌های خالی ابتدایی و انتهایی (شامل کاراکترهای پایان خط) از خط حذف می‌شوند؛
  • خطوط ۸۱–۸۳: اگر خط خالی نباشد و ردیابی درخواست شده باشد، خط دریافتی روی کنسول نمایش داده می‌شود؛
  • خطوط ۸۴–۸۶: اگر خط خالی علامت‌گذاری پایان هدرهای HTTP ارسال‌شده توسط سرور دریافت شده باشد، حلقه در خط ۷۶ خاتمه می‌یابد؛
  • خطوط ۹۰–۹۵: خطوط متنی پاسخ سرور را می‌توان خط به خط با استفاده از یک حلقه while خواند و در فایل متنی [html] ذخیره کرد. هنگامی که وب‌سرور کل صفحه درخواستی را ارسال کرد، اتصال خود را با کلاینت قطع می‌کند. در سمت کلاینت، این وضعیت به‌عنوان پایان فایل تشخیص داده می‌شود و حلقه در خطوط ۹۰–۹۵ متوقف می‌شود؛
  • خطوط ۹۶–۱۰۲: صرف‌نظر از اینکه خطایی رخ داده باشد یا خیر، تمام منابع مورد استفاده توسط کد آزاد می‌شوند؛

نتایج:

کنسول لاگ‌های زیر را نمایش می‌دهد:


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/programming-tutorials
<-- 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/programming-tutorials/
<-- Set-Cookie: SERVERID68971=2620178|XwH/h|XwH/h; path=/
<-- X-IPLB-Instance: 17095
Terminé...

Process finished with exit code 0

توضیحات

  • خط ۱۲: URL [http://localhost/] یافت شد (کد ۲۰۰);
  • خط ۲۹: URL [http://sergetahe.com/] یافت نشد (کد ۳۰۲). کد ۳۰۲ به این معنی است که صفحهٔ درخواست‌شده به URL تغییر یافته است. URL جدید توسط هدر HTTP [Location] در خط ۳۶ نشان داده شده است؛
  • خط ۴۹: درخواست ارسال‌شده به سرور [http://tahe.developpez.com] نامعتبر است (کد وضعیت ۴۰۰);
  • خط ۶۵: URL [http://www.sergetahe.com/] یافت نشد (کد وضعیت ۳۰۱). کد وضعیت ۳۰۱ به این معنی است که صفحهٔ درخواستی به‌طور دائم URL خود را تغییر داده است. سرور جدید URL توسط هدر HTTP [Location] در خط ۷۱ نشان داده شده است؛

به‌طور کلی، کدهای 3xx، 4xx و 5xx از یک سرور HTTP کدهای خطا هستند.

اجرا فایل‌های زیر را تولید کرد:

Image

فایل دریافتی [output/localhost.HTML] به شرح زیر است:


<!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>

ما در واقع همان سند را که با مرورگر فایرفاکس دریافت کردیم، به دست آوردیم.

سند دریافتی [output/sergetahe_com.html] به شرح زیر است:

Image

اکثر سرورهای HTTP پاسخ‌های خود به درخواست‌ها را به صورت تکه‌ها (chunks) ارسال می‌کنند. هر تکه ارسالی با یک خط که تعداد بایت‌های تکه بعدی را نشان می‌دهد، پیش‌روی می‌کند. این امر به کلاینت اجازه می‌دهد تا آن تعداد دقیق بایت را برای بازیابی تکه بخواند. در اینجا، عدد ۰ نشان می‌دهد که تکه بعدی صفر بایت طول دارد. شایان ذکر است که سرور اعلام کرده بود سند [http://sergetahe.com/] از URL تغییر کرده است. بنابراین سند جدیدی ارسال نکرد.

سند [output/tahe_developpez_com.html] به شرح زیر است:


<!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>
  • خطوط ۱–۱۲: سرور سند HTML را ارسال کرد، با وجود اینکه درخواست نادرست بود (خط ۴۹ نتایج). سند HTML به سرور اجازه می‌دهد علت خطا را مشخص کند. این موضوع در خطوط ۶ و ۷ نشان داده شده است:
    • خط ۷: کلاینت ما از پروتکل HTTP استفاده کرد؛
    • خط ۸: سرور از پروتکل HTTPS (S=secure) استفاده می‌کند و پروتکل HTTP را نمی‌پذیرد؛

سند [output/www_sergetahe_com.html] به شرح زیر است:


<!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>

در اینجا نیز خطایی رخ داده است (خط ۳). با این حال، سرور تضمین می‌کند که یک سند HTML برای تشریح خطا ارسال شود (خطوط ۱–۷).

21.4.4. مثال ۴

مثال‌های قبلی به ما نشان دادند که کلاینت ما HTTP ناکافی بود. اکنون ابزاری به نام [curl] را معرفی می‌کنیم که امکان بازیابی اسناد وب را در حالی که با مسائل ذکرشده (پروتکل HTTPS، ارسال اسناد به‌صورت تکه‌ها، تغییر مسیرها و غیره) برخورد می‌کند، فراهم می‌آورد. ابزار [curl] با Laragon نصب شده است:

Image

بیایید یک ترمینال PyCharm [1] را باز کنیم:

Image

  • در [1]، دسترسی به ترمینال‌های PyCharm؛
  • در [2-3]، ترمینال‌هایی که در حال حاضر فعال هستند؛
  • در [4]، پوشه‌ای که در حال حاضر در آن هستید. این برای مقاصد موارد بعدی بی‌ربط است؛

در ترمینال، دستور زیر را تایپ کنید:


(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] نتایج را تولید کرد، نشان می‌دهد که فرمان [curl] در دایرکتوری PATH ترمینال قرار دارد. در ویندوز، PATH مجموعه‌ای از پوشه‌هایی است که هنگام وارد کردن یک دستور اجرایی توسط کاربر، در این مورد [curl]، جستجو می‌شوند. مقدار PATH را می‌توان تعیین کرد:


(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;

خط ۲ فهرست پوشه‌های داخل PATH را با کاما جداشده فهرست می‌کند. هیچ پوشه‌ای مرتبط با Laragon در این فهرست دیده نمی‌شود. با بررسی بیشتر، متوجه می‌شویم که یک [curl] درون پوشه [c:\windows\system32] وجود دارد. این همان فایلی است که قبلاً پاسخ داده بود.

اگر می‌خواهید از ابزار [curl] که همراه Laragon ارائه شده است استفاده کنید، می‌توانید به شرح زیر عمل کنید:

Image

Image

  • به [2]، ترمینال Laragon؛
  • در [3]، این دکمه به شما امکان می‌دهد ترمینال‌های جدیدی ایجاد کنید که هر کدام در یک تب در پنجره بالا باز می‌شوند؛
  • در [4]، شما ترمینال PATH را از Laragon درخواست می‌کنید؛
  • نتیجه بسیار متفاوت از چیزی است که در یک ترمینال PyCharm به دست آمد. این PATH حاوی پوشه‌های متعددی است که در حین نصب Laragon ایجاد شده‌اند. پوشه‌ای که ابزار [curl] را در خود دارد یکی از آن‌هاست:

Image

سپس می‌توانید از هر ترمینالی که ترجیح می‌دهید استفاده کنید. فقط به خاطر داشته باشید که اگر می‌خواهید از ابزاری که توسط Laragon ارائه شده است استفاده کنید، ترمینال Laragon گزینه ترجیحی است.

دستور [curl --help] تمام گزینه‌های پیکربندی [curl] را نمایش می‌دهد. ده‌ها گزینه وجود دارد. ما از تعداد بسیار کمی از آن‌ها استفاده خواهیم کرد. برای درخواست یک URL، کافی است دستور [curl URL] را تایپ کنید. این دستور سند درخواستی را در کنسول نمایش خواهد داد. اگر همچنین می‌خواهید مبادلات HTTP بین کلاینت و سرور را مشاهده کنید، دستور [curl --verbose URL] را تایپ می‌کنید. در نهایت، برای ذخیره سند درخواستی HTML در یک فایل، دستور [curl --verbose --output fichier URL] را تایپ می‌کنید.

برای جلوگیری از شلوغ شدن سیستم فایل دستگاه خود، بیایید به مکان دیگری برویم (من در اینجا از ترمینال Laragon استفاده می‌کنم):


λ 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                                          
  • خط ۳: به پوشه [c:\temp] بروید. اگر این پوشه وجود ندارد، می‌توانید آن را ایجاد کنید یا پوشه دیگری را انتخاب کنید؛
  • در خط ۶، پوشه‌ای به نام [curl] ایجاد کنید؛
  • خط ۹: به آن بروید؛
  • خط ۱۲: محتویات آن را فهرست کنید. خالی است (خط ۲۰);

اطمینان حاصل کنید که سرور لاراون آپاچی در حال اجرا است و با استفاده از [curl با فرمان [curl –verbose –output localhost.html http://localhost/]، URL و [http://localhost/] را درخواست کنید. نتایج زیر به دست می‌آیند:


λ 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 بدون تغییر باقی ماند
  • خطوط ۱۰–۱۳: خطوط ارسال‌شده توسط [curl] به سرور [localhost]. پروتکل HTTP قابل تشخیص است؛
  • خطوط ۱۴–۲۰: خطوط ارسال‌شده در پاسخ توسط سرور؛
  • خط ۱۴: نشان می‌دهد که سند درخواستی با موفقیت دریافت شده است؛

فایل [localhost.html] حاوی سند درخواستی است. می‌توانید با باز کردن این فایل در یک ویرایشگر متن این موضوع را بررسی کنید.

اکنون بیایید URL و [https://tahe.developpez.com:443/] را درخواست کنیم. برای دریافت این URL، کلاینت HTTP باید قادر به ارتباط در HTTPS باشد. این مورد برای کلاینت [curl] صدق می‌کند.

خروجی کنسول به شرح زیر است:


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 بدون تغییر باقی ماند
  • خطوط ۱۰–۳۹: تبادل‌های کلاینت/سرور برای ایمن‌سازی اتصال: این بخش رمزگذاری خواهد شد؛
  • خطوط ۴۱–۴۴: سربرگ‌های HTTP که توسط کلاینت [curl] به سرور ارسال شده‌اند؛
  • خط ۵۲: سند درخواست‌شده یافت شد؛
  • خط ۵۷: سند به صورت تکه‌ها ارسال می‌شود؛

[curl] به‌درستی هم پروتکل امن HTTPS و هم این واقعیت را که سند به‌صورت تکه‌ها ارسال می‌شود، مدیریت می‌کند. سند ارسال‌شده را می‌توانید در اینجا در فایل [tahe.developpez.com.html] بیابید.

اکنون بیایید URL [http://sergetahe.com/cours-tutoriels-de-programmation] را درخواست کنیم. ما مشاهده کردیم که برای این URL، یک هدایت به URL و [http://sergetahe.com/cours-tutoriels-de-programmation/] (با یک / در انتها) وجود داشت.

خروجی کنسول سپس به شرح زیر است:


C:\Temp\curl
λ curl --verbose --output sergetahe.com.html --location http://sergetahe.com/programming-tutorials
  % 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/programming-courses-and-tutorials/
< 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 بدون تغییر باقی ماند
* Issue another request to this URL: 'http://sergetahe.com/programming-courses-and-tutorials/'
* Found bundle for host sergetahe.com: 0x14385f8 [can pipeline]
* Could pipeline, but not asked to!
* Re-using existing connection! (#0) با میزبان sergetahe.com
* 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/programming-courses-and-tutorials/
< 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 بدون تغییر باقی ماند
* Issue another request to this URL: 'https://sergetahe.com/programming-courses-and-tutorials/'
*   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/programming-courses-and-tutorials/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/programming-courses-and-tutorials/>; 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 برای میزبانی QZXW2HTMLCc2VyZ2V0YWhlLmNvbQZX بدون تغییر باقی ماند
  • خط ۲: گزینه [--location] برای نشان دادن اینکه می‌خواهیم ریدایرکت‌های ارسال‌شده توسط سرور را دنبال کنیم، استفاده می‌شود؛
  • خط ۱۳: سرور نشان می‌دهد که سند درخواست‌شده به URL تغییر کرده است؛
  • خط ۱۸: URL جدید برای سند درخواست‌شده مشخص می‌شود؛
  • خط ۳۱: [curl] یک درخواست جدید ارسال می‌کند، این بار به URL جدید؛
  • خط ۳۶: سرور دوباره پاسخ می‌دهد که URL تغییر کرده است؛
  • خط ۴۱: URL جدید دقیقاً مشابه همان چیزی است که هدایت شده بود، با یک تفاوت جزئی: پروتکل تغییر کرده است. این پروتکل به HTTPS (خط ۴۱) تبدیل شده است، در حالی که قبلاً http (خط ۳۱) بود؛
  • خط ۴۹: یک درخواست جدید به URL جدید ارسال می‌شود. این درخواست رمزگذاری شده است. در نتیجه، یک دیالوگ کامل برای برقراری امنیت، در خطوط ۵۳ تا ۹۱، انجام می‌شود؛
  • خط ۹۲: URL جدید درخواست می‌شود، این بار با استفاده از پروتکل HTTP/2؛
  • خط ۱۰0: سند یافت شد؛

سند درخواستی در فایل [sergetahe.com.html] یافت خواهد شد.


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. مثال ۵

پایتون ماژولی به نام [pyccurl] دارد که به شما امکان می‌دهد از قابلیت‌های ابزار [curl] در یک برنامه پایتون استفاده کنید. ما این ماژول را نصب می‌کنیم:

Image

ما قصد داریم یک اسکریپت جدید به نام [http/02/main.py] بنویسیم:

Image

فایل [http/02/config] به شرح زیر است:


def configure():
    # فهرست URL برای پرس‌وجو
    urls = [
        # site: سروری که باید به آن متصل شود
        # timeout: حداکثر زمان انتظار برای پاسخ از سرور
        # هدف: URL برای درخواست
        # کدگذاری: کدگذاری پاسخ سرور
        {
            "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"
        }
    ]
    #پیکربندی بازگردانده می‌شود
    return {
        ''urls': آدرس‌های URL
    }

این فایل شامل فهرستی از فرهنگ‌ها است که هر یک ساختار زیر را دارند:

  • site: نام یک سرور وب؛
  • encoding: نوع کدگذاری سند مورد انتظار؛
  • timeout: حداکثر زمان انتظار برای پاسخ از سرور، بیان‌شده به میلی‌ثانیه. اگر این زمان از حد عبور کند، کلاینت اتصال را قطع خواهد کرد؛
  • url: URL برای سند درخواست‌شده؛

کد اسکریپت [http/02/main.py] به شرح زیر است:


# واردات
import codecs
from io import BytesIO

import pycurl


# -----------------------------------------------------------------------
def get_url(url: dict, suivi=True):
    # urlURL را می‌خواند و آن را در فایل output/url['site'].html ذخیره می‌کند
    # اگر [suivi=True] باشد، خروجی کنسول تبادل کلاینت/سرور ثبت می‌شود
    #url[timeout] زمان‌بندی (timeout) تماس‌های کلاینت است؛
    #url [encoding] رمزگذاری سند درخواستی است

    #داده‌های پیکربندی بازیابی می‌شوند
    server = url['site']
    timeout = url['timeout']
    target = url['target']
    encoding = url['encoding']
    #ردیابی
    print(f"Client : début de la communication avec le serveur [{server}]")

    #اجازه می‌دهد استثناءها منتقل شوند
    html = None
    curl = None
    try:
        # ابتدای یک جلسه cURL
        curl = pycurl.Curl()
        #جریان باینری
        flux = BytesIO()
        # گزینه‌های curl
        options = {
            # URL
            curl.URL: target,
            # WRITEDATA: جایی که داده‌های دریافتی ذخیره خواهند شد
            curl.WRITEDATA: flux,
            # حالت تفصیلی
            curl.VERBOSE: suivi,
            # اتصال جدید – بدون کش
            curl.FRESH_CONNECT: True,
            # زمان‌بندی درخواست (به ثانیه)
            curl.TIMEOUT: timeout,
            curl.CONNECTTIMEOUT: timeout,
            #اعتبار گواهی را بررسی نکند SSL
            curl.SSL_VERIFYPEER: False,
            # دنبال کردن هدایت‌ها
            curl.FOLLOWLOCATION: True
        }
        #پیکربندی curl
        for option, value in options.items():
            curl.setopt(option, value)
        # اجرای درخواست CURL با این تنظیمات
        curl.perform()
        # ایجاد فایل server.html – جایگزینی کاراکترهای مشکل‌دار با نام فایل
        server2 = server.replace("/", "_")
        server2 = server2.replace(".", "_")
        html_filename = f'{server2}.html'
        html = codecs.open(f"output/{html_filename}", "w", encoding)
        #ذخیرهٔ سند دریافتی در فایل HTML
        html.write(flux.getvalue().decode(encoding))
    finally:
        # آزادسازی منابع
        if curl:
            curl.close()
        if html:
            html.close()


# -------------------main
# راه‌اندازی برنامه
import config
config = config.configure()

#بازیابی URL از فایل پیکربندی
for url in config['urls']:
    print("-------------------------")
    print(url['site'])
    print("-------------------------")
    try:
        #خواندن URL از سایت [site]
        get_url(url)
    # به جز BaseException به عنوان خطا:
    #     print(f"خطای زیر رخ داد: {error}")
    finally:
        pass
#پایان
print("Terminé...")

توضیحات

  • خط ۵: ماژول [pycurl] وارد می‌شود؛
  • خط ۳: کلاس [BytesIO] وارد می‌شود که به ما امکان می‌دهد داده‌های دریافتی از سرور را در یک جریان باینری ذخیره کنیم؛
  • خطوط ۷۰–۷۲: پیکربندی برنامه بازیابی می‌شود؛
  • خطوط ۷۵–۸۵: ما لیست ورودی‌های URL یافت‌شده در پیکربندی را به‌صورت حلقه بررسی می‌کنیم؛
  • خط ۸۱: برای هر URL، توابع [get_url] فراخوانی می‌شود که urlURL را با تایم‌اوت url['timeout'] دانلود می‌کند؛
  • خط ۹: تابع [get_url] پیکربندی URL را که قرار است پرس‌وجو شود، دریافت می‌کند؛
  • خطوط ۱۶–۱۹: پیکربندی URL در متغیرهای جداگانه بازیابی می‌شود؛
  • خطوط 26 و 61: تمام عملیات در داخل یک بلوک try/finally انجام می‌شوند. استثناها گرفته نمی‌شوند؛ بلکه به کد فراخوانی‌کننده منتقل می‌شوند که آن‌ها را می‌گیرد؛
  • خط ۲۸: یک جلسه [curl] آماده می‌شود. [pycurl.Curl()] یک منبع [curl] را بازمی‌گرداند که تراکنش را با یک سرور انجام خواهد داد؛
  • خط ۳۰: نمونه‌سازی جریان دودویی که داده‌های دریافتی را ذخیره خواهد کرد؛
  • خطوط ۳۲–۴۸: دیکشنری [options] اتصال [curl] را به سرور پیکربندی می‌کند. نقش‌های آن‌ها در توضیحات (کامنت‌ها) توضیح داده شده است؛
  • خطوط ۴۹–۵۱: گزینه‌های اتصال به منبع [curl] ارسال می‌شوند؛
  • خط ۵۳: یک اتصال به URL با استفاده از گزینه‌های تعریف‌شده درخواست می‌شود. به دلیل گزینه [curl.WRITEDATA: flux] (خط ۳۶)، تابع [curl.perform()] داده‌های دریافتی را در [flux] ذخیره خواهد کرد؛
  • خطوط 54–60: فایل HTML برای ذخیره سند دریافتی HTML ایجاد می‌شود؛
  • خط ۶۰: جریان بایت [flux.getvalue()] به صورت یک رشته کاراکتری در فایل HTML ذخیره خواهد شد. رمزگذاری این رشته در متد [decode(encoding)] مشخص شده است. بنابراین لازم است که از کدگذاری سند ارسال‌شده توسط سرور مطلع باشیم. اگر این کدگذاری نادرست باشد، رمزگشایی جریان باینری با شکست مواجه خواهد شد. این کدگذاری در فایل پیکربندی URL مشخص شده است (برای مثال در خط ۱۲). ما می‌توانستیم این اطلاعات را به‌صورت پویا، همان‌طور که سرور آن را در هدرهای HTTP خود ارسال می‌کند، مدیریت کنیم. این کار ارجح‌تر بود. برای ساده‌ نگه‌داشتن کد، ما این کار را انجام ندادیم. برای تعیین نوع رمزگذاری سند، به‌سادگی URL مورد نظر را با استفاده از یک مرورگر درخواست کنید و به هدرهای HTTP ارسال‌شده توسط مرورگر در حالت عیب‌یابی (F12) یا خود سند نگاه کنید، زیرا این هدرها نیز نوع رمزگذاری را مشخص می‌کنند:

Image

Image

  • خطوط ۶۱–۶۶: منابع تخصیص‌یافته آزاد می‌شوند؛

هنگامی که اسکریپت [main.py] اجرا می‌شود، خروجی کنسول زیر تولید می‌شود:


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/programming-tutorials
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 به میزبان sergetahe.com دست‌نخورده باقی ماند
* Issue another request to this URL: 'http://sergetahe.com/programming-courses-and-tutorials'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) با میزبان sergetahe.com
* 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/programming-courses-and-tutorials/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 به میزبان sergetahe.com بدون تغییر باقی ماند
* Issue another request to this URL: 'http://sergetahe.com/programming-courses-and-tutorials/'
* Found bundle for host sergetahe.com: 0x25eacafb5d0 [serially]
* Can not multiplex, even if we wanted to!
* Re-using existing connection! (#0) با میزبان sergetahe.com
* 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/programming-courses-and-tutorials/
< Set-Cookie: SERVERID68971=26218|XwLIo|XwLIo; path=/
< X-IPLB-Instance: 17102
< 
* Ignoring the response-body
* Connection #0 به میزبان sergetahe.com بدون تغییر باقی ماند
* Issue another request to this URL: 'https://sergetahe.com/programming-courses-and-tutorials/'
*   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/programming-courses-and-tutorials/wp-json/>; rel="https://api.w.org/"
< link: <https://sergetahe.com/programming-courses-and-tutorials/>; rel=shortlink
< vary: Accept-Encoding
< x-iplb-instance: 17080
< set-cookie: SERVERID68971=26218|XwLIp|XwLIp; path=/
< 
* Connection #1 به میزبان sergetahe.com بدون تغییر باقی ماند
-------------------------
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 بدون تغییر باقی ماند
-------------------------
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 بدون تغییر باقی ماند
* 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) با میزبان www.polytech-angers.fr
* 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 بدون تغییر باقی ماند
-------------------------
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 بدون تغییر باقی ماند
Terminé...

Process finished with exit code 0

توضیحات

  • به رنگ آبی، دستورات HTTP ارسال‌شده به سرور؛
  • به رنگ سبز، داده‌های دریافتی توسط کلاینت در پاسخ؛
  • همان مبادلات مشابه ابزار [curl] به دست می‌آیند؛
    • خط ۹: URL [http://sergetahe.com/] درخواست شده است؛
    • خط ۱۵: سرور پاسخ می‌دهد که صفحه جابجا شده است. خط ۲۱: URL جدید؛
    • خط ۳۲: URL [http://sergetahe.com/cours-tutoriels-de-programmation] درخواست شده است؛
    • خط ۳۸: سرور پاسخ می‌دهد که صفحه جابجا شده است. خط ۴۳: URL جدید؛
    • خط ۵۴: URL [http://sergetahe.com/cours-tutoriels-de-programmation/] درخواست شده است؛
    • خط ۶۰: سرور پاسخ می‌دهد که صفحه منتقل شده است. خط ۶۵: URL جدید. از پروتکل امن [HTTPS] استفاده می‌کند؛
    • خطوط ۷۱–۷۵: پروتکل امن با سرور برقرار می‌شود؛
    • خط ۷۶: URL [https://sergetahe.com/cours-tutoriels-de-programmation/] درخواست شده است؛
    • خط ۸۲: سند درخواست‌شده یافت شد؛

21.4.6. نتیجه‌گیری

در این بخش، پروتکل HTTP را بررسی کرده و یک اسکریپت [http/02/main.py] نوشته‌ایم که قادر به دانلود یک URL از وب است.

21.5. پروتکل SMTP (پروتکل ساده انتقال ایمیل)

21.5.1. مقدمه

Image

در این فصل:

  • [Serveur B] یک سرور محلی SMTP خواهد بود که ما آن را نصب خواهیم کرد؛
  • [Client A] یک کلاینت SMTP در اشکال مختلف خواهد بود:
    • کلاینت [RawTcpClient] برای بررسی پروتکل SMTP؛
    • یک اسکریپت پایتون که پروتکل SMTP کلاینت [RawTcpClient] را شبیه‌سازی می‌کند؛
    • یک اسکریپت پایتون که از ماژول [smtplib] برای ارسال انواع ایمیل‌ها استفاده می‌کند؛

21.5.2. ایجاد یک آدرس ایمیل [gmail]

برای انجام آزمایش‌های SMTP، به یک آدرس ایمیل نیاز داریم تا ایمیل‌ها را به آن ارسال کنیم. برای این کار، یک آدرس جیمیل به نام [https://www.google.com/intl/fr/gmail/about/] ایجاد خواهیم کرد:

Image

توجه: چند ایمیل به آدرسی که ایجاد کرده‌اید ارسال کنید. تنها زمانی به مرحله بعدی بروید که مطمئن شوید حسابی که ایجاد کرده‌اید قادر به دریافت ایمیل است.

21.5.3. نصب سرور SMTP

برای آزمایش‌های خود، سرور ایمیل [hMailServer] را نصب خواهیم کرد که هم یک سرور SMTP برای ارسال ایمیل است، یک سرور POP3 (پروتکل دفتر پستی) برای خواندن ایمیل‌های ذخیره‌شده روی سرور، و یک سرور IMAP (پروتکل دسترسی به پیام اینترنتی) که همچنین به شما امکان می‌دهد ایمیل‌های ذخیره‌شده روی سرور را بخوانید اما قابلیت‌های اضافی نیز ارائه می‌دهد. به‌ویژه، این پروتکل به شما امکان می‌دهد مدیریت ذخیره‌سازی ایمیل‌ها روی سرور را انجام دهید.

سرور ایمیل [hMailServer] در آدرس‌های URL و [https://www.hmailserver.com/] (مه ۲۰۱۹) در دسترس است.

Image

در طول نصب، از شما خواسته می‌شود تا جزئیات خاصی را ارائه دهید:

Image

  • در [1-2]، هم سرور ایمیل و هم ابزارهای مدیریت آن را انتخاب کنید؛
  • در طول نصب، از شما رمز عبور مدیر خواسته می‌شود: آن را یادداشت کنید، زیرا به آن نیاز خواهید داشت؛

[hMailServer] به‌عنوان یک سرویس ویندوز نصب می‌شود که هنگام راه‌اندازی کامپیوتر به‌طور خودکار شروع می‌شود. ترجیحاً راه‌اندازی دستی را انتخاب کنید:

  • در [3]، [services] را در کادر متنی نوار وضعیت تایپ کنید؛

Image

  • برای [4-8]، سرویس را روی حالت [manuel] (۶) تنظیم کرده، سپس آن را راه‌اندازی کنید (۷)؛

پس از راه‌اندازی، سرور [hMailServer] باید پیکربندی شود. این سرور با برنامهٔ مدیریتی [hMailServer Administrator] نصب شده است:

Image

  • در [2]، در کادر ورودی نوار وضعیت، [hmailserver] را تایپ کنید؛
  • در [3]، مدیر را راه‌اندازی کنید؛
  • در [4]، مدیر را به سرور [hMailServer] متصل کنید؛
  • در [5]، رمز عبوری را که هنگام نصب [hMailServer] تعیین کرده‌اید وارد کنید؛

اگر رمز عبور را فراموش کرده‌اید، طبق مراحل زیر عمل کنید:

  • سرور [hMailServer] را متوقف کنید؛
  • فایل [<hmailserver>/bin/hmailserver.ini] را باز کنید، که در آن <hmailserver> پوشه نصب سرور است:

Image

  • در [100]، کلمه عبور را از خط [AdministratorPassword] حذف کنید. این بدان معناست که مدیر دیگر کلمه عبوری نخواهد داشت. وقتی از شما خواسته شد، به سادگی [Entrée] را تایپ کنید؛

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

بیایید با پیکربندی سرور ادامه دهیم:

Image

  • در [1-2]، یک دامنه اضافه کنید (اگر قبلاً وجود ندارد)؛

Image

  • در [3]، می‌توانید برای آزمایش‌هایی که قرار است انجام دهیم تقریباً هر چیزی را وارد کنید. در عمل، لازم است نام یک دامنه موجود را وارد کنید؛

Image

ما قصد داریم یک حساب کاربری ایجاد کنیم:

  • روی [Accounts] (7) کلیک راست کرده و سپس (8) را برای افزودن کاربر جدید انتخاب کنید؛
  • در زبانه [General] (9)، کاربر [guest] (10) را با رمز عبور [guest] (11) ایجاد می‌کنیم. آنها آدرس ایمیل [guest@localhost] (10) را خواهند داشت؛
  • در [12]، کاربر [guest] فعال شده است؛

Image

  • در [13-14]، کاربر ایجاد می‌شود؛

Image

  • در [27]، پورت برای سرویس SMTP؛
  • در [28]، این سرویس به احراز هویت نیاز ندارد؛
  • در [30]، پیام خوش‌آمدگویی را که سرور SMTP برای مشتریان خود ارسال می‌کند، وارد کنید؛

Image

برای سرور POP3 نیز همین کار را انجام دهید:

Image

ما همین کار را برای سرور IMAP انجام می‌دهیم:

Image

ما دامنهٔ پیش‌فرض را برای سرور [hMailServer] مشخص می‌کنیم (ممکن است چندین دامنه باشد) :

Image

  • در [37]، مشخص کنید که دامنه پیش‌فرض برای سرور SMTP همان دامنه‌ای است که در [38] ایجاد کرده‌اید؛

پس از ذخیره این پیکربندی، می‌توانید آن را به شرح زیر آزمایش کنید. یک ترمینال PyCharm را در پوشه utilities باز کنید:

Image

سپس دستور زیر را وارد کنید:


(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]
  • خط ۱: ما به پورت ۲۵ روی ماشین [localhost] متصل می‌شویم. این پورت جایی است که یک سرور SMTP ناامن از سرور [hMailServer] در آن در حال اجرا است؛
  • خط ۴: ما پیغام خوش‌آمدگویی را دریافت می‌کنیم که در مرحله ۳۰ بالا پیکربندی کرده‌ایم؛

بنابراین سرور SMTP راه‌اندازی شده و در حال اجرا است. برای قطع اتصال با سرور SMTP روی پورت 25، دستور [quit] را تایپ کنید.

اکنون بیایید همین کار را با پورت 587 انجام دهیم که پورت پیش‌فرض برای سرویس رله ایمیل امن SMTP است:


(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]
  • خط ۴، پاسخ از سرور SMTP که روی پورت ۵۸۷ در حال اجرا است؛

اکنون بیایید همین کار را با پورت 110 انجام دهیم که پورت پیش‌فرض سرویس رله ایمیل POP3 است:


(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]
  • خط ۴، ما پیام خوش‌آمدگویی را از سرور POP3 دریافت کردیم؛

اکنون بیایید همین کار را با پورت 143 انجام دهیم که پورت پیش‌فرض برای سرویس رله ایمیل IMAP است:


(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]
  • خط ۴، پیام خوش‌آمدگویی را از سرور IMAP دریافت کردیم؛

21.5.4. نصب یک خوانندهٔ ایمیل

برای خواندن ایمیلی که قرار است ارسال کنیم، به یک خوانندهٔ ایمیل نیاز داریم. برای کسانی که چنین برنامه‌ای ندارند، نحوهٔ نصب و پیکربندی خوانندهٔ ایمیل [Thunderbird] را به شما نشان خواهیم داد:

  • در [1]: [thunderbird] را دانلود کرده و سپس آن را نصب کنید؛

Image

  • سرور ایمیل [hMailServer] را در صورتی که در حال اجرا نیست، راه‌اندازی کنید؛
  • در [2-3]: پس از راه‌اندازی Thunderbird، یک حساب ایمیل برای کاربر [guest@localhost] روی سرور ایمیل [hMailServer] ایجاد خواهیم کرد؛

Image

Image

Image

  • روی [7-11]: سرور POP3، که به ما امکان می‌دهد ایمیل‌ها را از سرور ایمیل [hMailServer] بخوانیم، در [localhost] قرار دارد و روی پورت 110 کار می‌کند؛
  • در [12-16]: سرور SMTP، که به ما امکان ارسال ایمیل به نمایندگی از کاربران سرور ایمیل [hMailServer] را می‌دهد، در [localhost] قرار دارد و روی پورت 25 کار می‌کند؛
  • [18]: ما می‌توانیم آزمایش کنیم که آیا این پیکربندی معتبر است؛

Image

Image

  • در [26]: از آنجایی که در SSL رمزنگاری وجود ندارد، Thunderbird به ما هشدار می‌دهد که پیکربندی ما خطراتی را به همراه دارد؛
  • به [28]: حساب کاربری ایجاد شده است؛

برای آزمایش حسابی که ایجاد کرده‌ایم، از Thunderbird برای:

  • ارسال ایمیل به کاربر [guest@localhost.com] (پروتکل SMTP);
  • ایمیل دریافتی توسط این کاربر را بخوانید (پروتکل POP3);

Image

  • در [3]: فرستنده؛
  • در [4]: گیرنده؛
  • در [5]: موضوع ایمیل؛
  • در [6]: محتوای ایمیل؛
  • در [7]: ارسال ایمیل؛

Image

  • در [8-9]: ایمیل کاربر از [guest@localhost] بازیابی می‌شود؛
  • در [10-15]: پیام دریافتی؛

ما همچنین یک ایمیل به کاربر [pymailparlexemple@gmail.com] ارسال خواهیم کرد. بیایید برای او در Thunderbird یک حساب کاربری ایجاد کنیم تا بتواند ایمیلی را که دریافت خواهد کرد بخواند:

Image

Image

  • در [4]: هر چیزی را که دوست دارید وارد کنید؛
  • برای [5]: آدرس [pymailparlexemple@gmail.com] است؛
  • در [6]: رمز عبوری را که هنگام ایجاد این کاربر برای او تعیین کرده‌اید وارد کنید؛
  • در [7]: این پیکربندی را تأیید کنید؛

Image

  • در [8]: Thunderbird اطلاعات زیر را از پایگاه داده خود بازیابی کرده است؛
  • در [9]: پروتکل بازیابی ایمیل دیگر POP3 نیست بلکه IMAP است. تفاوت اصلی بین این دو در این است که [POP3] ایمیل خوانده شده را در دستگاه محلی که کلاینت ایمیل روی آن در حال اجراست دانلود کرده و آن را از سرور راه دور حذف می‌کند، در حالی که [IMAP] ایمیل را روی سرور راه دور نگه می‌دارد؛
  • در [10]: شناسه سرور SMTP;
  • در [13]: برای کسب اطلاعات بیشتر در مورد سرورها IMAP و SMTP، به پیکربندی دستی سوئیچ کنید؛

Image

  • به [14-17]: مشخصات سرور IMAP؛
  • به [18-21]: مشخصات سرور برای SMTP;
  • در [22]: پیکربندی تکمیل شده است؛

Image

  • در [23-24]: حساب جدید Thunderbird؛
  • در [26]: نوشتن یک پیام جدید؛

Image

  • در [27]: فرستنده [pymailparlexemple@gmail.com] است؛
  • در [28]: گیرنده [pymailparlexemple@gmail.com] است؛
  • در [29-30]: پیام؛
  • در [31]: برای ارسال آن؛

Image

  • در [32]: ایمیل‌ها از حساب‌های مختلف بازیابی می‌شوند؛

Image

  • در [33-36]: ایمیلی که توسط کاربر دریافت شده است [pymailparlexemple@gmail.com]

ما همچنین ایجاد می‌کنیم:

  • یک حساب جدید جیمیل [pymail2parlexemple@gmail.com]؛
  • یک حساب جدید Thunderbird، [pymail2parlexemple@gmail.com برای بازیابی پیام‌ها برای کاربر با همین نام:

Image

Image

اکنون ابزارهایی در اختیار داریم تا پروتکل‌های SMTP، POP3 و IMAP را بررسی کنیم. با پروتکل SMTP شروع می‌کنیم.

21.5.5. پروتکل SMTP

Image

ما پروتکل SMTP را با بررسی لاگ‌های سرور [hMailServer] بررسی خواهیم کرد. برای این کار، ما آن‌ها را با استفاده از [hmailServerAdministrator] outl فعال خواهیم کرد:

Image

Image

  • در [2]، لاگ‌ها فعال شده‌اند؛
  • در [3-5]: ما آن‌ها را برای پروتکل‌های SMTP، POP3 و IMAP فعال می‌کنیم؛
  • در [7]، شما درخواست مشاهده آن‌ها را می‌دهید؛
  • در [8]، فایل لاگ را در هر ویرایشگر متنی باز کنید؛

Image

در مثال زیر، کلاینت [Thunderbird] و سرور [hMailServer] خواهد بود. با استفاده از Thunderbird، اطمینان حاصل کنید که کاربر [guest@localhost.com] پیامی برای خود ارسال کند:

Image

لاگ‌ها به شرح زیر است:


"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"

خطوط بالا مکالمه‌ای را که بین کلاینت SMTP (کلاینت ایمیل Thunderbird) و سرور SMTP (hMailServer) رخ داده است، توصیف می‌کنند. خطوط [SENT] نشان می‌دهند که سرور SMTP چه چیزی را برای کلاینت خود ارسال کرده است. خطوط [RECEIVED] نشان می‌دهند که سرور SMTP چه چیزی را از کلاینت خود دریافت کرده است.

  • خط ۱: بلافاصله پس از اتصال کلاینت به سرور SMTP، سرور یک پیام خوش‌آمدگویی برای کلاینت خود ارسال می‌کند؛
  • خط ۲: کلاینت فرمان [EHLO] را برای شناسایی خود ارسال می‌کند. در اینجا، آدرس خود IP [127.0.0.1] را ارائه می‌دهد که به ماشین [localhost]، یعنی ماشینی که کلاینت SMTP را اجرا می‌کند، اشاره دارد؛
  • خط ۳: سرور مجموعه‌ای از پاسخ‌ها را ارسال می‌کند: [250]. [nl] نمایانگر [newline]، یعنی کاراکتر \n است. پاسخ‌ها به شکل [250-] هستند، به جز پاسخ آخر که به شکل [250 ] است. به این ترتیب است که کلاینت SMTP می‌داند که پاسخ سرور SMTP پایان یافته و می‌تواند یک دستور ارسال کند. سلسله دستورات [250] برای اطلاع‌رسانی به کلاینت SMTP در مورد مجموعه‌ای از دستوراتی که می‌توانست استفاده کند، در نظر گرفته شده بود؛
  • خط ۴: کلاینت SMTP فرمان [MAIL FROM : adresse_mail_expéditeur] را ارسال می‌کند که فرستنده پیام را شناسایی می‌کند؛
  • خط ۵: سرور SMTP با [250 OK] پاسخ می‌دهد که نشان می‌دهد فرمان را درک کرده است؛
  • خط ۶: کلاینت SMTP فرمان [RCPT TO : adresse_mail_destinataire] را برای مشخص کردن آدرس گیرنده ارسال می‌کند؛
  • خط ۷: بار دیگر، سرور SMTP نشان می‌دهد که فرمان را درک کرده است؛
  • خط ۸: سرور SMTP فرمان [DATA] را ارسال می‌کند. این بدان معناست که قصد دارد محتوای پیام را ارسال کند؛
  • خط ۹: سرور SMTP از طریق پاسخ [354 OK] نشان می‌دهد که آماده دریافت پیام است. متن [send .] نشان می‌دهد که کلاینت SMTP باید پیام خود را با یک خط حاوی تنها یک نقطه پایان دهد؛
  • آنچه در ادامه نمی‌بینیم این است که کلاینت SMTP پیام خود را ارسال می‌کند. لاگ‌ها این را نشان نمی‌دهند؛
  • خط ۱۰: کلاینت SMTP نقطه را ارسال کرد که پایان پیام را نشان می‌دهد. سرور SMTP پاسخ می‌دهد که پیام را صف‌بندی کرده است؛
  • کلاینت SMTP فرمان [QUIT] را برای نشان دادن اینکه قصد دارد اتصال را ببندد، برای آن ارسال می‌کند؛
  • خط ۱۲: سرور پاسخ می‌دهد؛

اکنون که با دیالوگ کلاینت/سرور پروتکل SMTP آشنا شدیم، بیایید آن را با کلاینت خود [RawTcpClient] بازتولید کنیم. ما از یک ترمینال PyCharm استفاده می‌کنیم:

Image

بیایید به یک مثال جدید نگاه کنیم:

  • کلاینت A کلاینت عمومی TCP ([RawTcpClient]) خواهد بود؛
  • سرور B، سرور ایمیل [hMailServer] خواهد بود؛
  • کلاینت A از سرور B درخواست می‌کند تا ایمیلی را که کاربر [guest@localhost.com] برای خود ارسال کرده است، تحویل دهد؛
  • ما بررسی خواهیم کرد که گیرنده واقعاً ایمیل ارسال‌شده را دریافت کرده است؛

ما کلاینت را به شرح زیر راه‌اندازی می‌کنیم:


(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]: ما به پورت ۲۵ روی ماشین محلی متصل می‌شویم، جایی که سرویس SMTP از [hMailServer] در حال اجرا است. آرگومان [--quit bye] نشان می‌دهد که کاربر با وارد کردن فرمان [bye] از برنامه خارج خواهد شد. بدون این آرگومان، فرمان برای پایان دادن به برنامه [quit] است. با این حال، [quit] همچنین یک فرمان در پروتکل SMTP است. بنابراین باید از این ابهام اجتناب کنیم؛
  • خط [2]: کلاینت با موفقیت متصل شده است؛
  • خط [3]: کلاینت در انتظار دستورات وارد شده از طریق صفحه‌کلید است؛
  • خط [4]: سرور پیام خوش‌آمدگویی خود را برای کلاینت ارسال می‌کند؛

گفت‌وگو را به شرح زیر ادامه می‌دهیم:


(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]، کلاینت فرمان [EHLO nom-de-la-machine-client] را ارسال می‌کند. سرور با مجموعه‌ای از پیام‌ها در قالب [250-xx] (6) پاسخ می‌دهد. کد [250] نشان می‌دهد که فرمان ارسال‌شده توسط کلاینت موفقیت‌آمیز بوده است؛
  • در [10]، کلاینت فرستنده پیام را مشخص می‌کند، در این مورد [guest@localhost.com]؛
  • در [11]، پاسخ سرور؛
  • در [12]، گیرنده پیام مشخص می‌شود، در این مورد کاربر [guest@localhost.com]؛
  • در [13]، پاسخ سرور؛
  • در [14]، فرمان [DATA] به سرور اطلاع می‌دهد که کلاینت در آستانه ارسال محتوای پیام است؛
  • در [15]، پاسخ سرور؛
  • در [16-22]، کلاینت باید فهرستی از خطوط متنی را ارسال کند که با خطی که تنها شامل یک نقطه است پایان می‌یابد. پیام می‌تواند شامل خطوط [Subject:, From:, To:] (۱۶–۱۸) باشد تا به ترتیب موضوع پیام، فرستنده و گیرنده را تعریف کند؛
  • در [19]، سربرگ‌های قبلی باید با یک خط خالی دنبال شود؛
  • در [20-21]، بدنه پیام؛
  • در [22]، خطی حاوی یک نقطه کامل، که نشان‌دهنده پایان پیام است؛
  • در [23]، به محض اینکه سرور خط حاوی یک نقطه کامل را دریافت کرد، پیام را در صف قرار می‌دهد؛
  • در [24]، کلاینت به سرور اطلاع می‌دهد که کارش تمام شده است؛
  • در [25]، می‌بینیم که سرور اتصال با کلاینت را قطع کرده است؛

اکنون در Thunderbird بررسی می‌کنیم که کاربر [guest@localhost.com] واقعاً پیام را دریافت کرده است:

Image

  • در [1-6]، می‌بینیم که کاربر [guest@localhost.com] واقعاً پیام را دریافت کرده است؛

در نهایت، کلاینت ما [RawTcpClient] با موفقیت پیامی را از طریق سرور SMTP [localhost] ارسال کرده است. اکنون، بیایید از همان روش برای ارسال پیام به [pymailparlexemple@gmail.com] استفاده کنیم:


(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
  • خط ۱: ما از سرور SMTP جیمیل استفاده می‌کنیم که روی پورت ۵۸۷ کار می‌کند؛
  • خط ۱۵: ما مسدود شده‌ایم زیرا سرور SMTP از ما می‌خواهد یک اتصال امن برقرار کنیم، که ما نمی‌دانیم چگونه این کار را انجام دهیم. برخلاف مثال قبلی، سرور [smtp.gmail.com] (خط ۱) احراز هویت را الزامی می‌داند. این سرور تنها مشتریانی را می‌پذیرد که در دامنه [gmail.com] ثبت‌نام شده‌اند. این احراز هویت امن است و در یک اتصال رمزگذاری‌شده انجام می‌شود.

مثال اول مبانی ساخت یک کلاینت پایه SMTP در پایتون را در اختیار ما قرار داد. مثال دوم به ما نشان داد که برخی از سرورهای SMTP (در واقع، اکثر آنها) نیاز دارند که احراز هویت از طریق یک اتصال رمزگذاری‌شده انجام شود.

21.5.6. اسکریپت‌های [smtp/01]: یک کلاینت پایه‌ای SMTP

ما قصد داریم آنچه را که قبلاً در مورد پروتکل SMTP آموخته‌ایم، در پایتون بازتولید کنیم.

Image

فایل [smtp/01/config] برنامه را به شرح زیر پیکربندی می‌کند:


def configure() -> dict:
    return {
        # توضیحات: توضیحات ایمیل ارسال‌شده
        # smtp-server: سرور SMTP
        # smtp-port: پورت سرور SMTP
        # از: فرستنده
        # به: گیرنده
        # موضوع: موضوع ایمیل
        # پیام: پیام ایمیل
        "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 هستیم
                "content-type": 'text/plain; charset="utf-8"',
                # آزمایش کاراکترهای دارای نشانه
                "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
                "Content-type": 'text/plain; charset="utf-8"',
                # آزمایش کاراکترهای دارای نشانه
                "message": "aglaë séléné\nva au marché\nacheter des fleurs"
            }
        ]
    }
  • خطوط ۱۰–۳۵: فهرستی از ایمیل‌هایی که باید ارسال شوند. برای هر یک، اطلاعات زیر مشخص می‌شود:
    • [description]: متنی که ایمیل را توصیف می‌کند؛
    • [smtp-server]: سرور SMTP که باید استفاده شود؛
    • [smtp-port]: پورت سرویس آن؛
    • [from]: فرستنده ایمیل؛
    • [to]: گیرنده ایمیل؛
    • [subject]: موضوع ایمیل؛
    • [content-type]: رمزگذاری ایمیل؛
    • [message]: پیام ایمیل؛

کد [01/main] برای مشتری SMTP به شرح زیر است:


#وارد می‌کند
import socket


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: bool):
    # پیامی را از طرف فرستنده به سرور SMTP smtpserver ارسال می‌کند
    # به گیرنده. اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند

    # اجازه داده می‌شود خطاهای سیستمی گزارش شوند
    connexion = None
    try:
        # نام ماشین محلی (مورد نیاز پروتکل SMTP)
        client = socket.gethostbyaddr(socket.gethostbyname("localhost"))[0]
        # یک اتصال را روی پورت ۲۵ از smtpServer باز می‌کند
        connexion = socket.create_connection((mail["smtp-server"], 25))

        #نمایانگر یک جریان ارتباطی دوطرفه است
        #بین مشتری (این برنامه) و سرور SMTP مورد تماس
        #این کانال برای تبادل دستورات و اطلاعات استفاده می‌شود

        # پس از برقراری اتصال، سرور یک پیام خوش‌آمدگویی ارسال می‌کند که خوانده می‌شود
        send_command(connexion, "", verbose, True)
        # دستور 'ehlo':
        send_command(connexion, f"EHLO {client}", verbose, True)
        # فرمان 'mail from':
        send_command(connexion, f"MAIL FROM: <{mail['from']}>", verbose, True)
        # فرمان rcpt به:
        send_command(connexion, f"RCPT TO: <{mail['to']}>", verbose, True)
        # دستور داده
        send_command(connexion, "DATA", verbose, True)
        # در حال آماده‌سازی پیام برای ارسال
        # باید شامل خطوط زیر باشد
        # از: فرستنده
        # به: گیرنده
        # خط خالی
        # پیام
        # .
        data = f"{mail['message']}"
        #ارسال پیام
        send_command(connexion, data, verbose, False)
        #در حال ارسال.
        send_command(connexion, "\r\n.\r\n", verbose, False)
        # دستور خروج
        send_command(connexion, "QUIT", verbose, True)
        # پایان
    finally:
        # اتصال بسته شد
        if connexion:
            connexion.close()


# --------------------------------------------------------------------------
def send_command(connexion: socket, commande: str, verbose: bool, with_rclf: bool):
    # دستور را به کانال اتصال ارسال می‌کند
    # حالت تفصیلی اگر verbose=True
    #اگر with_rclf=True باشد، توالی rclf را به فرمان اضافه می‌کند

    # داده‌ها
    rclf = "\r\n" if with_rclf else ""
    # دستور را ارسال می‌کند اگر دستور خالی نباشد
    if commande:
        #خطاهای سیستمی مجاز به انتشار هستند
        #
        #ارسال فرمان
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        # هر اکو را بازگردانید
        if verbose:
            affiche(commande, 1)
        # خواندن پاسخ با کمتر از ۱۰۰۰ کاراکتر
        reponse = str(connexion.recv(1000), 'utf-8')
        # اکوی احتمالی
        if verbose:
            affiche(reponse, 2)
        #بازیابی کد خطا
        codeErreur = int(reponse[0:3])
        # خطایی که توسط سرور بازگردانده شده؟
        if codeErreur >= 500:
            # یک استثنا همراه با خطا پرتاب می‌شود
            raise BaseException(reponse[4:])
        # بازگشت بدون خطا


# --------------------------------------------------------------------------
def affiche(echange: str, sens: int):
    # نمایش تراکنش روی صفحه؟
    # اگر sens=1 باشد، -->exchange را نمایش می‌دهد
    # اگر direction=2 باشد، <-- exchange را بدون دو کاراکتر پایانی RCLF نمایش می‌دهد
    if sens == 1:
        print(f"--> [{echange}]")
        return
    elif sens == 2:
        l = len(echange)
        print(f"<-- [{echange[0:l - 2]}]")
        return


# اصلی ----------------------------------------------------------------

# کلاینت SMTP (پروتکل انتقال SendMail) برای ارسال یک پیام
#جزئیات از یک فایل پیکربندی گرفته می‌شود که شامل اطلاعات زیر برای هر سرور است

# توضیحات: توضیحات ایمیل ارسال‌شده
# smtp-server: سرور SMTP
# smtp-port: پورت سرور SMTP
# از: فرستنده
# به: گیرنده
# موضوع: موضوع ایمیل
# پیام: پیام ایمیل


# SMTP پروتکل ارتباطی کلاینت-سرور
# -> کلاینت به پورت ۲۵ سرور SMTP متصل می‌شود
# <- سرور یک پیام خوش‌آمدگویی ارسال می‌کند
# -> کلاینت فرمان EHLO را ارسال می‌کند: نام میزبان آن
# <- سرور با OK پاسخ می‌دهد یا خیر
# -> کلاینت فرمان «mail from: <sender>» را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
#-> مشتری فرمان rcpt را به: <recipient> ارسال می‌کند
#<- سرور با OK پاسخ می‌دهد یا خیر
#-> کلاینت فرمان 'data' را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
#-> کلاینت تمام خطوط پیام خود را ارسال می‌کند و با یک خط حاوی یک کاراکتر به پایان می‌رسد.
#<- سرور با OK پاسخ می‌دهد یا خیر
# -> کلاینت فرمان 'quit' را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر

#پاسخ‌های سرور به شکل xxx text هستند، که در آن xxx یک عدد سه‌رقمی است. هر عددی که xxx ≥ ۵۰۰ باشد
# نشان‌دهنده خطا است. پاسخ ممکن است شامل چندین خط باشد که همه با xxx- شروع می‌شوند، به جز خط آخر
# به شکل xxx (فاصله)

#خطوط متن مبادله شده باید با کاراکترهای RC(#13) و LF(#10) پایان یابند

#پیکربندی برنامه
import config
config = config.configure()

#ایمیل‌ها به‌صورت جداگانه پردازش می‌شوند
for mail in config['mails']:
    try:
        # لاگ‌ها
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        # آماده‌سازی پیام برای ارسال
        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']}"
        #ارسال پیام در حالت تفصیلی
        sendmail(mail, True)
        #پایان
        print("Message envoyé...")
    except BaseException as erreur:
        # نمایش خطا
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # ایمیل بعدی

نظرات

  • خطوط ۱۳۴–۱۳۶: برنامه پیکربندی شده است؛
  • خطوط ۱۳۹–۱۵۱: تمام ایمیل‌های یافت‌شده در پیکربندی پردازش می‌شوند؛
  • خطوط ۱۴۱–۱۴۳: نمایش آنچه قرار است انجام شود؛
  • خطوط ۱۴۴–۱۴۹: پیام ارسالی تعریف می‌شود. پیام [message] با سربرگ‌های [From, To, Subject, Content-type] پیش‌فرض می‌شود؛
  • خط ۱۵۱: ایمیل توسط تابع [sendmail] ارسال می‌شود که دو پارامتر می‌گیرد:
    • [mail]: دیکشنری حاوی اطلاعات مورد نیاز برای ارسال ایمیل؛
    • [verbose]: یک مقدار بولی که نشان می‌دهد آیا تبادله‌های کلاینت/سرور باید در کنسول ثبت شوند یا خیر؛
  • خطوط ۱۵۴–۱۵۶: تمام استثناهایی که توسط تابع [sendmail] پرتاب می‌شوند، گرفته می‌شوند. آنها نمایش داده می‌شوند؛
  • خط ۶: [mail] دیکشنری توصیف‌کننده ایمیلی است که باید ارسال شود؛
  • خط ۱۴: در پروتکل SMTP، کلاینت باید نام خود را ارسال کند. در اینجا، ما نام ماشین محلی را که به عنوان کلاینت عمل خواهد کرد، بازیابی می‌کنیم؛
  • خط ۱۶: اتصال به سرور SMTP، که پیام به آن ارسال خواهد شد؛
  • خطوط ۲۲–۲۳: اگر ارتباطی با سرور SMTP برقرار شده باشد، یک پیام خوش‌آمدگویی ارسال می‌شود که در اینجا خوانده می‌شود؛
  • تابع [sendmail] سپس دستورات مختلفی را که یک کلاینت SMTP باید ارسال کند، ارسال می‌کند:
    • خطوط ۲۴–۲۵: فرمان EHLO;
    • خطوط 26–27: فرمان MAIL FROM: ;
    • سطور ۲۸–۲۹: دستور RCPT TO: ;
    • خطوط ۳۰–۳۱: فرمان DATA;
    • خطوط ۳۲–۴۱: ارسال پیام (From, To, Subject, Content-type, text);
    • خطوط ۴۲–۴۳: ارسال کاراکتر پایان پیام؛
    • خطوط ۴۴–۴۵۷: فرمان QUIT، که دیالوگ مشتری با سرور SMTP را خاتمه می‌دهد؛
  • اجرای [sendmail] در داخل یک [try / finally] انجام می‌شود، که اجازه می‌دهد تمام استثناها به کد فراخوانی بازگردانده شوند. ما می‌دانیم که کد فراخوانی همه آن‌ها را برای نمایش گرفتن می‌کند؛
  • خطوط ۴۸–۵۰: آزادسازی منابع؛
  • خط ۵۴: تابع [send_command] مسئول ارسال دستورات مشتری به سرور SMTP است. این تابع چهار پارامتر می‌گیرد:
    • [connexion]: ارتباطی که کلاینت را به سرور متصل می‌کند؛
    • [commande]: دستوری که باید ارسال شود؛
    • [verbose]: اگر TRUE باشد، آنگاه تعاملات کلاینت/سرور در کنسول ثبت می‌شود؛
    • [with_rclf]: اگر TRUE باشد، فرمان را با دنباله \r\n ارسال کنید. این امر برای تمام دستورات در پروتکل SMTP الزامی است، اما [send_command] نیز برای ارسال پیام استفاده می‌شود. در این حالت، توالی \r\n اضافه نمی‌شود؛
  • خط ۶۲: فرمان تنها در صورتی ارسال می‌شود که خالی نباشد؛
  • خطوط ۶۵–۶۶: فرمان به عنوان یک رشته بایت UTF-8 به سرور ارسال می‌شود؛
  • خطوط ۷۰–۷۱: تمام خطوط پاسخ خوانده می‌شوند. فرض بر این است که پاسخ کمتر از ۱۰۰۰ کاراکتر طول دارد. پاسخ ممکن است از چندین خط تشکیل شده باشد. هر خط به شکل XXX-YYY است، که در آن XXX یک کد عددی است، به جز آخرین خط پاسخ که به شکل XXX YYY است (علامت تیره وجود ندارد)؛
  • خط ۷۶: کد خطا XXX را از خط اول بخوانید؛
  • خطوط ۷۸–۸۰: اگر کد عددی XXX بزرگتر از ۵۰۰ باشد، در این صورت سرور خطایی را بازگردانده است. سپس یک استثنا پرتاب می‌شود؛

نتایج

اجرای اسکریپت خروجی کنسول زیر را تولید می‌کند:


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
  • خطوط ۳–۳۰: استفاده از سرور SMTP [hMailServer] برای ارسال ایمیل به [guest@localhost] موفقیت‌آمیز است؛
  • خطوط ۳۲–۴۶: استفاده از سرورهای SMTP و [smtp.gmail.com] برای ارسال ایمیل به [pymailparlexemple@gmail.com] موفقیت‌آمیز نیست: در خط ۴۵، سرور SMTP کد خطای ۵۳۰ را همراه با یک پیام خطا بازمی‌گرداند. این نشان می‌دهد که کلاینت SMTP باید ابتدا از طریق یک اتصال امن احراز هویت کند. کلاینت ما این کار را انجام نداده و بنابراین رد می‌شود؛

نتایج در Thunderbird به شرح زیر است:

Image

21.5.7. اسکریپت‌های [smtp/02]: یک لینک SMTP نوشته شده با استفاده از کتابخانه [smtplib]

Image

کلاینت قبلی دست‌کم از دو کاستی رنج می‌برد:

  1. نمی‌تواند از اتصال امن استفاده کند اگر سرور آن را الزامی کند؛
  1. نمی‌تواند فایل‌ها را به پیام پیوست کند؛

نقص اول را در اسکریپت [smtp/02] برطرف خواهیم کرد. در اسکریپت جدیدمان از ماژول Python [smtplib] استفاده خواهیم کرد.

اسکریپت [smtp/02/main] از فایل پیکربندی زیر استفاده خواهد کرد: jSON [smtp/02/config]:


def configure() -> dict:
    return {
        # توضیحات: توضیحات ایمیل ارسال‌شده
        # smtp-server: سرور SMTP
        # پورت SMTP: پورت سرور SMTP
        # از: فرستنده
        # به: گیرنده
        # موضوع: موضوع ایمیل
        # پیام: پیام ایمیل
        "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",
                # آزمایش کاراکترهای دارای نشانه
                "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",
                # آزمایش کاراکترهای دارای نشانه
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP با احراز هویت
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlh@1QZ3TG",
            }
        ]
    }

همان فیلدها در فایل [smtp/01/config] وجود دارند، با دو فیلد اضافی هنگامی که سرور SMTP درخواست احراز هویت می‌کند:

  • خط ۳۱، [user]: نام کاربری مورد استفاده برای احراز هویت اتصال؛
  • خط ۳۲، [password]: رمز عبور آنها؛

این دو فیلد تنها در صورتی وجود دارند که سرور SMTP که با آن تماس گرفته می‌شود، به احراز هویت نیاز داشته باشد. این کار سپس از طریق یک اتصال امن انجام می‌شود.

کد اسکریپت [smtp/02/main.py] به شرح زیر است:


# وارد می‌کند
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate


# -----------------------------------------------------------------------
def sendmail(mail: dict, verbose: True):
    # پیامی را از طرف فرستنده به سرور SMTP 'smtpserver' ارسال می‌کند
    #برای گیرنده. اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند

    #از کتابخانه smtplib استفاده می‌کند
    # استثناها مجاز به انتشار هستند
    #
    # سرور SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    #حالت تفصیلی
    server.set_debuglevel(verbose)
    # اتصال امن؟
    if "user" in mail:
        # اتصال امن
        server.starttls()
        # EHLO فرمان + احراز هویت
        server.login(mail["user"], mail["password"])

   # در حال ساخت یک پیام چندبخشی – این همان پیام چندبخشی است که ارسال خواهد شد
    msg = MIMEText(mail["message"])
    msg['from'] = mail["from"]
    msg['to'] = mail["to"]
    msg['date'] = formatdate(localtime=True)
    msg['subject'] = mail["subject"]
    # پیام ارسال می‌شود
    server.send_message(msg)
    #خروج
    server.quit()


# اصلی ----------------------------------------------------------------

# اطلاعات از یک فایل پیکربندی گرفته می‌شود که شامل جزئیات زیر برای هر سرور است

# توضیحات: توضیحات ایمیل ارسال‌شده
# smtp-server: سرور SMTP
# smtp-port: پورت سرور SMTP
# از: فرستنده
# به: گیرنده
# موضوع: موضوع ایمیل
# نوع محتوا: رمزگذاری ایمیل
# پیام: پیام ایمیل


# پیکربندی برنامه
import config
config = config.configure()

#ایمیل‌ها به‌صورت جداگانه پردازش می‌شوند
for mail in config['mails']:
    try:
        # لاگ‌ها
        print("----------------------------------")
        print(f"Envoi du message [{mail['description']}]")
        #ارسال پیام در حالت تفصیلی
        sendmail(mail, True)
        # پایان
        print("Message envoyé...")
    except BaseException as erreur:
        # نمایش خطا
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
    # ایمیل بعدی

توضیحات

  • خطوط ۸–۳۵: فقط از تابع [sendmail] استفاده می‌شود. اکنون از ماژول [smtplib] (خط ۲) استفاده خواهد شد؛
  • خط ۱۶: اتصال به سرور SMTP؛
  • خط ۱۸: اگر [verbose=True] باشد، مبادلات کلاینت/سرور روی کنسول نمایش داده می‌شوند؛
  • خطوط ۲۰–۲۴: احراز هویت در صورت لزوم توسط سرور SMTP انجام می‌شود؛
  • خط ۲۲: احراز هویت از طریق یک اتصال امن انجام می‌شود؛
  • خط ۲۴: احراز هویت؛
  • خطوط ۲۶–۳۳: پیام ارسال می‌شود. سپس مکالمه با اسکریپت [smtp/01/main] انجام خواهد شد. اگر احراز هویت انجام شده باشد، این کار در یک اتصال امن صورت می‌گیرد؛
  • خط ۳۵: دیالوگ کلاینت/سرور خاتمه می‌یابد؛

قبل از اجرای اسکریپت [smtp/02/main]، باید پیکربندی حساب جیمیل [pymailparlexemple@gmail.com] را اصلاح کنید:

  • وارد حساب جیمیل [pymailparlexemple@gmail.com] شوید؛
  • تنظیمات زیر را تغییر دهید: Image
  • در [2]، به برنامه‌های کمتر امن اجازه دسترسی به حساب کاربری را بدهید؛

همین کار را برای حساب جیمیل دوم، [pymail2parlexemple@gmail.com انجام دهید.

نتایج

وقتی اسکریپت [smtp/02/main] اجرا می‌شود، خروجی کنسول زیر تولید می‌شود:


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
  • خط ۴۰: کلاینت [smtplib] دست‌داد (handshake) را برای برقراری یک اتصال رمزگذاری‌شده با سرور SMTP آغاز می‌کند، کاری که در اسکریپت [smtp/main/01] نتوانستیم به آن دست یابیم؛
  • در غیر این صورت، دستورات آشنای پروتکل SMTP را مشاهده می‌کنیم؛

اگر حساب جیمیل کاربر [pymail2parlexemple] را بررسی کنیم، موارد زیر را مشاهده می‌کنیم:

Image

21.5.8. اسکریپت‌های [smtp/03]: مدیریت فایل‌های پیوست

ما در حال تکمیل اسکریپت [smtp/02/main] هستیم تا ایمیلی که ارسال می‌شود بتواند پیوست داشته باشد.

Image

اسکریپت [smtp/03/main] توسط اسکریپت زیر [smtp/03/config] پیکربندی شده است:


import os


def configure() -> dict:
    # پیکربندی برنامه
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # توضیحات: توضیحات ایمیل ارسال‌شده
        # smtp-server: سرور SMTP
        # smtp-port: پورت سرور SMTP
        # از: فرستنده
        # به: گیرنده
        # موضوع: موضوع ایمیل
        # پیام: پیام ایمیل
        "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",
                # آزمایش کاراکترهای دارای نشانه
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP با احراز هویت
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # در اینجا باید مسیرهای مطلق برای فایل‌های پیوست را مشخص کنید
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                ]
            }
        ]
    }

فایل [smtp/03/config] تنها در گنجاندن اختیاری یک لیست [attachments] (خطوط ۳۰–۳۲)، که فهرست فایل‌هایی را که باید به پیام ارسال‌شده پیوست شوند مشخص می‌کند، با فایل قبلاً استفاده‌شده [smtp/02/config] متفاوت است.

اسکریپت [smtp/03/main] به شرح زیر است:


# وارد می‌کند
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):
    #یک ایمیل ([message]) را از طرف [from] به سرور SMTP ([smtp-server]) ارسال می‌کند
    #برای mail[to]. اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند

    # کتابخانه smtplib استفاده می‌شود
    # اجازه داده می‌شود استثناءها منتقل شوند
    #
    # سرور SMTP
    server = smtplib.SMTP(mail["smtp-server"])
    # حالت تفصیلی
    server.set_debuglevel(verbose)
    # اتصال امن؟
    if "user" in mail:
        server.starttls()
        server.login(mail["user"], mail["password"])

    # در حال ساخت یک پیام چندبخشی – این پیامی است که ارسال خواهد شد
    # اعتبار: 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"]
    #پیام متنی در قالب MIMEText ضمیمه شده است
    msg.attach(MIMEText(mail["message"]))
    # پیوست‌ها به‌صورت متوالی بررسی می‌شوند
    for path in mail["attachments"]:
        #مسیر باید یک مسیر مطلق باشد
        # نوع فایل پیوست حدس زده می‌شود
        ctype, encoding = mimetypes.guess_type(path)
        # اگر نوع قابل حدس نباشد
        if ctype is None or encoding is not None:
            #هیچ حدسی ممکن نبود، یا فایل رمزگذاری (فشرده) شده است، بنابراین
            # از یک نوع کلی «کیسهٔ بیت» استفاده می‌شود.
            ctype = 'application/octet-stream'
        # نوع به maintype/subtype تقسیم می‌شود
        maintype, subtype = ctype.split('/', 1)
        # ما موارد مختلف را مدیریت می‌کنیم
        if maintype == 'text':
            with open(path) as fp:
                # توجه: باید محاسبه مجموعه کاراکتری را مدیریت کنیم
                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)
        #برای نوع پیام / rfc822
        elif maintype == 'message':
            with open(path, 'rb') as fp:
                part = MIMEMessage(email.message_from_bytes(fp.read()))
        else:
            # موارد دیگر
            with open(path, 'rb') as fp:
                part = MIMEBase(maintype, subtype)
                part.set_payload(fp.read())
            # بار مفید را با Base64 رمزگذاری کنید
            encoders.encode_base64(part)
        # پارامتر نام فایل را تنظیم کنید
        basename = os.path.basename(path)
        part.add_header('Content-Disposition', 'attachment', filename=basename)
        # فایل را به پیام ارسالی پیوست کنید
        msg.attach(part)
    # تمام پیوست‌ها اضافه شده‌اند – پیام به صورت یک رشته ارسال می‌شود
    server.send_message(msg)


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

..

توضیحات

  • خطوط ۱۸–۳۲: تابع [sendmail] همان‌طور که زمانی که هیچ پیوست وجود نداشت، باقی می‌ماند؛
  • خط ۳۵: کد زیر از مستندات رسمی پایتون گرفته شده است؛
  • خط ۳۶: پیام ارسالی از چندین بخش تشکیل خواهد شد: متن و فایل‌های پیوست. این به عنوان پیام [Multipart] شناخته می‌شود؛
  • خطوط ۳۷–۴۰: پیام [Multipart] شامل فیلدهای معمول موجود در هر ایمیل است؛
  • خط ۴۲: بخش‌های مختلف پیام [Multipart] [msg] با استفاده از روش [msg.attach] (خط ۸۱) به پیام متصل شده‌اند. بخش‌های پیوست‌شده می‌توانند از هر نوعی باشند. این بخش‌ها با نوع MIME شناسایی می‌شوند. نوع MIME برای متن ساده، نوع [MIMEText] است؛
  • خطوط 44–81: تمام پیوست‌ها برای پیامی که باید ارسال شود (خط 81) به پیام [msg Multipart] ضمیمه خواهند شد؛
  • خط ۴۴: [path] نشان‌دهنده مسیر مطلق فایل پیوست است؛
  • خط ۴۷: برای تعیین نوع MIME که باید برای پیوست استفاده شود، از پسوند فایل (.docx، .php و غیره) فایل پیوستی استفاده خواهیم کرد. متد [mimetypes.guess_type] این کار را انجام می‌دهد. این متد دو اطلاعات را بازمی‌گرداند:
    • [ctype]: نوع فایل (MIME);
    • [encoding]: اطلاعاتی دربارهٔ رمزگذاری آن؛
  • خطوط ۴۹–۵۲: اگر نوع فایل MIME قابل تعیین نباشد، به عنوان یک فایل باینری شناسایی می‌شود (خط ۵۲);
  • خط ۵۴: نوع فایل MIME به یک نوع اصلی و یک نوع ثانویه، برای مثال [application/pdf]، تجزیه می‌شود. این دو عنصر از هم جدا می‌شوند؛
  • خطوط 56–76: موارد مختلف بسته به مقدار نوع اصلی MIME پردازش می‌شوند. برای مثال، در مورد فایلی با نوع [application/pdf] و نوع اصلی PDF، خطوط 70–76 اجرا می‌شوند:
    • خطوط ۵۶–۵۹: حالتی که فایل پیوست یک فایل متنی است. در این حالت، یک عنصر از نوع [MIMEText] با محتوای [fp.read] ایجاد می‌شود؛
    • خطوط ۶۰–۶۲: حالتی که فایل حاوی یک تصویر است. در این حالت، یک عنصر از نوع [MIMEImage] با محتوای [fp.read] ایجاد می‌شود؛
    • خطوط ۶۳–۶۵: حالتی که فایل یک فایل صوتی است. در این حالت، یک عنصر از نوع [MIMEAudio] با محتوای [fp.read] ایجاد می‌شود؛
    • خطوط ۶۶–۶۹: حالتی که فایل یک ایمیل است. در این حالت، یک عنصر از نوع [MIMEMessage] (خط ۶۹) با محتوای [email.message_from_bytes(fp.read())] ایجاد می‌شود. برخلاف موارد قبلی که محتوای عنصر MIME محتوای باینری فایل مربوطه بود، در اینجا محتوای عنصر MIMEMessage از نوع [email.message.Message] است؛
    • خطوط ۷۰–۷۶: موارد دیگر. این شامل، برای مثال، فایل‌های Word و PDF در مثال ما می‌شود؛
  • خط ۷۲: فایلی که باید پیوست شود در حالت باینری باز می‌شود (rb=خواندن باینری);
  • خط ۷۴: [fp.read] کل فایل باینری را می‌خواند؛
  • خطوط ۷۲–۷۴: ساختار [with open(…) as file] دو کار انجام می‌دهد:
    • فایل را باز می‌کند و آن را به توصیفگر [file] اختصاص می‌دهد؛
    • این ساختار تضمین می‌کند که پس از خروج از [with]، چه خطایی رخ دهد یا نه، دیسکریپتور [file] بسته خواهد شد. بنابراین این ساختار جایگزینی برای ساختار [try file=open(…)/ finally] است؛
  • خط ۷۳: یک عنصر جدید [part] برای گنجانده شدن در پیام چندبخشی ایجاد می‌شود. در اینجا از کلاس [MIMEBase] استفاده شده و عناصر [maintype, subtype] که در خط ۵۴ تعریف شده‌اند به سازنده (constructor) پاس داده می‌شوند؛
  • خط ۷۴: عنصری که باید در پیام چندبخشی گنجانده شود باید دارای محتوا باشد. این محتوا را می‌توان با استفاده از متد [set_payload] مقداردهی اولیه کرد؛
  • خطوط ۷۵–۷۶: فایل‌های پیوست باید با استفاده از رمزگذاری ۷ بیتی رمزگذاری شوند. از نظر تاریخی، برخی از سرورهای SMTP فقط از کاراکترهای رمزگذاری‌شده ۷ بیتی پشتیبانی می‌کردند. در اینجا، از رمزگذاری معروفی به نام «Base64» استفاده می‌شود؛
  • خط ۷۷: از این خط به بعد، پردازش شامل تمام انواع MIME است که در خطوط ۵۶–۷۶ ایجاد کرده‌ایم ([MIMEMessage, MIMEImage, MIMEAudio, MIMEBase, MIMEText]
  • خط ۷۹: عنصری که باید به پیام چندبخشی اضافه شود، دارای یک سربرگ توصیفی است. در اینجا، مشخص می‌کنیم که عنصر اضافه شده به یک فایل پیوست شده مربوط می‌شود. نام این فایل، پارامتر سوم ارسال‌شده به متد [add_header] است. این نام فایل اغلب توسط کلاینت‌های ایمیل برای ذخیره فایل پیوست با آن نام در سیستم فایل کلاینت استفاده می‌شود. تا اینجای کار، ما با مسیر مطلق فایل پیوست کار کرده‌ایم. در اینجا، ما به سادگی نام آن را بدون مسیر ارسال می‌کنیم (خط ۷۸);
  • خط ۸۱: داده‌های باینری فایل در پیام [msg Multipart] جاسازی می‌شود؛
  • خط ۸۳: هنگامی که تمام بخش‌های پیام به [msg Multipart] ضمیمه شدند، ارسال می‌شود؛

نتایج

اگر اسکریپت [smtp/03/main] را با فایل [smtp/02/config] که در بالا نشان داده شده اجرا کنید، حساب [pymail2parlexemple@gmail.com] موارد زیر را دریافت می‌کند:

Image

فایل‌های پیوست را می‌توان در [4, 9-11] مشاهده کرد.

حال بیایید نگاهی به یک مثال با پیوست ایمیل بیندازیم. ما ایمیل دریافتی را در [3] بالا ذخیره خواهیم کرد:

Image

ما ایمیل را با نام [mail attaché 1.eml] در پوشه [smtp/03/attachments] ذخیره می‌کنیم.

اکنون فایل [smtp/03/config] را به شرح زیر ویرایش خواهیم کرد:


import os


def configure() -> dict:
    # پیکربندی برنامه
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # توضیحات: توضیحات ایمیل ارسال‌شده
        # smtp-server: سرور SMTP
        # پورت-SMTP: پورت سرور SMTP
        # از: فرستنده
        # به: گیرنده
        # موضوع: موضوع ایمیل
        # پیام: پیام ایمیل
        "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",
                # آزمایش کاراکترهای دارای نشانه
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP با احراز هویت
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # در اینجا باید مسیرهای مطلق فایل‌های پیوست را مشخص کنید
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • در خط ۳۳، یک پیوست اضافه کرده‌ایم؛

اکنون اسکریپت [smtp/03/main] را دوباره اجرا می‌کنیم. این کار نتیجه زیر را در صندوق پستی کاربر [pymail2parlexemple@gmail.com] ایجاد می‌کند:

Image

  • در [1]، ایمیل دریافتی؛
  • در [2]: متن پیام؛
  • [3]: متن ایمیل پیوست‌شده؛
  • در [4]: Thunderbird پنج پیوست را یافت:
    • [fichier attaché.docx];
    • [fichier attaché.pdf];
    • [mail attaché 1.eml]. این پیوست خود یک ایمیل است که شامل دو پیوست دیگر می‌باشد:
      • [fichier attaché.docx];
      • [fichier attaché.pdf];

21.6. پروتکل POP3

21.6.1. مقدمه

برای خواندن ایمیل‌های ذخیره‌شده روی سرور ایمیل دو پروتکل وجود دارد:

  • پروتکل POP3 (پروتکل دفتر پستی)، که از نظر تاریخی اولین پروتکل بود اما اکنون به ندرت استفاده می‌شود؛
  • پروتکل IMAP (Internet Message Access Protocol)، که جدیدتر از POP3 است و در حال حاضر بیشترین استفاده را دارد؛

برای بررسی پروتکل POP3، از معماری زیر استفاده خواهیم کرد:

Image

  • [Serveur B] بسته به شرایط:
    • یک سرور محلی POP3، که توسط سرور ایمیل [hMailServer] پیاده‌سازی شده است؛
    • سرور [pop.gmail.com]، که سرور POP3 مدیر ایمیل [gmail.com] است؛
  • [Client A] به صورت‌های مختلف مشتری POP3 خواهد بود:
    • کلاینت [RawTcpClient] برای کشف پروتکل POP3؛
    • یک اسکریپت پایتون که پروتکل POP3 کلاینت [RawTcpClient] را بازسازی می‌کند؛
    • یک اسکریپت پایتون که از ماژول‌های پایتون برای مدیریت فایل‌های پیوست و استفاده از یک اتصال رمزگذاری‌شده و احراز هویت‌شده در صورت نیاز سرور POP3 استفاده می‌کند؛

21.6.2. بررسی پروتکل POP3

همان‌طور که با پروتکل SMTP انجام دادیم، پروتکل POP3 را با استفاده از لاگ‌های سرور ایمیل [hMailServer] بررسی خواهیم کرد. ابتدا باید این سرور را راه‌اندازی کنیم.

با استفاده از Thunderbird، ما:

  • ارسال یک ایمیل به کاربر [guest@localhost.com]؛
  • صندوق پستی این کاربر را بخوانیم؛

Image

Image

در [3-6] بالا، پیامی که توسط کاربر [guest@localhost.com] دریافت شده است.

ما اکنون در حال بررسی لاگ‌های سرور [hMailServer] هستیم. برای این کار از ابزار مدیریت [hMailServer Administrator] استفاده می‌کنیم:

Image

لاگ‌های POP3 به شرح زیر است (آخرین خطوط در فایل لاگ امروز):


"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..."
  • خط ۱: سرور POP3 یک پیام خوش‌آمدگویی به کلاینت (Thunderbird) که به تازگی متصل شده است، ارسال می‌کند؛
  • خط ۲: کلاینت فرمان [CAPA] (قابلیت‌ها) را برای درخواست فهرستی از فرمان‌هایی که می‌تواند استفاده کند، ارسال می‌کند؛
  • خط ۳: سرور پاسخ می‌دهد که می‌تواند از دستورات [USER, UIDL, TOP] استفاده کند. سرور POP پاسخ‌های خود را با [+OK] یا [-ERR] آغاز می‌کند تا نشان دهد که آیا فرمان مشتری را با موفقیت یا با شکست اجرا کرده است؛
  • خط ۴: کلاینت فرمان [USER guest] را ارسال می‌کند تا نشان دهد که مایل است صندوق پستی کاربر [guest] را مشاهده کند؛
  • خط ۵: سرور با [+OK] پاسخ می‌دهد و رمز عبور [guest] را درخواست می‌کند؛
  • خط ۶: کلاینت فرمان [PASS password] را برای ارسال رمز عبور کاربر [guest] ارسال می‌کند. در اینجا، رمز عبور به صورت متن ساده است زیرا سرور POP3 اتصال امن را اعمال نکرده است. خواهیم دید که این موضوع با سرور جیمیل POP3 متفاوت خواهد بود؛
  • خط ۷: سرور نام کاربری و رمز عبور را تأیید کرده است. این نشان می‌دهد که در حال مسدود کردن صندوق پستی کاربر [guest] است؛
  • خط ۸: کلاینت فرمان [STAT] را ارسال می‌کند و درخواست اطلاعات درباره صندوق پستی را می‌دهد؛
  • خط ۹: سرور پاسخ می‌دهد که یک پیام ۶۱۲ بایتی وجود دارد. به طور کلی، پاسخ می‌دهد که N پیام وجود دارد و مجموع اندازه این پیام‌ها را ارائه می‌دهد؛
  • خط ۱۰: کلاینت فرمان [LIST] را ارسال می‌کند. این فرمان فهرست پیام‌ها را درخواست می‌کند؛
  • خط ۱۱: سرور فهرست پیام‌ها را در قالب زیر برای کلاینت ارسال می‌کند:
    • یک خط خلاصه که تعداد پیام‌ها و حجم کلی آن‌ها را نشان می‌دهد؛
    • یک خط برای هر پیام، که شماره پیام و اندازه آن را نشان می‌دهد؛
  • خط ۱۳: کلاینت فرمان [UIDL] را ارسال می‌کند که فهرستی از پیام‌ها به همراه شناسه‌های آن‌ها را درخواست می‌کند. هر پیام با یک شماره منحصر به فرد در سرویس ایمیل شناسایی می‌شود؛
  • خط ۱۴: پاسخ سرور. می‌بینیم که پیام شمارهٔ ۱ در لیست شناسهٔ ۴۲ را دارد؛
  • خط ۱۵: کلاینت فرمان [RETR 1] را ارسال می‌کند و درخواست می‌کند که پیام شماره ۱ از لیست برای او فوروارد شود؛
  • خط ۱۶: سرور POP3 این کار را انجام می‌دهد؛
  • خط ۱۷: کلاینت فرمان [QUIT] را ارسال می‌کند تا نشان دهد که قصد قطع اتصال از سرور POP3 را دارد؛
  • خط ۱۸: سرور نیز اتصال خود را با کلاینت قطع می‌کند، اما ابتدا یک پیام خداحافظی برای آن ارسال می‌کند؛

اکنون ما با استفاده از کلاینت [RawTcpClient] که در پنجره‌ای به نام PyCharm در حال اجرا است، اجزای گفتگوی بالا را بازتولید خواهیم کرد:

Image

گفت‌وگو به شرح زیر است:


(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
  • خط ۱: یک اتصال با پورت ۱۱۰ روی ماشین [localhost] برقرار می‌شود. این همان جایی است که سرویس POP3 از [hMailServer] در آن اجرا می‌شود؛
  • در خطوط ۵، ۷، ۹، ۱۳ و ۳۴، از دستورات [USER, PASS, LIST, RETR, QUIT] استفاده می‌کنیم؛
  • خط ۴: پیام خوش‌آمدگویی از سرور POP3;
  • خط ۵: مشخص می‌کنیم که می‌خواهیم به صندوق پستی کاربر [guest] دسترسی پیدا کنیم؛
  • خط ۷: ما رمز عبور کاربر [guest] را به صورت متن ساده ارسال می‌کنیم؛
  • خط ۹: درخواستی برای فهرست پیام‌های صندوق پستی ارسال می‌شود؛
  • خط ۱۳: درخواست پیام شمارهٔ ۱؛
  • خطوط ۱۴–۳۳: سرور POP3 پیام شمارهٔ ۱ را ارسال می‌کند؛
  • خط ۳۴: جلسه خاتمه می‌یابد؛

در اینجا خلاصه‌ای از برخی دستورات رایج پذیرفته‌شده توسط سرور POP3 آورده شده است:

  • دستور [USER] برای مشخص کردن کاربری که می‌خواهید صندوق پستی او را بخوانید، استفاده می‌شود؛
  • دستور [PASS] برای تنظیم رمز عبور کاربر استفاده می‌شود؛
  • دستور [LIST] فهرست پیام‌های صندوق پستی کاربر را بازیابی می‌کند؛
  • فرمان [RETR] درخواست حذف پیامی را که با شمارهٔ ارائه‌شده مطابقت دارد، می‌کند؛
  • دستور [DELE] درخواست حذف پیام با شمارهٔ مشخص‌شده را می‌دهد؛
  • دستور [QUIT] به سرور اطلاع می‌دهد که عملیات کامل شده است؛

پاسخ سرور ممکن است به چند شکل باشد:

  • یک خط واحد که با [+OK] آغاز می‌شود تا نشان دهد فرمان قبلی مشتری موفق بوده است؛
  • یک خط واحد که با [-ERR] شروع می‌شود تا نشان دهد فرمان قبلی مشتری ناموفق بوده است؛
  • چند خط که در آن:
    • خط اول با [+OK] آغاز می‌شود؛
    • آخرین خط شامل یک نقطهٔ کامل است؛

21.6.3. اسکریپت‌های [pop3/01]: یک مشتری پایه POP3

Image

از آنجایی که پروتکل POP3 ساختار مشابهی با پروتکل SMTP دارد، اسکریپت [pop3/01/main.py] یک پورت از اسکریپت [smtp/01/main.py] است. این فایل پیکربندی زیر را خواهد داشت: [pop3/01/config.py]:


def configure() -> dict:
    # صندوق‌های پستی که ایمیل‌ها از آن‌ها بازیابی می‌شوند
    mailboxes = [
        # سرور: server POP3
        # پورت: پورت سرور POP3
        # user: کاربری که می‌خواهید پیام‌های او را بخوانید
        # رمز عبور: رمز عبور آنها
        # maxmails: حداکثر تعداد ایمیل‌هایی که باید دانلود شوند
        # timeout: حداکثر زمان انتظار برای پاسخ از سرور
        # رمزگذاری: رمزگذاری ایمیل‌های دریافتی
        # حذف: اگر True باشد، ایمیل‌ها از صندوق پستی حذف می‌شوند
        # پس از دانلود آن‌ها به‌صورت محلی

        {
            "server": "localhost",
            "port": "110",
            "user": "guest",
            "password": "guest",
            "maxmails": 10,
            "timeout": 1.0,
            "encoding": "utf-8",
            "delete": False
        }
    ]
    #پیکربندی بازیابی می‌شود
    return {
        "mailboxes": mailboxes
    }
  • خطوط ۳–۲۴: فهرست صندوق‌های پستی که باید بررسی شوند. در این مورد، تنها یک صندوق وجود دارد؛
  • خطوط ۴–۱۲: معانی ورودی‌های فرهنگ لغت که هر یک از صندوق‌های پستی را تعریف می‌کنند؛
  • خط ۱۵: سرور POP3 که در حال پرس‌وجو است، سرور محلی [hMailServer] است؛
  • خطوط ۱۷–۱۸: ما می‌خواهیم صندوق پستی متعلق به کاربر [guest@localhost] را بخوانیم؛
  • خط ۱۹: حداکثر ۱۰ ایمیل بازیابی خواهد شد؛
  • خط ۲۰: کلاینت حداکثر به مدت ۱ ثانیه منتظر پاسخ از سمت سرور خواهد ماند؛
  • خط ۲۱: نوع رمزگذاری پیام‌های بازیابی‌شده؛
  • خط ۲۲: پیام‌های دانلود شده حذف نخواهند شد؛

اسکریپت [pop3/01/main.py] به شرح زیر است:


# وارد می‌کند
import re
import socket


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    # نامهٔ الکترونیکی توصیف‌شده توسط فرهنگ لغت [mailbox] را می‌خواند
    # اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند



# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # یک دستور را به کانال اتصال ارسال می‌کند
    #حالت تفصیلی اگر verbose=True
    # اگر with_rclf=True باشد، توالی rclf را به تبادل اضافه می‌کند
    # اولین خط پاسخ را بازمی‌گرداند



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


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

# POP3 (پروتکل اداره پست) برای خواندن پیام‌ها از صندوق پستی
# POP3 پروتکل ارتباطی کلاینت-سرور
# -> کلاینت به پورت ۱۱۰ روی سرور SMTP متصل می‌شود
#<- سرور یک پیام خوش‌آمدگویی ارسال می‌کند
# -> کلاینت فرمان USER نام کاربری را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
#-> کلاینت فرمان PASS mot_de_passe را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
# -> کلاینت فرمان LIST را ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
# -> کلاینت فرمان RETR را با یک شماره منحصر به فرد برای هر ایمیل ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر. اگر OK باشد، محتوای ایمیل درخواستی را ارسال می‌کند
#-> سرور تمام خطوط ایمیل را ارسال می‌کند و با خطی حاوی پایان می‌دهد
# یک کاراکتر واحد.
# -> کلاینت فرمان DELE [شماره] را برای حذف یک ایمیل ارسال می‌کند
# <- سرور با OK یا عدم آن پاسخ می‌دهد
# # -> کلاینت فرمان QUIT را برای پایان دادن به دیالوگ با سرور ارسال می‌کند
# <- سرور با OK پاسخ می‌دهد یا خیر
#پاسخ‌های سرور به شکل +OK متن یا -ERR متن هستند
#پاسخ ممکن است شامل چندین خط باشد. در این صورت، آخرین خط شامل یک نقطهٔ کامل است
#خطوط متن مبادله شده باید با کاراکترهای RC (#13) و LF (#10) پایان یابند.
# 

#پیکربندی برنامه بازیابی می‌شود
import config
config = config.configure()

#صندوق‌های پستی را یکی‌یکی پردازش می‌کند
for mailbox in config['mailboxes']:
    try:
        #خروجی کنسول
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # خواندن صندوق پستی در حالت تفصیلی
        readmails(mailbox, True)
        # پایان
        print("Lecture terminée...")
    except BaseException as erreur:
        #خطا را نمایش می‌دهد
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass

نظرات

همان‌طور که گفته شد، [pop3/01/main.py] نسخه‌ای از اسکریپت [smtp/01/main.py] است که قبلاً در مورد آن بحث کرده‌ایم. ما فقط به تفاوت‌های اصلی اشاره خواهیم کرد:

  • خط ۶۴: تابع [readmails] مسئول خواندن ایمیل‌ها از یک صندوق پستی است. جزئیات لازم برای اتصال به این صندوق پستی در دیکشنری [mailbox] قرار دارد. پارامتر دوم، [Trueپارامتر [Verbose] است که در این مورد، نظارت بر مبادلات کلاینت/سرور را درخواست می‌کند؛

تابع [readmails] به شرح زیر است:


# -----------------------------------------------------------------------
def readmails(mailbox: dict, verbose: bool):
    #ایمیل‌ها را از صندوق پستی توصیف‌شده توسط دیکشنری [mailbox] می‌خواند
    # اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند

    #پارامترهای صندوق پستی ایزوله شده‌اند
    #فرض می‌کند که فرهنگ لغت [mailbox] معتبر است
    server = mailbox['server']
    port = int(mailbox['port'])
    user = mailbox['user']
    password = mailbox['password']
    maxmails = mailbox['maxmails']
    delete = mailbox['delete']
    timeout = mailbox['timeout']

    # اجازه داده می‌شود که خطاهای سیستم گزارش شوند
    connexion = None
    try:
        # باز کردن یک اتصال روی پورت [port] از [server] با تایم‌اوت یک ثانیه‌ای
        connexion = socket.create_connection((server, port), timeout=timeout)

        # این اتصال نمایانگر یک جریان ارتباطی دوطرفه است
        # بین مشتری (این برنامه) و سرور POP3 تماس‌گرفته شده
        #این کانال برای تبادل دستورات و اطلاعات استفاده می‌شود

        # خواندن پیام خوش‌آمدگویی
        send_command(mailbox, connexion, "", verbose, True)
        # فرمان USER
        send_command(mailbox, connexion, f"USER {user}", verbose, True)
        # فرمان PASS
        send_command(mailbox, connexion, f"PASS {password}", verbose, True)
        # دستور LIST
        première_ligne = send_command(mailbox, connexion, "LIST", verbose, True)
        # اولین خط را تحلیل کنید تا تعداد پیام‌ها را تعیین کنید
        match = re.match(r"^\+OK (\d+)", première_ligne)
        nbmessages = int(match.groups()[0])
        # حلقه روی پیام‌ها
        imessage = 0
        while imessage < nbmessages and imessage < maxmails:
            # دستور RETR
            send_command(mailbox, connexion, f"RETR {imessage + 1}", verbose, True)
            # دستور DELE
            if delete:
                send_command(mailbox, connexion, f"DELE {imessage + 1}", verbose, True)
            # پیام بعدی
            imessage += 1
        # فرمان QUIT
        send_command(mailbox, connexion, "QUIT", verbose, True)
        # پایان
    finally:
        # اتصال بسته شد
        if connexion:
            connexion.close()

نظرات

  • خطوط ۸–۱۴: جزئیات پیکربندی برای صندوق پستی مورد دسترسی بازیابی می‌شوند؛
  • خطوط ۱۹–۲۰: یک اتصال با سرور POP3 برقرار می‌شود؛
  • خطوط ۲۶–۲۷: پیام خوش‌آمدگویی ارسال‌شده توسط سرور خوانده می‌شود؛
  • خطوط ۲۸–۲۹: دستور [USER] ارسال می‌شود تا کاربر مورد نظر برای ایمیل‌ها شناسایی شود؛
  • خطوط ۳۰–۳۱: دستور [PASS] برای ارائه رمز عبور آن کاربر ارسال می‌شود؛
  • خطوط ۳۲–۳۳: دستور [LIST] ارسال می‌شود تا مشخص شود چند ایمیل در صندوق پستی آن کاربر وجود دارد. تابع [sendCommand] خط اول پاسخ سرور را بازمی‌گرداند. در این خط، سرور تعداد پیام‌های موجود در صندوق پستی را نشان می‌دهد؛
  • خطوط ۳۴–۳۶: تعداد پیام‌ها از خط اول پاسخ استخراج می‌شود؛
  • خطوط ۳۹–۴۶: ما روی هر یک از پیام‌ها حلقه می‌زنیم. برای هر کدام، دو دستور صادر می‌کنیم:
    • RETR i: برای بازیابی پیام شماره i (خطوط 40–41);
    • DELE i: برای حذف آن اگر پیکربندی ایجاب کند که پیام‌های خوانده‌شده از سرور حذف شوند (خطوط ۴۳–۴۴);
  • خطوط ۴۷–۴۸: فرمان [QUIT] ارسال می‌شود تا به سرور اطلاع دهد که فرآیند کامل شده است؛

تابع [send_command] به شرح زیر است:


# --------------------------------------------------------------------------
def send_command(mailbox: dict, connexion: socket, commande: str, verbose: bool, with_rclf: bool) -> str:
    # دستور را به کانال اتصال ارسال می‌کند
    # حالت تفصیلی اگر verbose=True
    # اگر with_rclf=True باشد، توالی rclf را به تبادل اضافه می‌کند
    #اولین خط پاسخ را بازمی‌گرداند

    # کاراکتر پایان خط
    if with_rclf:
        rclf = "\r\n"
    else:
        rclf = ""
    # دستور ارسال را ارسال می‌کند اگر خالی نباشد
    if commande:
        connexion.send(bytearray(f"{commande}{rclf}", 'utf-8'))
        #اکو احتمالی
        if verbose:
            affiche(commande, 1)
    # ساکت را طوری بخوانید که گویی یک فایل متنی است
    encoding = f"{mailbox['encoding']}" if mailbox['encoding'] else None
    file = connexion.makefile(encoding=encoding)
    # این فایل را خط به خط پردازش کن
    # خواندن خط اول
    première_ligne = réponse = file.readline().strip()
    #حالت تفصیلی؟
    if verbose:
        affiche(première_ligne, 2)
    #بازیابی کد خطا
    code_erreur = réponse[0]
    if code_erreur == "-":
        # یک خطا رخ داده است
        raise BaseException(réponse[5:])
    #حالت ویژه برای پاسخ‌های چندخطی: LIST, RETR
    cmd = commande.lower()[0:4]
    if cmd == "list" or cmd == "retr":
        # آخرین خط پاسخ؟
        dernière_ligne = False
        while not dernière_ligne:
            # خط بعدی را بخوان
            ligne_suivante = file.readline().strip()
            # حالت تفصیلی؟
            if verbose:
                affiche(ligne_suivante, 2)
            # آخرین خط؟
            dernière_ligne = ligne_suivante == "."
    # تمام شد – بازگشت خط اول
    return première_ligne

توضیحات

  • خطوط ۱۳–۱۸: دستور [command] تنها در صورتی به سرور POP3 ارسال می‌شود که خالی نباشد. این کار برای خواندن پیام خوش‌آمدگویی ارسال‌شده توسط سرور POP3 ضروری است، حتی اگر کلاینت هنوز هیچ دستوری ارسال نکرده باشد؛
  • خطوط ۱۹–۲۱: ساکت طوری خوانده می‌شود که گویی یک فایل متنی است. این کار به ما امکان می‌دهد از روش [readline] (خط ۲۴) استفاده کنیم و بدین ترتیب پیام را خط به خط بخوانیم. ما از کلید [encoding] در فرهنگ لغت [mailbox] برای مشخص کردن رمزگذاری خطوطی که باید خوانده شوند، استفاده می‌کنیم؛
  • خط ۲۴: اولین خط پاسخ را می‌خوانیم؛
  • خطوط ۲۸–۳۲: ما مورد خطای احتمالی را مدیریت می‌کنیم. این‌ها از نوع [-ERR invalid password, -ERR mailbox unknown, -ERR unable to lock mailbox…] هستند؛
  • خط ۳۲: یک استثنا با پیام خطا پرتاب می‌شود؛
  • خط ۳۵: تنها دستورات به شکل [list, retr] می‌توانند پاسخ‌های چندخطی داشته باشند؛
  • خطوط ۳۶–۴۵: در صورت پاسخ چندخطی، تمام خطوط دریافتی (خطوط ۴۲–۴۳) را نمایش می‌دهیم تا آخرین خط دریافت شود (خط ۴۵)؛
  • خط ۴۶: اولین خط خوانده شده بازگردانده می‌شود، همانند مورد دستور [LIST]، که حاوی تعداد پیام‌های موجود در صندوق پستی است؛

نتایج

بیایید مثال قبلی را در نظر بگیریم. با استفاده از Thunderbird، پیام زیر را برای کاربر [guest@localhost] ارسال کردیم (سرور hMailServer باید در حال اجرا باشد):

Image

هنگام اجرا، نتایج زیر به دست می‌آید:


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
  • خطوط ۱۵–۳۱: پیام ارسال‌شده به [guest@localhost] به درستی بازیابی می‌شود.

این یک کلاینت پایه POP3 است که فاقد برخی قابلیت‌ها است:

  1. توانایی برقراری ارتباط با یک سرور امن POP3؛
  2. توانایی خواندن پیوست‌های یک پیام؛

ما قصد داریم این دو قابلیت را با استفاده از یک اسکریپت جدید پیاده‌سازی کنیم که این بار پیچیده‌تر خواهد بود.

21.6.4. اسکریپت‌های [pop3/02]: کلاینت POP3 با ماژول‌های [poplib] و [email]

ما یک کلاینت POP3 برای مدیریت پیوست‌ها و برقراری ارتباط با سرورهای امن خواهیم نوشت. علاوه بر این، پیام‌ها و پیوست‌های آن‌ها را در فایل‌ها ذخیره خواهیم کرد.

ما از دو ماژول پایتون استفاده خواهیم کرد:

  • [poplib]: که پروتکل POP3 را مدیریت خواهد کرد؛
  • [email]: که شامل زیرماژول‌های متعددی است که به ما امکان تحلیل پیام‌های دریافتی را می‌دهد. هر پیام یک رشته ساختاریافته است که شامل:
    • سربرگ‌های پیام [From, To, Subject, Return-Path…]؛
    • متن پیام و، در صورت لزوم، نسخه‌های HTML؛
    • هر فایل پیوست؛

Image

اسکریپت [inet/pop3/02/main] [1] توسط فایل [inet/pop3/02/config] [2] پیکربندی شده و از ماژول [inet/shared/mail_parser] [3].

فایل [pop3/02/config] به شرح زیر است:


import os


def configure() -> dict:
    # پیکربندی برنامه
    config = {
        # فهرست صندوق‌های پستی برای مدیریت
        "mailboxes": [
            # سرور: سرور POP3
            # پورت: پورت سرور POP3
            # کاربر: کاربری که می‌خواهید پیام‌های او را بخوانید
            # رمز عبور: رمز عبور آنها
            # maxmails: حداکثر تعداد ایمیل‌هایی که باید دانلود شوند
            # timeout: حداکثر زمان انتظار برای دریافت پاسخ از سرور
            # delete: اگر می‌خواهید پیام‌های دانلود شده از سرور حذف شوند، روی true تنظیم کنید
            # ssl: اگر ایمیل‌ها از طریق اتصال امن بازیابی می‌شوند، روی true تنظیم کنید
            # خروجی: پوشه‌ای که پیام‌های دانلود شده در آن ذخیره می‌شوند

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # مسیر مطلق پوشه اسکریپت
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #مسیرهای مطلق پوشه‌هایی که باید در syspath گنجانده شوند
    absolute_dependencies = [
        # پوشه محلی
        f"{script_dir}/../../shared",
   ]

    #پیکربندی syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #پیکربندی رندر می‌شود
    return config

این فایل فهرست صندوق‌های پستی قابل بررسی را تعریف می‌کند و مسیر پایتون برنامه را تنظیم می‌کند.

در اینجا تنها یک صندوق پستی وجود دارد:

  • خطوط 22–23: نام کاربری که می‌خواهیم ایمیل‌های او را بخوانیم؛
  • خطوط ۲۰–۲۱: نام و پورت سرور POP3 که ایمیل‌های این کاربر را ذخیره می‌کند؛
  • خط ۲۴: حداکثر تعداد ایمیل‌هایی که باید بازیابی شوند. در واقع، اگر این اسکریپت را روی صندوق پستی خود اجرا کنید، احتمالاً نخواهید خواست صدها ایمیل موجود در آن را بازیابی کنید؛
  • خط ۲۵: یک مقدار بولی که نشان می‌دهد آیا یک ایمیل پس از خوانده شدن باید حذف شود (حذف=True
  • خط ۲۶: تنظیم ویژگی [ssl] به مقدار True به این معنی است که سرور POP3 که در خطوط ۲۰–۲۱ تعریف شده است، از یک اتصال رمزگذاری‌شده استفاده می‌کند؛
  • خط ۲۷: حداکثر زمان انتظار برای پاسخ‌های سرور، بیان‌شده به ثانیه؛
  • خط ۲۸: پوشه‌ای که ایمیل‌های خوانده شده در آن ذخیره می‌شوند. اگر این پوشه وجود نداشته باشد، ایجاد خواهد شد. نام داده‌شده در اینجا نسبی است. هنگامی که اسکریپت اجرا می‌شود، این نام نسبت به پوشه‌ای که اسکریپت را از آنجا اجرا می‌کنید، نسبی خواهد بود. با [Pycharm]، این پوشه همان پوشه اسکریپت [pop3/02] خواهد بود؛

اسکریپت [pop3/02/main] به شرح زیر است:


# واردات‌ها
import email
import os
import poplib
import shutil


# خواندن یک صندوق پستی
def readmails(mailbox: dict, verbose: bool):
    # پست‌صندوق توصیف‌شده توسط دیکشنری [mailbox] را می‌خواند
    # اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند


# main ----------------------------------------------------------------
#   POP3 کلاینت (پروتکل دفتر پستی) برای خواندن ایمیل‌ها

#پیکربندی برنامه را بازیابی می‌کند
import config
config = config.configure()

#صندوق‌های پستی را یکی‌یکی پردازش می‌کند
for mailbox in config['mailboxes']:
    try:
        #خروجی کنسول
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']}@{mailbox['server']}:{mailbox['port']}")
        # پست‌خانه را در حالت تفصیلی می‌خواند
        readmails(mailbox, True)
        # پایان
        print("Lecture terminée...")
    except BaseException as erreur:
        # نمایش خطا
        print(f"L'erreur suivante s'est produite : {erreur}")
    finally:
        pass
  • خطوط 17–36: بخش [main] اسکریپت مشابه بخش [pop3/01] اسکریپت است؛

تابع [readmails] به شرح زیر است:


# یک صندوق پستی را می‌خواند
def readmails(mailbox: dict, verbose: bool):
    # پست‌صوتی توصیف‌شده توسط فرهنگ لغت [mailbox] را می‌خواند
    # اگر verbose=True باشد، تعاملات کلاینت-سرور را ثبت می‌کند

    # واردات از mail_parser
    from mail_parser import save_message

    #تنظیمات حساب ایمیل را ایزوله می‌کند
    #فرض می‌کند که دیکشنری [mailbox] معتبر است
    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']

    # اجازه داده می‌شود که خطاهای سیستمی گزارش شوند
    pop3 = None
    try:
        # در صورت عدم وجود، پوشه‌های ذخیره‌سازی را ایجاد کنید
        if not os.path.isdir(output):
            os.mkdir(output)
        #کاربر
        dir2 = f"{output}/{user}"
        #پوشه [dir2] را اگر وجود داشته باشد حذف کرده، سپس دوباره آن را ایجاد کنید
        if os.path.isdir(dir2):
            # حذف
            shutil.rmtree(dir2)
        # ایجاد
        os.mkdir(dir2)
        #یک اتصال را روی پورت [port] از [server] باز می‌کند
        if ssl:
            pop3 = poplib.POP3_SSL(server, port, timeout=timeout)
        else:
            pop3 = poplib.POP3(server, port, timeout=timeout)

        # اتصال نشان‌دهنده یک جریان ارتباط دوطرفه است
        # بین کلاینت (این برنامه) و سرور POP3 مورد تماس
        #این کانال برای تبادل دستورات و اطلاعات استفاده می‌شود

        #حالت تفصیلی
        pop3.set_debuglevel(2 if verbose else 0)
        # خواندن پیام خوش‌آمدگویی
        pop3.getwelcome(    )
        # فرمان USER
        réponse = pop3.user(user)
        # فرمان PASS
        réponse = pop3.pass_(password)
        # فرمان LIST
        liste = pop3.list()
        #ایمیل‌ها در لیست هستند [1]
        imail = 0
        nb_mails = len(liste[1])
        fini = imail == maxmails or imail == nb_mails
        éléments = liste[1]
        while not fini:
            # عنصر فعلی
            élément = éléments[imail]
            #عنصر فهرستی از بایت‌ها است که به یک رشته رمزگشایی می‌شود
            desc = élément.decode()
            # ما یک رشته داریم که با فاصله از هم جدا شده است
            #عنصر اول شماره پیام است
            num = desc.split()[0]
            #پیام بازیابی می‌شود
            message = pop3.retr(int(num))
            #خطوط پیام در `message [1]` قرار دارند
            str_message = ""
            for ligne in message[1]:
                #یک خط توالی بایت‌هایی است که به یک رشته رمزگشایی می‌شود
                str_message += f"{ligne.decode()}\r\n"
            #پوشه پیام
            dir3 = f"{dir2}/message_{num}"
            # اگر پوشه وجود نداشته باشد، آن را ایجاد می‌کنیم
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            #موضوع email.message.Message
            save_message(dir3, email.message_from_string(str_message), 0)
            # ایمیل دیگر
            imail += 1
            #آیا به محدودیت رسیده‌ایم؟
            fini = imail == maxmails or imail == nb_mails

        # order QUIT
        pop3.quit()
    finally:
        #خروج
        if pop3:
            pop3.close()

توضیحات

  • خطوط ۶–۷: تابع [mail_parser.save_message] که در خط ۸۰ استفاده شده، وارد شده است؛
  • کد این تابع در داخل یک `try` (خط ۲۲) و `finally` (خط ۸۸) قرار گرفته است. این کار تضمین می‌کند که هرگونه استثنا به کد اصلی منتقل شده و توسط آن گرفته و نمایش داده شود؛
  • خطوط ۱۱–۱۸: جزئیات پیکربندی صندوق پستی بازیابی می‌شوند؛
  • خطوط 23–33: تمام پیام‌ها در پوشه [output/user] ذخیره خواهند شد، که در آن [output] و [user] در پیکربندی تعریف شده‌اند. بنابراین ابتدا پوشه‌های [output] و سپس [output/user] را به ترتیب ایجاد می‌کنیم. برای ایجاد مورد دوم، ابتدا از خط ۳۱ حذف می‌شود. [shutil] ماژولی است که باید وارد شود. [shutil.rmtree(dir)] پوشه [dir] و تمام محتویات آن را حذف می‌کند؛
  • برای تمام عملیات روی فایل‌های سیستمی، از ماژول [os] استفاده کنید که آن نیز باید وارد شود؛
  • خطوط ۳۴–۳۸: یک اتصال با سرور POP3 برقرار می‌شود. اگر سرور امن باشد، از کلاس [poplib.POP3_SSL] استفاده می‌شود؛ در غیر این صورت، از کلاس [poplib.POP3] استفاده می‌شود. ویژگی [ssl] که در خط ۳۵ استفاده شده، از پیکربندی صندوق پستی گرفته می‌شود؛
  • خط ۴۵: یک سطح لاگ‌گیری تنظیم می‌شود:
    • 0: بدون گزارش؛
    • ۱: دستورات صادر شده توسط کلاینت POP3 ثبت می‌شوند؛
    • ۲: گزارش‌های تفصیلی. ما همچنین می‌توانیم ببینیم که کلاینت POP3 چه چیزی دریافت می‌کند؛
  • خط ۴۷: پس از اتصال، سرور POP3 یک پیام خوش‌آمدگویی ارسال می‌کند. ما می‌توانیم این پیام را بخوانیم؛
  • خطوط ۴۸–۴۹: فرمان USER از پروتکل POP3;
  • خطوط ۵۰–۵۱: فرمان PASS از پروتکل POP3؛
  • خطوط ۵۲–۵۳: فرمان LIST از پروتکل POP3. پاسخ یک تپل (response, ['mesg_num octets'…], bytes) است، برای مثال list=(b'+OK 3 messages (3859 bytes)', [b'1 584', b'2 550', b'3 2725'], 22). می‌توانیم ببینیم که دو عنصر اول این تِپل، بایت هستند (که با 'b' پیش‌وند شده‌اند). `liste[1]` یک آرایه است که هر عنصر آن یک دنباله بایت حاوی دو بخش اطلاعات است: شماره پیام و اندازه آن به بایت؛
  • خط ۵۶: از موارد فوق، می‌توان نتیجه گرفت که تعداد پیام‌ها در صندوق پستی را می‌توان از طریق [email.message_from_bytes(data2[0][1])] به دست آورد؛
  • خطوط 59–84: ما روی هر یک از پیام‌ها حلقه می‌زنیم. وقتی همه آن‌ها خوانده شوند یا به حداکثر تعداد ایمیل‌های تعیین‌شده در پیکربندی رسیدیم، متوقف می‌شویم؛
  • خط ۶۱: عنصر فعلی آرایه `liste[1]`، یعنی چیزی شبیه به `b'1 584'`، یک دنباله بایت؛
  • خط ۶۳: ما توالی بایت‌ها را به یک رشتهٔ کاراکتری تبدیل می‌کنیم. اکنون ما رشتهٔ '1 584' را داریم؛
  • خط ۶۶: شماره پیام را بازیابی می‌کنیم، در این مورد رشته '1';
  • خط ۶۸: دستور POP3 RETR num صادر می‌شود. پاسخی با شکل زیر دریافت می‌شود:

[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)]
  • (ادامه)
    • پیام یک تِپل سه‌عنصری است؛
    • message[1] یک آرایه از خطوط است. هر خط یک دنباله بایت (با پیشوند «b») است. پیام کامل از این مجموعه خطوط تشکیل می‌شود؛
    • [Return-Path, Received, To, Subject, Message-ID, Content-Type, Content-Transfer-Encoding, Content-Language] سربرگ‌های پیام هستند. هر کدام اطلاعاتی در مورد پیام دریافت‌شده ارائه می‌دهند. این اطلاعات به ما امکان می‌دهد تا بدنه پیام (عنصر دوم از آخر آرایه message[1]) را بازیابی کنیم؛
  • خطوط ۷۱–۷۳: ما رشته [strMessage] را که شامل تمام خطوط پیام است، ایجاد می‌کنیم. اکنون پیام را به شکل یک رشته کاراکتری داریم. این پیام ممکن است حاوی پیام‌های دیگر و همچنین پیوست‌ها باشد. این به آن دلیل است که پیوست‌ها به صورت رشته‌های کاراکتری ذخیره می‌شوند. بنابراین یک نکته کلیدی که باید به خاطر بسپاریم این است که یک ایمیل در ابتدا یک رشته از کاراکترها است و این رشته از کاراکترهاست که باید تحلیل شود تا پیوست‌ها، هر پیام نهفته دیگر و البته متن پیام - آنچه فرستنده نوشته است - از آن استخراج شود؛
  • خطوط ۷۴–۷۸: ما بدنه پیام و پیوست‌های پیام را در پوشه [dir3] ذخیره خواهیم کرد؛
  • خطوط ۷۹–۸۰: ما تحلیل پیام را به تابع [save_message] واگذار می‌کنیم:
    • پارامتر اول [dir3] است، پوشه‌ای که محتوای پیام در آن ذخیره می‌شود؛
    • پارامتر دوم از نوع [email.message.Message] است. این شیء متدهایی را برای بازیابی بخش‌های مختلف پیام (بدنه، پیوست‌ها) و همچنین تمامی سربرگ‌های آن فراهم می‌کند. برای دسترسی به این شیء باید ماژول [email] را وارد کنید. تابع [email.message_from_string] به شما امکان می‌دهد یک شیء [email.message.Message] را از رشتهٔ کاراکتری پیام بسازید؛

تابع [save_message] بخشی از ماژول [mail_parser] است:

Image

ماژول [mail_parser] در خطوط ۶–۷ تابع [readmails] وارد شده است؛

در [mail_parser.py]، تابع [save_message] به شرح زیر است:


# واردات‌ها
import codecs
import email.contentmanager
import email.header
import email.iterators
import email.message
import os


#ذخیره کردن پیامی از نوع QZXW2HTMLCZW1haWwubWVzc2FnZS.NZXNzYWdlZQX
#این تابع را می‌توان به‌صورت بازگشتی فراخوانی کرد
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #خروجی: پوشه پشتیبان پیام
    #email_message: پیامی که باید ذخیره شود
    #irfc822: شمارهٔ فعلی در توالی ایمیل‌های پیوست‌شده
    #
    # بدنه پیام
    part = email_message
    # سربرگ‌ها [From, To, Subject] در یکی از بخش‌های چندبخشی یافت می‌شوند
    # یا در بخشی [text/*] وقتی هیچ بخشی [multipart] وجود ندارد
    keys = part.keys()
    # 'From' باید در سربرگ‌ها موجود باشد؛ در غیر این صورت، بخش حاوی سربرگ‌هایی که به دنبالشان هستیم نخواهد بود
    if "From" in keys:
        # ما برخی سربرگ‌ها را بازیابی می‌کنیم
        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'))}"]
        # سربرگ‌ها را در یک فایل متنی ذخیره می‌کند
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # نوشتن در فایل
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    #نوع رکورد [part]
    main_type = part.get_content_maintype()

توضیحات

  • خط ۱۲: این تابع حداکثر سه پارامتر را می‌پذیرد:
  • [output]: پوشه‌ای که پیام در آن ذخیره می‌شود (پارامتر دوم)؛
  • [email_message]: پیامی از نوع [email.message.Message]. این نوع یک نوع ساختاریافته است. این نوع شامل متن ایمیل و همچنین تمام فایل‌های پیوست شده است و روش‌هایی را برای بازیابی عناصر مختلف آن فراهم می‌کند؛
  • [irfc822]: این پارامتر برای شماره‌گذاری ایمیل‌های گنجانده‌شده در [email_message] استفاده می‌شود؛
  • خط ۱۸: شیء [email_message] در داخل [part] قرار می‌گیرد. نوع [email.message.Message] شامل بخش‌هایی از نوع [part] (متن پیام، پیوست‌ها، ایمیل‌های دربرگرفته شده) است که آن‌ها نیز از نوع [email.message.Message] هستند. هر بخش [part] ممکن است زیر‌بخش‌هایی داشته باشد. بنابراین، نوع [email.message.Message] درختی از عناصر نوع [email.message.Message] است:
    • [part.ismultipart()] برابر با [True] است اگر بخش [part] دارای زیربخش‌ها باشد. این زیربخش‌ها سپس از طریق [part.get_payload()] قابل دسترسی هستند؛
    • وقتی [part.ismultipart()] برابر با [False] باشد، این بدان معناست که به یک گره برگ در درخت پیام اولیه رسیده‌ایم: این ممکن است باشد:
      • بدنه پیام به صورت متن ساده؛
      • بدنه پیام به شکل متن HTML؛
      • یک پیوست (به استثنای یک پیام دربرگرفته شده، که در آن [part.ismultipart()] معادل [True] است)؛
  • به دلیل ماهیت درختی پارامتر [email.message.Message]، تابع [save_message] به صورت بازگشتی فراخوانی خواهد شد. تکرار زمانی متوقف می‌شود که به برگ‌های درخت رسید، یعنی بخشی از [part] که در آن [part.ismultipart()] برابر [False] باشد؛
  • خط ۲۱: ما کلیدها (یا سربرگ‌ها) پیام در حال تحلیل را درخواست می‌کنیم (که به دلیل بازگشتی بودن، ممکن است زیربخشی از پیام اولیه باشد)؛
  • خطوط ۲۳–۳۵: ما می‌خواهیم سربرگ‌ها را ثبت کنیم:
    • [From]: فرستنده پیام؛
    • [To]: گیرنده پیام؛
    • [Subject]: موضوع پیام؛
    • [Return-Path]: گیرنده‌ای که در صورت لزوم پاسخ باید برای او ارسال شود. این اطلاعات همیشه در [From] گنجانده نمی‌شود؛
    • [User-Agent]: کلاینت POP3 که با سرور POP3 ارتباط برقرار می‌کند؛
    • [Date]: تاریخ ارسال ایمیل؛
  • خط ۲۳: تنها یکی از بخش‌های پیام حاوی این سربرگ‌ها است. برای بخش‌های دیگر، کد در خطوط ۲۳–۳۵ نادیده گرفته می‌شود؛
  • خطوط ۲۵–۳۰: لیستی شامل شش سربرگ ایجاد می‌شود؛
  • خط ۲۵: بیایید اولین هدر را تحلیل کنیم:
    • [part.get(key)] هدر مرتبط با کلید [key] را فراهم می‌کند؛
    • ممکن است این هدر رمزگذاری شده باشد. اگر رمزگذاری UTF-8 نباشد، هدر رمزگشایی شده و سپس با استفاده از تابع [decode_header] مجدداً به UTF-8 رمزگذاری می‌شود؛
    • سربرگ اول به شکل [From: pymail2lexemple@gmail.com] خواهد بود؛
  • خطوط ۳۱–۳۵: سربرگ‌ها در فایل [output/headers.txt] ذخیره می‌شوند؛

تابع [decode_header] به شرح زیر است (هنوز در داخل [mail_parser.py]):


# رمزگشایی سربرگ‌ها
def decode_header(header: object) -> str:
    # سربرگ رمزگشایی می‌شود
    header = email.header.decode_header(f"{header}")
    # نتیجه یک آرایه است – در اینجا فقط شامل یک عنصر از نوع (سربرگ، رمزگذاری) خواهد بود
    # اگر encoding==None باشد، آنگاه header یک رشته است
    #در غیر این صورت، یک لیست بایت است که با استفاده از encoding رمزگذاری شده است
    header, encoding = header[0]
    if not encoding:
        # اگر رمزگذاری وجود نداشته باشد
        return header
    else:
        # اگر رمزگذاری مشخص شده باشد، رمزگشایی کن
        return header.decode(encoding)

توضیحات

  • خط ۴: سربرگ رمزگشایی می‌شود:
    • ماژول [email.header] باید وارد شود؛
    • یک لیست از توپل‌های [(header1,encoding1) , (header2, encoding2)…] به دست می‌آید؛
    • برای سربرگ‌های [From, To, Subject, Return-Path, Dateلیست تنها شامل یک عنصر خواهد بود؛
    • خط ۸: هدر واحد و رمزگذاری آن را بازیابی کنید:
      • اگر [encoding==None] باشد، آنگاه [header] سربرگ را به صورت یک رشتهٔ کاراکتری در بر می‌گیرد؛
      • در غیر این صورت، [header] دنباله‌ای از بایت‌ها است که نمایانگر سربرگ رمزگذاری‌شده می‌باشد؛
  • خطوط ۱۰–۱۱: اگر رمزگذاری وجود نداشته باشد، سربرگ بازگردانده می‌شود؛
  • خطوط ۱۲–۱۴: اگر رمزگذاری انجام شده باشد، آنگاه دنباله بایت‌های بازیابی‌شده به یک رشته کاراکتری رمزگشایی شده و این مقدار بازگردانده می‌شود؛

بیایید به تابع [save_message] بازگردیم:


#ذخیره کردن پیامی از نوع QZXW2HTMLCZW1haWwubWVzc2FnZS.NZXNzYWdlZQX
#این تابع می‌تواند به‌صورت بازگشتی فراخوانی شود
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #خروجی: پوشه پشتیبان پیام
    #email_message: پیامی که باید ذخیره شود
    #irfc822: شمارهٔ فعلی در توالی ایمیل‌های پیوست‌شده
    #
    # بدنه پیام
    part = email_message
    # سربرگ‌ها [From, To, Subject] در یکی از بخش‌های چندبخشی یافت می‌شوند
    # یا در بخشی [text/*] وقتی هیچ بخشی [multipart] وجود ندارد
    keys = part.keys()
    # 'From' باید در سربرگ‌ها موجود باشد؛ در غیر این صورت، بخش حاوی سربرگ‌هایی که به دنبالشان هستیم نیست
    if "From" in keys:
        # ما برخی سربرگ‌ها را بازیابی می‌کنیم
        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'))}"]
        # سربرگ‌ها را در یک فایل متنی ذخیره می‌کند
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # نوشتن در فایل
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # نوع پیام [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # اگر پیام از نوع text/plain باشد
    if type_of_part == "text/plain":
        # پیام متنی
        save_textmessage(output, part, 0)

    # اگر پیام از نوع text/html باشد
    elif type_of_part == "text/html":
        # پیام HTML
        save_textmessage(output, part, 1)

    # اگر پیام حاوی بخش‌ها باشد
    elif part.is_multipart():
        
    else:
        
    # سایر بخش‌ها نادیده گرفته می‌شوند (نه text/plain، نه text/html، نه پیوست)
    #مقدار فعلی irfc822 را بازمی‌گرداند (شماره‌گذاری ایمیل‌های پیوست‌شده ذخیره شده در پوشه خروجی)
    return irfc822

توضیحات

  • خطوط ۱–۲۶: سربرگ‌های پیام اولیه پردازش شده‌اند؛
  • خطوط ۲۸–۳۱: بخش‌های یک پیام از نوع [email.message.Message] دارای یک نوع اصلی و یک زیرنوع هستند. ما آن‌ها را بازیابی می‌کنیم؛
  • خطوط ۳۲–۳۵: اگر بخش پردازش‌شده از نوع [text/plain] باشد، در این صورت به یک گره برگ در درخت پیام اولیه رسیده‌ایم. این همان متنی است که فرستنده در پیام خود نوشته است؛
  • خط ۳۵: این متن در یک فایل نوشته می‌شود:
    • پارامتر اول، [outputپوشه‌ای است که متن در آن ذخیره می‌شود؛
    • پارامتر دوم بخشی از پیام است که حاوی متنی است که باید ذخیره شود؛
    • پارامتر سوم عدد ۰ برای ذخیره متن عادی و عدد ۱ برای متن HTML است؛
  • خطوط ۳۷–۴۰: اگر بخش از نوع [text/html] باشد، آنگاه ما نیز به یک گره برگ در درخت پیام اولیه رسیده‌ایم. این متنی است که فرستنده در پیام خود نوشته است، این بار در قالب HTML. همهٔ کلاینت‌های ایمیل از این فرمت پشتیبانی نمی‌کنند؛

تابع [save_textmessage] به شرح زیر است:


#ذخیرهٔ یک پیام متنی
def save_textmessage(output: str, part: email.message.Message, type_of_text: int):
    # سربرگ‌ها
    headers = []
    # مجموعه کاراکتری پیام
    charset = part.get_content_charset()
    if charset is not None:
        charset = part.get_content_charset().lower()
        headers.append(f"Charset: {charset}")
    # حالت رمزگذاری محتوا
    content_transfer_encoding = part.get("Content-Transfer-Encoding")
    if content_transfer_encoding is not None:
        headers.append(f"Transfer-Content-Encoding: {content_transfer_encoding}")
    # حالت ۸ بیتی باعث مشکل شد
    if content_transfer_encoding == "8bit":
        #بازیابی پیام از ایمیل
        msg = part.get_payload()
    else:
        #بازیابی پیام ایمیل
        msg = email.contentmanager.raw_data_manager.get_content(part)
    # بسته به انواع متن
    filename = None
    if type_of_text == 0:
        #ذخیرهٔ سربرگ‌ها
        with codecs.open(f"{output}/headers.txt", "a", "utf-8") as file:
            # نوشتن در فایل
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")
        # فایل متنی برای محتوا
        filename = f"{output}/mail.txt"
    elif type_of_text == 1:
        # فایل HTML برای محتوا
        filename = f"{output}/mail.html"
    #ذخیره کردن پیام
    with codecs.open(filename, "w", "utf-8") as file:
        # نوشتن در فایل
        file.write(msg)

نظرات

  • مانند سربرگ‌ها، بدنه پیام نیز ممکن است رمزگذاری شده باشد. ممکن است دو نوع رمزگذاری وجود داشته باشد:
    • رمزگذاری اولیه متن (UTF-8، ISO-8859-1 و غیره). این همان رمزگذاری است که توسط کلاینت ایمیل فرستنده پیام استفاده شده است. این با هدر [Content-Type] در پیام دریافت‌شده شناسایی می‌شود؛
    • یک رمزگذاری دوم که متن اصلی ممکن است برای ارسال به آن تبدیل شده باشد. این مورد با هدر [Transfer-Content-Encoding] در پیام دریافت‌شده شناسایی می‌شود؛
  • خط ۶: رمزگذاری اولیه متن؛
  • خط ۱۱: رمزگذاری دوم که متن برای انتقال به گیرنده پشت سر گذاشته است؛
  • خطوط ۹ و ۱۳: این دو مورد اطلاعات در لیست [headers] قرار می‌گیرند. آنها به اطلاعات موجود در فایل [headers.txt] که سربرگ‌های خاصی از پیام را ثبت می‌کند، اضافه خواهند شد؛
  • خط ۲۰: [email.contentmanager.raw_data_manager.get_content] امکان بازیابی پیام را با رمزگذاری اصلی ۱ فراهم می‌کند. رمزگذاری ۲ حذف شده است. با این حال، شیء [email.contentmanager.raw_data_manager] تنها از دو نوع [Transfer-Content-Encoding] پشتیبانی می‌کند:
    • [quoted-printable];
    • [base64];

سایر موارد نادیده گرفته می‌شوند. با این حال، برای مثال، Thunderbird از [Transfer-Content-Encoding]، معروف به «8bit» استفاده می‌کند. این رمزگذاری نادیده گرفته می‌شود و پیام‌هایی که حاوی کاراکترهای دارای نقطه هستند، به هم می‌ریزند. سپس می‌توان پیام را با استفاده از روش [part.get_payload()] (خطوط 15–17) بازیابی کرد؛

  • خط ۲۱: در این مرحله، کدگذاری انتقالی پیام حذف شده است، بنابراین این همان پیامی است که توسط فرستنده نوشته شده است؛
  • خطوط ۲۲–۳۷: این موردی است که در آن یک پیام متنی نیاز به ذخیره شدن دارد؛
    • خطوط ۲۴–۲۸: دو هدر که در خطوط ۹ و ۱۳ ایجاد شده‌اند، در فایل [headers.txt] ذخیره می‌شوند. این فایل از قبل وجود دارد و حاوی هدرها است. بنابراین از حالت «a» (خط 25) برای باز کردن این فایل استفاده می‌کنیم. «a» مخفف «append» (ضمیمه) است و سربرگ‌های جدید (در انتهای فایل) به محتوای موجود فایل [headers.txt] اضافه می‌شوند؛
    • خط ۳۰: نام فایلی که پیام متنی در آن ذخیره می‌شود؛
    • خط ۳۳: نام فایلی که پیام HTML در آن ذخیره می‌شود؛
    • خطوط ۳۴–۳۷: متن UTF-8 در یک فایل ذخیره می‌شود؛

بیایید به تابع [save_message] بازگردیم:


#ذخیره یک پیام از نوع QZXW2HTMLCZW1haWwubWVzc2FnZS.NZXNzYWdlZQX
# این تابع می‌تواند به‌صورت بازگشتی فراخوانی شود
def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #خروجی: پوشهٔ ذخیرهٔ پیام
    # email_message: پیامی که باید ذخیره شود
    #irfc822: شمارهٔ فعلی در توالی ایمیل‌های پیوست‌شده
    #
    # بدنه پیام
    part = email_message
    # سربرگ‌ها [From, To, Subject] در یکی از بخش‌های چندبخشی یافت می‌شوند
    # یا در بخشی [text/*] وقتی هیچ بخشی [multipart] وجود ندارد
    keys = part.keys()
    # 'From' باید در سربرگ‌ها موجود باشد؛ در غیر این صورت، بخش حاوی سربرگ‌هایی که به دنبالشان هستیم نخواهد بود
    if "From" in keys:
        # ما برخی سربرگ‌ها را بازیابی می‌کنیم
        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'))}"]
        # سربرگ‌ها را در یک فایل متنی ذخیره می‌کند
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # نوشتن در فایل
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    # نوع پیام [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # اگر پیام از نوع text/plain باشد
    if type_of_part == "text/plain":
        # پیام متنی
        save_textmessage(output, part, 0)

    # اگر پیام از نوع text/html باشد
    elif type_of_part == "text/html":
        # پیام HTML
        save_textmessage(output, part, 1)

    #اگر پیام حاوی بخش‌ها باشد
    elif part.is_multipart():
        # مورد ویژهٔ ایمیلی با پیوست
        if type_of_part == "message/rfc822":
            # ایجاد یک پوشه جدید «output2» برای ایمیل پیوست‌شده
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            #ذخیرهٔ زیربخش‌های پیام IRFC822 در 'output2'
            for subpart in part.get_payload():
                #در پوشهٔ جدید «irfc822»، شماره‌گذاری از 0 آغاز می‌شود
                save_message(output2, subpart, 0)

        else:
            #این یک پیوست ایمیل نیست
            # ذخیرهٔ زیربخش‌ها در پوشهٔ فعلی «output»
            #سپس irfc822 باید برای هر پیام/زیربخش rfc822 افزایش یابد
            for subpart in part.get_payload():
                # save_message آخرین مقدار irfc822 را بازمی‌گرداند
                # در صورتی که subpart="message/rfc822" باشد، ۱ افزایش می‌یابد، در غیر این صورت افزایش نمی‌یابد
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # سایر موارد (نه text/plain، نه text/html، نه multipart)
        # پیوست؟
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # سایر بخش‌ها نادیده گرفته می‌شوند (نه text/plain، نه text/html، نه پیوست)
    # روی مقدار فعلی irfc822 تنظیم می‌شود (شماره‌گذاری ایمیل‌های پیوست‌شده ذخیره شده در پوشه خروجی)
    return irfc822

توضیحات

  • خطوط ۳۳–۴۰: ما با دو حالت ممکن برای یک پیام در یک انتهای درخت پیام اولیه (بدون زیربخش) برخورد کردیم. هنوز دو حالت دیگر باقی مانده است که باید با آنها برخورد کنیم:
    • خطوط ۴۳–۶۲: موردی که بخش تحلیل‌شده خود شامل زیربخش‌هاست (part.ismultipart()==True);
    • خطوط ۶۳–۶۸: برای موارد باقی‌مانده، ما فقط موردی را مدیریت می‌کنیم که بخش تحلیل‌شده یک پیوست باشد؛

اکنون در حال رسیدگی به این مورد دوم هستیم. بار دیگر، در یکی از انتهای پیام اولیه (بدون زیربخش) قرار داریم. ما قبلاً با دو نمونه از این نوع مواجه شده‌ایم: انواع text/plain و text/html. اکنون به مورد فایل پیوست‌شده می‌پردازیم.

  • خط ۶۶: پیوست با کلید [Content-Disposition] شناسایی می‌شود؛
  • خط ۶۷: اگر این کلید وجود داشته باشد و با رشته [attachment] شروع شود، در این صورت با فایلی پیوست‌شده به پیام سروکار داریم؛
  • خط ۶۸: پیوست در پوشه [output] ذخیره می‌شود؛

تابع [save_attachment] به شرح زیر است:


#ذخیرهٔ یک پیوست
def save_attachment(output: str, part: email.message.Message):
    # نام فایل پیوست
    filename = os.path.basename(part.get_filename())

    # نام فایل ممکن است رمزگذاری شود
    # برای مثال =?utf-8?Q?Courses-Tutorials-Serge-Tah=C3=A9-1568x268=2Ep
    filename = decode_header(filename)
    # فایل پیوست ذخیره شد
    with open(f"{output}/{filename}", "wb") as file:
        file.write(part.get_payload(decode=True))
  • خط ۴: اگر [part] یک پیوست باشد، نام فایل پیوست از طریق [part.get_filename] به دست می‌آید. فقط نام فایل حفظ می‌شود، نه مسیر آن؛
  • خط ۸: نام فایل‌ها عموماً به همان روشی که سربرگ‌های پیام رمزگذاری می‌شوند، رمزگذاری می‌گردند. بنابراین، از تابع [decode_header] برای رمزگشایی آن‌ها استفاده می‌شود؛
  • خط ۱۱: محتوای فایل پیوست در حال حاضر یک رشته کاراکتری است که با رمزگذاری (اغلب Base64) محتوای اصلی فایل به صورت متن تولید شده است. برای بازیابی این محتوای اصلی، از تابع [part.get_payload(decode=True)] استفاده می‌شود. پارامتر [decode=True] نشان می‌دهد که محتویات فایل پیوست باید رمزگشایی شوند. این کار منجر به تولید یک دنباله بایت می‌شود؛
  • خط ۱۰: این دنباله بایت‌ها در فایل [output/filename] ذخیره می‌شود. حالت «wb» برای باز کردن فایل به معنای «نوشتن باینری» است؛

بیایید به کد تابع [save_message] بازگردیم:


def save_message(output: str, email_message: email.message.Message, irfc822=0) -> int:
    #خروجی: پوشه پشتیبان پیام
    #email_message: پیامی که باید ذخیره شود
    #irfc822: شمارهٔ فعلی در توالی ایمیل‌های پیوست
    #
    # بدنه پیام
    part = email_message
    # سربرگ‌ها [From, To, Subject] در یکی از بخش‌های چندبخشی یافت می‌شوند
    # یا در بخشی [text/*] وقتی هیچ بخشی [multipart] وجود ندارد
    keys = part.keys()
    # 'From' باید در سربرگ‌ها موجود باشد؛ در غیر این صورت، بخش حاوی سربرگ‌هایی که به دنبالشان هستیم نخواهد بود
    if "From" in keys:
        # ما برخی سربرگ‌ها را بازیابی می‌کنیم
        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'))}"]
        # سربرگ‌ها را در یک فایل متنی ذخیره می‌کند
        with codecs.open(f"{output}/headers.txt", "w", "utf-8") as file:
            # نوشتن در فایل
            string = '\r\n'.join(headers)
            file.write(f"{string}\r\n")

    #نوع پیام [part]
    main_type = part.get_content_maintype()
    sub_type = part.get_content_subtype()
    type_of_part = f"{main_type}/{sub_type}"
    # اگر پیام از نوع text/plain باشد
    if type_of_part == "text/plain":
        # پیام متنی
        save_textmessage(output, part, 0)

    # اگر پیام از نوع text/html باشد
    elif type_of_part == "text/html":
        # پیام HTML
        save_textmessage(output, part, 1)

    #اگر پیام حاوی بخش‌ها باشد
    elif part.is_multipart():
        # مورد ویژهٔ ایمیلی با پیوست
        if type_of_part == "message/rfc822":
            # ایجاد یک پوشه جدید «output2» برای ایمیل پیوست‌شده
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            #ذخیرهٔ زیربخش‌های پیام IRFC822 در 'output2'
            for subpart in part.get_payload():
                #در پوشهٔ جدید «irfc822»، شماره‌گذاری از 0 آغاز می‌شود
                save_message(output2, subpart, 0)

        else:
            #این یک پیوست ایمیل نیست
            # زیربخش‌ها را در پوشهٔ فعلی «output» ذخیره می‌کند
            # سپس irfc822 باید برای هر پیام/زیربخش rfc822 افزایش یابد
            for subpart in part.get_payload():
                # save_message آخرین مقدار irfc822 را بازمی‌گرداند
                # در صورتی که subpart="message/rfc822" باشد، ۱ افزایش می‌یابد، در غیر این صورت افزایش نمی‌یابد
                irfc822 = save_message(output, subpart, irfc822)
    else:
        # سایر موارد (نه text/plain، نه text/html، نه multipart)
        # پیوست؟
        disposition = part.get('Content-Disposition')
        if disposition and disposition.startswith('attachment'):
            save_attachment(output, part)
    # سایر بخش‌ها نادیده گرفته می‌شوند (نه text/plain، نه text/html، نه پیوست)
    # روی مقدار فعلی irfc822 تنظیم می‌شود (شماره‌گذاری ایمیل‌های پیوست‌شده در پوشه خروجی ذخیره می‌شود)
    return irfc822

توضیحات

  • ما مواردی را که درخت پیام اولیه در آنها پایان می‌یابد، بررسی کرده‌ایم: بخش‌های [text/plain, text/html et Content-Disposition=attachment;…]. اکنون باید به موردی بپردازیم که بخش تحلیل‌شده یک ظرف (container) از بخش‌ها است، یعنی شامل زیربخش‌های [part.is_multipart()==True] در خط 41 می‌باشد. بنابراین برای رسیدن به گره‌های برگ درخت پیام، باید این زیربخش‌ها را تجزیه کنیم؛
  • خط ۴۳: ما موردی را که بخش تحلیل‌شده دارای نوع [message/rfc822] است، به روشی خاص مدیریت می‌کنیم. این نوع یک ایمیل است. بنابراین این موردی است که یک ایمیل، ایمیل دیگری را به عنوان پیوست دارد؛

کد به شرح زیر است:


    # اگر پیام حاوی بخش‌ها باشد
    elif part.is_multipart():
        #حالت ویژهٔ ایمیل پیوست
        if type_of_part == "message/rfc822":
            #یک پوشه جدید «output2» برای ایمیل پیوست‌شده ایجاد می‌کند
            irfc822 += 1
            output2 = f"{output}/rfc822_{irfc822}"
            os.mkdir(output2)
            #ذخیرهٔ زیربخش‌های پیام IRFC822 در 'output2'
            for subpart in part.get_payload():
                #در پوشهٔ جدید «irfc822»، شماره‌گذاری از 0 آغاز می‌شود
                save_message(output2, subpart, 0)

        else:
            #این یک پیوست ایمیل نیست
            # زیربخش‌ها را در پوشهٔ فعلی «output» ذخیره می‌کند
            # سپس irfc822 باید برای هر پیام/زیربخش rfc822 افزایش یابد
            for subpart in part.get_payload():
                # save_message جدیدترین مقدار irfc822 را بازمی‌گرداند
                # در صورتی که subpart="message/rfc822" باشد، ۱ افزایش می‌یابد، در غیر این صورت افزایش نمی‌یابد
                irfc822 = save_message(output, subpart, irfc822)

    return irfc822
  • تفاوت بین یک بخش [message/rfc822] و سایر بخش‌های چندبخشی در این است که دایرکتوری ذخیره تغییر می‌کند؛
    • خطوط ۶–۸: برای بخش [message/rfc822]، پوشه ذخیره‌سازی به پوشه خط ۷، یعنی [output/rfc822_xتبدیل می‌شود، که در آن x شماره ایمیل پیوست‌شده است—۱ برای اولی، ۲ برای دومی و غیره؛
    • خط ۲۱: برای بخش‌های چندبخشی دیگر، پوشهٔ ذخیره همچنان پوشهٔ [output] از پیام اصلی باقی می‌ماند. پوشه تغییر نمی‌کند؛
  • خطوط ۱۰–۱۲: هر زیربخش از طریق یک فراخوانی بازگشتی به [save_message] ذخیره می‌شود. پارامتر سوم، شماره نمایه ایمیل‌های توکار در [subpart] است. در ابتدا، این نمایه ۰ است؛
  • خط ۲۱: توضیح مشابه با خط ۱۲، اما مقدار پارامتر سوم [irfc822] تغییر می‌کند. اگر چندین ایمیل جاسازی‌شده در حلقه خطوط ۱۸–۲۱ وجود داشته باشد، باید در پوشه‌های […/rfc822-1…/rfc822_2…] ذخیره شوند. بنابراین، پارامتر سوم تابع [save_message] باید به‌ترتیب مقادیر ۱، ۲، ۳ و ... را بگیرد. برای این منظور، [save_message] مقدار [irfc822] (خط ۲۱) را تعیین می‌کند.

بیایید یک مثال بزنیم و فرض کنیم که لیست زیرمجموعه‌ها در خط ۱۸، [subpart1, subpart2, subpart3, subpart4, subpart5] است و [subpart1, subpart3, subpart5] ایمیل‌های پیوست‌شده هستند، [subpart2] یک بخش text/plain است و [subpart4] یک پیوست است، و اینکه هنوز در پیام [irfc822=0] با هیچ پیوست ایمیلی مواجه نشده‌ایم. در این صورت:

  • (ادامه)
    • [subpart1] توسط خط ۲۱ ذخیره می‌شود: تابع [saveMessage] با irfc822=0 اجرا می‌شود؛
    • [subpart1] یک ایمیل با پیوست است، بنابراین irfc822 روی 1 تنظیم می‌شود (خط 6 کد). پوشه‌ای به نام [output/irfc822_1] ایجاد می‌شود. بنابراین مقدار بازگشتی [saveMessage(ouput,subpart1,0)] برابر 1 است (خط 23)؛
    • [subpart2] در خط ۲۱ ذخیره می‌شود: تابع [saveMessage] با مقدار irfc822=1 اجرا می‌شود؛
    • [subpart2] یک پیوست ایمیل نیست. بنابراین، irfc822 روی 1 باقی می‌ماند. این مقداری است که در خط 21 بازیابی می‌شود؛
    • [subpart3] در خط 21 ذخیره می‌شود: تابع [save_message] با irfc822=1 اجرا می‌شود;
    • [subpart3] یک ایمیل با پیوست است، بنابراین irfc822 به 2 تغییر می‌کند (خط 6 کد). پوشه‌ای به نام [output/irfc822_2] ایجاد می‌شود. بنابراین مقدار بازگشتی [save_message(ouput,subpart1,1)] برابر 2 است (خط 21);
    • [subpart4] در خط ۲۱ ذخیره می‌شود: تابع [save_message] با irfc822=2 اجرا می‌شود؛
    • [subpart4] یک پیوست ایمیل نیست. بنابراین، irfc822 روی 2 باقی می‌ماند. این همان مقداری است که در خط 21 بازیابی می‌شود؛
    • [subpart5] توسط خط 21 ذخیره می‌شود: تابع [save_message] با irfc822=2 اجرا می‌شود;
    • [subpart5] یک ایمیل با پیوست است، بنابراین irfc822 به 3 تغییر می‌کند (خط 6 کد). یک پوشه با نام [output/irfc822_3] ایجاد می‌شود. بنابراین مقدار بازگشتی [save_message(ouput,subpart1,2)] برابر 3 است (خط 21);

نمونه‌هایی از اجرا

ما ۴ ایمیل به [pymail2parlexemple@gmail.com] از: [Gmail, Outlook, em Client, Thunderbird] ارسال می‌کنیم

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

تمام ایمیل‌ها دارای خط موضوع [hélène va au marché] و متن بدنه [acheter des légumes] خواهند بود. ما می‌خواهیم بررسی کنیم که کاراکترهای دارای علامت diakritik چگونه نمایش داده می‌شوند.

ما آن‌ها را با استفاده از اسکریپت [pop3/02/main] که با فایل [pop3/02/config] پیکربندی شده است، می‌خوانیم:


import os


def configure() -> dict:
    # پیکربندی برنامه
    config = {
        # فهرست صندوق‌های پستی برای مدیریت
        "mailboxes": [
            # سرور: سرور POP3
            # پورت: پورت سرور POP3
            # کاربر: کاربری که می‌خواهید پیام‌های او را بخوانید
            # رمز عبور: رمز عبور آنها
            # maxmails: حداکثر تعداد ایمیل‌هایی که باید دانلود شوند
            # timeout: حداکثر زمان انتظار برای پاسخ از سرور
            # delete: اگر می‌خواهید پیام‌های دانلود شده از سرور حذف شوند، آن را روی true تنظیم کنید
            # ssl: اگر ایمیل‌ها از طریق اتصال امن بازیابی می‌شوند، روی true تنظیم کنید
            # خروجی: پوشه‌ای که پیام‌های دانلود شده در آن ذخیره می‌شوند

            {
                "server": "pop.gmail.com",
                "port": "995",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prD&@1QZ3TG",
                "maxmails": 10,
                "delete": False,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # مسیر مطلق پوشهٔ اسکریپت
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #مسیرهای مطلق پوشه‌هایی که باید در syspath گنجانده شوند
    absolute_dependencies = [
        # پوشه محلی
        f"{script_dir}/../../shared",
    ]

    # پیکربندی syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #پیکربندی رندر می‌شود
    return config

نتیجه به شرح زیر است:

Image

پیام ۱ همان پیامی است که توسط Thunderbird ارسال شده است:

Image

  • در [5]، Thunderbird [3] از یک [Transfer-Content-Encoding] از نوع [8bit] استفاده می‌کند؛
  • در [4]: پیام در UTF-8 رمزگذاری شده است؛

پیام ۲ همان پیامی است که توسط em Client ارسال شده است:

Image

Image

شایان ذکر است که [em Client] متن‌ها را در UTF-8 ([4]) رمزگذاری می‌کند و آن‌ها را به [quoted-printable] و [5] منتقل می‌نماید. همچنین یک نسخه از پیام را در HTML و [7-8] ارسال کرد. تمام کلاینت‌های ایمیل آزمایش‌شده در اینجا قادر به انجام این کار هستند. این یک تنظیم پیکربندی است.

پیام ۳ همان پیامی است که توسط جیمیل ارسال شده است:

Image

توجه داشته باشید که جیمیل متن را در UTF-8 ([3]) رمزگذاری می‌کند و آن را به صورت [quoted-printable] و [4] فوروارد می‌کند. در [6]، نسخه HTML پیام.

پیام ۴ همان پیامی است که توسط Outlook ارسال می‌شود:

Image

توجه کنید که Outlook متن را در ISO-8859-1 ([3]) رمزگذاری می‌کند و آن را به [quoted-printable] و [4] منتقل می‌کند.

مثال‌های قبلی دو نکته را نشان می‌دهند:

  • کلاینت ما [pop3/02] به درستی کار می‌کرد؛
  • کلاینت‌های ایمیل روش‌های متفاوتی برای ارسال ایمیل دارند؛

حال بیایید به فایل‌های پیوست شده نگاهی بیندازیم. با استفاده از Thunderbird، صندوق پستی کاربر [pymail2parlexemple@gmail.com] را خالی می‌کنیم. سپس از اسکریپت [smtp/03/main] برای ارسال ایمیلی با پیکربندی زیر [smtp/03/config] استفاده می‌کنیم:


import os


def configure() -> dict:
    # پیکربندی برنامه
    script_dir = os.path.dirname(os.path.abspath(__file__))

    return {
        # توضیحات: توضیحات ایمیل ارسال‌شده
        # smtp-server: سرور SMTP
        # پورت SMTP: پورت سرور SMTP
        # از: فرستنده
        # به: گیرنده
        # موضوع: موضوع ایمیل
        # پیام: پیام ایمیل
        "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",
                # آزمایش کاراکترهای دارای نشانه
                "message": "aglaë séléné\nva au marché\nacheter des fleurs",
                # SMTP با احراز هویت
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                # در اینجا باید مسیرهای مطلق برای فایل‌های پیوست را مشخص کنید
                "attachments": [
                    f"{script_dir}/attachments/fichier attaché.docx",
                    f"{script_dir}/attachments/fichier attaché.pdf",
                    f"{script_dir}/attachments/mail attaché 1.eml",
                ]
            }
        ]
    }
  • خطوط ۳۱–۳۳: موارد زیر را به ایمیل پیوست می‌کنیم:
  • یک فایل ورد؛
  • یک فایل PDF؛
  • یک ایمیل حاوی همان دو فایل پیوست؛

پس از ارسال ایمیل، اسکریپت [pop3/02] را برای خواندن صندوق ورودی کاربر [pymail2parlexemple@gmail.com] اجرا می‌کنیم. نتایج به شرح زیر است:

Image

  • در [1]: پیام به همراه دو فایل پیوست آن؛
  • در [2]: خود ایمیل پیوست‌شده به همراه دو پیوست آن؛

نتیجه‌گیری

ماژول [mail_parser.py] به‌ویژه پیچیده است. این امر به دلیل پیچیدگی خود ایمیل‌ها است. ما این ماژول را برای پروتکل IMAP مجدداً استفاده خواهیم کرد.

21.7. پروتکل IMAP

21.7.1. مقدمه

برای خواندن ایمیل‌های ذخیره‌شده روی سرور ایمیل دو پروتکل وجود دارد:

  • پروتکل POP3 (پروتکل دفتر پستی)، که از نظر تاریخی اولین پروتکل بود اما اکنون به ندرت استفاده می‌شود؛
  • پروتکل IMAP (Internet Message Access Protocol)، که جدیدتر از POP3 است و در حال حاضر بیشترین استفاده را دارد؛

برای بررسی پروتکل IMAP، از معماری زیر استفاده خواهیم کرد:

Image

  • [Serveur B] بسته به شرایط خواهد بود:
    • یک سرور محلی IMAP، که توسط سرور ایمیل [hMailServer] پیاده‌سازی شده است؛
    • سرور [imap.gmail.com:993]، که سرور IMAP مدیر ایمیل [Gmail] است؛
  • [Client A] یک اسکریپت پایتون خواهد بود که از ماژول‌های پایتون برای مدیریت پیوست‌ها و برقراری یک اتصال رمزگذاری‌شده و احراز هویت‌شده در صورت نیاز سرور IMAP استفاده می‌کند؛

پروتکل IMAP فراتر از پروتکل POP3 است:

  • ایمیل‌ها روی سرور IMAP ذخیره می‌شوند و می‌توان آن‌ها را در پوشه‌ها سازماندهی کرد؛
  • کلاینت IMAP می‌تواند دستوراتی برای ایجاد، ویرایش یا حذف این پوشه‌ها ارسال کند؛

بیایید با استفاده از Thunderbird یک مثال ببینیم. در معماری زیر:

Image

  • تاندربرد کلاینت A است؛
  • [imap.gmail.com] سرور B (جی‌میل) است؛

بیایید با استفاده از Thunderbird یک پوشه در ایمیل‌های کاربر [pymail2parlexemple@gmail.com] ایجاد کنیم:

Image

  • در [1-6]، ما پوشه [dossier1] را ایجاد می‌کنیم؛

Image

  • در [7-8]، ما تمام فایل‌ها را (با استفاده از ماوس) از پوشه [Courrier entrant] به پوشه [dossier1] منتقل می‌کنیم؛

حالا بیایید وارد وب‌سایت Gmail شویم و با حساب کاربری [pymail2parlexemple@gmail.com] وارد شویم:

Image

  • در [2-3]، صندوق ورودی خالی است؛
  • به‌عنوان [1]، پوشه [dossier1] ایجاد شده است؛

Image

  • در [4-6]: ایمیل‌هایی که به پوشه [dossier1] منتقل شده‌اند؛

ما در حال بررسی معماری زیر هستیم:

Image

  • کلاینت A برنامه Thunderbird است؛
  • کلاینت C اپلیکیشن وب جیمیل است؛
  • سرور B سرور IMAP جیمیل است؛

درخت پوشه‌های کاربر توسط سرور IMAP نگهداری می‌شود. سپس همهٔ کلاینت‌های IMAP با آن همگام‌سازی می‌کنند تا پوشه‌های حساب کاربری را نمایش دهند. در اینجا، Thunderbird چندین دستور را ارسال کرده است به:

  • ایجاد پوشه [dossier1]؛
  • انتقال پیام‌ها به این پوشه؛

21.7.2. اسکریپت [imap/main]: کلاینت IMAP با ماژول [imaplib]

Image

اسکریپت [imap/main] توسط اسکریپت زیر [imap/config] پیکربندی شده است:


import os


def configure() -> dict:
    # پیکربندی برنامه
    config = {
        # فهرست صندوق‌های پستی برای مدیریت
        "mailboxes": [
            # سرور: سرور IMAP
            # پورت: پورت برای سرور IMAP
            # کاربر: کاربری که می‌خواهید پیام‌های او را بخوانید
            # رمز عبور: رمز عبور آنها
            # maxmails: حداکثر تعداد ایمیل‌هایی که باید دانلود شوند
            # timeout: حداکثر زمان انتظار برای پاسخ از سرور
            # delete: اگر به مقدار true تنظیم شود، پیام‌های دانلود شده از سرور حذف می‌شوند
            # ssl: اگر ایمیل‌ها از طریق اتصال امن بازیابی می‌شوند، روی true تنظیم کنید
            # خروجی: پوشه‌ای که پیام‌های دانلود شده در آن ذخیره می‌شوند

            {
                "server": "imap.gmail.com",
                "port": "993",
                "user": "pymail2parlexemple@gmail.com",
                "password": "#6prIlhD&@1QZ3TG",
                "maxmails": 10,
                "ssl": True,
                "timeout": 2.0,
                "output": "output"
            }
        ]
    }
    # مسیر مطلق پوشه اسکریپت
    script_dir = os.path.dirname(os.path.abspath(__file__))

    #مسیرهای مطلق پوشه‌هایی که باید در syspath گنجانده شوند
    absolute_dependencies = [
        # پوشه محلی
        f"{script_dir}/../shared",
    ]

    # پیکربندی syspath
    from myutils import set_syspath
    set_syspath(absolute_dependencies)

    #پیکربندی رندر می‌شود
    return config

نظرات

  • خطوط ۸–۲۹: کلید [mailboxes] با فهرست صندوق‌های پستی که باید بررسی شوند مرتبط است؛
  • خط ۲۰: سرور IMAP؛
  • خط ۲۱: پورت سرویس آن؛
  • خطوط ۲۲–۲۳: کاربری که می‌خواهید ایمیل‌های او را بخوانید؛
  • خط ۲۴: حداکثر تعداد ایمیل‌هایی که باید بازیابی شوند؛
  • خط ۲۵: مشخص می‌کند که آیا باید یک اتصال امن با سرور IMAP برقرار شود (True) یا خیر (False);
  • خط ۲۶: حداکثر زمان انتظار برای پاسخ از سرور؛
  • خط ۲۷: پوشه برای ذخیره ایمیل‌های خوانده شده؛

اسکریپت [imap/main] به شرح زیر است:


# واردات‌ها
import email
import imaplib
import os
import shutil


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

def readmails(mailbox: dict):
    


# main ----------------------------------------------------------------
#   IMAP کلاینت برای خواندن ایمیل‌ها

#بازیابی پیکربندی برنامه
import config
config = config.configure()

# صندوق‌های پستی را یکی‌یکی پردازش می‌کند
for mailbox in config['mailboxes']:
    try:
        #خروجی کنسول
        print("----------------------------------")
        print(
            f"Lecture de la boîte mail POP3 {mailbox['user']} / {mailbox['server']}:{mailbox['port']}")
        # خواندن صندوق پستی
        readmails(mailbox)
        # پایان
        print("Lecture terminée...")
    # به جز BaseException به‌عنوان خطا:
    #     # نمایش خطا
    #     print(f"خطای زیر رخ داده است: {error}")
    finally:
        pass

نظرات

  • خطوط 14–36: این رویکرد مشابه اسکریپت |pop3/02/main| است؛

تابع [readmails] به شرح زیر است:


def readmails(mailbox: dict):
    # اجازه دهید استثناءها منتقل شوند
    #
    # ماژول تجزیه و تحلیل ایمیل
    from mail_parser import save_message

    #بازیابی اطلاعات پیکربندی
    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']
    #
    # بیایید شروع کنیم
    imap_resource = None
    try:
        # ایجاد پوشه‌های ذخیره‌سازی در صورت عدم وجود
        if not os.path.isdir(output):
            os.mkdir(output)
        #کاربر
        dir2 = f"{output}/{user}"
        # حذف پوشه [dir2] در صورت وجود، سپس بازسازی آن
        if os.path.isdir(dir2):
            # حذف
            shutil.rmtree(dir2)
        # ایجاد
        os.mkdir(dir2)
        #اتصال به سرور IMAP
        if ssl:
            imap_resource = imaplib.IMAP4_SSL(server, port)
        else:
            imap_resource = imaplib.IMAP4(server, port)
        # اتمام زمان ارتباط مشتری
        sock = imap_resource.socket()
        sock.settimeout(timeout)
        # احراز هویت
        imap_resource.login(user, password)
        #پوشه INBOX (پست ورودی) انتخاب شده است
        imap_resource.select('INBOX')
        #بازیابی تمام پیام‌ها از این پوشه: معیار ALL
        # رمزگذاری مشخصی وجود ندارد: None
        typ1, data1 = imap_resource.search(None, 'ALL')
        # print(f"typ={typ1}, data={data1}")

        # data1[0] یک آرایه بایت است که شامل شماره‌های تمام پیام‌ها، جدا شده با یک فاصله، می‌باشد
        nums = data1[0].split()
        imail = 0
        fini = imail >= maxmails or imail >= len(nums)
        #ایمیل‌ها یکی یکی خوانده می‌شوند
        while not fini:
            #num شماره پیام در مبنای دو است
            num = nums[imail]
            # print(f"شماره پیام: {num}")

            # بازیابی شماره پیام num
            typ2, data2 = imap_resource.fetch(num, '(RFC822)')
            # print(f"type={typ2}, data={data2}")

            #`data` یک لیست حاوی تپل‌ها است؛ در این مورد، تنها یک تپل وجود دارد
            #`data[0]` تِپل است؛ `dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX` عنصر دوم تِپل است
            #dataQZXW2HTMLBWzBdZQXQZXW2HTMLBWzFdZQX شامل دنباله‌ای از بایت‌ها است که تمام خطوط پیام را نشان می‌دهد
            #«پیام» به متن پیام به‌علاوهٔ تمام فایل‌های پیوست‌شده اطلاق می‌شود

            #پیام به عنوان نوع email.message.Message بازیابی می‌شود
            message = email.message_from_bytes(data2[0][1])
            #پوشه پیام
            dir3 = f"{dir2}/message_{int(num)}"
            # اگر پوشه وجود نداشته باشد، ایجاد می‌شود
            if not os.path.isdir(dir3):
                os.mkdir(dir3)
            # آن را ذخیره کن
            save_message(dir3, message)
            # پیام بعدی
            imail += 1
            fini = imail >= maxmails or imail >= len(nums)
    finally:
        if imap_resource:
            # قطع اتصال به صندوق پستی
            imap_resource.close()
            # خروج از سرور IMAP
            imap_resource.logout()

توضیحات

  • خطوط ۷–۱۵: عناصر پیکربندی بازیابی می‌شوند؛
  • خطوط ۱۹ و ۷۹: کد توسط یک بلوک try/finally کنترل می‌شود. بنابراین خطاها گرفته نمی‌شوند (زیرا هیچ عبارت except وجود ندارد) و به کد فراخوانی‌کننده منتقل می‌شوند، که آن‌ها را متوقف کرده و نمایش می‌دهد؛
  • خطوط ۲۳–۳۰: پوشه برای ذخیره ایمیل‌ها ایجاد می‌شود؛
  • خطوط ۳۱–۳۵: ما به سرور IMAP متصل می‌شویم. کلاس مورد استفاده بسته به اینکه سرور IMAP امن باشد (IMAP4_SSL) یا خیر (IMAP4) متفاوت است؛
  • خطوط ۳۶–۳۸: زمان‌انتظار ارتباط کلاینت/سرور تنظیم می‌شود؛
  • خطوط ۳۹–۴۰: احراز هویت با سرور IMAP انجام می‌شود؛
  • خطوط ۴۱–۴۲: ما دیده‌ایم که صندوق پستی کاربر (IMAP) می‌تواند به پوشه‌ها سازماندهی شود. پوشه [INBOX] صندوق ورودی است. برای انتخاب پوشه [dossier1می‌نویسیم [imapResource.select('dossier1')];
  • خطوط ۴۳–۴۵: ما فهرستی از تمام پیام‌های یافت‌شده در [INBOX] را درخواست می‌کنیم:
    • پارامتر اول [imapResource.search] نوع رمزگذاری است. [None] به معنای «بدون فیلتر رمزگذاری» است؛
    • پارامتر دوم یک معیار است. راه‌های مختلفی برای بیان آن وجود دارد. معیار [ALL] به این معنی است که ما همه پیام‌ها را در پوشه می‌خواهیم؛

نتیجه [imapResource.search] به این شکل است:


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

[data] فهرستی است که شامل شماره‌های پیام‌های بازیابی‌شده می‌باشد. این شماره‌ها به صورت باینری هستند. در مثال بالا، دو پیام در پوشه [INBOX] یافت شدند؛

  • خط ۴۹: شماره‌های پیام‌ها بازیابی می‌شوند. در بالا، ما لیست [b'1' b'2'] را خواهیم داشت، لیستی از اعداد رمزگذاری‌شده به صورت باینری؛
  • خطوط ۵۳–۷۸: ما یک حلقه ایجاد می‌کنیم تا پیام‌ها را از پوشه [INBOX] بخوانیم؛
  • خطوط ۵۴–۵۵: شماره پیام؛
  • خطوط ۵۸–۵۹: شماره پیام [num] از سرور IMAP درخواست می‌شود؛
    • پارامتر اول، شماره پیام مورد نظر است؛
    • پارامتر دوم یک رشته "(part1)(part2)…" است که در آن [parti] نام بخشی از پیام است. من به طور دقیق به این مورد نپرداخته‌ام. این نام (RFC822) به کل ایمیل اشاره دارد؛

شما چیزی را در قالب زیر دریافت می‌کنید:


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')']

عنصر [data] در اینجا یک لیست است که شامل یک عنصر واحد می‌باشد، و آن عنصر واحد یک تِپل سه‌عنصری است:


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')'
]

عنصر دوم این توبل یک رشتهٔ دودویی است که کل پیام درخواستی را نشان می‌دهد. همان‌طور که در بالا مشاهده می‌شود، این‌ها عناصری هستند که هنگام مطالعهٔ ماژول [mail_parser] قبلاً معرفی شده‌اند.

data[0]** نمایانگر یک تپل با دو عنصر است. **data[0][1] نمایانگر خطوط پیام به صورت باینری است.

  • خط ۶۸: تابع [taxpayers[slice(10,12)]] یک شی از نوع [email.message.Message] را از خطوط پیام ایجاد می‌کند. نوع [email.message.Message]، نوع پارامتر ماژول [mail_parser] است که قبلاً نوشتیم؛
  • خطوط ۶۹–۷۳: ما پوشهٔ ذخیره را برای پیام شمارهٔ [num] ایجاد می‌کنیم؛
  • خط ۷۵: ما تابع [save_message] را از ماژول [mail_parser] در خط ۵ فراخوانی می‌کنیم. این تابع در بخش |pop3/02/main| توضیح داده شده است؛
  • خطوط ۷۶–۷۸: ما به صورت حلقه‌ای به سراغ پردازش پیام بعدی می‌رویم؛
  • خطوط ۷۹–۸۴: صرف‌نظر از اینکه خطایی رخ داده باشد یا خیر:
    • خط ۸۲: اتصال به پوشهٔ پرس‌وجو شده بسته می‌شود؛
    • خط ۸۴: از سرور IMAP خارج می‌شویم؛

نتایج به‌دست‌آمده با نتایج به‌دست‌آمده با اسکریپت [pop3/02/main] یکسان است. این امر قابل انتظار است، زیرا از همان پارسر ایمیل [mail_parser] استفاده می‌شود.