اذهب إلى المحتوى

إرسال رسائل البريد الإلكتروني باستخدام لغة بايثون


Ola Abbas

يحتاج التحقق من البريد الإلكتروني والرد عليه الكثير من الوقت، ولا يمكنك كتابة برنامج للتعامل مع جميع رسائل بريدك الإلكتروني، إذ تتطلب كل رسالة ردًا خاصًا بها، ولكن يمكنك أتمتة الكثير من المهام الأخرى المتعلقة بالبريد الإلكتروني بعد أن تعرف كيفية كتابة البرامج التي يمكنها إرسال واستقبال رسائل البريد الإلكتروني.

قد يكون لديك مثلًا جدول بيانات يحتوي على الكثير من سجلات العملاء وتريد إرسال رسالة تحتوي على استمارة Form خاصة بكل عميل اعتمادًا على تفاصيل عمره وموقعه، وقد لا تتمكن البرمجيات التجارية من فعل ذلك نيابةً عنك، ولكن يمكنك كتابة برنامجك الخاص لإرسال هذه الرسائل عبر البريد الإلكتروني، مما يوفّر عليك الكثير من الوقت لنسخ ولصق رسائل البريد الإلكتروني التي تحتوي على استمارات.

سنوضّح في هذا المقال الوحدة EZGmail التي تُعَد طريقة بسيطة لإرسال وقراءة رسائل البريد الإلكتروني من حسابات جيميل Gmail، وهي بايثون Python لاستخدام بروتوكولات البريد الإلكتروني المعيارية SMTP و IMAP.

ملاحظة: نوصي بشدة بإعداد حساب بريد إلكتروني منفصل لأيّ سكربتات ترسل أو تستقبل رسائل البريد الإلكتروني، مما يؤدي إلى منع الأخطاء الموجودة في برامجك من التأثير على حساب بريدك الإلكتروني الشخصي مثل حذف رسائل البريد الإلكتروني أو إرسال رسائل غير مرغوب بها Spam إلى جهات الاتصال الخاصة بك عن طريق الخطأ. يُفضَّل أن تجري أولًا تشغيلًا تجريبيًا من خلال تعليق الشيفرة البرمجية التي ترسل رسائل البريد الإلكتروني أو تحذفها فعليًا ووضع استدعاء مؤقت للدالة print()‎ مكانها، وبالتالي يمكنك اختبار برنامجك قبل تشغيله تشغيلًا حقيقيًا.

إرسال واستقبال رسائل البريد الإلكتروني باستخدام واجهة برمجة تطبيقات جيميل Gmail API

يمتلك جيميل ما يقرب من ثلث حصة سوق عملاء البريد الإلكتروني، إذ لا بد أنّ لديك عنوان بريد إلكتروني واحد على الأقل على جيميل. يتميز جيميل بتدابير الأمان الإضافية ومكافحة البريد الالكتروني غير المرغوب به، لذا من الأسهل التحكم في حساب جيميل باستخدام الوحدة EZGmail بدلًا من التحكم به باستخدام الوحدتين smtplib و imapclient اللتين سنناقشهما لاحقًا في هذا المقال. كتب Al Sweigart وحدة EZGmail، حيث تعمل هذه الوحدة فوق واجهة برمجة تطبيقات جيميل الرسمية وتوفّر دوالًا تسهّل استخدام جيميل من شيفرة بايثون. اطّلع على تفاصيل EZGmail الكاملة على GitHub، حيث لا تنتِج جوجل هذه الوحدة وليست تابعة لها، واطّلع على التوثيق الرسمي لواجهة برمجة تطبيقات جيميل Gmail API.

يمكنك تثبيت وحدة EZGmail من خلال تشغيل الأمر pip install --user --upgrade ezgmail على نظام ويندوز، أو استخدم الأداة pip3 على نظامي ماك macOS ولينكس Linux. يضمن الخيار ‎--upgrade تثبيت أحدث إصدار من الحزمة، وهو أمر ضروري للتفاعل مع خدمة دائمة التغير عبر الإنترنت مثل واجهة برمجة تطبيقات جيميل.

تفعيل واجهة برمجة تطبيقات جيميل

يجب عليك أولًا التسجيل للحصول على حساب بريد إلكتروني على جيميل قبل أن تكتب شيفرتك البرمجية. انتقل بعد ذلك إلى صفحة البدء السريع لاستخدام بايثون، وانقر على زر تفعيل واجهة برمجة تطبيقات جيميل Enable the Gmail API في تلك الصفحة، واملأ الاستمارة التي ستظهر.

ستقدم الصفحة رابطًا للملف credentials.json بعد ملء الاستمارة، حيث يجب أن تنزّل هذا الملف وتضعه في المجلد نفسه لملف ‎.py الخاص بك. يحتوي الملف credentials.json على معرّف العميل Client ID ومعلومات العميل السرية Client Secret، والتي يجب عليك التعامل معها مثل كلمة مرور حسابك على جيميل وعدم مشاركتها مع أيّ شخص آخر.

لندخِل الآن الشيفرة البرمجية التالية في الصدفة التفاعلية Interactive Shell:

>>> import ezgmail, os
>>> os.chdir(r'C:\path\to\credentials_json_file')
>>> ezgmail.init()

تأكّد من ضبط مجلد العمل الحالي على المجلد نفسه الذي يوجد به الملف credentials.json وأنك متصل بالإنترنت. تفتح الدالة ezgmail.init()‎ متصفحك على صفحة تسجيل الدخول إلى جوجل، لذا أدخِل عنوان جيميل وكلمة مرورك. قد تحذّرك الصفحة بعدم التحقق من هذا التطبيق This app isn’t verified"‎"، ولكن لا بأس بذلك. انقر بعد ذلك على "خيارات متقدمة Advanced"، وانقر على خيار الانتقال إلى صفحة البدء السريع (غير آمن) "Go to Quickstart (unsafe)‎". (إذا أردتَ كتابة سكربتات بايثون لأشخاص آخرين ولا تريد ظهور هذا التحذير لهم، فيجب أن تتعرّف على عملية التحقق من تطبيق جوجل، والتي لن نناقشها في هذا المقال. انقر على خيار "السماح Allow" ثم أغلق المتصفح عندما تعرض الصفحةُ التالية الرسالةَ "تريد صفحة البدء السريع الوصول إلى حسابك جوجل Quickstart wants to access your Google Account".

يتولّد بعد ذلك ملف token.json لمنح سكربتات بايثون الخاصة بك إمكانية الوصول إلى حساب جيميل الذي أدخلته، ولن يفتح المتصفح إلّا على صفحة تسجيل الدخول إن لم يتمكّن من العثور على ملف token.json موجودٍ مسبقًا. يمكن لسكربتات بايثون الخاصة بك باستخدام الملفين credentials.json و token.json إرسالَ رسائل البريد الإلكتروني وقراءتها من حسابك على جيميل دون مطالبتك بتضمين كلمة مرور جيميل في شيفرتك المصدرية.

إرسال رسائل البريد الإلكتروني من حساب جيميل

يجب أن تكون وحدة EZGmail قادرةً على إرسال بريد إلكتروني باستخدام استدعاء دالةٍ واحد بعد حصولك على الملف token.json كما يلي:

>>> import ezgmail
>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email')

إذا أردتَ إرفاق ملفاتٍ ببريدك الإلكتروني، فيمكنك توفير وسيط قائمة إضافي للدالة send()‎:

>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email',
['attachment1.jpg', 'attachment2.mp3'])

لاحظ أنه -كجزء من ميزات الأمان ومكافحة الرسائل غير المرغوب بها- قد لا يرسل جيميل رسائل بريد إلكتروني متكررة تحتوي على النص نفسه لأنها يمكن أن تكون رسائلًا غير مرغوب بها، أو رسائل بريد إلكتروني تحتوي على مرفقات ملفات لها الامتداد ‎.exe‎ أو ‎.zip‎ لأنها يمكن أن تكون فيروسات.

يمكنك أيضًا توفير وسطاء الكلمات المفتاحية Keyword Arguments الاختيارية cc و bcc لإرسال نسخ مطابقة Carbon Copies ونسخ مطابقة مخفية Blind Carbon Copies:

>>> import ezgmail
>>> ezgmail.send('recipient@example.com', 'Subject line', 'Body of the email',
cc='friend@example.com', bcc='otherfriend@example.com,someoneelse@example.com')

