رشتهها
`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=)، در حالی که کلیدهای بدنهٔ درخواست همان نامهای camelCase خودِ API را نگه میدارند (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_…')وضعیت خواندهشدن اینجا روی هر backend یک برچسب است، پس همراه فهرستهای برچسب سفر میکند و وقتی هر دو را تنظیم کنید ترتیب قطعی است. دستکم یکی از آن سه فیلد باید حاضر باشد.
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 است، و وقتی بایتهای ذخیرهشده پیدا نشوند یک رشتهٔ خالی، پس پیش از decode کردن طولش را بررسی کنید. متن رمزِ یک پیام رمزنگاریشده در این فهرست هست و مثل هر فایل دیگری دانلود میشود؛ بخش نسخهٔ 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 دقیقاً به همین دلیل عرضه میشود: سرور مجموعهٔ مهرومومشده را یک بار بیان میکند، و نسخهٔ سومی که از روی union نوشته شود همان نسخهای است که واگرا میشود.
نبودن به معنای متن آشکار نیست. 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. هر مدخل تنها یک نشانگر object و یک شناسه است. این فهرست نه موضوع دارد، نه خلاصه، نه مشارکتکنندگان و نه برچسب، پس هر چیز بیشتری یعنی صدا زدن `threads.get` روی رشتههایی که میخواهید.
items[].idstr- شناسهٔ رشته، که بیتغییر به `threads.get`، `threads.update` و بقیه داده میشود. چه ردیف از یک فهرست فیلترشده آمده باشد چه از یک جستوجوی `query`، همان شناسه است.
hasMorebool- اینکه صفحهٔ دیگری هست یا نه، که هر جا API آن را بیان نکند از `nextCursor` مشتق میشود.
nextCursorstr | None- همان `nextPageToken` در API، که برای صفحهٔ بعدی بهصورت `cursor` بازفرستاده میشود، یا وقتی صفحهٔ دیگری نباشد `None`. توکن خالی به `None` نرمال میشود، پس بررسی falsy و بررسی `None` همداستاناند.