المحادثات
`threads->list` و`listAll` و`iterate` و`get` و`update` و`trash` و`snooze` و`unsnooze` و`listAttachments`.
القراءة
$page = $client->threads->list( folder: 'inbox', query: 'from:ada', labelIds: ['INBOX', 'IMPORTANT'], limit: 25,); if ($page->nextCursor !== null) { $nextPage = $client->threads->list(folder: 'inbox', cursor: $page->nextCursor); echo count($nextPage), PHP_EOL;} $thread = $client->threads->get('CAHk7pQ2x9LmZ4-mail.example.com');echo $thread['messageCount'], ' ', $thread['hasUnread'] ? 'unread' : 'read', ' ', $thread['totalReplies'], PHP_EOL;يقسّم API المحادثات إلى صفحات باستخدام pageToken. ويسلّمه لك العميل باسم nextCursor ويستردّه باسم cursor:، كأي قائمة أخرى، ويتبعه listAll وiterate نيابةً عنك. وهو معتم: أعِد ما أُعطيت ولا تبنِ واحدًا أبدًا.
مرشِّحات القائمة وسائط مسمّاة (labelIds: وdateFrom:)، بينما حقول متن الطلب مفاتيح مصفوفة بأسماء API (addLabelIds في update). وتعود المحادثة في صورة مصفوفة مفاتيحها بصيغة camelCase، فيقرأ $thread['messageCount'] العدد.
use OpenEmail\Constants\ThreadSorts; $lastWeek = $client->threads->listAll( sort: ThreadSorts::OLDEST, dateFrom: new \DateTimeImmutable('-7 days'), dateTo: new \DateTimeImmutable(), fromContacts: true,);echo count($lastWeek), PHP_EOL; foreach ($client->threads->iterate(sort: ThreadSorts::SENDER) as $thread) { echo $thread['id'], PHP_EOL;}sort: وdateFrom: وdateTo: وfromContacts: هي أدوات التحكم الخاصة بقائمة المحادثات. وقيمة sort: هي newest أو oldest أو sender أو subject، ويسمّيها OpenEmail\Constants\ThreadSorts. وتأخذ التواريخ DateTimeInterface، يُرسَل كلحظة بتوقيت UTC، أو سلسلة ISO 8601 فيها وقت وإزاحة زمنية، ويُشمل الطرفان كلاهما. وسلسلة التاريخ التي لا وقت فيها تُرفض بـ 422. ويُبقي fromContacts: true البريد الذي جاءت أحدث رسائله من جهة اتصال محفوظة. وكل ترتيب يرقّم الصفحات حتى النهاية دون تخطي محادثة أو تكرارها.
يعيد listAll مصفوفة واحدة بمجرد وصول الصفحة الأخيرة. ويعيد iterate كائن Generator يسلّم كل محادثة ولا يجلب الصفحة التالية إلا حين تحتاجها الحلقة، فيوقف break الطلبات بمجرد حصولك على ما تحتاجه.
التنظيم
$threadId = 'CAHk7pQ2x9LmZ4-mail.example.com'; $client->threads->update($threadId, ['read' => true, 'addLabelIds' => ['USER_DONE'], 'removeLabelIds' => ['INBOX']]); $client->threads->trash($threadId);$client->threads->snooze($threadId, new \DateTimeImmutable('+1 day'));$client->threads->unsnooze($threadId);حالة القراءة تسمية في كل واجهة خلفية هنا، فهي تنتقل مع قوائم التسميات، والترتيب ثابت حين تضبط الاثنين: تُطبَّق الإزالات قبل الإضافات، فالمعرّف الموجود في القائمتين ينتهي به الأمر على المحادثة. ويجب أن يوجد واحد على الأقل من الحقول الثلاثة.
يأخذ addLabelIds معرّفات من labels->list ومعرّفات النظام مثل ARCHIVE وSTARRED. والمعرّف الذي لا يسمّي أي تسمية يُرفض بالخطأ 422 label_not_found بدل أن يُنشأ، لذا أنشئ التسمية أولًا بـ labels->create. ويسرد $client->threads->list(folder: 'USER_DONE') كل محادثة تحمل تسمية أيًّا كان مجلدها.
مرفقات رسالة
$files = $client->threads->listAttachments('CAHk7pQ2x9LmZ4-mail.example.com', 'message_4c1b257a'); foreach ($files as $file) { echo $file['filename'], ' ', $file['contentType'], ' ', $file['size'], PHP_EOL; $bytes = base64_decode($file['content'], true); if ($file['content'] !== '' && $bytes !== false) { file_put_contents(basename($file['filename']), $bytes); }}يعيد listAttachments قائمة من المصفوفات. ويكون content بترميز base64، يعيده base64_decode() إلى بايتات، ويكون سلسلة فارغة حين يتعذّر العثور على البايتات المخزَّنة، فافحصه قبل فك الترميز. والنص المشفَّر لرسالة مشفّرة موجود في هذه القائمة ويُنزَّل مثل أي ملف آخر. أما جزء إصدار PGP/MIME وأي توقيع منفصل فليسا فيها. بل يحتفظان بمعرّفاتهما في encryption.parts لا أكثر.
رسالة وصلت مشفَّرة
هذه الحزمة لا تشفّر ولا تفك التشفير. فهي لا تستطيع فتح رسالة شفّرها شخص آخر، ولا تستطيع إرسال رسالة مشفَّرة. ويُرفض طلب الإرسال إن حمل علامة تشفير، لأن عميلًا بلا مفتاح لا شأن له بتأكيد تشفير. والمفاتيح المولَّدة في تطبيق OpenEmail تقيم في المتصفح الذي أنشأها ولا تصل إلى شيء هنا. وحين يفتح ذلك المتصفح رسالة مختومة يبقى النص الصريح فيه، وتبقى الرسالة المخزَّنة التي يقرأها هذا الاستدعاء نصًا مشفَّرًا. وما يعطيك إياه threads->get هو الغلاف، معروفًا. والرسالة التي وصلت ملفوفة بـ PGP أو S/MIME تحمل مصفوفة باسم encryption، فيكفّ decodedBody الفارغ عن أن يكون كل ما يُسلَّم إليك. وencryption هو الحقل الوحيد في الرسالة الذي تلتزم به API، لأنه الحقل الذي لا يمكنك أن تنجو من التخمين بشأن غيابه.
use OpenEmail\OpenEmail; $thread = $client->threads->get('CAHk7pQ2x9LmZ4-mail.example.com'); foreach ($thread['messages'] as $message) { if (!isset($message['encryption']) || !OpenEmail::isSealed($message)) { continue; } error_log('cannot read this one: ' . $message['encryption']['format']);}تفرّع بـ OpenEmail::isSealed()، لا بوجود الحقل أبدًا. فصيغتان من الخمس، pgp-signed وsmime-signed، تصفان متنًا وصل مكشوفًا إلى جانب توقيع منفصل، فالتحكم بناءً على الوجود يخفي بريدًا لم يكن أحد بحاجة إلى إخفائه، ولا يستطيع المستخدم رؤيته ولا تفسيره. ويوجد OpenEmail::isSealed() لهذا السبب بالذات. فالخادم يذكر المجموعة المختومة مرة واحدة، ونسخة الحزمة مولَّدة من المصدر نفسه، والنسخة الثالثة المكتوبة يدويًا هي النسخة التي تنحرف. ويسمّي OpenEmail\Constants\MessageEncryptionFormats الصيغ الخمس كلها.
الغياب ليس نصًا صريحًا. فـencryption غائب عن كل رسالة خُزّنت قبل شحن الكشف، وعن كل ما وصل إلى صندوق البريد عبر مسار لم يعمل فيه الكاشف قط. وهو يسجّل أن أحدًا لم ينظر، وهي حقيقة عن تغطيتنا لا عن البريد، ولا شيء يعيد ملأه بأثر رجعي.
أين تختلف هذه عن البقية
- كل عنصر في
messagesالخاصة بالمحادثة هو المصفوفة التي خزّنها صندوق البريد، دون قائمة ثابتة من الحقول، فاقرأ أي مفتاح غيرencryptionبـ?? null. والوعد بأكثر من ذلك يعني أن العميل يؤكد توحيدًا لا يجريه أحد. وencryptionهو الحقل الوحيد الذي تلتزم به API على أي حال، لأن العميل الذي لا يستطيع التفرّع عليه يقرأ الرسالة المختومة كأنها رسالة فارغة. - الطلب الذي لا يمكن تلبيته بأمانة يعطي 422
capability_unsupported، يُرمى في صورةValidationException، لا استجابة تبدو صحيحة وهي خاطئة في صمت.
المعاملات: 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:` كل مرفق في المحادثة كاملة، وتقرأ التسميات والمجلدات المحادثة كاملة. وهو يضيّق الفهرس نفسه الذي يقرأه السرد غير المرشَّح. والرسائل المختومة لا تخزّن نص متن، فلا يمكن أن يتطابق منها إلا المرسِل والمستلمون والموضوع. وتطابق الكلمة المجردة أيضًا اسم أي مرفق في المحادثة، أيًّا كانت الرسالة التي حملته.
labelIdsstring or array- قصر السرد على المحادثات التي تحمل هذه التسميات. تأخذ نقطة النهاية سلسلة مفصولة بفواصل، ويضم العميل المصفوفة في سلسلة واحدة نيابةً عنك. ولا حد لعدد ما تسمّيه.
limitint- كم محادثة تُعاد، من 1 إلى 100. وإن أُغفل، استخدم المعالج 25. والقيمة الافتراضية تقيم في المعالج لا في المخطط، فتتصرف القيمة الغائبة والقيمة 25 الصريحة التصرف نفسه.
cursorstring- `nextCursor` الخاص بالصفحة السابقة، يُمرَّر كما هو حرفيًا. وهو `pageToken` الخاص بـ API تحت الاسم الذي تستخدمه كل قائمة أخرى، وهو معتم، فلا تبنِه ولا تعدّله أبدًا.
الاستجابة: OpenEmail\Result\Page
itemsarray- مصفوفة واحدة لكل محادثة في هذه الصفحة، مستخرج من غلاف `data` في API. وكل واحد منها مجرد علامة `object` و`id`. ولا يحمل السرد موضوعًا ولا مقتطفًا ولا مشاركين ولا تسميات، فأي شيء أكثر يعني استدعاء `threads->get` على المحادثات التي تريدها.
items[].idstring- معرّف المحادثة، يُقرأ بوصفه `$item['id']`، لتمرّره دون تغيير إلى `threads->get` و`threads->update` وغيرهما. وهو المعرّف نفسه سواء جاء الصف من سرد مرشَّح أو من بحث `query:`.
hasMorebool- ما إذا كانت هناك صفحة أخرى، مأخوذ من API حيث تذكر ذلك، ومشتق من `nextCursor` حيث لا تذكره.
nextCursorstring or null- `nextPageToken` الخاص بـ API، لتعيد إرساله بوصفه `cursor:` للصفحة التالية، أو null حين لا توجد صفحة أخرى. والرمز الفارغ يُوحَّد إلى null، فيكفيك فحص null وحده.