إذا أردتَ أن تتذكر عنوان جيميل الذي ضُبِط الملف token.json عليه، فيمكنك فحص المتغير ezgmail.EMAIL_ADDRESS، حيث يُملَأ هذا المتغير فقط بعد استدعاء الدالة ezgmail.init()‎ أو أي دالة أخرى خاصة بالوحدة EZGmail.

>>> import ezgmail
>>> ezgmail.init()
>>> ezgmail.EMAIL_ADDRESS
'example@gmail.com'

تأكّد من التعامل مع الملف token.json بالطريقة نفسها للتعامل مع كلمة مرورك، حيث إذا حصل شخصٌ آخر على هذا الملف، فيمكنه الوصول إلى حسابك على جيميل بالرغم من أنه لن يتمكّن من تغيير كلمة مرور حسابك على جيميل. يمكنك إبطال ملفات token.json الصادرة مسبقًا من خلال الانتقال إلى الرابط https://security.google.com/settings/security/permissions?pli=1/‎، ثم أبطِل الوصول إلى تطبيق البدء السريع Quickstart، ولكن يجب تشغيل الدالة ezgmail.init()‎ ومتابعة عملية تسجيل الدخول مرة أخرى للحصول على ملف token.json جديد.

قراءة رسائل البريد الإلكتروني من حساب جيميل

ينظّم جيميل رسائل البريد الإلكتروني التي تمثل ردودًا على بعضها البعض ضمن سلاسل محادثات Conversation Threads. إذا سجّلتَ الدخول إلى جيميل في متصفح الويب أو من خلال أحد التطبيقات، فسترى سلاسل رسائل البريد الإلكتروني بدلًا من رسائل البريد الإلكتروني الفردية، حتى لو احتوت إحدى تلك السلاسل على رسالة بريد إلكتروني واحدة فقط.

تحتوي الوحدة EZGmail على كائنات GmailThread و GmailMessage لتمثيل سلاسل المحادثات ورسائل البريد الإلكتروني الفردية على التوالي، ويحتوي الكائن GmailThread على سمةٍ Attribute هي السمة messages التي تحتوي على قائمة بكائنات GmailMessage. تعيد الدالة unread()‎ قائمةً بكائنات GmailThread لجميع رسائل البريد الإلكتروني غير المقروءة، والتي يمكن بعد ذلك تمريرها إلى الدالة ezgmail.summary()‎ لطباعة ملخصٍ لسلاسل المحادثات في تلك القائمة:

>>> import ezgmail
>>> unreadThreads = ezgmail.unread() # ‫قائمة بكائنات GmailThread
>>> ezgmail.summary(unreadThreads)
Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09
Jon - Thanks for stopping me from buying Bitcoin. - Dec 09

تُعَد الدالة summary()‎ مفيدة لعرض ملخصٍ سريع لسلاسل رسائل البريد الإلكتروني، ولكن يمكنك الوصول إلى رسائل محددة (وأجزاء منها) من خلال فحص السمة messages الخاصة بالكائن GmailThread، حيث تحتوي هذه السمة على قائمة بكائنات GmailMessage التي تشكّل سلسلة المحادثات، وتحتوي هذه الكائنات على سمات subject و body و timestamp و sender و recipient التي توصف البريد الإلكتروني.

>>> len(unreadThreads)
2
>>> str(unreadThreads[0])
"<GmailThread len=2 snippet= Do you want to watch RoboCop this weekend?'>"
>>> len(unreadThreads[0].messages)
2
>>> str(unreadThreads[0].messages[0])
"<GmailMessage from='Al Sweigart <al@inventwithpython.com>' to='Jon Doe
<example@gmail.com>' timestamp=datetime.datetime(2018, 12, 9, 13, 28, 48)
subject='RoboCop' snippet='Do you want to watch RoboCop this weekend?'>"
>>> unreadThreads[0].messages[0].subject
'RoboCop'
>>> unreadThreads[0].messages[0].body
'Do you want to watch RoboCop this weekend?\r\n'
>>> unreadThreads[0].messages[0].timestamp
datetime.datetime(2018, 12, 9, 13, 28, 48)
>>> unreadThreads[0].messages[0].sender
'Al Sweigart <al@inventwithpython.com>'
>>> unreadThreads[0].messages[0].recipient
'Jon Doe <example@gmail.com>'

تعيد الدالة ezgmail.recent()‎ أحدث 25 سلسلة محادثات في حسابك على جيميل كما تفعل الدالة ezgmail.unread()‎، ولكن يمكنك تمرير وسيط الكلمات المفتاحية maxResults الاختياري لتغيير هذا الحد كما يلي:

>>> recentThreads = ezgmail.recent()
>>> len(recentThreads)
25
>>> recentThreads = ezgmail.recent(maxResults=100)
>>> len(recentThreads)
46

البحث عن رسائل البريد الإلكتروني في حساب جيميل

يمكنك البحث عن رسائل بريد إلكتروني محددة باستخدام الطريقة نفسها التي تستخدمها لإدخال استعلامات في مربع البحث على جيميل من خلال استدعاء الدالة ezgmail.search()‎:

>>> resultThreads = ezgmail.search('RoboCop')
>>> len(resultThreads)
1
>>> ezgmail.summary(resultThreads)
Al, Jon - Do you want to watch RoboCop this weekend? - Dec 09

يجب أن يؤدي الاستدعاء السابق للدالة search()‎ إلى النتائج نفسها عندما تدخل الكلمة "RoboCop" في مربع البحث كما في الشكل التالي:

01 000038

البحث عن رسائل البريد الإلكتروني "RoboCop" في موقع جيميل الإلكتروني

تعيد الدالة search()‎ قائمةً بكائنات GmailThread كما تفعل الدالتان unread()‎ و recent()‎، ويمكنك أيضًا تمرير أيٍّ من معاملات البحث الخاصة التي يمكنك إدخالها في مربع البحث إلى الدالة search()‎ مثل المعاملات التالية:

  • 'label:UNREAD': لرسائل البريد الإلكتروني غير المقروءة.
  • 'from:al@inventwithpython.com': لرسائل البريد الإلكتروني الواردة من al@inventwithpython.com.
  • 'subject:hello': لرسائل البريد الإلكتروني التي تحتوي على الكلمة "hello" في موضوعها.
  • 'has:attachment': لرسائل البريد الإلكتروني التي تحتوي على ملفات مرفقة.

ملاحظة: اطّلع على القائمة الكاملة لمعاملات البحث.

تنزيل المرفقات من حساب جيميل

تحتوي كائنات GmailMessage على السمة attachments، والتي هي قائمة بأسماء الملفات المُرفَقة مع الرسالة، حيث يمكنك تمرير أيٍّ من هذه الأسماء إلى التابع downloadAttachment()‎ الخاص بكائن GmailMessage لتنزيل الملفات، ويمكنك أيضًا تنزيلها جميعًا دفعةً واحدة باستخدام التابع downloadAllAttachments()‎. تحفظ الوحدة EZGmail المرفقات في مجلد العمل الحالي افتراضيًا، ولكن يمكنك تمرير وسيط الكلمات المفتاحية الإضافي downloadFolder إلى التابعين downloadAttachment()‎ و downloadAllAttachments()‎ أيضًا لتنزيل المجلد. لندخِل مثلًا ما يلي في الصدفة التفاعلية:

