تخطَّ إلى المستندات
Ruby

المحادثات

`threads.list` و`list_all` و`iterate` و`get` و`update` و`trash` و`snooze` و`unsnooze` و`list_attachments`.

القراءة

read_threads.rb
page = client.threads.list(  folder: "inbox",  query: "from:ada",  label_ids: ["INBOX", "IMPORTANT"],  limit: 25) if page.next_cursor  next_page = client.threads.list(folder: "inbox", cursor: page.next_cursor)  puts next_page.items.sizeend thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com")puts thread[:messageCount], thread[:hasUnread], thread[:totalReplies]

يقسّم API المحادثات إلى صفحات باستخدام pageToken. ويسلّمه لك العميل باسم next_cursor ويستردّه باسم cursor:، كأي قائمة أخرى، ويتبعه list_all وiterate نيابةً عنك. وهو معتم: أعِد ما أُعطيت ولا تبنِ واحدًا أبدًا.

مرشِّحات القائمة وسائط مسمّاة في Ruby بصيغة snake_case (label_ids: وdate_from:)، بينما تحتفظ حقول متن الطلب بأسماء API بصيغة camelCase (addLabelIds: في update). وتعود المحادثة في صورة Hash بمفاتيح من نوع Symbol، فيقرأ thread[:messageCount] العدد.

sort_threads.rb
last_week = client.threads.list_all(  sort: "oldest",  date_from: Time.now - (7 * 86_400),  date_to: Time.now,  from_contacts: true)puts last_week.size client.threads.iterate(sort: "sender") do |thread|  puts thread[:id]end

sort: وdate_from: وdate_to: وfrom_contacts: هي أدوات التحكم الخاصة بقائمة المحادثات. وقيمة sort: هي newest أو oldest أو sender أو subject، ويسمّيها OpenEmail::THREAD_SORTS. وتأخذ التواريخ Time أو DateTime أو سلسلة ISO 8601 فيها وقت وإزاحة زمنية، ويُشمل الطرفان كلاهما. ويُرسَل Date في Ruby كتاريخ مجرد، وترفضه هذه الحقول بـ 422. ويُبقي from_contacts: true البريد الذي جاءت أحدث رسائله من جهة اتصال محفوظة. وكل ترتيب يرقّم الصفحات حتى النهاية دون تخطي محادثة أو تكرارها.

يعيد list_all مصفوفة Array واحدة بمجرد وصول الصفحة الأخيرة. ويمرّر iterate كل محادثة إلى كتلة ولا يجلب الصفحة التالية إلا حين تحتاجها الحلقة. ودون كتلة يعيد Enumerator، فيتوقف first(10) أو lazy بمجرد حصولهما على ما يحتاجانه.

التنظيم

organise_threads.rb
thread_id = "CAHk7pQ2x9LmZ4-mail.example.com" client.threads.update(thread_id, read: true, addLabelIds: ["USER_DONE"], removeLabelIds: ["INBOX"]) client.threads.trash(thread_id)client.threads.snooze(thread_id, Time.now + 86_400)client.threads.unsnooze(thread_id)

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

يأخذ addLabelIds معرّفات من labels.list ومعرّفات النظام مثل ARCHIVE وSTARRED. والمعرّف الذي لا يسمّي أي تسمية يُرفض بالخطأ 422 label_not_found بدل أن يُنشأ، لذا أنشئ التسمية أولًا بـ labels.create. ويسرد client.threads.list(folder: "USER_DONE") كل محادثة تحمل تسمية أيًّا كان مجلدها.

مرفقات رسالة

attachments.rb
files = client.threads.list_attachments("CAHk7pQ2x9LmZ4-mail.example.com", "message_4c1b257a") files.each do |file|  puts "#{file[:filename]} #{file[:contentType]} #{file[:size]}"  File.binwrite(file[:filename], file[:content].unpack1("m")) unless file[:content].to_s.empty?end

يعيد list_attachments مصفوفة Array من Hash. ويكون content بترميز base64، يحوّله unpack1("m") إلى String ثنائي، ويكون سلسلة فارغة حين يتعذّر العثور على البايتات المخزَّنة، فافحص طوله قبل فك الترميز. والنص المشفَّر لرسالة مشفّرة موجود في هذه القائمة ويُنزَّل مثل أي ملف آخر. أما جزء إصدار PGP/MIME وأي توقيع منفصل فليسا فيها. بل يحتفظان بمعرّفاتهما في encryption.parts لا أكثر.

رسالة وصلت مشفَّرة

هذا الـ gem لا يشفّر ولا يفك التشفير. فهو لا يستطيع فتح رسالة شفّرها غيره، ولا يستطيع إرسال رسالة مشفَّرة. ويُرفض طلب الإرسال إن حمل علامة تشفير، لأن عميلًا بلا مفتاح لا شأن له بتأكيد تشفير. والمفاتيح المولَّدة في تطبيق OpenEmail تقيم في المتصفح الذي أنشأها ولا تصل إلى شيء هنا. وحين يفتح ذلك المتصفح رسالة مختومة يبقى النص الصريح فيه، وتبقى الرسالة المخزَّنة التي يقرأها هذا الاستدعاء نصًا مشفَّرًا. وما يعطيك إياه threads.get هو الغلاف، معروفًا. والرسالة التي وصلت ملفوفة بـ PGP أو S/MIME تحمل Hash باسم encryption، فيكفّ decodedBody الفارغ عن أن يكون كل ما يُسلَّم إليك. وencryption هو الحقل الوحيد في الرسالة الذي تلتزم به API، لأنه الحقل الذي لا يمكنك أن تنجو من التخمين بشأن غيابه.

