رشتهها
`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 همراه با ساعت و offset، و هر دو سر شاملاند. رشتهٔ تاریخی بدون ساعت با یک 422 رد میشود. fromContacts: true نامههایی را نگه میدارد که تازهترین پیامشان از یک مخاطب ذخیرهشده آمده است. هر ترتیب تا انتها صفحهبندی میشود بیآنکه رشتهای جا بیفتد یا تکرار شود.
listAll وقتی آخرین صفحه رسید یک آرایه برمیگرداند. iterate یک Generator برمیگرداند که هر رشته را yield میکند و صفحهٔ بعد را فقط وقتی حلقه به آن نیاز دارد میگیرد، پس به محض اینکه آنچه لازم دارید را داشته باشید یک 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);وضعیت خوانده شدن اینجا روی هر backend یک برچسب است، پس همراه فهرستهای برچسب میآید، و وقتی هر دو را تنظیم کنید ترتیب ثابت است: حذفها پیش از افزودنها اعمال میشوند، پس شناسهای که در هر دو فهرست باشد در نهایت روی رشته میماند. دستکم یکی از آن سه فیلد باید حاضر باشد.
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 تنها آزمونی است که لازم دارید.