تخطَّ إلى المستندات
Ruby

الإعداد

ثلاث طرق لإنشاء عميل، وكل خيار متاح، وما يرفضه قبل إرسال أي طلب.

الخيارات

clients.rb
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 هو الاستدعاء نفسه.
options.rb
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 لذلك الطلب ولا يترك أثرًا على العميل.

per_call_key.rb
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 الأساسي والمهلة وسياسة إعادة المحاولة، ويعيد الجسم المحلَّل كما يفعل التابع.

raw_request.rb
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.rb
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. يُستدعى مرة واحدة لكل استدعاء، وتعيد محاولات ذلك الاستدعاء استخدام ما أعاده، فجدّد الرمز داخله حين يقترب من الانتهاء، ولن تحتاج إلى إعادة بناء العميل أبدًا.

access_token.rb
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.

step_up.rb
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، فيستطيع الاختبار أن يتحقق بالضبط مما كان سيُرسَل.

fake_adapter.rb
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 حين تحاكي حالات الفشل. وإلا فإن الحالة القابلة لإعادة المحاولة أو فشل الشبكة في استدعاء يمكن تكراره بأمان يُجرَّب ثلاث مرات، مع فترات انتظار حقيقية بينها.