الإعداد
ثلاث طرق لإنشاء عميل، وكل خيار متاح، وما يرفضه قبل إرسال أي طلب.
الخيارات
require "openemail" OpenEmail.init(api_key: ENV.fetch("OPENEMAIL_API_KEY"))OpenEmail.me.ping pinned = OpenEmail::Client.new(api_key: ENV.fetch("OPENEMAIL_API_KEY"), base_url: "https://api.openemail.uk")quick = OpenEmail::Client.new(ENV.fetch("OPENEMAIL_API_KEY"))billing = OpenEmail.create_client(api_key: ENV.fetch("BILLING_API_KEY")) p pinned.mode, quick.mode, billing.mode| نقطة الدخول | ما الذي يمنحك إياه |
|---|---|
| OpenEmail.init(...) | يضبط العميل المشترك ويعيده. ومن بعدها يكون OpenEmail.client هو ذلك العميل، في كل ملف وكل خيط تنفيذ، وكل ما تتركه يُقرأ من البيئة. |
| OpenEmail.client وOpenEmail.emails وOpenEmail.threads وكل مساحة أسماء أخرى | العميل المشترك واختصارات إلى مساحات أسمائه. وإن استُخدم قبل init، يبني نفسه عند أول استدعاء من OPENEMAIL_API_KEY وOPENEMAIL_BASE_URL. |
| OpenEmail.reset_client | يتخلّص من العميل المشترك، فيبني الاستدعاء التالي عميلًا جديدًا من البيئة. |
| OpenEmail.create_client(...) | عميل منفصل مع الرجوع نفسه إلى البيئة، لمفتاح ثانٍ إلى جانب المفتاح المشترك، أو لعميل تحتفظ به شيفرتك وتمرّره. |
| OpenEmail::Client.new(...) or OpenEmail::Client.new(api_key) | عميل منفصل يُبنى مما تمرّره بالضبط. لا يقرأ أي بيئة، فهو يحتاج إلى api_key: أو access_token:. وOpenEmail.new هو الاستدعاء نفسه. |
OpenEmail.init( api_key: ENV.fetch("OPENEMAIL_API_KEY"), base_url: "https://api.openemail.uk", timeout: 30, max_retries: 2, adapter: OpenEmail::NetHttpAdapter.new(max_idle: 8, keep_alive_timeout: 2), headers: {"X-Team" => "billing"}, user_agent: "billing-service/1.4", disable_update_notice: true)| الخيار | القيمة الافتراضية | ملاحظات |
|---|---|---|
| api_key: | OPENEMAIL_API_KEY | يقرؤه init وcreate_client والعميل المشترك من البيئة. يجب أن يبدأ بـ oe_live_ أو oe_test_. ويمكن أيضًا تمريره كأول وسيط، لكن لا الاثنين معًا. |
| access_token: | OPENEMAIL_ACCESS_TOKEN | رمز وصول OAuth، أو أي شيء يستجيب لـ call ويعيد رمزًا. انظر قسم رموز وصول OAuth أدناه. مرّر مفتاحًا أو رمزًا، لا الاثنين معًا أبدًا. |
| base_url: | https://api.openemail.uk | أو OPENEMAIL_BASE_URL. تُحذف الشرطات المائلة في النهاية، ويضع init وcreate_client البادئة https:// قبل المضيف المجرد، أو http:// قبل مضيف على هذا الجهاز: localhost، أو عنوان 127.x.x.x، أو ::1. ولا يُرسَل الاعتماد أبدًا عبر http العادي إلى أي مضيف آخر، ويرفع 0.0.0.0 أو [::] خطأً عند بناء العميل، لأنهما عنوانان يستمع عليهما الخادم، لا عنوانان تُرسَل إليهما الطلبات. |
| timeout: | 30 | ثوانٍ لكل محاولة، لا لكل استدعاء. ومع المحوّل الافتراضي تشمل الاتصال وقراءة الجسم كله، لا الترويسات فقط. والقيمة 0 تعطّلها. وينتظر files.upload ما لا يقل عن 600 ثانية ما لم تمرّر timeout: في ذلك الاستدعاء. |
| max_retries: | 2 | محاولات إضافية بعد الأولى، على الاستدعاءات التي يمكن تكرارها بأمان. تُضبط على العميل، لا لكل استدعاء. والقيمة 0 توقف إعادة المحاولة. |
| adapter: | OpenEmail::NetHttpAdapter.new | طبقة HTTP. يحتفظ المحوّل الافتراضي بما يصل إلى 8 اتصالات خاملة لكل مضيف لمدة ثانيتين لكل منها، ويغيّر max_idle: وkeep_alive_timeout: ذلك. ويمكن لأي شيء يستجيب لـ call(request) أن يحل محله، وبهذا يعمل الاختبار دون شبكة. |
| headers: | {} | تُرسل مع كل طلب. |
| user_agent: | openemail-ruby/<version> | تُرسل مع كل طلب. |
| disable_update_notice: | false | يتخطّى الفحص الذي يجري مرة واحدة في كل عملية بحثًا عن إصدار أحدث على RubyGems. لا يجري الفحص إلا عندما يكون الخرج القياسي طرفية، ويعطّله OPENEMAIL_DISABLE_UPDATE_NOTICE أيضًا. |
متغيرات البيئة
| المتغير | ما تفعله |
|---|---|
| OPENEMAIL_API_KEY | المفتاح الذي يستخدمه init وcreate_client والعميل المشترك حين لا تمرّر api_key: ولا access_token:. |
| OPENEMAIL_ACCESS_TOKEN | رمز وصول OAuth، لا يُقرأ إلا حين لا تمرّر أيًّا من الاعتمادين ولا يكون OPENEMAIL_API_KEY مضبوطًا، فالمفتاح الموجود في البيئة هو الذي يغلب. |
| OPENEMAIL_BASE_URL | عنوان URL الأساسي حين لا تمرّر أي عنوان. والمضيف المجرد مثل localhost:2222 يُضاف إليه المخطط. |
| OPENEMAIL_DISABLE_UPDATE_NOTICE | أي قيمة غير فارغة توقف إشعار التحديث، لكل عميل في العملية. |
| HTTPS_PROXY وNO_PROXY، أو https_proxy وno_proxy | الوكيل الذي يتصل عبره المحوّل الافتراضي، والمضيفات التي يُتصل بها مباشرة. انظر قسم الوكلاء أدناه. |
لا يقرأ OpenEmail::Client.new أيًّا من المتغيرات الثلاثة الأولى، فالعميل المبني بهذه الطريقة لا يلتقط أبدًا مفتاحًا من البيئة عن طريق الخطأ. والمتغير المضبوط لكنه فارغ يُعدّ غير مضبوط.
ما يرفضه قبل الإرسال
هذه الحالات ترفع ArgumentError من السطر الذي فيه القيمة الخاطئة، بدل أن تظهر كفشل محيّر عند أول إرسال لك. وتقول الرسالة ما الخطأ وما الذي يجب تمريره بدلًا منه، ولا تكرّر أبدًا أي اعتماد.
| المرفوض | السبب |
|---|---|
| لا اعتماد على الإطلاق | لم يُمرَّر api_key: ولا access_token:، وفي حالة init وcreate_client لم يُضبط أي من المتغيرين أيضًا، فلا يوجد ما يُصادَق به. يُرفع عند بناء العميل. |
| مفتاح ورمز معًا | كل طلب يحمل اعتمادًا واحدًا، فلا يستطيع العميل أن يعرف أيهما قصدت. والمفتاح الممرَّر كأول وسيط وكـ api_key: معًا يُرفض للسبب نفسه. |
| ملف تعريف ارتباط جلسة، أو رمز جلسة، أو مفتاح لخدمة أخرى | لا يصادق هنا سوى oe_live_ وoe_test_، وهذا ما تقوله واجهة API أيضًا. والفحص مجرد بادئة لا أكثر، لذا فإن مفتاحًا ملغى يفشل عند الإرسال الفعلي، في صورة OpenEmail::AuthenticationError. |
| قيمة base_url: ليست عنوان URL من نوع http أو https، أو تحتوي على اسم مستخدم أو كلمة مرور | لا يمكن الوصول إلى غير ذلك، ومكان الاعتماد هو api_key: أو access_token:، لا عنوان URL. يُرفع عند بناء العميل. |
| اعتماد عبر http العادي إلى مضيف ليس على هذا الجهاز | يرفعه الاستدعاء قبل إرسال أي شيء. استخدم عنوان URL أساسيًا من نوع https. |
| قيمة timeout: ليست عددًا من الثواني، أو قيمة سالبة | مرّر عدد الثواني، أو 0 لإلغاء المهلة. يُرفع عند بناء العميل. |
| اسم ترويسة ليس رمزًا صالحًا (token)، أو فاصل أسطر في قيمة ترويسة | يُفحص في headers: وuser_agent: وidempotency_key:، لأن فاصل الأسطر قد يبدأ ترويسة ثانية. |
| معرّف فارغ أو مكوّن من نقاط فقط في أي دالة | يُرفع عند استدعاء التابع. فأي مقطع مسار مكوّن من نقاط يحذفه كل محلل عناوين، فيصل الطلب عندئذ إلى نقطة نهاية مختلفة. ويُرفض أيضًا المعرّف الذي ليس UTF-8 صالحًا. |
| جسم طلب ليس Hash | مرّر وسائط مسمّاة أو Hash واحدًا. وأي شيء يستجيب لـ to_hash يُعدّ Hash. |
لا يوجد خيار test_mode: ولن يوجد. فمخطط المفتاح جزء من الاعتماد نفسه لا مجرد تلميح، ومن ثَم فالوضع خاصية من خصائص المفتاح. وclient.mode يقرأ البادئة، "live" أو "test"، ولا يقرر شيئًا.
عميل واحد، عدة مفاتيح
أنشئ العميل مرة واحدة وشاركه. فإنشاء عميل جديد لكل طلب يهدر اتصالاته المفتوحة دون مقابل، ولا شيء من حالته خاص بمستدعٍ بعينه. والعميل يُجمَّد بمجرد بنائه، ويمكن استخدامه بأمان من خيوط تنفيذ كثيرة في آن واحد، فلا تحتاج عملية Puma أو Sidekiq إلا إلى عميل واحد، وبعد عملية fork تفتح العملية الابنة اتصالاتها الخاصة.
أما الحالة التي كانت ستفرض عميلًا لكل مفتاح، مثل مهمة ترسل بالنيابة عن عدة مساحات عمل، فمرّر فيها api_key: مع الاستدعاء. إذ يستبدل ترويسة Authorization لذلك الطلب ولا يترك أثرًا على العميل.
message = {from: "[email protected]", to: "[email protected]", subject: "Your invoice", text: "Attached."}workspace_key = ENV.fetch("OPENEMAIL_API_KEY") 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 يأخذه كوسيط مسمّى، إلى جانب المرشِّحات في القوائم، أما توابع temp_mail فتأخذ inbox_token: بدلًا منه. ويُفحص قبل إرسال الطلب، بالقاعدة نفسها التي يستخدمها العميل، فيرفع الخطأ المطبعي ArgumentError يخص api_key الممرَّر إلى هذا الاستدعاء، بدل خطأ 401 عن اعتماد يتعين عليك بعدها أن تبحث عنه. والاستدعاء الذي تُعاد محاولته يحتفظ بالمفتاح الذي أُعطي له.
يصف client.mode المفتاح الذي بُني به العميل تحديدًا، ولا يتبع أي تجاوز. فحين يخدم عميل واحد عدة مفاتيح لا يوجد وضع واحد يمكن الإبلاغ عنه، فاقرأه من المفتاح الذي مرّرته. ويعرض client.inspect الوضع وعنوان URL الأساسي، ولا يعرض المفتاح أبدًا.
نقاط نهاية لا يغلّفها أي تابع
client.raw هو طبقة النقل التي يمر عبرها كل تابع. ويستدعي client.raw.request مسارًا لم يغلّفه أي تابع بعد، مع تطبيق اعتماد العميل وعنوان URL الأساسي والمهلة وسياسة إعادة المحاولة، ويعيد الجسم المحلَّل كما يفعل التابع.
ping = client.raw.request("/ping") label = client.raw.request("/labels", method: :post, body: {name: "Invoices"}) p ping[:ok], label[:id]| الوسيط المسمّى | ما تفعله |
|---|---|
| method: | :get ما لم تحدد غير ذلك: :post أو :put أو :patch أو :delete. |
| query: | Hash من معاملات الاستعلام. تُستبعد القيم nil والفارغة، وتُضم Array أو Set بفواصل، ويُرسَل Time كلحظة ISO 8601. |
| body: | Hash يُرسَل بصيغة JSON. |
| raw: وcontent_type: | بايتات تُرسَل كما هي، في صورة String ثنائي أو IO أو Pathname، مع application/octet-stream ما لم تحدد نوعًا. |
| accept: وbinary: | قيمة accept: غير JSON تعيد الجسم نصًا، وbinary: true تعيده في صورة String ثنائي. |
| idempotent: وidempotency_key: | يرفق idempotent: true ترويسة Idempotency-Key، تُولَّد ما لم تمرّر مفتاحك الخاص. |
| repeatable: | ما إذا كانت المحاولة تُعاد عند الفشل. لا تُعاد إلا مع GET، ما لم تمرّر repeatable: true. |
| api_key: وtimeout: | المفتاح نفسه الخاص بكل استدعاء، ومهلة بالثواني لهذا الاستدعاء وحده. |
يجب أن يبدأ المسار بـ / واحدة، والمسار الذي يخرج عنوانه النهائي عن أصل عنوان URL الأساسي يرفع ArgumentError قبل إرسال أي شيء، فلا يصل الاعتماد أبدًا إلى مضيف آخر.
صناديق وارد مؤقتة
يبني OpenEmail.create_temp_mail عميلًا لصناديق البريد المؤقتة لا يحمل أي مفتاح API ولا يقرأ أي مفتاح من البيئة. ينشئ صناديق البريد بشكل مجهول، وكل قراءة ترسل رمز الصندوق الذي أعاده create، أو الرمز الأحدث الذي أعاده extend، إما لكل استدعاء عبر inbox_token: وإما مرة واحدة عبر OpenEmail.create_temp_mail(inbox_token:).
temp_mail = OpenEmail.create_temp_mail inbox = temp_mail.createpage = temp_mail.list_messages(inbox[:id], inbox_token: inbox[:token]) p page.items.size, page.expires_atيأخذ create_temp_mail الوسائط base_url: وadapter: وmax_retries: وtimeout: وuser_agent: وheaders: وdisable_update_notice: مثل أي عميل، ويقرأ OPENEMAIL_BASE_URL حين لا تمرّر عنوان URL أساسيًا.
رموز وصول OAuth
التطبيق الذي ربطه شخص عبر OAuth، مثل أداة سطر أوامر أو وكيل، يحمل رمز وصول بدل مفتاح API. مرّره بوصفه access_token:، إما الرمز نفسه، وإما أي شيء يستجيب لـ call ويعيده، مثل lambda أو Method. يُستدعى مرة واحدة لكل استدعاء، وتعيد محاولات ذلك الاستدعاء استخدام ما أعاده، فجدّد الرمز داخله حين يقترب من الانتهاء، ولن تحتاج إلى إعادة بناء العميل أبدًا.
tokens = {current: "token-from-your-oauth-flow"} oauth_client = OpenEmail::Client.new(access_token: -> { tokens.fetch(:current) }) me = oauth_client.me.get puts me[:clientId], me[:expiresAt] if me[:object] == "oauth_token"| الحالة | ما الذي يحدث |
|---|---|
| api_key: وaccess_token: معًا، أو لا أحد منهما | يرفع العميل ArgumentError عند بنائه. وفي حالة غياب الاثنين، تسمّي الرسالة OPENEMAIL_API_KEY وOPENEMAIL_ACCESS_TOKEN. |
| قيمة ليست رمزًا | الرمز من 1 إلى 512 حرفًا ولا يبدأ بـ oe_، وهو الفحص الذي يجريه OpenEmail.access_token?. والـ String الذي يفشل فيه يرفع خطأً عند بناء العميل، والكائن القابل للاستدعاء الذي يعيد مثله يرفع ArgumentError من الاستدعاء قبل إرسال أي شيء. |
| OPENEMAIL_ACCESS_TOKEN | يقرؤه init وcreate_client والعميل المشترك حين لا تمرّر أيًّا من الاعتمادين ولا يكون OPENEMAIL_API_KEY مضبوطًا، فالمفتاح الموجود في البيئة هو الذي يغلب. |
| كائن قابل للاستدعاء يرفع خطأً | يرفع الاستدعاء ذلك الخطأ دون تغيير، ولا يُرسَل شيء. |
| api_key: خاص بالاستدعاء | يحل محل الرمز في ذلك الطلب وحده، ولا يُستدعى الكائن القابل للاستدعاء. |
| client.mode | دائمًا "live" مع الرمز. |
| OpenEmail.create_temp_mail | لا يرسل أي اعتماد، مهما كان في البيئة. |
| me.get وme.ping | مع الرمز، يجيب get بـ object مضبوطًا على oauth_token، وid وroleId بقيمة nil، وclientId للتطبيق المربوط، وexpiresAt، أي متى تنتهي موافقة الشخص على التطبيق. ويجيب ping بـ kind مضبوطًا على oauth، وkeyId بقيمة nil، وclientId. افحص object أو kind قبل أن تقرأ id أو keyId. |
الرمز يعمل باسم شخص ويقرأ بريده كما يستطيع هو، فأبقه على خادم كما تفعل بالمفتاح.
رموز التحقق
قبل تغيير حساس، مثل حذف نطاق أو تغيير webhook، تطلب الواجهة البرمجية من رمز الوصول رمز التحقق الذي كان تطبيق الويب سيطلبه من الشخص. ويرفع الاستدعاء OpenEmail::PermissionError، أي خطأ 403 قيمة step_up_required? فيه true، ولم يتغيّر شيء. اطلب رمزًا، وتحقق من الرمز الذي يعطيك إياه الشخص، ثم أعد الاستدعاء. ولا يُطلب ذلك أبدًا من مفتاح API.
domain_id = "b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f" begin client.domains.delete(domain_id)rescue OpenEmail::ApiError => error raise unless error.step_up_required? challenge = client.security.begin_step_up if challenge[:method] == "email" puts "Enter the code we emailed to #{challenge[:sentTo]}" else puts "Enter the code from your authenticator app, or a backup code" end client.security.verify_step_up(code: $stdin.gets.to_s.strip) client.domains.delete(domain_id)end| الطريقة | ما تفعله |
|---|---|
| security.step_up_status | هل التطبيق متحقَّق منه الآن (elevated، elevatedUntil)، وكيف يُفحص الرمز التالي (method، email أو totp)، وminutes، طول النافذة. لا ترسل شيئًا، ولا تُبلغ عن إيقاف مؤقت. |
| security.begin_step_up | يفتح تحققًا. مع email يُرسل رمز من ستة أرقام إلى العنوان الذي يسجّل به الشخص دخوله، ويعرضه sentTo مُقنَّعًا. ومع totp يقرأ الشخص رمزًا من تطبيق المصادقة أو يستخدم رمز استرداد. والتحقق الذي ما زال مفتوحًا وله محاولات متبقية يُعاد استخدامه ما لم تمرّر resend: true، والتحقق المقفل أو المنتهي يُستبدل باستدعاء عادي. ويمكن لكل تطبيق أن يفتح 5 في الساعة و20 في 24 ساعة لكل شخص، والتالي يرفع 429 step_up_throttled. |
| security.verify_step_up(code:) | يفحص الرمز ويفتح التغييرات الحساسة لهذا التطبيق مدة 60 دقيقة، حتى elevatedUntil، عبر REST وعبر أدوات MCP التي تُجري التغييرات نفسها. وبعد 10 رموز خاطئة في 24 ساعة من هذا التطبيق، أو 20 من كل تطبيقات الشخص معًا، يرفع هذا الاستدعاء وbegin_step_up الخطأ 429 step_up_locked مع رسالة تقول متى يُستأنف التحقق. |
لا يطلب العميل رمزًا ولا يعيد الاستدعاء من تلقاء نفسه، ولا يُعاد أيٌّ من التوابع الثلاثة تلقائيًا، لأن إعادة المحاولة بعد رد ضائع قد ترسل بريدًا ثانيًا أو تستهلك محاولة ثانية. ولا تحتاج إلى نطاق، ومفتاح API الذي يستدعي أحدها يتلقى 400 step_up_not_applicable. ويسمّي OpenEmail::STEP_UP_ERROR_CODES كل طريقة يمكن أن يفشل بها التحقق، وتقول صفحة أخطاء API ما يجب فعله في كل حالة.
إشعار التحديث
حين يتوفر إصدار أحدث من الـ gem على RubyGems، يخبرك العميل بذلك مرة واحدة في كل عملية، على مجرى الأخطاء القياسي، بسطر مثل ℹ openemail 0.0.2 is available, you are on 0.0.1. يتبعه رابط صفحة الـ gem. ويجري الفحص عند بناء أول عميل، في خيط تنفيذ في الخلفية بمهلة ثانيتين، ولا يجري إلا حين يكون الخرج القياسي طرفية، ويُتجاهل الفشل في الوصول إلى RubyGems.
يمر الفحص عبر محوّل العميل، فقد يرى محوّل الاختبار طلبًا إلى RubyGems حين تعمل الاختبارات في طرفية. ابنِ عملاء الاختبار مع disable_update_notice: true، أو اضبط OPENEMAIL_DISABLE_UPDATE_NOTICE.
الوكلاء
يعثر المحوّل الافتراضي على وكيله عبر URI#find_proxy الخاص بـ Ruby نفسها، فيتبع القواعد نفسها التي تتبعها بقية المكتبة القياسية: يسمّي https_proxy أو HTTPS_PROXY الوكيل، ويسرد no_proxy أو NO_PROXY المضيفات التي يُتصل بها مباشرة. ويُرسَل اسم المستخدم وكلمة المرور الموجودان في عنوان URL الخاص بالوكيل إلى الوكيل، ولا يُوصَل أبدًا إلى خادم على هذا الجهاز عبر وكيل.
تستخدم الاتصالات TLS 1.2 أو أحدث وتتحقق من شهادة الخادم، فالوكيل الذي يفحص TLS يحتاج إلى أن تكون جهة إصدار شهاداته موثوقة لدى OpenSSL على الجهاز.
الاختبار دون شبكة
يستبدل adapter: طبقة HTTP. وهو أي شيء يستجيب لـ call(request)، بما في ذلك lambda، ويعيد OpenEmail::HttpResponse يحتوي على status وheaders وbody. والطلب هو OpenEmail::HttpRequest يحتوي على method وurl وheaders وbody وtimeout، فيستطيع الاختبار أن يتحقق بالضبط مما كان سيُرسَل.
requests = [] adapter = lambda do |request| requests << request OpenEmail::HttpResponse.new( status: 200, headers: {"content-type" => "application/json"}, body: JSON.generate({id: "msg_test", status: "sent", replayed: false}) )end test_client = OpenEmail::Client.new(api_key: "oe_test_fake", adapter:, max_retries: 0, disable_update_notice: true) sent = test_client.emails.send(from: "[email protected]", to: "[email protected]", subject: "Hi", text: "Hello") p sent[:status], requests.first.method, requests.first.url, requests.first.headers["Idempotency-Key"]p requests.firstطباعة الطلب تعرض ترويسة Authorization الخاصة به على أنها [redacted]، فلا يحتوي سجل الاختبار على المفتاح أبدًا.
- أعد حالة خارج 2xx مع غلاف أخطاء API بوصفه المتن، مثل
{"error": {"type": "validation_error", "code": "invalid_parameter", "message": "..."}}، لتحصل على الصنف الفرعي المطابق منOpenEmail::ApiError. - ارفع
Timeout::Errorمنcall، أوNet::ReadTimeoutالذي هو من نوعه، لتحصل علىOpenEmail::NetworkErrorقيمةtimeout?فيه true. وأي StandardError آخر، مثلErrno::ECONNREFUSED، يصبحNetworkErrorقيمةtimeout?فيه false. - تُعدّ
NameErrorوTypeErrorوArgumentErrorالمرفوعة داخل المحوّل أخطاءً برمجية فيه. فتُرفع دون تغيير ولا تُعاد محاولتها أبدًا.
ابنِ عميل الاختبار مع max_retries: 0 حين تحاكي حالات الفشل. وإلا فإن الحالة القابلة لإعادة المحاولة أو فشل الشبكة في استدعاء يمكن تكراره بأمان يُجرَّب ثلاث مرات، مع فترات انتظار حقيقية بينها.