الإعداد
كيف تُنشئ عميلًا، وكل خيار متاح، وما يرفضه قبل إرسال أي طلب.
الخيارات
use OpenEmail\OpenEmail; $client = new OpenEmail(); OpenEmail::init(timeout: 10);OpenEmail::getClient()->me->ping(); $billing = new OpenEmail(apiKey: (string) getenv('OPENEMAIL_BILLING_API_KEY')); echo $client->mode, ' ', $billing->mode, PHP_EOL;| نقطة الدخول | ما الذي يمنحك إياه |
|---|---|
| new OpenEmail(...) | عميل يُبنى من الوسائط المسمّاة التي تمرّرها. وكل ما تتركه يُقرأ من البيئة: المفتاح من OPENEMAIL_API_KEY أو رمز من OPENEMAIL_ACCESS_TOKEN حين لا تمرّر أي بيانات اعتماد، والعنوان الأساسي من OPENEMAIL_BASE_URL حين لا تمرّر عنوانًا. |
| OpenEmail::createClient(...) | العميل نفسه الذي يبنيه new OpenEmail(...)، للشيفرة التي تفضّل استدعاء دالة مصنع. |
| OpenEmail::init(...) | يبني عميلًا، ويحتفظ به عميلًا مشتركًا، ويعيده. ويأخذ الوسائط المسمّاة نفسها. |
| OpenEmail::getClient() | العميل المشترك، من أي مكان في العملية. وإن استُدعي قبل init، يبني عميلًا من البيئة عند أول استدعاء. |
| OpenEmail::resetClient() | يتخلّص من العميل المشترك، فيبني getClient() التالي عميلًا جديدًا، وهو ما يحتاجه الاختبار بين الحالات. |
تحويل getenv() إلى سلسلة نصية مقصود. فالمتغير غير المضبوط يصبح مفتاحًا فارغًا، يرفضه العميل برسالة تسمّي المتغير الذي يحتاجه، بينما كانت null سترجع بصمت إلى OPENEMAIL_API_KEY.
use OpenEmail\Http\CurlHttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail( apiKey: (string) getenv('OPENEMAIL_API_KEY'), baseUrl: 'https://api.openemail.uk', httpClient: new CurlHttpClient(), maxRetries: 2, timeout: 30, userAgent: 'billing-service/1.4', headers: ['X-Team' => 'billing'], disableUpdateNotice: true,);| الخيار | القيمة الافتراضية | ملاحظات |
|---|---|---|
| apiKey: | OPENEMAIL_API_KEY | يجب أن يبدأ بـ oe_live_ أو oe_test_. ويُقرأ من البيئة فقط حين لا تمرّر apiKey: ولا accessToken:. |
| accessToken: | OPENEMAIL_ACCESS_TOKEN | رمز وصول OAuth، أو callable يعيد رمزًا. انظر قسم رموز وصول OAuth أدناه. مرّر مفتاحًا أو رمزًا، لا الاثنين معًا أبدًا. |
| baseUrl: | https://api.openemail.uk | أو OPENEMAIL_BASE_URL. تُحذف الشرطات المائلة في النهاية، وتوضع البادئة https:// قبل المضيف المجرد، أو http:// قبل مضيف على هذا الجهاز: localhost، أو عنوان 127.x.x.x، أو ::1. ولا يُرسَل الاعتماد أبدًا عبر http العادي إلى أي مضيف آخر، ويُرفض 0.0.0.0 أو [::] عند بناء العميل، لأنهما عنوانان يستمع عليهما الخادم، لا عنوانان تُرسَل إليهما الطلبات. |
| timeout: | 30 | ثوانٍ لكل محاولة، لا لكل استدعاء، تشمل الاتصال وقراءة الاستجابة كلها. والقيمة 0 تعطّلها. وينتظر files->upload ما لا يقل عن 600 ثانية ما لم تمرّر timeout: في ذلك الاستدعاء. |
| maxRetries: | 2 | محاولات إضافية بعد الأولى، على الاستدعاءات التي يمكن تكرارها بأمان. تُضبط على العميل، لا لكل استدعاء. والقيمة 0 توقف إعادة المحاولة. |
| httpClient: | CurlHttpClient | طبقة HTTP: أي شيء ينفّذ OpenEmail\Http\HttpClient، مثل Psr18HttpClient حول Guzzle أو Symfony HttpClient، أو بديل مزيّف في اختبار. وتتناول صفحة عملاء HTTP كلًّا منها. |
| headers: | [] | تُرسل مع كل طلب. |
| userAgent: | openemail-php/<version> | تُرسل مع كل طلب. |
| disableUpdateNotice: | false | يتخطّى الفحص الذي يجري مرة واحدة في كل عملية بحثًا عن إصدار أحدث على Packagist. لا يجري الفحص إلا في سطر الأوامر عندما يكون الخرج القياسي طرفية، ويعطّله OPENEMAIL_DISABLE_UPDATE_NOTICE أيضًا. |
متغيرات البيئة
| المتغير | ما تفعله |
|---|---|
| OPENEMAIL_API_KEY | المفتاح الذي يستخدمه العميل حين لا تمرّر apiKey: ولا accessToken:. |
| 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 | الوكيل الذي يتصل عبره cURL، والمضيفات التي يُتصل بها مباشرة. انظر قسم الوكلاء وTLS أدناه. |
يُقرأ كل متغير بـ getenv() أولًا، ثم من $_SERVER و$_ENV، فتُحتسب أيضًا القيمة التي حمّلها إطار العمل من ملف .env. والمتغير المضبوط لكنه فارغ يُعدّ غير مضبوط.
ما يرفضه قبل الإرسال
هذه الحالات ترمي OpenEmail\Exception\InvalidArgumentException من السطر الذي فيه القيمة الخاطئة، بدل أن تظهر كفشل محيّر عند أول إرسال لك. وتقول الرسالة ما الخطأ وما الذي يجب تمريره بدلًا منه، ولا تكرّر أبدًا أي اعتماد.
| المرفوض | السبب |
|---|---|
| لا اعتماد على الإطلاق | لم يُمرَّر apiKey: ولا accessToken:، ولم يُضبط أي من المتغيرين، فلا يوجد ما يُصادَق به. يُرمى عند بناء العميل. |
| مفتاح ورمز معًا | كل طلب يحمل اعتمادًا واحدًا، فلا يستطيع العميل أن يعرف أيهما قصدت. |
| ملف تعريف ارتباط جلسة، أو رمز جلسة، أو مفتاح لخدمة أخرى | لا يصادق هنا سوى oe_live_ وoe_test_، وهذا ما تقوله واجهة API أيضًا. والفحص مجرد بادئة لا أكثر، لذا فإن مفتاحًا ملغى يفشل عند الإرسال الفعلي، في صورة AuthenticationException. |
| قيمة baseUrl: ليست عنوان URL من نوع http أو https، أو تحتوي على اسم مستخدم أو كلمة مرور | لا يمكن الوصول إلى غير ذلك، ومكان الاعتماد هو apiKey: أو accessToken:، لا عنوان URL. يُرمى عند بناء العميل. |
| اعتماد عبر http العادي إلى مضيف ليس على هذا الجهاز | يرميه الاستدعاء قبل إرسال أي شيء. استخدم عنوان URL أساسيًا من نوع https. |
| timeout: بقيمة سالبة | مرّر عدد الثواني، أو 0 لإلغاء المهلة. يُرمى عند بناء العميل، أو يرميه الاستدعاء إن مُرّرت المهلة لاستدعاء واحد. |
| اسم ترويسة ليس رمزًا صالحًا (token)، أو فاصل أسطر أو أي محرف تحكم آخر في قيمة ترويسة | يُفحص في headers: وuserAgent: وidempotencyKey:، لأن فاصل الأسطر قد يبدأ ترويسة ثانية. وتُزال المسافات ومسافات الجدولة وفواصل الأسطر حول القيمة أولًا، كما يزيلها fetch، لذا يعمل المفتاح المقروء من ملف ينتهي بسطر جديد. |
| معرّف فارغ أو مكوّن من نقاط فقط في أي دالة | يُرمى عند استدعاء التابع. فأي مقطع مسار مكوّن من نقاط يحذفه كل محلل عناوين، فيصل الطلب عندئذ إلى نقطة نهاية مختلفة. ويُرفض أيضًا المعرّف الذي ليس UTF-8 صالحًا. |
| محتوى مرفق ليس base64 | تُقرأ السلسلة النصية دائمًا على أنها base64، فالبايتات الخام فيها ستُرسل كبيانات مشوّهة. رمّزها بـ OpenEmail::toBase64()، أو مرّر SplFileInfo أو تدفقًا أو تدفق PSR-7 ويرمّزه العميل. |
يرث هذا الصنف InvalidArgumentException الخاص بـ PHP نفسها، فتبقى الشيفرة التي تلتقطه أصلًا عاملة، وينفّذ OpenEmail\Exception\OpenEmailException مثل كل استثناء آخر ترميه الحزمة. أما القيمة من النوع الخاطئ، كرقم في مكان معرّف نصي، فهي TypeError من PHP نفسها، لأن كل تابع يصرّح بأنواعه.
لا يوجد خيار testMode: ولن يوجد. فمخطط المفتاح جزء من الاعتماد نفسه لا مجرد تلميح، ومن ثَم فالوضع خاصية من خصائص المفتاح. و$client->mode يقرأ البادئة، live أو test، ولا يقرر شيئًا.
عميل واحد، عدة مفاتيح
أنشئ العميل مرة واحدة وشاركه. فإنشاء عميل جديد لكل طلب يهدر اتصاله المفتوح دون مقابل، ولا شيء من حالته خاص بمستدعٍ بعينه.
أما الحالة التي كانت ستفرض عميلًا لكل مفتاح، مثل مهمة ترسل بالنيابة عن عدة مساحات عمل، فمرّر فيها apiKey: مع الاستدعاء. إذ يستبدل ترويسة Authorization لذلك الطلب ولا يترك أثرًا على العميل.
$message = [ 'from' => '[email protected]', 'to' => '[email protected]', 'subject' => 'Your invoice', 'text' => 'Attached.',];$workspaceKey = (string) getenv('OPENEMAIL_API_KEY'); $client->emails->send($message); $client->emails->send($message, apiKey: $workspaceKey); $client->threads->list(folder: 'inbox', apiKey: $workspaceKey);$client->webhooks->list(apiKey: $workspaceKey);كل تابع خارج tempMail يأخذه كآخر وسيط مسمّى، بعد المرشِّحات في القوائم، أما توابع tempMail فتأخذ inboxToken: بدلًا منه. ويُفحص قبل إرسال الطلب، بالقاعدة نفسها التي يستخدمها العميل، فيرمي الخطأ المطبعي InvalidArgumentException يخص apiKey الممرَّر إلى هذا الاستدعاء، بدل خطأ 401 عن اعتماد يتعين عليك بعدها أن تبحث عنه. والاستدعاء الذي تُعاد محاولته يحتفظ بالمفتاح الذي أُعطي له.
يصف $client->mode المفتاح الذي بُني به العميل تحديدًا، ولا يتبع أي تجاوز. فحين يخدم عميل واحد عدة مفاتيح لا يوجد وضع واحد يمكن الإبلاغ عنه، فاقرأه من المفتاح الذي مرّرته. ويعرض var_dump($client) الوضع وعنوان URL الأساسي، ولا يعرض المفتاح أبدًا، وكل معامل يأخذ اعتمادًا موسوم بـ #[\SensitiveParameter]، فيطبع تتبّع المكدس عنصرًا نائبًا مكانه.
نقاط نهاية لا يغلّفها أي تابع
$client->raw هو طبقة النقل التي يمر عبرها كل تابع. ويستدعي $client->raw->request() مسارًا لم يغلّفه أي تابع بعد، مع تطبيق اعتماد العميل وعنوان URL الأساسي والمهلة وسياسة إعادة المحاولة، ويعيد الجسم بعد فك ترميزه كما يفعل التابع.
$ping = $client->raw->request('/ping'); $label = $client->raw->request('/labels', method: 'POST', body: ['name' => 'Invoices']); var_dump($ping, $label);| الوسيط المسمّى | ما تفعله |
|---|---|
| method: | GET ما لم تحدد غير ذلك: POST أو PUT أو PATCH أو DELETE. |
| query: | مصفوفة من معاملات الاستعلام. تُستبعد القيم null والفارغة، وتُضم القائمة بفواصل، ويُرسَل DateTimeInterface كلحظة ISO 8601 بتوقيت UTC. |
| body: | مصفوفة تُرسَل بصيغة JSON. |
| raw: وcontentType: | بايتات تُرسَل كما هي، في صورة سلسلة نصية أو مورد تدفق أو SplFileInfo أو تدفق PSR-7، مع application/octet-stream ما لم تحدد نوعًا. |
| accept: وbinary: | قيمة accept: غير JSON تعيد الجسم نصًا، وbinary: true تعيده في صورة سلسلة من البايتات. |
| idempotent: وidempotencyKey: | يرفق idempotent: true ترويسة Idempotency-Key، تُولَّد ما لم تمرّر مفتاحك الخاص. |
| repeatable: | ما إذا كانت المحاولة تُعاد عند الفشل. لا تُعاد إلا مع GET، ما لم تمرّر repeatable: true. |
| anonymous: | true لا يرسل أي اعتماد إطلاقًا. |
| apiKey: وinboxToken: وtimeout: | بيانات الاعتماد نفسها الخاصة بكل استدعاء، ومهلة بالثواني لهذا الاستدعاء وحده. |
يجب أن يبدأ المسار بـ / واحدة، والمسار الذي يخرج عنوانه النهائي عن أصل عنوان URL الأساسي يرمي InvalidArgumentException قبل إرسال أي شيء، فلا يصل الاعتماد أبدًا إلى مضيف آخر.
صناديق وارد مؤقتة
يبني OpenEmail::createTempMail() عميلًا لصناديق البريد المؤقتة لا يحمل أي مفتاح API ولا يقرأ أي مفتاح من البيئة. ينشئ صناديق البريد بشكل مجهول، وكل قراءة ترسل رمز الصندوق الذي أعاده create، أو الرمز الأحدث الذي أعاده extend، إما لكل استدعاء عبر inboxToken: وإما مرة واحدة عبر OpenEmail::createTempMail(inboxToken: ...).
use OpenEmail\OpenEmail; $tempMail = OpenEmail::createTempMail(); $inbox = $tempMail->create();$page = $tempMail->listMessages($inbox['id'], inboxToken: $inbox['token']); echo count($page->items), ' ', $page->expiresAt, PHP_EOL;يأخذ OpenEmail::createTempMail() الوسائط baseUrl: وhttpClient: وmaxRetries: وtimeout: وuserAgent: وheaders: وdisableUpdateNotice: مثل أي عميل، ويقرأ OPENEMAIL_BASE_URL حين لا تمرّر عنوان URL أساسيًا.
رموز وصول OAuth
التطبيق الذي ربطه شخص عبر OAuth، مثل أداة سطر أوامر أو وكيل، يحمل رمز وصول بدل مفتاح API. مرّره بوصفه accessToken:، إما الرمز نفسه، وإما callable يعيده، مثل closure أو callable من الدرجة الأولى. يعمل الـ callable مرة واحدة لكل استدعاء، وتعيد محاولات ذلك الاستدعاء استخدام ما أعاده، فجدّد الرمز داخله حين يقترب من الانتهاء، ولن تحتاج إلى إعادة بناء العميل أبدًا.
use OpenEmail\OpenEmail; $tokens = ['current' => 'token-from-your-oauth-flow']; $oauthClient = new OpenEmail(accessToken: fn(): string => $tokens['current']); $me = $oauthClient->me->get(); if ($me['object'] === 'oauth_token') { echo $me['clientId'], ' ', $me['expiresAt'], PHP_EOL;}| الحالة | ما الذي يحدث |
|---|---|
| apiKey: وaccessToken: معًا، أو لا أحد منهما | يرمي العميل InvalidArgumentException عند بنائه. وفي حالة غياب الاثنين، تسمّي الرسالة OPENEMAIL_API_KEY وOPENEMAIL_ACCESS_TOKEN. |
| قيمة ليست رمزًا | الرمز من 1 إلى 512 حرفًا ولا يبدأ بـ oe_، وهو الفحص الذي يجريه OpenEmail::isAccessToken(). والسلسلة النصية التي تفشل فيه تُرفض عند بناء العميل، والـ callable الذي يعيد مثلها يجعل الاستدعاء يرمي InvalidArgumentException قبل إرسال أي شيء. |
| OPENEMAIL_ACCESS_TOKEN | يُقرأ حين لا تمرّر أيًّا من الاعتمادين ولا يكون OPENEMAIL_API_KEY مضبوطًا، فالمفتاح الموجود في البيئة هو الذي يغلب. |
| callable يرمي استثناءً | يرمي الاستدعاء ذلك الاستثناء دون تغيير، ولا يُرسَل شيء. |
| apiKey: خاص بالاستدعاء | يحل محل الرمز في ذلك الطلب وحده، ولا يُستدعى الكائن القابل للاستدعاء. |
| $client->mode | دائمًا live مع الرمز. |
| OpenEmail::createTempMail() | لا يرسل أي اعتماد، مهما كان في البيئة. |
| me->get() وme->ping() | مع الرمز، يجيب get بـ object مضبوطًا على oauth_token، وid وroleId بقيمة null، وclientId للتطبيق المربوط، وexpiresAt، أي متى تنتهي موافقة الشخص على التطبيق. ويجيب ping بـ kind مضبوطًا على oauth، وkeyId بقيمة null، وclientId. افحص object أو kind قبل أن تقرأ id أو keyId. |
الرمز يعمل باسم شخص ويقرأ بريده كما يستطيع هو، فأبقه على خادم كما تفعل بالمفتاح.
رموز التحقق
قبل تغيير حساس، مثل حذف نطاق أو تغيير webhook، تطلب الواجهة البرمجية من رمز الوصول رمز التحقق الذي كان تطبيق الويب سيطلبه من الشخص. ويرمي الاستدعاء PermissionException، أي خطأ 403 قيمة isStepUpRequired() فيه true، ولم يتغيّر شيء. اطلب رمزًا، وتحقق من الرمز الذي يعطيك إياه الشخص، ثم أعد الاستدعاء. ولا يُطلب ذلك أبدًا من مفتاح API.
use OpenEmail\Exception\ApiException; $domainId = 'b3e1f0a4-6c2d-4e8a-9f17-2d5c8a0b4e6f'; try { $client->domains->delete($domainId);} catch (ApiException $error) { if (!$error->isStepUpRequired()) { throw $error; } $challenge = $client->security->beginStepUp(); if ($challenge['method'] === 'email') { echo 'Enter the code we emailed to ', $challenge['sentTo'], PHP_EOL; } else { echo 'Enter the code from your authenticator app, or a backup code', PHP_EOL; } $client->security->verifyStepUp(['code' => trim((string) fgets(STDIN))]); $client->domains->delete($domainId);}| الطريقة | ما تفعله |
|---|---|
| security->stepUpStatus() | هل التطبيق متحقَّق منه الآن (elevated، elevatedUntil)، وكيف يُفحص الرمز التالي (method، email أو totp)، وminutes، طول النافذة. لا ترسل شيئًا، ولا تُبلغ عن إيقاف مؤقت. |
| security->beginStepUp() | يفتح تحققًا. مع email يُرسل رمز من ستة أرقام إلى العنوان الذي يسجّل به الشخص دخوله، ويعرضه sentTo مُقنَّعًا. ومع totp يقرأ الشخص رمزًا من تطبيق المصادقة أو يستخدم رمز استرداد. والتحقق الذي ما زال مفتوحًا وله محاولات متبقية يُعاد استخدامه ما لم تمرّر ['resend' => true]، والتحقق المقفل أو المنتهي يُستبدل باستدعاء عادي. ويمكن لكل تطبيق أن يفتح 5 في الساعة و20 في 24 ساعة لكل شخص، والتالي يرمي 429 step_up_throttled. |
| security->verifyStepUp(['code' => ...]) | يفحص الرمز ويفتح التغييرات الحساسة لهذا التطبيق مدة 60 دقيقة، حتى elevatedUntil، عبر REST وعبر أدوات MCP التي تُجري التغييرات نفسها. وبعد 10 رموز خاطئة في 24 ساعة من هذا التطبيق، أو 20 من كل تطبيقات الشخص معًا، يرمي هذا الاستدعاء وbeginStepUp الخطأ 429 step_up_locked مع رسالة تقول متى يُستأنف التحقق. |
لا يطلب العميل رمزًا ولا يعيد الاستدعاء من تلقاء نفسه، ولا يُعاد أيٌّ من التوابع الثلاثة تلقائيًا، لأن إعادة المحاولة بعد رد ضائع قد ترسل بريدًا ثانيًا أو تستهلك محاولة ثانية. ولا تحتاج إلى نطاق، ومفتاح API الذي يستدعي أحدها يتلقى 400 step_up_not_applicable. ويسمّي OpenEmail\Constants\StepUpErrorCodes كل طريقة يمكن أن يفشل بها التحقق، وتقول صفحة أخطاء API ما يجب فعله في كل حالة.
إشعار التحديث
حين يتوفر إصدار أحدث من الحزمة على Packagist، يخبرك العميل بذلك مرة واحدة في كل عملية، على مجرى الأخطاء القياسي، بسطر مثل ℹ openemail/sdk 0.0.2 is available, you are on 0.0.1. يتبعه رابط صفحة الحزمة. ولا يجري الفحص إلا في سطر الأوامر، حين يكون الخرج القياسي طرفية، ولا يجري أبدًا تحت خادم ويب. ويبدأ عند بناء أول عميل ويعمل إلى جانب طلباتك، وفي نهاية السكربت ينتظر ما تبقى من مهلة مقدارها ثانيتان. ويُتجاهل الفشل في الوصول إلى Packagist.
يُجري الفحص طلبه الخاص عبر cURL، خارج httpClient: الخاص بالعميل، فلا يراه أبدًا عميل HTTP مزيّف في اختبار. مرّر disableUpdateNotice: true أو اضبط OPENEMAIL_DISABLE_UPDATE_NOTICE لتعطيله.
الوكلاء وTLS
يترك CurlHttpClient الافتراضي الوكلاء لـ cURL، الذي يقرأ https_proxy أو HTTPS_PROXY لمعرفة الوكيل، وno_proxy أو NO_PROXY لمعرفة المضيفات التي يُتصل بها مباشرة. ومرّر proxy: لتسمية وكيل في الشيفرة بدلًا من ذلك. ويُرسَل اسم المستخدم وكلمة المرور الموجودان في عنوان URL الخاص بالوكيل إلى الوكيل.
use OpenEmail\Http\CurlHttpClient;use OpenEmail\OpenEmail; $client = new OpenEmail(httpClient: new CurlHttpClient( caBundle: '/etc/ssl/certs/corporate-ca.pem', proxy: 'http://proxy.internal:3128', curlOptions: [CURLOPT_IPRESOLVE => CURL_IPRESOLVE_V4],));تستخدم الاتصالات TLS 1.2 أو أحدث وتتحقق من شهادة الخادم واسم المضيف، ولا تُتبع عمليات إعادة التوجيه أبدًا. ويسمّي caBundle: سلطات الشهادات الموثوق بها، للوكيل الذي يفحص TLS. ويضبط curlOptions: أي خيار آخر من خيارات cURL، لكن الإعدادات التي يحتاجها الطلب تغلب دائمًا: عنوان URL ومنفذه، والطريقة، والترويسات، والجسم، وإبقاء إعادة التوجيه معطّلة. ويُرفض CURLOPT_REQUEST_TARGET، والخيار الذي لا يقبله cURL يرمي InvalidArgumentException يسمّيه.