المحادثات
`threads.list` و`listAll` و`iterate` و`get` و`update` و`trash` و`snooze` و`unsnooze` و`listAttachments`.
القراءة
const page = await openemail.threads.list({ folder: 'inbox', query: 'from:ada', labelIds: ['INBOX', 'IMPORTANT'], limit: 25,}) const next = page.nextCursor ? await openemail.threads.list({ folder: 'inbox', cursor: page.nextCursor }) : null const thread = await openemail.threads.get('thread_…')console.log(thread.messageCount, thread.hasUnread, thread.totalReplies)يقسّم API المحادثات إلى صفحات باستخدام pageToken. ويسلّمه لك العميل باسم nextCursor ويستردّه باسم cursor، كأي قائمة أخرى، ويتبعه listAll وiterate نيابةً عنك. وهو معتم: أعِد ما أُعطيت ولا تبنِ واحدًا أبدًا.
التنظيم
await openemail.threads.update('thread_…', { read: true, addLabelIds: ['Done'], removeLabelIds: ['INBOX'],}) await openemail.threads.trash('thread_…')await openemail.threads.snooze('thread_…', new Date(Date.now() + 86_400_000))await openemail.threads.unsnooze('thread_…')حالة القراءة هي تسمية في كل خلفية هنا، فهي تنتقل مع قوائم التسميات ويكون الترتيب حتميًا حين تضبط الاثنين معًا. ويجب وجود حقل واحد على الأقل من الحقول الثلاثة.
مرفقات رسالة
const files = await openemail.threads.listAttachments('thread_…', 'message_…') for (const file of files) { console.log(file.filename, file.contentType, file.size) if (file.content) await save(file.filename, Buffer.from(file.content, 'base64'))}الحقل content بصيغة base64، ويكون سلسلة فارغة حين يتعذّر العثور على البايتات المخزَّنة، فتحقّق من طوله قبل فك الترميز. والنص المشفَّر لرسالة مشفَّرة موجود في هذه القائمة ويُنزَّل كأي ملف آخر؛ أما جزء إصدار PGP/MIME وأي توقيع منفصل فليسا فيها. ويحتفظان بمعرّفيهما في encryption.parts ولا شيء أكثر.
رسالة وصلت مشفَّرة
هذا SDK لا يشفّر ولا يفك التشفير: فهو لا يستطيع فتح رسالة شفّرها غيره، ولا يستطيع إرسال رسالة مشفَّرة. ويُرفض طلب الإرسال إن حمل علامة تشفير، لأن عميلًا بلا مفتاح لا شأن له بتأكيد تشفير. والمفاتيح المولَّدة في تطبيق OpenEmail تقيم في المتصفح الذي أنشأها ولا تصل إلى شيء هنا، وحين يفتح ذلك المتصفح رسالة مختومة يبقى النص الصريح فيه، وتبقى الرسالة المخزَّنة التي يقرأها هذا النداء نصًا مشفَّرًا. وما يعطيك إياه threads.get هو الغلاف، معروفًا. والرسالة التي وصلت ملفوفة بـPGP أو S/MIME تحمل كائن encryption، فيكفّ decodedBody الفارغ عن أن يكون كل ما يُسلَّم إليك، وencryption هو الحقل الوحيد في MessageResource ذو نوع حقيقي، لأنه الحقل الذي لا تنجو من التخمين عند غيابه.
import { isSealed, openemail } from '@openemail/sdk' const thread = await openemail.threads.get('thread_…') for (const message of thread.messages) { if (!message.encryption) continue if (!isSealed(message)) continue console.warn('cannot read this one:', message.encryption.format)}تفرّع بـisSealed، لا بوجود الحقل أبدًا. فصيغتان من الخمس، pgp-signed وsmime-signed، تصفان متنًا وصل مكشوفًا إلى جانب توقيع منفصل، فالتحكم بناءً على الوجود يخفي بريدًا لم يكن أحد بحاجة إلى إخفائه، ولا يستطيع المستخدم رؤيته ولا تفسيره. وتُشحن isSealed لهذا السبب بالذات: فالخادم يذكر المجموعة المختومة مرة واحدة، والنسخة الثالثة المكتوبة انطلاقًا من الاتحاد هي النسخة التي تنحرف.
الغياب ليس نصًا صريحًا. فـencryption غائب عن كل رسالة خُزّنت قبل شحن الكشف، وعن كل ما وصل إلى صندوق البريد عبر مسار لم يعمل فيه الكاشف قط. وهو يسجّل أن أحدًا لم ينظر، وهي حقيقة عن تغطيتنا لا عن البريد، ولا شيء يعيد ملأه بأثر رجعي.
أين تختلف هذه عن البقية
- كل مدخل في
ThreadResource.messagesهوMessageResource، أيRecord<string, unknown>يحمل حقلًا مسمّى واحدًا بالضبط. وتحديد أنواع البقية سيكون تأكيدًا من العميل على تطبيع لا يجريه أحد، وencryptionمسمّى على أي حال لأن عميلًا لا يستطيع التفرّع عليه يقرأ رسالة مختومة على أنها رسالة فارغة. - الطلب الذي لا يمكن تلبيته بأمانة يعطي 422
capability_unsupported، لا استجابة تبدو صحيحة وهي خاطئة في صمت.
المعاملات: threads.list (ThreadListOptions)
folderstring- أي مجلد تسرد. ويضبطه الخادم افتراضيًا على `inbox`، فإغفاله يضيّق السرد بدل توسيعه ليشمل كل شيء. وهو يسري على بحث `query` أيضًا، ما لم يسمِّ الاستعلام مجلدًا بنفسه عبر `in:` أو عبر `is:` خاص بمجلد مثل `is:sent`.
querystring- صياغة البحث في صندوق البريد. يجب أن تظهر الكلمات المجردة كلها، وتتطابق كل منها بتسامح: تُتجاهل حالة الأحرف والعلامات الصوتية والفواصل، ويُحتسب جزء من كلمة أطول، فـ`min` و`ben jamin` كلاهما يجد “Benjamin”. وتُطابَق العبارة بين علامتي اقتباس كما كُتبت باستثناء الحالة والعلامات الصوتية، فـ`"ben jamin"` لا يجد “Ben-Jamin”، وتُسقَط كلمات الحشو حين يبقى شيء آخر للبحث عنه. ضيّق النتائج بمعاملات مثل `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 | string[]- قصر السرد على المحادثات الحاملة لهذه التسميات. وتأخذ نقطة النهاية سلسلة مفصولة بفواصل، ويضمّ العميل المصفوفة في سلسلة واحدة نيابةً عنك؛ ولا حد لعدد ما تسمّيه منها.
limitnumber- كم محادثة تُعاد، من 1 إلى 100. وإن أُغفل، استخدم المعالج 25. والقيمة الافتراضية تقيم في المعالج لا في المخطط، فتتصرف القيمة الغائبة والقيمة 25 الصريحة التصرف نفسه.
cursorstring- قيمة `nextCursor` من الصفحة السابقة، تُعاد حرفيًا. وهي `pageToken` الخاص بـAPI تحت الاسم الذي تستخدمه كل قائمة أخرى، وهي معتمة، فلا تبنِ واحدة ولا تعدّلها أبدًا.
الاستجابة: Page<ThreadSummaryResource>
itemsThreadSummaryResource[]- مدخل واحد لكل محادثة في هذه الصفحة، مرفوعًا من غلاف `data` الخاص بـAPI. وكل مدخل ليس إلا علامة كائن ومعرّفًا. ولا يحمل السرد موضوعًا ولا مقتطفًا ولا مشاركين ولا تسميات، فأي شيء أكثر من ذلك يعني مناداة `threads.get` على المحادثات التي تريدها.
items[].idstring- معرّف المحادثة، ليُسلَّم إلى `threads.get` و`threads.update` وبقية التوابع دون تغيير. وهو المعرّف نفسه سواء جاء الصف من سرد مرشَّح أو من بحث `query`.
hasMoreboolean- ما إذا كانت هناك صفحة تالية، مشتقًّا من `nextCursor` حيث لا يذكره API.
nextCursorstring | null- قيمة `nextPageToken` الخاصة بـAPI، تُرسَل مجددًا باسم `cursor` للصفحة التالية، أو null حين لا توجد صفحة تالية. والرمز الفارغ يُطبَّع إلى null، فيتفق فحص القيمة الزائفة مع فحص null.