>>> import ezgmail
>>> threads = ezgmail.search('vacation photos')
>>> threads[0].messages[0].attachments
['tulips.jpg', 'canal.jpg', 'bicycles.jpg']
>>> threads[0].messages[0].downloadAttachment('tulips.jpg')
>>> threads[0].messages[0].downloadAllAttachments(downloadFolder='vacat
ion2023')
['tulips.jpg', 'canal.jpg', 'bicycles.jpg']

إذا وُجِد ملف يحمل اسم الملف المرفق نفسه، فسيحل الملف المرفق الذي نزّلناه محله تلقائيًا.

تحتوي الوحدة EZGmail على ميزات إضافية، حيث يمكنك العثور عليها ضمن توثيقها الكامل على Github.

بروتوكول SMTP

تستخدم الحواسيب بروتوكول HTTP لإرسال صفحات الويب عبر الإنترنت، ويُستخدَم بروتوكول نقل البريد البسيط Simple Mail Transfer Protocol -أو SMTP اختصارًا- لإرسال البريد الإلكتروني، حيث يتمتع هذان البروتوكولان بالمقدار نفسه من الأهمية. يحدّد بروتوكول SMTP كيفية تنسيق رسائل البريد الإلكتروني وتشفيرها ونقلها بين خوادم البريد وجميع التفاصيل الأخرى التي يعالجها حاسوبك بعد النقر على زر الإرسال، ولكنك لست بحاجة إلى معرفة هذه التفاصيل التقنية، لأن الوحدة smtplib الخاصة بلغة بايثون تبسّطها إلى بضع دوال.

يتعامل بروتوكول SMTP فقط مع إرسال رسائل البريد الإلكتروني إلى المستخدمين الآخرين، ويتعامل بروتوكول مختلف هو بروتوكول IMAP مع استرداد رسائل البريد الإلكتروني المرسَلة إليك، حيث سنوضّح هذا البروتوكول لاحقًا.

يوفّر معظم مزوّدي خدمات البريد الإلكتروني المستندة إلى الويب -بالإضافة إلى بروتوكولَي SMTP و IMAP- حاليًا إجراءات أمنية أخرى للحماية من البريد غير المرغوب به والتصيد الاحتيالي Phishing واستخدامات البريد الإلكتروني الضارة الأخرى. تمنع هذه الإجراءات سكربتات بايثون من تسجيل الدخول إلى حساب بريد إلكتروني باستخدام وحدتي smtplib و imapclient، ولكن تحتوي العديد من هذه الخدمات على واجهات برمجة التطبيقات API ووحدات بايثون محددة تسمح للسكربتات بالوصول إليها. سنشرح في هذا المقال الوحدة الخاصة بخدمة جيميل، ولكنك ستحتاج إلى الرجوع إلى التوثيق الرسمي للخدمات الأخرى.

إرسال البريد الإلكتروني

قد تكون على دراية بإرسال رسائل البريد الإلكتروني من أوت لوك Outlook أو ثندربرد Thunderbird أو من خلال موقع ويب مثل جيميل Gmail أو بريد ياهو Yahoo Mail، ولكن لسوء الحظ لا تقدّم لغة بايثون واجهة مستخدم رسومية جميلة مثل تلك التي تقدّمها هذه الخدمات، لذا يمكنك بدلًا من ذلك استدعاء الدوال لإجراء الخطوات الرئيسية من بروتوكول SMTP كما هو موضّح في مثال الصدفة التفاعلية الآتي.

ملاحظة: لا تدخِل المثال التالي في الصدفة التفاعلية، إذ لن ينجح الأمر، لأن smtp.example.com و bob@example.com و MY_SECRET_PASSWORD و alice@example.com هي عناصر بديلة، إذ تُعَد هذه الشيفرة البرمجية مجرد نظرة عامة على عملية إرسال بريد إلكتروني باستخدام بايثون.

>>> import smtplib
>>> smtpObj = smtplib.SMTP('smtp.example.com', 587)
>>> smtpObj.ehlo()
(250, b'mx.example.com at your service, [216.172.148.131]\nSIZE 35882577\
n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING')
>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')
>>> smtpObj.login('bob@example.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')
>>> smtpObj.sendmail('bob@example.com', 'alice@example.com', 'Subject: So
long.\nDear Alice, so long and thanks for all the fish. Sincerely, Bob')
{}
>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')

سنوضّح في الأقسام التالية كل خطوة من هذه الشيفرة البرمجية مع استبدال العناصر البديلة بمعلوماتك للاتصال بخادم SMTP وتسجيل الدخول إليه، وإرسال بريد إلكتروني، وقطع الاتصال بالخادم.

الاتصال بخادم SMTP

إذا أعددتَ ثندربيرد أو أوت لوك أو أي برنامج آخر للاتصال بحساب بريدك الإلكتروني، فقد تكون على دراية بضبط خادم ومنفذ SMTP، حيث ستكون هذه الإعدادات مختلفة بحسب مزوّد البريد الإلكتروني، ولكن يجب أن يمكنك البحث عبر الويب عن إعدادات مزوّدك لبروتوكول SMTP للحصول على الخادم والمنفذ لاستخدامهما.

يكون عادةً اسم النطاق Domain لخادم SMTP هو اسم نطاق مزوّد بريدك الإلكتروني مع وجود البادئة smtp.‎ قبله، فمثلًا خادم SMTP الخاص بشركة Verizon موجودٌ على النطاق smtp.verizon.net. يسرد الجدول التالي بعضًا من مزوّدي البريد الإلكتروني وخوادم SMTP الخاصة بهم، حيث يُعَد المنفذ Port قيمةً صحيحة وتكون دائمًا تقريبًا 587، ويستخدمه معيار تشفير الأوامر TLS.

مزوّد البريد الإلكتروني اسم نطاق خادم SMTP
Gmail*‎ اسم النطاق smtp.gmail.com
Outlook.com/Hotmail.com*‎ اسم النطاق smtp-mail.outlook.com
Yahoo Mail*‎ اسم النطاق smtp.mail.yahoo.com
AT&T‎ اسم النطاق smpt.mail.att.net (المنفذ 465)
Comcast‎ اسم النطاق smtp.comcast.net
Verizon‎ اسم النطاق smtp.verizon.net (المنفذ 465)

ملاحظة: تمنع الإجراءات الأمنية الإضافية شيفرة بايثون من تسجيل الدخول إلى هذه الخوادم التي وضعنا بجانب اسمها المحرف (*) باستخدام الوحدة smtplib، ولكن يمكن لوحدة EZGmail تجاوز هذه الصعوبة لحسابات جيميل.

إذا حصلتَ على اسم النطاق ومعلومات المنفذ لمزوّد بريدك الإلكتروني، فيمكنك إنشاء كائن SMTP من خلال استدعاء الدالة smptlib.SMTP()‎، وتمرير اسم النطاق كوسيط من نوع السلسلة النصية والمنفذ كوسيط من نوع عدد صحيح إليها. يمثل الكائن SMTP اتصالًا بخادم بريد SMTP ويمتلك توابع لإرسال رسائل البريد الإلكتروني، فمثلًا ينشئ الاستدعاء التالي كائن SMTP للاتصال بخادم بريد إلكتروني وهمي:

>>> smtpObj = smtplib.SMTP('smtp.example.com', 587)
>>> type(smtpObj)
<class 'smtplib.SMTP'>

يُظهِر إدخال الدالة type(smtpObj)‎ وجود كائن SMTP مخزّنٍ في المتغير smtpObj، حيث ستحتاج إلى هذا الكائن لاستدعاء التوابع التي تسجل دخولك وترسل رسائل البريد الإلكتروني. إن لم ينجح استدعاء الدالة smptlib.SMTP()‎، فقد لا يدعم خادم SMTP الخاص بك بروتوكول TLS على المنفذ 587، وبالتالي يجب إنشاء كائن SMTP باستخدام الدالة smtplib.SMTP_SSL()‎ والمنفذ 465 بدلًا من ذلك.

>>> smtpObj = smtplib.SMTP_SSL('smtp.example.com', 465)

ملاحظة: إن لم تكن متصلًا بالإنترنت، فسترفع شيفرة بايثون استثناء socket.gaierror: [Errno 11004] getaddrinfo failed أو أيّ استثناء آخر مشابه.

لا تُعَد الاختلافات بين بروتوكولَي TLS و SSL مهمة بالنسبة لبرامجك، فما عليك سوى معرفة معيار التشفير الذي يستخدمه خادم SMTP الخاص بك حتى تعرف كيفية الاتصال به. سيحتوي المتغير smtpObj في كافة أمثلة الصدفة التفاعلية التالية على كائن SMTP الذي تعيده الدالة smtplib.SMTP()‎ أو الدالة smtplib.SMTP_SSL()‎.

إرسال رسالة الترحيب "Hello" الخاصة ببروتوكول SMTP

إذا حصلنا على كائن SMTP، فيمكننا استدعاء التابع ehlo()‎ للترحيب بخادم البريد الإلكتروني SMTP، حيث يُعَد هذا الترحيب الخطوة الأولى في بروتوكول SMTP وهو مهم لتأسيس اتصال مع الخادم. لا حاجة لمعرفة تفاصيل هذه البروتوكولات، ولكن تأكّد من استدعاء التابع ehlo()‎ أولًا بعد الحصول على كائن SMTP، وإلّا ستؤدي استدعاءات التوابع اللاحقة إلى حدوث أخطاء. إليك مثال على استدعاء التابع ehlo()‎ وقيمته المُعادة:

>>> smtpObj.ehlo()
(250, b'mx.example.com at your service, [216.172.148.131]\nSIZE 35882577\
n8BITMIME\nSTARTTLS\nENHANCEDSTATUSCODES\nCHUNKING')

إذا كان العنصر الأول في المجموعة Tuple المُعادة هو العدد الصحيح 250 (رمز النجاح في بروتوكول SMTP)، فهذا يعني أن الترحيب قد نجح.

بدء تشفير TLS

إذا كنتَ متصلًا بالمنفذ 587 على خادم SMTP (أي أنك تستخدم تشفير TLS)، فيجب استدعاء التابع starttls()‎ لاحقًا، حيث تؤدي هذه الخطوة المطلوبة إلى تفعيل التشفير على اتصالك. إذا كنت متصلًا بالمنفذ 465 (أي أنك تستخدم بروتوكول SSL)، فهذا يعني التشفير مُعَد مسبقًا، ويجب عليك تخطي هذه الخطوة.

إليك مثال لاستدعاء التابع starttls()‎:

>>> smtpObj.starttls()
(220, b'2.0.0 Ready to start TLS')

يضع التابع starttls()‎ اتصال SMTP الخاص بك في وضع TLS، ويخبرك العدد 220 الموجود في القيمة المُعادة أن الخادم جاهز.

تسجيل الدخول إلى خادم SMTP

إذا أعددتَ اتصالك المشفّر بخادم SMTP، فيمكنك تسجيل الدخول باستخدام اسم المستخدم الخاص بك (وهو عنوان بريدك الإلكتروني عادةً) وكلمة مرور بريدك الإلكتروني من خلال استدعاء التابع login()‎.

>>> smtpObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
(235, b'2.7.0 Accepted')

مرّر سلسلةً نصية تمثّل عنوان بريدك الإلكتروني كوسيطٍ أول وسلسلة نصية تمثّل كلمة مرورك كوسيطٍ ثانٍ إلى التابع login()‎، وتعني القيمة 235 الموجودة في القيمة المُعادة أن الاستيثاق Authentication ناجح. ترفع شيفرة بايثون الاستثناء smtplib.SMTPAuthenticationError لكلمات المرور غير الصحيحة.

ملاحظة: كن حذرًا بشأن وضع كلمات المرور في شيفرتك المصدرية، حيث إذا نسخ شخصٌ ما برنامجك، فسيكون بإمكانه الوصول إلى حساب بريدك الإلكتروني، لذا يُفضَّل استدعاء الدالة input()‎ وجعل المستخدم يكتب كلمة المرور. قد يكون اضطرارك إلى إدخال كلمة المرور في كل مرة تشغّل فيها برنامجك أمرًا غير مريح، ولكن تمنعك هذه الطريقة من ترك كلمة مرورك في ملف غير مشفّر على حاسوبك بحيث يمكن للمخترق أو للص الذي يسرق حاسوبك المحمول مثلًا الحصول عليها بسهولة.

إرسال رسالة عبر البريد الإلكتروني

سجّلنا الدخول إلى خادم SMTP الخاص بمزوّد بريدك الإلكتروني، وبالتالي يمكننا الآن استدعاء التابع sendmail()‎ لإرسال البريد الإلكتروني فعليًا، حيث يبدو استدعاء هذا التابع كما يلي:

>>> smtpObj.sendmail('my_email_address@example.com
', 'recipient@example.com', 'Subject: So long.\nDear Alice, so long and thanks for all the fish.
Sincerely, Bob')
{}

يتطلب التابع sendmail()‎ ثلاثة وسطاء هي:

  • عنوان بريدك الإلكتروني كسلسلة نصية (للعنوان "from مِن" الخاص بالبريد الإلكتروني).
  • عنوان البريد الإلكتروني للمستلم كسلسلة نصية أو قائمةً من السلاسل النصية لمستلمين متعددين (للعنوان "to إلى").
  • نص Body البريد الإلكتروني كسلسلة نصية.

يجب أن تبدأ السلسلة النصية لنص البريد الإلكتروني بالعبارة 'Subject: \n' لسطر موضوع البريد الإلكتروني، حيث يفصل محرف السطر الجديد '‎\n' سطر الموضوع عن النص الرئيسي للبريد الإلكتروني.

القيمة المُعادة من التابع sendmail()‎ هي قاموس، إذ سيكون هناك زوج مفتاح-قيمة واحد في القاموس لكل مستلمٍ فشل تسليم البريد الإلكتروني إليه، ويعني القاموس الفارغ أن البريد الإلكتروني اُرسِل بنجاح إلى جميع المستلمين.

قطع الاتصال بخادم SMTP

تأكّد من استدعاء التابع quit()‎ عند الانتهاء من إرسال رسائل البريد الإلكتروني، مما يؤدي إلى قطع اتصال برنامجك بخادم SMTP.

>>> smtpObj.quit()
(221, b'2.0.0 closing connection ko10sm23097611pbd.52 - gsmtp')

تعني القيمة 221 الموجودة في القيمة المُعادة انتهاءَ الجلسة.

البروتوكول IMAP

يُعَد البروتوكول SMTP بروتوكول إرسالٍ لرسائل البريد الإلكتروني، ولكن يحدّد بروتوكول الوصول إلى رسائل الإنترنت Internet Message Access Protocol -أو IMAP اختصارًا- كيفية الاتصال بخادم مزوّد البريد الإلكتروني لاسترداد رسائل البريد الإلكتروني المُرسَلة إلى عنوان بريدك الإلكتروني. تحتوي لغة بايثون على الوحدة imaplib، ولكن تُعَد الوحدة imapclient الخارجية أسهل في الاستخدام. يقدّم هذا المقال مقدمة لاستخدام الوحدة IMAPClient، لذا اطلّع على توثيقها الرسمي الكامل على موقعها الرسمي.

تنزّل الوحدة imapclient رسائل البريد الإلكتروني من خادم IMAP بتنسيقٍ معقد إلى حد ما، لذا قد تحتاج إلى تحويلها من هذا التنسيق إلى قيم سلاسل نصية بسيطة. تنفّذ الوحدة pyzmail المهمة الصعبة المتمثلة في تحليل رسائل البريد الإلكتروني نيابةً عنك، لذا اطّلع على التوثيق الكامل لهذه الوحدة.

ثبّت الوحدتين imapclient و pyzmail من النافذة الطرفية Terminal باستخدام الأمرين pip install --user -U imapclient==2.1.0 و pip install --user -U pyzmail36== 1.0.4 على نظام ويندوز Windows، أو باستخدام الأداة pip3 على نظامي ماك macOS ولينكس Linux.

استرداد وحذف رسائل البريد الإلكتروني باستخدام بروتوكول IMAP

يُعَد البحث عن بريد إلكتروني واسترداده في لغة بايثون عملية متعددة الخطوات وتتطلب كلًا من الوحدتين الخارجيتين imapclient و pyzmail. إليك مثال كامل لتسجيل الدخول إلى خادم IMAP والبحث عن رسائل البريد الإلكتروني وجلبها، ثم استخراج نص رسائل البريد الإلكتروني منها:

>>> import imapclient
>>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True)
>>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
'my_email_address@example.com Jane Doe authenticated (Success)'
>>> imapObj.select_folder('INBOX', readonly=True)
>>> UIDs = imapObj.search(['SINCE 05-Jul-2023'])
>>> UIDs
[40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041]
>>> rawMessages = imapObj.fetch([40041], ['BODY[]', 'FLAGS'])
>>> import pyzmail
>>> message = pyzmail.PyzMessage.factory(rawMessages[40041][b'BODY[]'])
>>> message.get_subject()
'Hello!'
>>> message.get_addresses('from')
[('Edward Snowden', 'esnowden@nsa.gov')]
>>> message.get_addresses('to')
[('Jane Doe', 'jdoe@example.com')]
>>> message.get_addresses('cc')
[]
>>> message.get_addresses('bcc')
[]
>>> message.text_part != None
True
>>> message.text_part.get_payload().decode(message.text_part.charset)
'Follow the money.\r\n\r\n-Ed\r\n'
>>> message.html_part != None
True
>>> message.html_part.get_payload().decode(message.html_part.charset)
'<div dir="ltr"><div>So long, and thanks for all the fish!<br><br></div>-
Al<br></div>\r\n'
>>> imapObj.logout()

لا حاجة لحفظ جميع هذه الخطوات، إذ يمكنك العودة إلى هذا المثال العام لتحديث ذاكرتك بعد أن استعراض جميع الخطوات بالتفصيل.

الاتصال بخادم IMAP

احتجنا كائن SMTP للاتصال بخادم SMTP وإرسال البريد الإلكتروني، وبالمثل نحتاج كائن IMAPClient للاتصال بخادم IMAP وتلقي البريد الإلكتروني، ولكن يجب أولًا الحصول على اسم النطاق لخادم IMAP الخاص بمزوّد بريدك الإلكتروني، والذي سيكون مختلفًا عن اسم نطاق خادم SMTP. يوضّح الجدول التالي خوادم IMAP للعديد من مزوّدي البريد الإلكتروني:

مزوّد البريد الإلكتروني اسم نطاق خادم IMAP
Gmail*‎ اسم النطاق imap.gmail.com
Outlook.com/Hotmail.com*‎ اسم النطاق imap-mail.outlook.com
Yahoo Mail*‎ اسم النطاق imap.mail.yahoo.com
AT&T‎ اسم النطاق imap.mail.att.net
Comcast‎ اسم النطاق imap.comcast.net
Verizon‎ اسم النطاق incoming.verizon.net

ملاحظة: تمنع الإجراءات الأمنية الإضافية شيفرة بايثون من تسجيل الدخول إلى هذه الخوادم التي وضعنا بجانب اسمها المحرف (*) باستخدام الوحدة imapclient.

نحصل على اسم النطاق لخادم IMAP، ثم يمكننا استدعاء الدالة imapclient.IMAPClient()‎ لإنشاء كائن IMAPClient. يتطلب معظم مزوّدي البريد الإلكتروني تشفير SSL، لذا مرّر وسيط الكلمات المفتاحية ssl=True إلى هذه الدالة، ولندخل مثلًا ما يلي في الصدفة التفاعلية مع استخدام اسم النطاق الخاص بمزوّدك:

>>> import imapclient
>>> imapObj = imapclient.IMAPClient('imap.example.com', ssl=True)

سيحتوي المتغير imapObj على كائن IMAPClient الذي تعيده الدالة imapclient.IMAPClient()‎ في كافة أمثلة الصدفة التفاعلية الموجودة في الأقسام التالية، والعميل Client هو الكائن الذي يتصل بالخادم.

تسجيل الدخول إلى خادم IMAP

نحصل على كائن IMAPClient، ثم يمكننا استدعاء التابع login()‎ الخاص بهذا الكائن، وتمرير اسم المستخدم (وهو عنوان بريدك الإلكتروني عادةً) وكلمة المرور كسلاسل نصية إلى هذا التابع.

>>> imapObj.login('my_email_address@example.com', 'MY_SECRET_PASSWORD')
'my_email_address@example.com Jane Doe authenticated (Success)'

ملاحظة: تذكّر ألّا تكتب كلمة المرور مباشرة في شيفرتك البرمجية، لذا صمّم برنامجك لقبول كلمة المرور التي تعيدها الدالة input()‎.

إذا رفض خادم IMAP اسم المستخدم/كلمة المرور، فسترفع شيفرة بايثون استثناء imaplib.error.

البحث عن رسالة البريد الإلكتروني

تُعَد عملية استرداد البريد الإلكتروني التي تهمك عمليةً مكونة من خطوتين بعد أن تسجّل الدخول، حيث يجب أولًا تحديد المجلد الذي تريد البحث فيه، ثم يجب استدعاء التابع search()‎ الخاص بكائن IMAPClient وتمرير السلسلة النصية التي تمثّل الكلمات المفتاحية للبحث باستخدام بروتوكول IMAP.

تحديد المجلد

يحتوي كل حساب تقريبًا على مجلد البريد الوارد INBOX افتراضيًا، ولكن يمكنك أيضًا الحصول على قائمة المجلدات من خلال استدعاء التابع list_folders()‎ الخاص بالكائن IMAPClient، مما يؤدي إلى إعادة قائمة من المجموعات Tuples، حيث تحتوي كل مجموعة على معلومات حول مجلد واحد. تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي:

>>> import pprint
>>> pprint.pprint(imapObj.list_folders())
[(('\\HasNoChildren',), '/', 'Drafts'),
(('\\HasNoChildren',), '/', 'Filler'),
 (('\\HasNoChildren',), '/', 'INBOX'),
 (('\\HasNoChildren',), '/', 'Sent'),
--snip--
 (('\\HasNoChildren', '\\Flagged'), '/', 'Starred'),
 (('\\HasNoChildren', '\\Trash'), '/', 'Trash')]

القيم الثلاث في كل مجموعة مثل ‎(('\\HasNoChildren',), '/', 'INBOX')‎ هي كما يلي:

  • مجموعة من رايات Flags المجلد (لن نوضّح في هذا المقال ما تمثله هذه الرايات، ويمكنك تجاهل هذا الحقل).
  • المُحدِّد Delimiter المُستخدَم في سلسلة الاسم النصية لفصل المجلدات الأب عن المجلدات الفرعية.
  • الاسم الكامل للمجلد.

يمكنك تحديد مجلدٍ للبحث فيه من خلال تمرير اسم المجلد بوصفه سلسلة نصية إلى التابع select_folder()‎ الخاص بالكائن IMAPClientكما يلي:

>>> imapObj.select_folder('INBOX', readonly=True)

يمكنك تجاهل القيمة التي يعيدها التابع select_folder()‎، وإذا كان المجلد المحدَّد غير موجود، فسترفع شيفرة بايثون استثناء imaplib.error. يمنعك وسيط الكلمات المفتاحية readonly=True من إجراء تغييرات أو حذفٍ عن طريق الخطأ على أيٍّ من رسائل البريد الإلكتروني الموجودة في هذا المجلد أثناء استدعاءات التوابع اللاحقة. إن لم تكن ترغب في حذف رسائل البريد الإلكتروني، فيُفضَّل دائمًا ضبط الوسيط readonly على القيمة True.

إجراء البحث

حدّدنا المجلد، ويمكننا الآن البحث عن رسائل البريد الإلكتروني باستخدام التابع search()‎ الخاص بالكائن IMAPClient، حيث يكون وسيط هذا التابع قائمةً من السلاسل النصية، وتكون كل سلسلة نصية بتنسيق مفاتيح بحث IMAP، حيث سنوضّح في الجدول الآتي مفاتيح البحث Search Keys. لاحظ أن بعض خوادم IMAP قد يكون لها طرق تطبيق مختلفة فيما يتعلق بكيفية التعامل مع الرايات ومفاتيح البحث الخاصة بها، لذا قد يتطلب الأمر بعض التجارب في الصدفة التفاعلية لمعرفة كيف تتصرّف بالضبط.

يمكنك تمرير عدة سلاسل نصية لمفاتيح بحث IMAP في وسيط القائمة إلى التابع search()‎، وتكون الرسائل المُعادة هي الرسائل التي تتطابق مع جميع مفاتيح البحث. إذا أردتَ المطابقة مع أيٍّ من مفاتيح البحث، فاستخدم مفتاح البحث OR، ولاحظ أن مفتاحَ البحث NOT يتبعه مفتاح بحث كامل، وأن مفتاحَ البحث OR يتبعه مفتاحا بحث كاملان.

مفتاح البحث معناه
'ALL' يعيد جميع الرسائل الموجودة في المجلد. قد تواجهك قيود حجم الوحدة imaplib إذا طلبت جميع الرسائل الموجودة في مجلد كبير، لذا اطلع على القسم "قيود الحجم" التي سنوضحها لاحقًا.
'BEFORE date'‎و 'ON date' و 'SINCE date' تعيد مفاتيح البحث الثلاثة هذه الرسائل التي استلمها خادم IMAP قبل التاريخ date المُحدَّد أو فيه أو بعده على التوالي، حيث يجب أن يكون التاريخ بالتنسيق ‎05-Jul-2023. يطابق مفتاح البحث 'SINCE 05-Jul-2023' الرسائل في تاريخ 5 من الشهر السابع وبعده، ولكن يطابق مفتاح البحث 'BEFORE 05-Jul-2023' الرسائل قبل تاريخ 5 من الشهر السابع فقط دون مطابقة رسائل هذا التاريخ.
'SUBJECT string' و 'BODY string' و ‎'TEXT string' تعيد الرسائل التي تكون فيها السلسلة النصية string موجودة في موضوع Subject الرسالة أو نصها Body أو أيٍّ منهما على التوالي. إذا احتوت السلسلة النصية string على مسافات، فأحِطها بعلامات اقتباس مزدوجة مثل: ‎'TEXT "search with spaces"'‎.
'FROM string' و 'TO string' و 'CC string' و ‎'BCC string' تعيد جميع الرسائل التي تكون فيها السلسلة النصية string موجودة في عنوان البريد الإلكتروني "من from"، أو عناوين "إلى to"، أو عناوين "cc" (نسخة مطابقة)، أو عناوين "bcc" (نسخة مطابقة مخفية) على التوالي. إذا كانت هناك عناوين بريد إلكتروني متعددة في السلسلة النصية string، فافصل بينها بمسافات وأحِط كلها بعلامات اقتباس مزدوجة مثل: ‎'CC "firstcc@example.com secondcc@example.com"'‎.
'SEEN' و ‎'UNSEEN' تعيد جميع الرسائل مع أو بدون الراية ‎\Seen على التوالي، حيث تحصل رسالة البريد الإلكتروني على الراية ‎‎\Seen‎ إذا وصلنا إليها باستخدام استدعاء التابع fetch()‎ التي سنوضّحها لاحقًا أو إذا نقرنا عليها عند التحقق من البريد الإلكتروني في برنامج بريد إلكتروني أو متصفح ويب. من الشائع أن نقول أن البريد الإلكتروني "مقروء Read" بدلًا من "مُشاهَد Seen"، لكنهما يعنيان الشيء نفسه.
'ANSWERED' و 'UNANSWERED' تعيد جميع الرسائل مع أو بدون الراية ‎\Answered على التوالي، حيث تحصل الرسالة على الراية ‎\Answered عند الرد عليها.
'DELETED' و 'UNDELETED' تعيد جميع الرسائل مع أو بدون الراية ‎\Deleted على التوالي. تُعطَى رسائل البريد الإلكتروني المحذوفة باستخدام التابع delete_messages()‎ الرايةَ ‎\Deleted ولكنها لا تُحذَف نهائيًا حتى نستدعي التابع expunge()‎ (اطّلع على القسم "حذف رسائل البريد الإلكتروني" التي سنوضّحها لاحقًا). لاحظ أن بعض مزوّدي خدمة البريد الإلكتروني يحذفون نهائيًا Expunge رسائل البريد الإلكتروني تلقائيًا.
'DRAFT' و 'UNDRAFT' تعيد جميع الرسائل مع أو بدون الراية ‎\Draft على التوالي. تُحفَظ عادةً رسائل المسودات في مجلد منفصل هو مجلد المسودات Drafts بدلًا من مجلد البريد الوارد INBOX.
'FLAGGED' و 'UNFLAGGED' تعيد جميع الرسائل مع أو بدون الراية ‎\Flagged على التوالي، حيث تُستخدَم هذه الراية عادةً لوضع علامة على رسائل البريد الإلكتروني بوصفها "مهمة Important" أو "عاجلة Urgent".
'LARGER N' و 'SMALLER N' تعيد جميع الرسائل الأكبر أو الأصغر من N بايت على التوالي.
'NOT search-key' يعيد الرسائل التي لا يعيدها مفتاح البحث search-key.
'OR search-key1 search-key2' يعيد الرسائل التي تطابق مفتاح البحث search-key الأول أو الثاني.

إليك فيما يلي بعض الأمثلة على استدعاءات التابع search()‎ مع معانيها:

  • imapObj.search(['ALL'])‎: يعيد جميع الرسائل الموجودة في المجلد المُحدَّد حاليًا.
  • imapObj.search(['ON 05-Jul-2023'])‎: يعيد جميع الرسائل المُرسَلة في 5 من الشهر السادس من عام 2023.
  • imapObj.search(['SINCE 01-Jan-2023', 'BEFORE 01-Feb-2023', 'UNSEEN'])‎: يعيد جميع الرسائل غير المقروءة المُرسَلة في الشهر الأول من عام 2023. لاحظ أن ذلك يعني الرسائل المُرسَلة في 1 من الشهر الأول وما بعده من الشهر الأول ولا يتضمّن 1 من الشهر الثاني.
  • imapObj.search(['SINCE 01-Jan-2023', 'FROM alice@example.com'])‎: يعيد جميع الرسائل المُرسَلة من العنوان alice@example.com منذ بداية عام 2023.
  • imapObj.search(['SINCE 01-Jan-2023', 'NOT FROM alice@example.com'])‎: يعيد جميع الرسائل المُرسَلة من الجميع باستثناء العنوان alice@example.com منذ بداية عام 2023.
  • imapObj.search(['OR FROM alice@example.com FROM bob@example.com'])‎: يعيد جميع الرسائل المُرسَلة من العنوان alice@example.com أو العنوان bob@example.com.
  • imapObj.search(['FROM alice@example.com', 'FROM bob@example.com'])‎: لا يعيد هذا البحث أيّ رسائل مطلقًا، لأن الرسائل يجب أن تتطابق مع جميع كلمات البحث المفتاحية. لا يمكن أن يكون هناك سوى عنوان "من from" واحد فقط، فمن المستحيل أن تكون الرسالة من العنوان alice@example.com والعنوان bob@example.com.

لا يعيد التابع search()‎ رسائل البريد الإلكتروني، بل يعيد المعرّفات الفريدة UID لرسائل البريد الإلكتروني بوصفها قيمًا صحيحة. يمكنك بعد ذلك تمرير هذه المعرّفات الفريدة إلى التابع fetch()‎ للحصول على محتوى البريد الإلكتروني.

تابع مثال الصدفة التفاعلية من خلال إدخال ما يلي:

>>> UIDs = imapObj.search(['SINCE 05-Jul-2023'])
>>> UIDs
[40032, 40033, 40034, 40035, 40036, 40037, 40038, 40039, 40040, 40041]

خزّنا قائمة معرّفات الرسائل (للرسائل المُستلمة في 5 من الشهر السابع وما بعده) التي يعيدها التابع search()‎ في المتغير UIDs. تكون قائمة المعرّفات UIDs المُعادة على حاسوبك مختلفة عن القائمة الموضحة في مثالنا، لأنها فريدة لحساب بريد إلكتروني معين. استخدم قيم المعرّف الفريد UID التي تلقيتها وليس القيم الواردة في هذا المقال عندما تمرّرها لاحقًا إلى استدعاءات دوال أخرى.

قيود الحجم

إذا تطابق بحثك مع عدد كبير من رسائل البريد الإلكتروني، فقد ترفع شيفرة بايثون الاستثناء imaplib.error: got more than 10000 bytes، وعندها يجب قطع الاتصال بخادم IMAP وإعادة الاتصال به والمحاولة مرة أخرى.

وُضِع هذا القيد لمنع برامج بايثون الخاصة بك من استهلاك الكثير من الذاكرة، ولكن يكون الحد الأقصى للحجم الافتراضي غالبًا صغيرًا جدًا، إذ يمكنك تغييره من 10000 بايت إلى 10000000 بايت من خلال تشغيل الشيفرة البرمجية التالية:

>>> import imaplib
>>> imaplib._MAXLINE = 10000000

يُفترَض أن تمنع الشيفرة البرمجية السابقة ظهور رسالة الخطأ مرة أخرى، لذا قد ترغب في جعل هذين السطرين جزءًا من كل برنامج IMAP تكتبه.

جلب بريد إلكتروني ووضع علامة عليه كمقروء

يمكنك استدعاء التابع fetch()‎ الخاص بكائن IMAPClient للحصول على محتوى البريد الإلكتروني الفعلي بعد حصولك على قائمة المعرّفات الفريدة UID التي ستكون الوسيط الأول لهذا التابع، والوسيط الثاني هو القائمة ‎['BODY[]']‎ التي تطلب من التابع fetch()‎ تنزيل كل المحتوى الخاص بنص رسائل البريد الإلكتروني المُحدَّدة في قائمة المعرّفات الفريدة UID الخاصة بك.

لنتابع الآن مثالنا على الصدفة التفاعلية:

>>> rawMessages = imapObj.fetch(UIDs, ['BODY[]'])
>>> import pprint
>>> pprint.pprint(rawMessages)
{40040: {'BODY[]': 'Delivered-To: my_email_address@example.com\r\n'
                   'Received: by 10.76.71.167 with SMTP id '
--snip--
                   '\r\n'
                   '------=_Part_6000970_707736290.1404819487066--\r\n',
         'SEQ': 5430}}

استورد الوحدة pprint ومرّر القيمة المُعادة من التابع fetch()‎ والمُخزَّنة في المتغير rawMessages إلى الدالة pprint.pprint()‎ "لطباعتها بمظهر جميل Pretty Print"، وسترى أن هذه القيمة المُعادة هي قاموس متداخل للرسائل ذات المعرفات الفريدة UID بوصفها مفاتيحًا. تُخزَّن كل رسالة كقاموس له مفتاحان هما: ‎'BODY[]'‎ و 'SEQ'، حيث يُربَط المفتاح ‎'BODY[]'‎ مع النص الفعلي للبريد الإلكتروني. يُعَد المفتاح 'SEQ' مُخصَّصًا للرقم التسلسلي Sequence Number، والذي له دورٌ مماثل للمعرّف الفريد (UID)، ولكن يمكنك تجاهله. يُعَد محتوى الرسالة الموجود في المفتاح ‎'BODY[]'‎ غير مفهوم إلى حد كبير، فهو بتنسيق اسمه RFC 822، وهو مصمم لتقرأه خوادم IMAP، ولكن لا حاجة إلى فهم هذا التنسيق، إذ سنوضّحه لاحقًا عند شرح وحدة pyzmail في هذا المقال.

استدعينا الدالة select_folder()‎ مع وسيط الكلمات المفتاحية readonly=True عند تحديد مجلد للبحث فيه، حيث يؤدي ذلك إلى منعك من حذف رسالة بريد إلكتروني عن طريق الخطأ، ولكنه يعني أيضًا عدم وضع علامة على رسائل البريد الإلكتروني بوصفها مقروءة إذا جلبتها باستخدام التابع fetch()‎، لذا إذا أردتَ وضع علامة على رسائل البريد الإلكتروني بوصفها مقروءة عند جلبها، فيجب تمرير الوسيط readonly=False إلى الدالة select_folder()‎. إذا كان المجلد المُحدَّد في وضع القراءة فقط، فيمكنك إعادة تحديد المجلد الحالي باستدعاء آخر للدالة select_folder()‎ مع وسيط الكلمات المفتاحية readonly=False كما يلي:

>>> imapObj.select_folder('INBOX', readonly=False)

الحصول على عناوين البريد الإلكتروني من رسالة خام Raw Message

لا تُعَد الرسائل الخام التي يعيدها التابع fetch()‎ مفيدة جدًا للأشخاص الذين يريدون قراءة رسائل بريدهم الإلكتروني فقط، لذا تحلّل الوحدة pyzmail هذه الرسائل الخام وتعيدها بوصفها كائنات PyzMessage، مما يجعل أقسام الموضوع والنص والحقل "إلى To" والحقل "من From" والأقسام الأخرى من البريد الإلكتروني قابلة للوصول بسهولة من شيفرة بايثون الخاصة بك.

تطبيق عملي: إرسال رسائل البريد الإلكتروني للتذكير الأعضاء بدفع مستحقاتهم

لنفترض أنك تطوعتَ لتعقّب دفع الأعضاء لمستحقاتهم في نادي تطوع إلزامي، حيث تُعَد هذه المهمة مملةً جدًا، إذ تتضمّن استخدام جدول بيانات يحتوي جميع الأشخاص الذين دفعوا في كلّ شهر وإرسال رسائل تذكير عبر البريد الإلكتروني إلى الأشخاص الذين لم يدفعوا، وبالتالي ستمر على كامل جدول البيانات بنفسك وتنسخ وتلصق رسالة البريد الإلكتروني نفسها لكلّ مَن تأخر في سداد مستحقاته، إذًا لنكتب سكربتًا ينفّذ هذه المهمة نيابةً عنك.

إليك الخطوات العامة التي سيطبّقها برنامجك:

  1. قراءة البيانات من جدول بيانات إكسل.
  2. البحث عن جميع الأعضاء الذين لم يسدّدوا مستحقاتهم للشهر الأخير.
  3. البحث عن عناوين بريدهم الإلكتروني وإرسال رسائل تذكير مُخصَّصة لهم.

يجب أن تطبّق شيفرتك البرمجية الخطوات التالية:

  1. فتح وقراءة خلايا مستند إكسل باستخدام الوحدة openpyxl كما تعلّمنا في مقالٍ سابق.
  2. إنشاء قاموس للأعضاء الذين لم يسددوا مستحقاتهم.
  3. تسجيل الدخول إلى خادم SMTP من خلال استدعاء smtplib.SMTP()‎ و ehlo()‎ و starttls()‎ وlogin()‎.
  4. إرسال رسالة تذكير مُخصَّصة عبر البريد الإلكتروني من خلال استدعاء التابع sendmail()‎ إلى جميع الأعضاء الذين لم يسددوا مستحقاتهم.

افتح تبويبًا جديدًا في محرّرك لإنشاء ملف جديد واحفظه بالاسم sendDuesReminders.py.

الخطوة الأولى: فتح ملف إكسل

لنفترض أن جدول بيانات إكسل الذي تستخدمه لتعقّب دفعات مستحقات العضوية يشبه الشكل التالي، وهو موجود في ملف اسمه duesRecords.xlsx ويمكنك تنزيله من nostarch.

02 000130

جدول تعقّب دفعات مستحقات الأعضاء

يحتوي جدول البيانات على اسم كل عضو وعنوان بريده الإلكتروني، ويكون لكل شهر عمودٌ لتعقّب حالات الدفع الخاصة بالأعضاء، حيث تُميَّز الخلية الخاصة بكل عضو بالكلمة "paid" بعد دفع المستحقات.

يجب أن يفتح البرنامج الملف duesRecords.xlsx ويعرف العمود الخاص بالشهر الأخير من خلال قراءة السمة sheet.max_column. اطّلع على المقال الخاص بالتعامل مع جداول بيانات إكسل باستخدام بايثون لمزيد من المعلومات حول الوصول إلى الخلايا في ملفات جداول بيانات إكسل باستخدام الوحدة openpyxl.

أدخِل الشيفرة البرمجية التالية في تبويب محرّر ملفاتك:

   #! python3
   # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات

   import openpyxl, smtplib, sys

   # فتح جدول البيانات والحصول على حالة المستحقات الأخيرة

 wb = openpyxl.load_workbook('duesRecords.xlsx')
 sheet = wb.get_sheet_by_name('Sheet1')
 lastCol = sheet.max_column
 latestMonth = sheet.cell(row=1, column=lastCol).value

   # التحقق من حالة الدفع لكل عضو

   # تسجيل الدخول إلى حساب البريد الإلكتروني

   # إرسال رسائل التذكير عبر البريد الإلكتروني

نستورد الوحدات openpyxl و smtplib و sys، ثم نفتح الملف duesRecords.xlsx ونخزّن كائن Workbook الناتج في المتغير wb ➊. نحصل بعد ذلك على الورقة Sheet1 ونخزّن الكائن Worksheet الناتج في المتغير sheet ➋. أصبح لدينا كائن Worksheet، وبالتالي يمكننا الآن الوصول إلى الصفوف والأعمدة والخلايا، حيث نخزّن العمود الأعلى في المتغير lastCol ➌، ثم نستخدم الصف رقم 1 والعمود lastCol للوصول إلى الخلية التي يجب أن تحتوي على الشهر الأخير، حيث نحصل على القيمة الموجودة في هذه الخلية ونخزّنها في المتغير latestMonth ➍.

الخطوة الثانية: البحث عن جميع الأعضاء الذين لم يدفعوا مستحقاتهم

حدّدنا رقم العمود للشهر الأخير (المُخزَّن في المتغير lastCol)، ويمكننا الآن المرور ضمن حلقة على جميع الصفوف بعد الصف الأول الذي يحتوي على ترويسات الأعمدة لمعرفة الأعضاء الذين يكون لديهم النص "paid" في الخلية الخاصة بمستحقات ذلك الشهر. إن لم يدفع العضو، فيمكنك الحصول على اسم العضو وعنوان بريده الإلكتروني من العمودين 1 و2 على التوالي، حيث ستُدخِل هذه المعلومات في القاموس unpaidMembers الذي سيتعقّب جميع الأعضاء الذين لم يدفعوا في الشهر الأخير. أدخِل الشيفرة البرمجية التالية إلى برنامج sendDuesReminder.py:

   #! python3
   # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات

   --snip--

   # التحقق من حالة الدفع لكل عضو
   unpaidMembers = {}
 for r in range(2, sheet.max_row + 1):
     payment = sheet.cell(row=r, column=lastCol).value
       if payment != 'paid':
         name = sheet.cell(row=r, column=1).value
         email = sheet.cell(row=r, column=2).value
         unpaidMembers[name] = email

تُعِد الشيفرة البرمجية السابقة القاموس الفارغ unpaidMembers، ثم تمر ضمن حلقة على جميع الصفوف بعد الصف الأول ➊، ثم تُخزَّن القيمة الموجودة في العمود الأخير ضمن المتغير payment لكل صف ➋. إذا لم يساوِ المتغير payment القيمة 'paid'، فستُخزَّن قيمة العمود الأول في المتغير name ➌، وتُخزَّن قيمة العمود الثاني في المتغير email ➍، ويُضاف المتغيران name و email إلى القاموس unpaidMembers ➎.

الخطوة الثالثة: إرسال رسائل تذكير مخصصة عبر البريد الإلكتروني

حصلنا على قائمة بجميع الأعضاء غير الدافعين، وحان الوقت الآن لإرسال رسائل تذكير لهؤلاء الأعضاء عبر البريد الإلكتروني. أدخِل الشيفرة البرمجية التالية إلى برنامجك، ولكن مع عنوان بريدك الإلكتروني ومعلومات مزوّدك الحقيقية:

#! python3
# sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات

--snip--

# تسجيل الدخول إلى حساب البريد الإلكتروني
smtpObj = smtplib.SMTP('smtp.example.com', 587)
smtpObj.ehlo()
smtpObj.starttls()
smtpObj.login('my_email_address@example.com', sys.argv[1])

أنشئ كائن SMTP من خلال استدعاء الدالة smtplib.SMTP()‎ ومرّر إليها اسم النطاق والمنفذ الخاص بمزوّدك، ثم استدعِ التوابع ehlo()‎ و starttls()‎، ثم استدعِ التابع login()‎ ومرّر إليه عنوان بريدك الإلكتروني والقائمة sys.argv[1]‎ التي ستخزّن السلسلة النصية لكلمة مرورك، حيث ستدخِل كلمة المرور بوصفها وسيط سطر أوامر في كل مرة تشغّل فيها البرنامج لتجنّب حفظ كلمة مرورك في شيفرتك المصدرية.

يسجّل برنامجك الدخول إلى حساب بريدك الإلكتروني، ثم يجب أن يمر على القاموس unpaidMembers ويرسل بريدًا إلكترونيًا مُخصَّصًا إلى عنوان البريد الإلكتروني لكل عضو. أضِف ما يلي إلى برنامج sendDuesReminders.py:

   #! python3
   # sendDuesReminders.py - إرسال رسائل البريد الإلكتروني بناءً على حالة الدفع في جدول البيانات

   --snip--

   # إرسال رسائل التذكير عبر البريد الإلكتروني
   for name, email in unpaidMembers.items():
     body = "Subject: %s dues unpaid.\nDear %s,\nRecords show that you have not
   paid dues for %s. Please make this payment as soon as possible. Thank you!'" %
   (latestMonth, name, latestMonth)
     print('Sending email to %s...' % email)
     sendmailStatus = smtpObj.sendmail('my_email_address@example.com', email,
body)

     if sendmailStatus != {}:
           print('There was a problem sending email to %s: %s' % (email,
           sendmailStatus))
   smtpObj.quit()

تمر الشيفرة البرمجية السابقة ضمن حلقة على الأسماء ورسائل البريد الإلكتروني الموجودة في القاموس unpaidMembers، ونخصّص رسالةً لكل عضو لم يدفع، حيث تتضمّن هذه الرسالة الشهر الأخير واسم العضو، ونخزّنها في المتغير body ➊. نطبع بعد ذلك خرجًا يفيد بأننا نرسل بريدًا إلكترونيًا إلى عنوان البريد الإلكتروني لهذا العضو ➋، ثم نستدعي التابع sendmail()‎، ونمرّر إليه عنوان البريد الإلكتروني والرسالة المُخصَّصة ➌، ونخزّن القيمة المُعادة في المتغير sendmailStatus. تذكّر أن التابع sendmail()‎ يعيد قيمة قاموس غير فارغ إذا أبلغ خادم SMTP عن خطأ أثناء إرسال هذا البريد الإلكتروني، لذا يتحقق الجزء الأخير من حلقة for ➍ مما إذا كان القاموس المُعاد غير فارغ، فإذا كان كذلك، فسيطبع عنوان البريد الإلكتروني للمستلم والقاموس المُعاد. نستدعي التابع quit()‎ لقطع الاتصال بخادم SMTP بعد أن ينتهي البرنامج من إرسال كافة رسائل البريد الإلكتروني.

ستكون النتيجة كما يلي عند تشغيل البرنامج:

Sending email to alice@example.com...
Sending email to bob@example.com...
Sending email to eve@example.com…

يتلقّى المستلمون رسالة بريد إلكتروني حول دفعاتهم الفائتة والتي ستشبه البريد الإلكتروني الذي ترسله يدويًا.

الخلاصة

نتواصل مع بعضنا بعضًا عبر الإنترنت وعبر شبكات الهاتف المحمول باستخدام العديد من الطرق، ولكن يُعَد البريد الإلكتروني والرسائل النصية الطرق الأكثر انتشارًا، حيث يمكن لبرامجك التواصل عبر هذه القنوات، مما يمنحها ميزات إشعارات جديدة. يمكنك أيضًا كتابة برامج تعمل على حواسيب مختلفة وتتواصل مع بعضها بعضًا مباشرةً عبر البريد الإلكتروني، حيث يرسل أحد البرامج رسائل البريد الإلكتروني باستخدام بروتوكول SMTP بينما يستردها البرنامج الآخر باستخدام بروتوكول IMAP.

توفّر وحدة smtplib الخاصة بلغة بايثون دوالًا لاستخدام بروتوكول SMTP لإرسال رسائل البريد الإلكتروني عبر خادم SMTP الخاص بمزوّد بريدك الإلكتروني، وتتيح الوحدتان imapclient و pyzmail الخارجيتان الوصولَ إلى خوادم IMAP واسترداد رسائل البريد الإلكتروني المرسلة إليك. يُعَد بروتوكول IMAP أكثر تعقيدًا من بروتوكول SMTP، ولكنه قوي جدًا ويسمح بالبحث عن رسائل بريد إلكتروني معينة وتنزيلها وتحليلها لاستخراج موضوع ونص البريد الإلكتروني بوصفها قيمًا نوعها سلسلة نصية.

لا تسمح بعض خدمات البريد الإلكتروني الشائعة مثل جيميل بأن تستخدم بروتوكولات SMTP و IMAP المعيارية للوصول إلى خدماتها كإجراء احترازي من الرسائل غير المرغوب بها وبهدف الأمان، ولكن تعمل وحدة EZGmail بمثابة مغلِّف مناسب لواجهة برمجة تطبيقات جيميل، مما يسمح لسكربتات بايثون الخاصة بك بالوصول إلى حسابك على جيميل. نوصي بشدة بإعداد حساب جيميل منفصل لاستخدام سكربتاتك حتى لا تتسبب الأخطاء المحتملة في برنامجك في حدوث مشكلات لحسابك الشخصي على جيميل.

ترجمة -وبتصرُّف- للقسم Sending Email من مقال Sending Email and Text Messages لصاحبه Al Sweigart.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...