الإعداد
ثلاث طرق لإنشاء عميل، وكل خيار متاح، وما يرفضه قبل إرسال أي طلب.
الخيارات
import os from openemail import AsyncOpenEmail, OpenEmail, init, openemail init(os.environ['OPENEMAIL_API_KEY'])openemail.me.ping() billing = OpenEmail(os.environ['BILLING_API_KEY']) pinned = OpenEmail('oe_live_...', base_url='https://api.openemail.uk') background = AsyncOpenEmail()| نقطة الدخول | ما الذي يمنحك إياه |
|---|---|
| init(...) | يضبط العميل المشترك ويعيده. يصبح openemail هو ذلك العميل منذ تلك اللحظة، في كل وحدة، وكل ما تتركه دون تحديد يُقرأ من بيئة التشغيل. |
| openemail | العميل المشترك. إذا استُخدم قبل init فإنه يبني نفسه من OPENEMAIL_API_KEY وOPENEMAIL_BASE_URL عند أول استدعاء. |
| OpenEmail(...) | عميل منفصل، لمفتاح ثانٍ إلى جانب المفتاح المشترك، أو لبناء النسخة التي تصدّرها وحدتك الخاصة. وكل ما تتركه دون تحديد يُقرأ من بيئة التشغيل، كما يفعل init، وcreate_client هو الصنف نفسه باسم آخر. |
| AsyncOpenEmail(...) | عميل غير متزامن منفصل بالخيارات نفسها، وكل دالة فيه تُستدعى مع await. أما openemail المشترك فمتزامن، لذا تبني هذا العميل بنفسك. |
| get_client() وreset_client() | يعيد get_client العميل المشترك نفسه، ويبنيه من بيئة التشغيل إذا لم يُستدعَ init بعد. أما reset_client فيتخلّص منه، فيبني الاستخدام التالي عميلًا جديدًا. |
import httpxfrom openemail import init init( 'oe_live_...', base_url='https://api.openemail.uk', timeout=30, max_retries=2, http_client=httpx.Client(proxy='http://proxy.internal:3128'), headers={'X-Team': 'billing'}, user_agent='billing-service/1.4', disable_update_notice=True,)| الخيار | القيمة الافتراضية | ملاحظات |
|---|---|---|
| api_key | OPENEMAIL_API_KEY | يُقرأ من بيئة التشغيل عبر init وOpenEmail. ويجب أن يبدأ بـ oe_live_ أو oe_test_. |
| access_token | OPENEMAIL_ACCESS_TOKEN | رمز وصول OAuth، أو دالة تعيده، بدلًا من api_key. راجع قسم رموز وصول OAuth أدناه. |
| base_url | https://api.openemail.uk | أو OPENEMAIL_BASE_URL. تُحذف الشرطة المائلة الأخيرة، ويضع init وOpenEmail البادئة https:// أمام اسم مضيف مجرّد، أو http:// أمام مضيف على هذا الجهاز: localhost، أو عنوان 127.x.x.x، أو ::1. ولا تُرسَل بيانات الاعتماد أبدًا عبر http غير المشفّر إلى أي مضيف آخر، و0.0.0.0 أو [::] يرفع خطأً عند إنشاء العميل، لأنهما عنوانان يستمع عليهما الخادم، لا عنوانان تُرسَل إليهما الطلبات. |
| timeout | 30 | بالثواني، لكل محاولة، لا لكل استدعاء. ويشمل قراءة جسم الاستجابة، لا الترويسات وحدها. والقيمة 0 تعطّله. ويمنح files.upload مهلة لا تقل عن 600 ثانية ما لم يمرّر الاستدعاء timeout خاصًا به. |
| max_retries | 2 | محاولات إضافية بعد الأولى، على الاستدعاءات التي يمكن تكرارها بأمان. يُضبط على العميل، لا لكل استدعاء. |
| http_client | httpx.Client جديد | مرّر عميلك الخاص من أجل وكيل (proxy)، أو إعدادات TLS خاصة بك، أو ناقل مُركَّب، أو بديل للاختبار: httpx.Client إلى OpenEmail، وhttpx.AsyncClient إلى AsyncOpenEmail. وإغلاق العميل يترك العميل الذي مرّرته مفتوحًا. |
| headers | {} | تُرسل مع كل طلب. |
| user_agent | openemail-python/<version> | تُرسل مع كل طلب. |
| disable_update_notice | False | يتخطى الفحص الذي يجري مرة واحدة لكل عملية بحثًا عن إصدار أحدث على PyPI. ولا يعمل هذا الفحص إلا عندما يذهب الإخراج إلى طرفية، كما أن OPENEMAIL_DISABLE_UPDATE_NOTICE يوقفه أيضًا. |
ما يرفضه قبل الإرسال
ترفع هذه الحالات ValueError، أو TypeError حيث يذكر الجدول ذلك، قبل إرسال أي طلب، بدلًا من أن تظهر كفشل محيّر عند أول إرسال. وتوضح الرسالة ما الخطأ وما الذي ينبغي تمريره بدلًا منه.
| المرفوض | السبب |
|---|---|
| لا مفتاح على الإطلاق | لم يُضبط api_key ولا OPENEMAIL_API_KEY، فلا يوجد ما يمكن المصادقة به. |
| ملف تعريف ارتباط جلسة أو رمز جلسة | لا يصادق هنا سوى oe_live_ وoe_test_، وهذا ما تقوله واجهة API أيضًا. والفحص مجرد بادئة لا أكثر، لذا فإن مفتاحًا ملغى يفشل عند الإرسال الفعلي. |
| قيمة base_url ليست عنوان http أو https | لا يمكن جلب أي شيء آخر، لذا يرفضه العميل عند إنشائه بدلًا من أن يفشل عند أول طلب. |
| قيمة base_url على 0.0.0.0 أو [::] | عنوان يستمع عليه الخادم، لا عنوان تُرسَل إليه الطلبات. وتقترح الرسالة بدلًا منه 127.0.0.1 أو [::1] بالمنفذ نفسه. |
| بيانات اعتماد عبر http غير مشفّر | يُرفض عند الاستدعاء، قبل أن يغادر الطلب، ما لم يكن الخادم على هذا الجهاز. فبإمكان أي شخص على الشبكة قراءتها. |
| معرّف فارغ أو مكوّن من نقاط فقط في أي دالة | يُرفع عند استدعاء الدالة. فأي مقطع مسار مكوّن من نقاط يحذفه كل محلل عناوين، فيصل الطلب عندئذ إلى نقطة نهاية مختلفة. |
| http_client من النوع الخاطئ | TypeError عند إنشاء العميل. فـOpenEmail يقبل httpx.Client، وAsyncOpenEmail يقبل httpx.AsyncClient. |
| قيمة في جسم الطلب لا يستطيع JSON حملها | TypeError يذكر نوعها. وتمرّ أنواع JSON كما هي، ويُحوَّل datetime أو date أو set نيابةً عنك. |
لا يوجد خيار test_mode ولن يوجد. فمخطط المفتاح جزء من بيانات الاعتماد نفسها لا مجرد تلميح، ومن ثَم فالوضع خاصية من خصائص المفتاح. وclient.mode يقرأ البادئة ولا يقرر شيئًا.
عميل واحد، عدة مفاتيح
أنشئ العميل مرة واحدة وشاركه. فإنشاء نسخة جديدة لكل طلب يهدر مجمّع الاتصالات والإعدادات دون مقابل، ولا شيء من حالته خاص بمستدعٍ بعينه.
يمكن مشاركة عميل واحد بين خيوط التنفيذ بأمان. ويغلق close() أو نهاية كتلة with مجمّع الاتصالات الذي فتحه العميل، ويبقى http_client الذي مرّرته مفتوحًا لتغلقه أنت.
أما الحالة التي كانت ستفرض نسخة لكل مفتاح، مثل مهمة ترسل بالنيابة عن عدة مساحات عمل، فمرّر فيها api_key مع الاستدعاء. إذ يستبدل ترويسة Authorization لذلك الطلب ولا يترك أثرًا على العميل.
from openemail.types import EmailSend workspace_key = 'oe_live_...' message: EmailSend = {'from': sender, 'to': recipient, 'subject': subject, 'text': text} client.emails.send(message) client.emails.send(message, api_key=workspace_key) client.threads.list(folder='inbox', api_key=workspace_key)client.webhooks.list(api_key=workspace_key)تقبله كل دالة خارج temp_mail وسيطًا مسمّى، إلى جانب timeout. ويُفحص قبل إرسال الطلب، بالقاعدة نفسها التي يستخدمها المُنشئ، فيؤدي خطأ مطبعي إلى رفع ValueError يذكر api_key= on this call بدلًا من 401 عن بيانات اعتماد عليك البحث عنها بعد ذلك. والاستدعاء المُعاد يحتفظ بالمفتاح الذي أُعطي له.
timeout بالثواني، ويحلّ محل مهلة العميل لذلك الاستدعاء وحده، في كل محاولة من محاولاته.
client.mode يصف المفتاح الذي أُنشئ به العميل ولا يتبع أي تجاوز لاحق. فما إن يخدم عميل واحد عدة مفاتيح حتى لا يعود هناك وضع واحد يمكن الإبلاغ عنه، لذا اقرأه من المفتاح الذي مرّرته.
نقطة نهاية لا تغلّفها أي دالة
يرسل client.raw.request طلبًا مع تطبيق بيانات اعتماد العميل وعنوانه الأساسي ومهلته وسياسة إعادة المحاولة لديه، ويعيد JSON المحلَّل. ويقبل method وquery وbody وapi_key وtimeout. ويعيد محاولة طلب GET ويرسل أي شيء آخر مرة واحدة، ما لم تمرّر repeatable=True. ويضيف idempotent=True ترويسة Idempotency-Key، وهي التي تمرّرها في idempotency_key أو قيمة جديدة.
result = client.raw.request('/something-new', method='POST', body={'name': 'Invoices'})يجب أن يبدأ المسار بـ/ واحدة. وأي شيء آخر، مثل //host/x، يرفع خطأً قبل إرسال الطلب، وكذلك المسار الذي يغادر عنوانه النهائي أصلَ العنوان الأساسي، فلا تصل بيانات الاعتماد التي يحملها إلى مضيف آخر أبدًا.
صناديق وارد مؤقتة
ينشئ create_temp_mail() عميلًا لا يحمل أي مفتاح API، وcreate_async_temp_mail() هو نظيره غير المتزامن. ينشئ صناديق بريد بشكل مجهول، وكل قراءة ترسل رمز الصندوق الذي أعاده create، أو الرمز الأحدث الذي أعاده extend، إما لكل استدعاء عبر inbox_token أو مرة واحدة عبر create_temp_mail(inbox_token=...).
from openemail import create_temp_mail temp_mail = create_temp_mail() inbox = temp_mail.create()messages = temp_mail.list_messages(inbox['id'], inbox_token=inbox['token']) print(messages['items'], messages['expiresAt'])رموز وصول OAuth
لم يُطلق بعدالتطبيق الذي ربطه شخص عبر OAuth، مثل أداة سطر أوامر أو وكيل، يحمل رمز وصول بدل مفتاح API. مرّره بوصفه access_token، إما الرمز نفسه، وإما دالة تعيده، ويجوز أن تكون async في AsyncOpenEmail. تُستدعى الدالة مرة واحدة لكل استدعاء، وتعيد محاولات ذلك الاستدعاء استخدام ما أعادته، فجدّد الرمز داخلها حين يقترب من الانتهاء، ولن تحتاج إلى إعادة بناء العميل أبدًا.
from openemail import OpenEmail client = OpenEmail(access_token=session.fresh_access_token) me = client.me.get() if me['object'] == 'oauth_token': print(me['clientId'], me['expiresAt'])| الحالة | ما الذي يحدث |
|---|---|
| api_key وaccess_token معًا، أو لا شيء منهما | يرفع المُنشئ ValueError. وإذا غاب الاثنان، تذكر الرسالة OPENEMAIL_API_KEY وOPENEMAIL_ACCESS_TOKEN. |
| قيمة ليست رمزًا | الرمز من 1 إلى 512 حرفًا ولا يبدأ بـ oe_، وهو الفحص الذي يجريه is_access_token. والسلسلة التي لا تجتازه ترفع خطأً من المُنشئ، والدالة التي تعيد قيمة كهذه تُفشل الاستدعاء قبل إرسال أي شيء. |
| OPENEMAIL_ACCESS_TOKEN | يقرؤه init وOpenEmail وopenemail المشترك حين لا تمرّر أيًّا من الاعتمادين ولا يكون OPENEMAIL_API_KEY مضبوطًا، فالمفتاح في البيئة هو الغالب. |
| دالة ترفع خطأً | يرفع الاستدعاء ذلك الخطأ نفسه دون تغيير، ولا يُرسل شيء. |
| دالة على OpenEmail تعيد كائنًا قابلًا للانتظار (awaitable) | ValueError، لأن العميل المتزامن لا يستطيع انتظاره. أما في AsyncOpenEmail فيجوز أن تكون الدالة async. |
| api_key لكل استدعاء | يحلّ محل الرمز لذلك الطلب وحده، ولا تُستدعى الدالة. |
| mode | دائمًا live مع الرمز. |
| create_temp_mail() | لا يرسل أي اعتماد، مهما كان في البيئة. |
| me.get() وme.ping() | مع الرمز، يردّ get بـ object مساويًا لـ oauth_token، وid وroleId مساويين لـ None، وclientId التطبيق المرتبط، وexpiresAt، وهو موعد انتهاء موافقة الشخص على التطبيق. ويردّ ping بـ kind مساويًا لـ oauth، وkeyId مساويًا لـ None، وclientId. وKeyResource وPingResource اتحادان، فميّز بحسب object أو kind قبل قراءة clientId أو expiresAt. |
الرمز يعمل باسم شخص ويقرأ بريده كما يستطيع هو، فأبقه على خادم كما تفعل بالمفتاح.
رموز التحقق
لم يُطلق بعدقبل تغيير حساس، مثل حذف نطاق أو تغيير webhook، تطلب الواجهة البرمجية من رمز الوصول رمز التحقق الذي كان تطبيق الويب سيطلبه من الشخص. ويرفع الاستدعاء OpenEmailApiError قيمة is_step_up_required فيه True، ولم يتغيّر شيء. اطلب رمزًا، وتحقق من الرمز الذي يعطيك إياه الشخص، ثم أعد الاستدعاء. ولا يُطلب ذلك من مفتاح API أبدًا.
from openemail import OpenEmailApiError try: client.domains.delete(domain_id)except OpenEmailApiError as error: if not error.is_step_up_required: raise challenge = client.security.begin_step_up() if challenge['method'] == 'email': prompt = f'Enter the code we emailed to {challenge.get("sentTo")}: ' else: prompt = 'Enter the code from your authenticator app, or a backup code: ' client.security.verify_step_up({'code': input(prompt)}) client.domains.delete(domain_id)| الطريقة | ما تفعله |
|---|---|
| security.step_up_status() | هل التطبيق متحقَّق منه الآن (elevated، elevatedUntil)، وكيف يُفحص الرمز التالي (method، email أو totp)، وminutes، طول النافذة. لا ترسل شيئًا، ولا تُبلغ عن إيقاف مؤقت. |
| security.begin_step_up(body=None) | تفتح تحققًا. مع email يُرسل رمز من ستة أرقام إلى العنوان الذي يسجّل به الشخص دخوله، ويعرضه sentTo مُقنَّعًا. ومع totp يقرأ الشخص رمزًا من تطبيق المصادقة أو يستخدم رمز استرداد. والتحقق الذي ما زال مفتوحًا وله محاولات متبقية يُعاد استخدامه ما لم تمرّر {'resend': True}، والتحقق المقفل أو المنتهي يُستبدل باستدعاء عادي. ويمكن لكل تطبيق أن يفتح 5 في الساعة و20 في 24 ساعة لكل شخص، والتالي يرفع 429 step_up_throttled. |
| security.verify_step_up({'code': code}) | تفحص الرمز وتفتح التغييرات الحساسة لهذا التطبيق مدة 60 دقيقة، حتى elevatedUntil، عبر REST وعبر أدوات MCP التي تُجري التغييرات نفسها. وبعد 10 رموز خاطئة في 24 ساعة من هذا التطبيق، أو 20 من كل تطبيقات الشخص معًا، يرفع هذا الاستدعاء وbegin_step_up الخطأ 429 step_up_locked مع رسالة تقول متى يُستأنف التحقق. |
لا يطلب العميل رمزًا ولا يعيد الاستدعاء من تلقاء نفسه، ولا يُعاد begin_step_up ولا verify_step_up تلقائيًا، لأن إعادة المحاولة بعد رد ضائع قد ترسل بريدًا ثانيًا أو تستهلك محاولة ثانية. ولا تحتاجان إلى نطاق، ومفتاح API الذي يستدعي إحداهما يتلقى 400 step_up_not_applicable. ويسمّي STEP_UP_ERROR_CODES كل طريقة قد يفشل بها التحقق، وتقول صفحة أخطاء الواجهة البرمجية ما عليك فعله في كل منها.