المحادثات
`threads.list` و`list_all` و`iterate` و`get` و`update` و`trash` و`snooze` و`unsnooze` و`list_attachments`.
القراءة
from openemail import openemail page = openemail.threads.list( folder='inbox', query='from:ada', label_ids=['INBOX', 'IMPORTANT'], limit=25,) next_page = ( openemail.threads.list(folder='inbox', cursor=page['nextCursor']) if page['nextCursor'] else None) thread = openemail.threads.get('thread_…')print(thread['messageCount'], thread['hasUnread'], thread['totalReplies'])يقسّم API المحادثات إلى صفحات باستخدام pageToken. ويسلّمه لك العميل باسم nextCursor ويستردّه باسم cursor، كأي قائمة أخرى، ويتبعه list_all وiterate نيابةً عنك. وهو معتم: أعِد ما أُعطيت ولا تبنِ واحدًا أبدًا.
مرشِّحات القائمة وسائط مسمّاة بصيغة snake_case (label_ids= وdate_from=)، بينما تحتفظ مفاتيح جسم الطلب بأسماء API بصيغة camelCase (addLabelIds في update). وتعود الصفحة والمحادثة في صورة قواميس، فتقرؤهما page['nextCursor'] وthread['messageCount'].
from datetime import datetime, timedelta, timezone from openemail import openemail now = datetime.now(timezone.utc) last_week = openemail.threads.list_all( sort='oldest', date_from=now - timedelta(days=7), date_to=now, from_contacts=True,) for thread in openemail.threads.iterate(sort='sender'): print(thread['id'])sort وdate_from وdate_to وfrom_contacts هي عناصر التحكم الخاصة بقائمة المحادثات. sort هو newest أو oldest أو sender أو subject، وتقبل التواريخ datetime أو سلسلة ISO 8601 وكلا الطرفين مشمول، ويُبقي from_contacts البريد الذي جاءت أحدث رسائله من جهة اتصال محفوظة. وكل ترتيب يُتصفَّح حتى آخره دون تخطي محادثة أو تكرارها. وdatetime الذي ليس له tzinfo يُقرأ بالتوقيت المحلي.
التنظيم
from datetime import datetime, timedelta, timezone from openemail import openemail openemail.threads.update('thread_…', { 'read': True, 'addLabelIds': ['USER_DONE'], 'removeLabelIds': ['INBOX'],}) openemail.threads.trash('thread_…')openemail.threads.snooze('thread_…', datetime.now(timezone.utc) + timedelta(days=1))openemail.threads.unsnooze('thread_…')حالة القراءة هي تسمية في كل خلفية هنا، فهي تنتقل مع قوائم التسميات ويكون الترتيب حتميًا حين تضبط الاثنين معًا. ويجب وجود حقل واحد على الأقل من الحقول الثلاثة.
يأخذ addLabelIds معرّفات من labels.list ومعرّفات النظام مثل ARCHIVE وSTARRED. والمعرّف الذي لا يسمّي أي تصنيف يُرفض برمز 422 label_not_found بدل أن يُنشأ، لذا أنشئ التصنيف أولًا بـlabels.create. ويسرد threads.list(folder='USER_DONE') كل سلسلة تحمل تصنيفًا أيًّا كان مجلدها.
مرفقات رسالة
import base64from pathlib import Path from openemail import openemail files = openemail.threads.list_attachments('thread_…', 'message_…') for file in files: print(file['filename'], file['contentType'], file['size']) if file['content']: name = Path(file['filename']).name Path(name).write_bytes(base64.b64decode(file['content']))الحقل content بصيغة base64، ويكون سلسلة فارغة حين يتعذّر العثور على البايتات المخزَّنة، فتحقّق من طوله قبل فك الترميز. والنص المشفَّر لرسالة مشفَّرة موجود في هذه القائمة ويُنزَّل كأي ملف آخر؛ أما جزء إصدار PGP/MIME وأي توقيع منفصل فليسا فيها. ويحتفظان بمعرّفيهما في encryption.parts ولا شيء أكثر.
رسالة وصلت مشفَّرة
هذا SDK لا يشفّر ولا يفك التشفير: فهو لا يستطيع فتح رسالة شفّرها غيره، ولا يستطيع إرسال رسالة مشفَّرة. ويُرفض طلب الإرسال إن حمل علامة تشفير، لأن عميلًا بلا مفتاح لا شأن له بتأكيد تشفير. والمفاتيح المولَّدة في تطبيق OpenEmail تقيم في المتصفح الذي أنشأها ولا تصل إلى شيء هنا، وحين يفتح ذلك المتصفح رسالة مختومة يبقى النص الصريح فيه، وتبقى الرسالة المخزَّنة التي يقرأها هذا الاستدعاء نصًا مشفَّرًا. وما يعطيك إياه threads.get هو الغلاف، معروفًا. والرسالة التي وصلت ملفوفة بـ PGP أو S/MIME تحمل قاموس encryption، فيكفّ decodedBody الفارغ عن أن يكون كل ما يُسلَّم إليك. وهو المفتاح الوحيد الذي لا تنجو من التخمين عند غيابه، ويصفه MessageEncryption في openemail.types.
import sys from openemail import is_sealed, openemail thread = openemail.threads.get('thread_…') for message in thread['messages']: if not message.get('encryption'): continue if not is_sealed(message): continue print('cannot read this one:', message['encryption']['format'], file=sys.stderr)تفرّع بـis_sealed، لا بوجود الحقل أبدًا. فصيغتان من الخمس، pgp-signed وsmime-signed، تصفان متنًا وصل مكشوفًا إلى جانب توقيع منفصل، فالتحكم بناءً على الوجود يخفي بريدًا لم يكن أحد بحاجة إلى إخفائه، ولا يستطيع المستخدم رؤيته ولا تفسيره. وتُشحن is_sealed لهذا السبب بالذات: فالخادم يذكر المجموعة المختومة مرة واحدة، والنسخة الثالثة المكتوبة انطلاقًا من الاتحاد هي النسخة التي تنحرف.
الغياب ليس نصًا صريحًا. فـencryption غائب عن كل رسالة خُزّنت قبل شحن الكشف، وعن كل ما وصل إلى صندوق البريد عبر مسار لم يعمل فيه الكاشف قط. وهو يسجّل أن أحدًا لم ينظر، وهي حقيقة عن تغطيتنا لا عن البريد، ولا شيء يعيد ملأه بأثر رجعي.
أين تختلف هذه عن البقية
- كل مدخل في
ThreadResource.messagesهوMessageResource، أيdict[str, Any]عادي لا يسمّي نوعه أي حقل، ولا حتىencryption. فتحديد أنواع الحقول سيكون تأكيدًا من العميل على تطبيع لا يجريه أحد. اقرأencryptionبـmessage.get('encryption')وتفرّع بـis_sealed، لأن عميلًا لا يستطيع التفرّع عليه يقرأ رسالة مختومة على أنها رسالة فارغة. - الطلب الذي لا يمكن تلبيته بأمانة يعطي 422
capability_unsupported، لا استجابة تبدو صحيحة وهي خاطئة في صمت.
المعاملات: threads.list
folderstr- أي مجلد تسرد. ويضبطه الخادم افتراضيًا على `inbox`، فإغفاله يضيّق السرد بدل توسيعه ليشمل كل شيء. وهو يسري على بحث `query` أيضًا، ما لم يسمِّ الاستعلام مجلدًا بنفسه عبر `in:` أو عبر `is:` خاص بمجلد مثل `is:sent`.
querystr- صياغة البحث في صندوق البريد. يجب أن تظهر الكلمات المجردة كلها، وتتطابق كل منها بتسامح: تُتجاهل حالة الأحرف والعلامات الصوتية والفواصل، ويُحتسب جزء من كلمة أطول، فـ`min` و`ben jamin` كلاهما يجد “Benjamin”. وتُطابَق العبارة بين علامتي اقتباس كما كُتبت باستثناء الحالة والعلامات الصوتية، فـ`"ben jamin"` لا يجد “Ben-Jamin”، وتُسقَط كلمات الحشو حين يبقى شيء آخر للبحث عنه. وحين لا يطابق شيءٌ تمامًا تُعاد بدلًا منه التهجئات القريبة، فيجد `benjimin` الاسم “Benjamin”: فالكلمة المجردة، أو قيمة `from:` أو `to:` أو `cc:` أو `subject:` أو `body:` أو `filename:` أو `label:`، يجوز أن تختلف عن بداية كلمة بخطأ مطبعي واحد (حرف مُبدَل أو ناقص أو زائد أو مُبادَل مع جاره) إذا كانت من أربعة إلى سبعة أحرف، وبخطأين إذا كانت من ثمانية أحرف فأكثر، بينما تبقى العبارة المقتبسة والكلمة التي تحوي رقمًا والكلمة الأقصر والكلمة المستبعدة مطابِقةً تمامًا فقط، وتواصل الصفحات التالية المطابقة بالطريقة نفسها. ضيّق النتائج بمعاملات مثل `from:ada` و`label:Invoices` و`is:unread` و`has:pdf` و`before:2026/01/31` و`older_than:1y`، واجمعها بـ`OR` والأقواس وعلامة `-` في البداية؛ والقيمة التي لا يستطيع البحث استخدامها تُتجاهل بدل أن تضيّق. وتقرأ الكلمات والمعاملات `from:` و`to:` و`cc:` و`subject:` و`body:` مرسِلَ أحدث رسالة ومستلميها وموضوعها وأول 4,000 حرف من متنها بعد تجريد الوسوم، بينما يقرأ `filename:` و`has:` كل مرفق في المحادثة كاملة، وتقرأ التسميات والمجلدات المحادثة كاملة. وهو يضيّق الفهرس نفسه الذي يقرأه السرد غير المرشَّح. والرسائل المختومة لا تخزّن نص متن، فلا يمكن أن يتطابق منها إلا المرسِل والمستلمون والموضوع. وتطابق الكلمة العادية أيضًا اسم أي مرفق في المحادثة، أيًّا كانت الرسالة التي حملته.
label_idsstr | Sequence[str]- قصر السرد على المحادثات الحاملة لهذه التسميات. وتأخذ نقطة النهاية سلسلة مفصولة بفواصل، ويضمّ العميل القائمة أو الصف (tuple) في سلسلة واحدة نيابةً عنك. ولا حد لعدد ما تسمّيه منها.
limitint- كم محادثة تُعاد، من 1 إلى 100. وإن أُغفل، استخدم المعالج 25. والقيمة الافتراضية تقيم في المعالج لا في المخطط، فتتصرف القيمة الغائبة والقيمة 25 الصريحة التصرف نفسه.
cursorstr- قيمة `nextCursor` من الصفحة السابقة، تُعاد حرفيًا. وهي `pageToken` الخاص بـAPI تحت الاسم الذي تستخدمه كل قائمة أخرى، وهي معتمة، فلا تبنِ واحدة ولا تعدّلها أبدًا.
الاستجابة: Page[ThreadSummaryResource]
itemslist[ThreadSummaryResource]- مدخل واحد لكل محادثة في هذه الصفحة، مرفوعًا من غلاف `data` الخاص بـAPI. وكل مدخل ليس إلا علامة كائن ومعرّفًا. ولا يحمل السرد موضوعًا ولا مقتطفًا ولا مشاركين ولا تسميات، فأي شيء أكثر من ذلك يعني مناداة `threads.get` على المحادثات التي تريدها.
items[].idstr- معرّف المحادثة، ليُسلَّم إلى `threads.get` و`threads.update` وبقية التوابع دون تغيير. وهو المعرّف نفسه سواء جاء الصف من سرد مرشَّح أو من بحث `query`.
hasMorebool- ما إذا كانت هناك صفحة تالية، مشتقًّا من `nextCursor` حيث لا يذكره API.
nextCursorstr | None- قيمة `nextPageToken` الخاصة بـ API، تُرسَل مجددًا باسم `cursor` للصفحة التالية، أو `None` حين لا توجد صفحة تالية. والرمز الفارغ يُطبَّع إلى `None`، فيتفق فحص القيمة الزائفة (falsy) مع فحص `None`.