encrypted_mail.rb
thread = client.threads.get("CAHk7pQ2x9LmZ4-mail.example.com") thread[:messages].each do |message|  next unless message[:encryption]  next unless OpenEmail.sealed?(message)   warn "cannot read this one: #{message[:encryption][:format]}"end

تفرّع بـ OpenEmail.sealed?، لا بوجود الحقل أبدًا. فصيغتان من الخمس، pgp-signed وsmime-signed، تصفان متنًا وصل مكشوفًا إلى جانب توقيع منفصل، فالتحكم بناءً على الوجود يخفي بريدًا لم يكن أحد بحاجة إلى إخفائه، ولا يستطيع المستخدم رؤيته ولا تفسيره. ويوجد OpenEmail.sealed? لهذا السبب بالذات. فالخادم يذكر المجموعة المختومة مرة واحدة، ونسخة الـ gem مولَّدة من المصدر نفسه، والنسخة الثالثة المكتوبة يدويًا هي النسخة التي تنحرف. ويسمّي OpenEmail::MESSAGE_ENCRYPTION_FORMATS الصيغ الخمس كلها.

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

أين تختلف هذه عن البقية

  • كل عنصر في messages الخاصة بالمحادثة هو الـ Hash الذي خزّنه صندوق البريد، دون قائمة ثابتة من الحقول. والوعد بأكثر من ذلك يعني أن العميل يؤكد توحيدًا لا يجريه أحد. وencryption هو الحقل الوحيد الذي تلتزم به API على أي حال، لأن العميل الذي لا يستطيع التفرّع عليه يقرأ الرسالة المختومة كأنها رسالة فارغة.
  • الطلب الذي لا يمكن تلبيته بأمانة يعطي 422 capability_unsupported، يُرفع في صورة OpenEmail::ValidationError، لا استجابة تبدو صحيحة وهي خاطئة في صمت.

المعاملات: threads.list

folderString
أي مجلد تسرد. ويضبطه الخادم افتراضيًا على `inbox`، فإغفاله يضيّق السرد بدل توسيعه ليشمل كل شيء. وهو يسري على بحث `query:` أيضًا، ما لم يسمِّ الاستعلام مجلدًا بنفسه عبر `in:` أو عبر `is:` خاص بمجلد مثل `is:sent`.
queryString
صياغة البحث في صندوق البريد. يجب أن تظهر الكلمات المجردة كلها، وتتطابق كل منها بتسامح: تُتجاهل حالة الأحرف والعلامات الصوتية والفواصل، ويُحتسب جزء من كلمة أطول، فـ `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_idsString or Array<String>
قصر السرد على المحادثات التي تحمل هذه التسميات. تأخذ نقطة النهاية سلسلة مفصولة بفواصل، ويضم العميل Array أو Set في سلسلة واحدة نيابةً عنك. ولا حد لعدد ما تسمّيه.
limitInteger
كم محادثة تُعاد، من 1 إلى 100. وإن أُغفل، استخدم المعالج 25. والقيمة الافتراضية تقيم في المعالج لا في المخطط، فتتصرف القيمة الغائبة والقيمة 25 الصريحة التصرف نفسه.
cursorString
`next_cursor` الخاص بالصفحة السابقة، يُمرَّر كما هو حرفيًا. وهو `pageToken` الخاص بـ API تحت الاسم الذي تستخدمه كل قائمة أخرى، وهو معتم، فلا تبنِه ولا تعدّله أبدًا.

الاستجابة: OpenEmail::Page

itemsArray<Hash>
Hash واحد لكل محادثة في هذه الصفحة، مستخرج من غلاف `data` في API. وكل واحد منها مجرد علامة `object` و`id`. ولا يحمل السرد موضوعًا ولا مقتطفًا ولا مشاركين ولا تسميات، فأي شيء أكثر يعني استدعاء `threads.get` على المحادثات التي تريدها.
items[].idString
معرّف المحادثة، يُقرأ بوصفه `item[:id]`، لتمرّره دون تغيير إلى `threads.get` و`threads.update` وغيرهما. وهو المعرّف نفسه سواء جاء الصف من سرد مرشَّح أو من بحث `query:`.
has_more?Boolean
ما إذا كانت هناك صفحة أخرى، مأخوذ من API حيث تذكر ذلك، ومشتق من `next_cursor` حيث لا تذكره.
next_cursorString or nil
`nextPageToken` الخاص بـ API، لتعيد إرساله بوصفه `cursor:` للصفحة التالية، أو nil حين لا توجد صفحة أخرى. والرمز الفارغ يُوحَّد إلى nil، فيتفق `if page.next_cursor` مع فحص nil.