البث
`broadcasts->preview` و`send` و`list` و`listAll` و`iterate` و`get` و`listRecipients` و`listAllRecipients` و`iterateRecipients` و`getRecipient` و`stats` و`analytics` و`cancel`.
كل الدوالّ
use OpenEmail\Constants\BroadcastRecipientFilters;use OpenEmail\Constants\BroadcastStatuses; $draft = [ 'audienceIds' => ['aud_4c1b8e2a7d9f05c36b4e8a71'], 'from' => 'Acme <[email protected]>', 'subject' => '{{firstName|Hello}}, the September release is out', 'html' => '<p>Hi {{firstName|there}},</p><p>Here is what changed this month.</p>', 'text' => 'Hi {{firstName|there}}, here is what changed this month. Unsubscribe: {{unsubscribeUrl}}', 'tags' => ['campaign' => 'release-2026-09'],]; $reach = $client->broadcasts->preview($draft);echo $reach['recipients'], ' ', $reach['unsubscribed'], ' ', $reach['suppressed'], PHP_EOL; $broadcast = $client->broadcasts->send($draft); $latest = $client->broadcasts->get($broadcast['id']); while (in_array($latest['status'], [BroadcastStatuses::SCHEDULED, BroadcastStatuses::QUEUED, BroadcastStatuses::SENDING], true)) { sleep(5); $latest = $client->broadcasts->get($broadcast['id']);} foreach ($client->broadcasts->iterateRecipients($broadcast['id']) as $copy) { echo $copy['email'], ' ', $copy['status'], ' ', $copy['opens'], ' ', $copy['clicks'], PHP_EOL;} $bounced = $client->broadcasts->listRecipients($broadcast['id'], filter: BroadcastRecipientFilters::BOUNCED); foreach ($bounced as $row) { echo $row['emailId'], ' ', $row['email'], PHP_EOL;} $copy = $client->broadcasts->getRecipient($broadcast['id'], 'msg_01dad25067bc4dac966d515d');echo $copy['subject'], ' ', $copy['bouncedAt'] ?? 'not bounced', PHP_EOL; $stats = $client->broadcasts->stats($broadcast['id'], grain: 'day');echo $stats['totals']['opened'], ' ', $stats['totals']['clicked'], ' ', $stats['totals']['unsubscribed'], PHP_EOL; $lately = $client->broadcasts->stats($broadcast['id'], days: 1);echo $lately['window']['opened'] ?? 0, PHP_EOL; $later = $client->broadcasts->send([...$draft, 'scheduledAt' => 'P1D']);$client->broadcasts->cancel($later['id']); $history = $client->broadcasts->list(audienceId: $draft['audienceIds'][0]);echo $latest['status'], ' ', $latest['counts']['sent'], ' ', count($history), PHP_EOL; $month = $client->broadcasts->analytics(days: 30); foreach ($month['broadcasts'] as $row) { echo $row['subject'], ' ', $row['sent'], ' ', $row['opened'], PHP_EOL;}البث يرسل رسالة واحدة إلى كل من في جمهور أو أكثر، بنسخة منفصلة لكل شخص. لكل نسخة مستلم واحد بالضبط بلا cc ولا bcc، فلا يرى أحد لمن ذهبت أيضًا، وكل نسخة رسالة عادية لها معرّف msg_ وأحداث وتتبع وخطافات ويب خاصة بها. ويعرضها listRecipients مع ما حدث لكل منها. ولا تُحفظ النسخ في مجلد المرسل، لأن البث هو السجل.
يعود send فورًا والبث في الحالة queued، أو scheduled حين يحمل المتن scheduledAt، ويجري الإرسال في الخلفية. ويتطلب send كلًّا من emails:send وaudiences:read، ويتطلب preview النطاق audiences:read. ويتطلب list وlistAll وiterate وget وlistRecipients وlistAllRecipients وiterateRecipients وgetRecipient وstats وanalytics النطاق emails:read، ويتطلب cancel النطاق emails:send.
يحمل كل send ترويسة Idempotency-Key، إما مفتاحك عبر idempotencyKey: وإما مفتاحًا يصنعه العميل، فإعادة المحاولة بعد فشل في الشبكة تجيب بالبث الذي أنشأته المحاولة الأولى، مع replayed مضبوطًا على true، بدل الإرسال مرتين. وpreview وget وcancel وكل قراءة آمنة للتكرار وتُعاد محاولتها.
$broadcast = $client->broadcasts->send([ 'audienceIds' => ['aud_4c1b8e2a7d9f05c36b4e8a71'], 'from' => 'Acme <[email protected]>', 'subject' => 'Doors open on Friday', 'text' => 'Hi {{firstName|there}}, doors open at nine. Unsubscribe: {{unsubscribeUrl}}', 'scheduledAt' => new \DateTimeImmutable('+1 hour'),], idempotencyKey: 'doors-open-2026-10'); echo $broadcast['id'], ' ', $broadcast['status'], ' ', $broadcast['replayed'] ? 'replayed' : 'new', PHP_EOL;حقول البث مفاتيح مصفوفة واحدة بأسماء API بصيغة camelCase (audienceIds وscheduledAt). وidempotencyKey: وapiKey: وسيطان مسمّيان للاستدعاء ولا يُرسلان أبدًا كحقول. ونشر مسودة في مصفوفة جديدة إلى جانب حقل يرسل المسودة نفسها مع ذلك التغيير وحده، فيرسلها send([...$draft, 'scheduledAt' => 'P1D']) بعد يوم. ويأخذ scheduledAt قيمة DateTimeInterface أو سلسلة ISO 8601 أو مدة مثل PT2H، ويخرج DateTimeInterface كلحظة UTC. ولا يرسل preview مما تعطيه سوى audienceIds، فهو يأخذ المصفوفة نفسها التي يأخذها send. والاستجابة مصفوفة مفاتيحها بصيغة camelCase، فيقرأ $broadcast['status'] الحالة.
حقول الدمج
تُملأ subject وhtml وtext لكل شخص من جهة اتصاله. {{firstName}} هي الكلمة الأولى من اسم جهة الاتصال، و{{lastName}} بقيته، و{{name}} الاسم كاملًا، و{{email}} العنوان الذي تذهب إليه النسخة، و{{unsubscribeUrl}} الرابط الذي يلغي اشتراكه.
يقبل كل حقل قيمة بديلة بعد شرطة تُستخدم حين لا تكون لجهة الاتصال قيمة له، فيصير {{firstName|there}} "there" لجهة اتصال محفوظة بلا اسم. تُهرَّب القيم في html، ويُترك أي {{…}} آخر كما كُتب تمامًا.
مرّر template بدل html وtext لإرسال قالب مخزَّن، في صورة مصفوفة فيها id واختياريًا version وprops وslots. وتصل إليه القيم الخمس نفسها كخصائص، لكن فقط الخصائص التي يعلنها القالب، فالقالب الذي يعلن firstName يحصل عليه، والذي لا يعلنه لا يُرفض أبدًا بسببه. وأي شيء في props الخاصة به يذهب إلى كل نسخة بالتساوي.
إلغاء الاشتراك
تحمل كل نسخة ترويسات إلغاء الاشتراك بنقرة واحدة التي تتيح لبرنامج البريد عرض زر إلغاء الاشتراك الخاص به، وهو ما يطلبه كبار مزودي صناديق البريد من البريد الجماعي. والمتن html أو text الذي لا يضع {{unsubscribeUrl}} بنفسه يحصل على تذييل من سطر واحد فيه الرابط. أما القالب فيُرسل كما هو تمامًا، فضع {{unsubscribeUrl}} في القالب.
إلغاء الاشتراك يعلّم الشخص كملغٍ للاشتراك في كل جمهور ذهب إليه ذلك البث، ويُظهر audiences->listContacts ذلك في unsubscribedAt الخاص بصفه، كما تصف صفحة الجماهير. ويبقى في الجمهور وفي دفتر العناوين، ولا تُمس جماهيره الأخرى، ويظل البريد المرسل إليه رسالةً رسالة يخرج. وإخراجه من الجمهور ثم إضافته من جديد يعيد اشتراكه.
من يُتخطى
يصل البث إلى كل جهة اتصال في واحد على الأقل من audienceIds، مرة واحدة مهما كان عدد ما يضمها منها. ويتخطى جهة الاتصال التي ألغت اشتراكها في كل جمهور من تلك الجماهير تنتمي إليه، والعنوان الموجود في قائمة الحظر بعد ارتداد أو شكوى أو لأن أحدًا أضافه إليها. وجهة الاتصال المضافة إلى أحد الجماهير بعد send وقبل أن يصل إليها الإرسال تُشمل.
يعيد preview الأرقام نفسها دون إرسال: recipients وunsubscribed وsuppressed. وsend الذي لن يصل إلى أحد يرمي 422 no_recipients في صورة ValidationException.
يُقارن الإرسال كله بعدد الرسائل الشهري في الخطة قبل كتابة أي شيء، فالبث الذي لا تغطيه الحصة يرمي 429 send_quota_exceeded في صورة RateLimitException ولا يترك شيئًا خلفه. وتُحسب كل نسخة رسالة واحدة.
الحالة والتقدم
يقرأ get قيم counts مباشرة من النسخ، فاستطلعه أثناء إرسال البث، مع sleep() بين الاستدعاءات كما يفعل المثال أعلاه. تنتقل status من scheduled أو queued إلى sending وتستقر على sent حين تخرج كل نسخة سُلّمت أو تفشل. وتبقى sending ما دامت نسخ تنتظر، حتى بعد أن يقول completedAt إن آخر شخص قد وُصل إليه. وfailed تعني أن البث كله توقف، ويقول lastError السبب: لم يعد ممكنًا الإرسال من عنوان from، أو توقف القالب عن الحل، أو نفدت الخطة في منتصف الطريق، أو ظل الإرسال نفسه يفشل، أو تعذرت كتابة أي نسخة.
يوقف cancel بثًا حالته scheduled أو queued أو sending. لا يُضاف أحد آخر وتُلغى كل نسخة ما زالت تنتظر، بينما النسخ التي خرجت لا يمكن استرجاعها. وبمجرد أن تخرج كل النسخ يرمي cancel الخطأ 409 broadcast_not_cancellable في صورة ConflictException، وإلغاء بث مُلغى يعيده كما هو.
من وصل إليهم
يعيد listRecipients صفحة OpenEmail\Result\Page واحدة من الأشخاص الذين ذهب إليهم البث، صفًا لكل نسخة، مرتّبة حسب العنوان، مع items وhasMore وnextCursor. ويمر listAllRecipients على كل الصفحات في مصفوفة واحدة، ويعيد iterateRecipients كائن Generator يسلّم نسخة واحدة في كل مرة ولا يجلب الصفحة التالية إلا حين تطلبها الحلقة. ويتراوح limit: من 1 إلى 200 والافتراضي 50، ويُعاد cursor: مع filter: وq: نفسيهما.
| `filter:` | يحتفظ بـ |
|---|---|
| pending | نسخ ما زالت في الطابور أو مجدولة أو قيد الإرسال. |
| sent | نسخ خرجت. |
| delivered | نسخ قبلها الخادم المستقبِل. |
| opened | نسخ فُتحت مرة واحدة على الأقل. |
| not_opened | نسخ أُرسلت ولم تُفتح قط. |
| clicked | نسخ فيها نقرة متتبَّعة واحدة على الأقل. |
| bounced | نسخ ارتدّت. |
| complained | نسخ أبلغ عنها الشخص كرسالة مزعجة. |
| failed | نسخ فشلت أو أُلغيت. |
| unsubscribed | أشخاص ألغوا اشتراكهم بعد خروج البث. |
يسمّي OpenEmail\Constants\BroadcastRecipientFilters كل مرشِّح، ويبحث q: في العنوان والاسم، متجاهلًا حالة الأحرف. وتستبعد مرات الفتح والنقرات وكلاء الصور وأدوات فحص الروابط، وتبقى 0 حين يخرج البث والتتبّع معطَّل.
يعيد getRecipient($id, $emailId) نسخة واحدة: الصف نفسه، إضافة إلى subject وhtml وtext كما تلقّاها ذلك الشخص تمامًا، مع ملء حقول الدمج ورابط إلغاء الاشتراك الخاص به. مرّر emailId الخاص بالصف بوصفه الوسيط الثاني. وHTML من قبل إضافة تتبّع الفتح والنقر. وemailId الذي ليس نسخة من هذا البث يرمي 404 recipient_not_found، والبث غير المعروف يرمي 404 broadcast_not_found، وكلاهما في صورة NotFoundException.
يعيد stats المجاميع وسلسلة زمنية. تعدّ totals النسخ sent وdelivered وbounced وcomplained وfailed، مع pending للنسخ التي ما زالت تنتظر، والأشخاص الذين opened وclicked وunsubscribed، مع opens وclicks كأعداد للأحداث. وseries متفرقة والأقدم أولًا، فاصل واحد لكل grain: (minute أو hour أو day، والافتراضي hour) حدث فيه شيء، مقسّمة في المنطقة الزمنية التي تبعد offsetMinutes: شرق UTC. مرّر intdiv((int) date('Z'), 60) للمنطقة المحلية. وهي تعدّ كل شخص مرة واحدة، عند أول مرة حدث له ذلك، فيتطابق مجموعها مع المجاميع.
مرّر days: أو minutes: إلى stats لتقرأ أيضًا ما حدث مؤخرًا. فيعدّ window عندئذ ما سُلّم، وما ارتد، وما أُبلغ عنه كبريد مزعج، وما فُتح، وما نُقر عليه، وما أُلغي الاشتراك فيه داخلها، ولا تحتفظ series إلا بفواصلها، بينما تظل totals تغطي البث كله. ودون أيٍّ منهما يكون window بقيمة null.
المفتاح المقيّد بعناوين أو نطاقات بعينها لا يصل إلا إلى البثوث المرسلة من عنوان أو نطاق يملكه. ويستبعد list وlistAll وiterate البقية، ويرمي get وتوابع المستلمين وstats وcancel الخطأ 404 broadcast_not_found لها.
الاستجابة: بث
يعيد كلٌّ من send وget وcancel واحدًا من هذه في صورة مصفوفة مفاتيحها بصيغة camelCase، ويضيف send الحقل replayed. ويعيد list صفحة OpenEmail\Result\Page منها، من الأحدث، ويعيد listAll كل واحد منها في مصفوفة واحدة، ويعيد iterate كائن Generator يمر عليها. ويعيد preview مصفوفة فيها audienceIds وrecipients وunsubscribed وsuppressed. ويعيد listRecipients صفحة Page من صفوف المستلمين، ويعيد getRecipient صفًا واحدًا مع محتواه، ويعيد stats مصفوفة فيها broadcastId وgrain وtotals وwindow وseries. ويعيد analytics مصفوفة فيها totals وseries وصف لكل بث في broadcasts. والأوقات سلاسل ISO 8601 يقرؤها new \DateTimeImmutable().
idstring- المعرّف الدائم، `brd_` يليه 24 حرفًا ست عشريًا.
statusstring- `scheduled` أو `queued` أو `sending` أو `sent` أو `cancelled` أو `failed`. ويسمّي `OpenEmail\Constants\BroadcastStatuses` كلًّا منها.
modestring- `live` أو `test`، بحسب المفتاح الذي أنشأه. ونسخ البث التجريبي تُعلَّم كمرسلة ولا تُسلَّم إلى أحد.
sourcestring- من أين بدأ: `api` لمفتاح، و`oauth` لتطبيق متصل، و`composer` للتطبيق، و`mcp` لمساعد.
audienceIdsarray- الجماهير التي أُرسل إليها، كل منها مرة واحدة.
fromstring- العنوان الذي تُرسل منه كل نسخة.
subjectstring- الموضوع كما كُتب، بحقول الدمج كلها. فارغ حين يوفر القالب الموضوع.
countsarray- `recipients` هو التقدير المأخوذ عند `send`. ويحسب `created` النسخ المكتوبة، و`skipped` الأشخاص الذين تُخطّوا لأن عناوينهم كانت محظورة حينها، و`failedToQueue` الأشخاص الذين تعذرت كتابة نسختهم. وتحسب `queued` و`sending` و`sent` و`failed` و`cancelled` النسخ حسب الحالة التي فيها كل منها الآن.
lastErrorstring or null- لماذا فشل البث، أو أحدث نسخة تعذّرت كتابتها ولماذا. ويكون null ما دام لم يحدث خطأ.
scheduledAtstring or null- بصيغة ISO-8601 بتوقيت UTC، متى يُتوقع أن يبدأ الإرسال. ويكون null لبث يُرسل فورًا.
startedAtstring or null- ISO-8601 UTC، وقت وصول الإرسال إلى أول الأشخاص.
completedAtstring or null- ISO-8601 UTC، وقت الوصول إلى آخر شخص. وقد تظل نسخ تنتظر الخروج بعده.
cancelledAtstring or null- ISO-8601 UTC، وقت إيقاف `cancel` له.
createdAtstring- ISO-8601 UTC، وقت استدعاء `send`. ويحدد ترتيب القائمة.
updatedAtstring- ISO-8601 UTC، يُحدَّث مع تقدم الإرسال.
الاستجابة: صف مستلم
كل صف من listRecipients وlistAllRecipients وiterateRecipients، في صورة مصفوفة مفاتيحها بصيغة camelCase. وتضيف المصفوفة التي يعيدها getRecipient الحقول subject وhtml وtext.
emailIdstring- معرّف `msg_` لنسخة هذا الشخص. يقرؤها `getRecipient` مع محتواها، ويقرؤها `emails->get` كرسالة مرسلة، كما تصف صفحة السرد والجلب.
contactIdstring or null- جهة الاتصال التي ذهبت إليها، أو null حين تكون جهة الاتصال قد حُذفت منذ ذلك الحين.
emailstring- العنوان الذي ذهبت إليه النسخة.
namestring or null- الاسم المسجّل في جهة الاتصال.
statusstring- حالة النسخة: `queued` أو `scheduled` أو `sending` أو `sent` أو `failed` أو `cancelled`.
sentAtstring or null- ISO-8601 بتوقيت UTC، وقت خروج النسخة.
deliveredAtstring or null- ISO-8601 بتوقيت UTC، وقت قبول الخادم المستقبِل لها، أول `email.delivered`.
bouncedAtstring or null- ISO-8601 بتوقيت UTC، وقت ارتدادها، أول `email.bounced`.
complainedAtstring or null- ISO-8601 بتوقيت UTC، وقت إبلاغ الشخص عنها كرسالة مزعجة، أول `email.complained`.
failurestring or null- سبب فشل النسخة، إن فشلت.
opensint- الفتحات المسجّلة، دون تلك التي تحدثها وكلاء الصور والماسحات. 0 حين كان التتبع معطّلًا.
firstOpenAtstring or null- ISO-8601 بتوقيت UTC، أول فتح.
clicksint- النقرات المسجّلة على الروابط المتتبَّعة، دون الماسحات.
firstClickAtstring or null- ISO-8601 بتوقيت UTC، أول نقرة.
unsubscribedAtstring or null- بصيغة ISO-8601 بتوقيت UTC، متى ألغى هذا الشخص اشتراكه في أحد جماهير البث بعد خروجه، عبر رابطه أو بطريقة أخرى.