Перейти к документации
PHP

Отправка письма

`emails->send`: одно сообщение, сейчас или позже.

emails->send

send_email.php
$email = $client->emails->send([    'from' => ['email' => '[email protected]', 'name' => 'Acme Billing'],    'to' => ['[email protected]', 'Grace <[email protected]>'],    'cc' => '[email protected]',    'bcc' => [['email' => '[email protected]']],    'replyTo' => '[email protected]',    'subject' => 'Your September invoice',    'html' => '<p>Invoice attached.</p>',    'text' => 'Invoice attached.',    'headers' => ['X-Campaign' => 'invoices'],    'attachments' => [['filename' => 'invoice.pdf', 'content' => new \SplFileInfo('invoice.pdf')]],    'threadId' => 'CAHk7pQ2x9LmZ4-mail.example.com',    'scheduledAt' => 'PT1H',    'tags' => ['order' => '4021'],    'tracking' => ['opens' => true, 'clicks' => true],]); echo $email['id'], ' ', $email['status'], PHP_EOL;

to, cc и bcc принимают одного получателя или список получателей, а одиночный оборачивается за вас. Каждый может быть голым адресом, Name <addr@host> или массивом с email и name.

Сообщение представляет собой один массив с ключами по именам полей API, поэтому replyTo и scheduledAt остаются в camelCase, тогда как idempotencyKey: и apiKey: являются именованными аргументами вызова и никогда не входят в сообщение. Чтобы изменить одно поле в сообщении, собранном ранее, распакуйте его в новый массив: $client->emails->send([...$message, 'subject' => 'Re: your invoice']) сохраняет все остальные поля и заменяет тему.

Параметры

fromstring or arrayобязательно
Отправитель. Голый адрес, `Name <addr@host>` или массив с `email` и `name`. Должен быть адресом, от имени которого этому ключу разрешено отправлять, иначе вызов выбрасывает 403 `from_address_forbidden`. Запасного отправителя нет, поэтому отправка всегда называет адрес, от имени которого уходит.
tostring or arrayобязательно
Один получатель или список получателей, а одиночный оборачивается за вас. Не более 50 в сумме по `to`, `cc` и `bcc`, а больше даёт 422 `too_many_recipients`.
ccstring or array
Засчитывается в лимит 50 получателей.
bccstring or array
Никогда не упоминается в байтах, которые получает кто-либо другой, потому что на каждого получателя передаётся отдельный конверт. Тоже учитывается в пределе 50.
replyTostring or array
Один адрес, отправляемый как заголовок Reply-To.
subjectstring
Не более 998 символов, предел строки по RFC 5322. По умолчанию пусто, а при пустой теме используется тема шаблона или черновика.
htmlstring
Требуется одно из `html`, `text`, `draftId` или `template`. Когда заданы и `html`, и `text`, получатели видят HTML. Не более 1 000 000 символов.
textstring
Текстовая часть, не более 1 000 000 символов.
templatearray
Отрисовать сохранённый шаблон на сервере: массив с `id`, который принимает идентификатор или слаг, и необязательными `version` (int), `props` и `slots`. `version` закрепляет ревизию. Опустите его, чтобы использовать то, что опубликовано на момент принятия запроса. Неизвестный или отсутствующий prop даёт 422, а не пустое место в сообщении.
draftIdstring
Отправить сохранённый черновик под этим конвертом в том виде, в каком он написан. Нельзя сочетать с `template` или `translate`.
headersarray
Имя заголовка, сопоставленное строковому значению, только для `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority и Feedback-ID. Всё, что транспорт задаёт сам, отклоняется с 422 `reserved_header`, а не отбрасывается молча.
attachmentsarray
Список, каждый элемент которого является массивом с `filename`, `content` и необязательным `contentType` или массивом только с `fileId`, который называет файл, уже находящийся в рабочем пространстве, например загруженный через `files->upload`. `content` передаётся в base64: поток из `fopen`, `SplFileInfo` или поток PSR-7 читается и кодируется за вас, а строка уже должна быть в base64. Не более 20 файлов, причём встроенные файлы ограничены 5 МБ в сумме после декодирования. Сохранённый файл может быть больше и передаётся как ссылка для скачивания.
attachmentDeliverystring
`mime`, `link` или `auto`. `auto` отправляет файлы ссылками на скачивание, когда они переваливают за 2 МБ на домене с активным доменом файлов, и внутри сообщения в остальных случаях. Если не указано, применяется настройка почтового ящика, а она по умолчанию `auto`.
threadIdstring
Ответить в существующую цепочку. Транспорт пишет In-Reply-To и References.
scheduledAtDateTimeInterface or string
`DateTimeInterface`, который отправляется как момент ISO 8601 в UTC, момент ISO 8601 в виде строки или длительность вроде `PT1H`. Не дальше чем на год вперёд и никогда в прошлом. Нельзя сочетать с `cancellableForSeconds`. Строка с датой без времени, например `2027-01-01`, читается как полночь UTC в этот день, поэтому передавайте момент времени, когда важен час.
cancellableForSecondsint
От 0 до 900. Окно отмены у немедленной отправки: механизм отмены из редактора, вынесенный наружу, а не зашитый.
tagsarray
До 10 меток с ключами от 1 до 64 символов из букв, цифр, `_` или `-` и строковыми значениями до 256 символов. Возвращаются при каждом чтении и никогда не интерпретируются.
signaturebool
Несёт ли это сообщение подпись адреса, с которого оно отправлено: собственную подпись этого адреса, иначе подпись catch-all для адреса, принятого catch-all, иначе нижнюю строку OpenEmail, если этот адрес её не отключил. Если параметр не указан, тело `html` уходит ровно в написанном виде без подписи, а тело только с `text` её несёт. Задайте false для писем, которые программа отправляет от чьего-то имени, например чека, сброса пароля или дайджеста: ни одному из них не нужна подпись человека. Отправки по шаблону и зашифрованные отправки никогда её не несут.
trackingarray
Массив с необязательными `opens` и `clicks`, каждый типа bool: добавлять ли пиксель открытия и переписывать ли ссылки в этом сообщении. Выключено, если трекинг не включён для адреса, с которого идёт отправка (или для catch-all, который его поймал), а любой из ключей, указанный здесь, решает судьбу этого одного сообщения независимо от настройки адреса.
translatearray
Отправить на языке получателя: массив с `to` и необязательными `from`, `subject` и `includeOriginal`. `to` принимает код, английское название или самоназвание языка, а `subject` и `includeOriginal` по умолчанию равны true. Фиксируется при принятии запроса, поэтому запланированное сообщение несёт одобренный текст. Отклоняется вместе с `draftId`.
idempotencyKeystring
Именованный аргумент вызова, а не поле сообщения. Ваш собственный ключ для этой отправки, от 1 до 255 символов из букв, цифр, `_`, `.`, `:` или `-`. Без него клиент генерирует ключ для каждого вызова, поэтому его собственные повторы никогда не отправляют дважды, а с ним отправка, запущенная снова в другом процессе, воспроизводится, а не повторяется.
apiKeystring
Тоже именованный аргумент. Отправляет с этим ключом вместо ключа клиента. Для процесса, который отправляет от имени нескольких рабочих пространств.

Ответ

Массив с ключами по именам API в camelCase, поэтому $email['status'] читает статус.

idstring
Идентификатор отправки: `msg_` и 24 шестнадцатеричных символа. Используйте его для `get`, `cancel`, `reschedule` и `getTracking`.
statusstring
queued, scheduled, sending, sent, partial, bounced, cancelled или failed. Читайте это поле, а не сам факт возврата из вызова: немедленная отправка выполняется внутри запроса и обычно возвращается как `sent`, `partial` или `failed`, а отложенная возвращается как `queued` или `scheduled`. `partial` является самостоятельным состоянием: у части получателей сообщение уже есть, и отменить его отправку нельзя, поэтому повтор будет ошибкой, а сообщение о сбое будет неправдой.
modestring
`live` или `test`: какой вид ключа его отправил. Тестовая отправка записывается и никогда не передаётся. Её статус `sent`, а `transport` равен `test`, поэтому проверяйте ответ, а не почтовый ящик.
fromstring
Адрес, который на самом деле был авторизован и поставлен на провод, а это не всегда тот, который запрашивали.
subjectstring or null
Как отправлено.
messageIdstring or null
Message-ID по RFC 5322. null, пока не существует MIME. Сервис отправки переписывает заголовок на выходе, поэтому ни один отказ или отчёт о доставке не несёт этого значения. Событие возвращается с `id`.
threadIdstring or null
Цепочка, в которую оно попало.
transportstring or null
Каким путём ушло сообщение. null до отправки.
attemptsint
Сколько раз отправка была предпринята.
lastErrorstring or null
Почему последняя попытка не удалась, дословно.
scheduledAtstring or null
Момент ISO 8601, когда оно должно уйти.
cancellableUntilstring or null
Пока текущее время раньше этого момента, `cancel` ещё работает.
sentAtstring or null
Момент ISO 8601, когда оно ушло.
tagsarray
То, что вы отправили, возвращённое обратно.
sourcestring
composer, api, mcp, ai или queue: какая поверхность запросила. `api` означает данный клиент.
createdAtstring
Момент ISO 8601, когда была создана запись.
replayedbool
True, когда Idempotency-Key совпал с уже существующей отправкой. Ничего нового не отправлено, а это исходное сообщение в его текущем состоянии.
translationarray
Присутствует только у переведённого сообщения и только там, где передаётся весь сохранённый запрос: в этом ответе и в `get`. Содержит `language`, `languageName`, `detectedSourceLanguage`, `subject` и `includeOriginal`, с кодами, а не целыми строками языков. В строке списка его никогда нет, поэтому его отсутствие там ни о чём не говорит.

На языке получателя

translate пишет сообщение на чужом языке перед его уходом. Тело, а также тема, если вы это не отключите, переводятся в момент приёма запроса, и что получилось, то и уходит: перевод, который не удалось получить, отклоняет отправку, а не отправляет письмо на том языке, на котором вы его написали.

translate.php
$email = $client->emails->send([    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Your September invoice',    'html' => '<p>Invoice attached. Payment is due on the 14th.</p>',    'translate' => ['to' => 'de'],]); print_r($email['translation'] ?? []);

Тогда $email['translation'] содержит language, равный de, languageName, равный German, detectedSourceLanguage, равный en, а subject и includeOriginal оба равны true.

Никто не прочитал этот текст до отправки. emails->translate проходит тот же путь, но останавливается на шаг раньше. Покажите результат человеку, дайте ему внести правки, затем отправьте одобренное вообще без translate в вызове. Повторная передача переведёт текст ещё раз и отбросит его правки.

preview_translation.php
$preview = $client->emails->translate([    'subject' => 'Your September invoice',    'html' => '<p>Invoice attached. Payment is due on the 14th.</p>',    'to' => 'de',]); echo $preview['language']['native'], PHP_EOL, $preview['subject'], PHP_EOL, $preview['html'], PHP_EOL;echo 'Send it as it is? [y/N] '; $answer = fgets(STDIN); if ($answer !== false && strtolower(trim($answer)) === 'y') {    $client->emails->send([        'from' => '[email protected]',        'to' => '[email protected]',        'subject' => $preview['subject'],        'html' => $preview['html'],    ]);}
languages.php
use OpenEmail\Constants\Languages;use OpenEmail\OpenEmail; echo count(Languages::ALL), PHP_EOL; $current = $client->languages->list();echo count($current), PHP_EOL; echo OpenEmail::resolveLanguage('Deutsch')['code'] ?? 'none', PHP_EOL;echo OpenEmail::resolveLanguage('zh-TW')['code'] ?? 'none', PHP_EOL;echo OpenEmail::languageByCode('DE')['native'] ?? 'none', PHP_EOL;var_dump(OpenEmail::isRtlLanguage('ar'));

Эти строки печатают 200, число строк, с которыми поставляется эта версия, затем сколько их сейчас в API, затем de, zh-Hant, Deutsch и bool(true). Таблица встроена в пакет в порядке списка выбора как OpenEmail\Constants\Languages::ALL, список массивов с code, label, native, flag и rtl, поэтому список выбора можно заполнить до первого запроса. languages->list возвращает те же строки по сети как обычный список для тех, кому нужны текущие строки, а не те, с которыми вышла эта версия. OpenEmail::resolveLanguage() принимает код, английское название, самоназвание или псевдоним (zh-TW является псевдонимом кода, которого больше нет в списке) и возвращает null, если ничего не совпало, OpenEmail::languageByCode() ищет точное совпадение кода без учёта регистра, а OpenEmail::isRtlLanguage() сообщает, пишется ли язык справа налево, как шестнадцать из этих строк. Ищите по native, label и code вместе, показывайте сначала native и сохраняйте код.

emails->translate не повторяется автоматически. Он тратит вызовы модели и ничего не пишет, так что делать идемпотентным нечего, а повтор после неотвеченного запроса лишь купил бы тот же ответ дважды.

  • Язык, который API не может распознать, даёт validation_error для translate.to ещё до отправки.
  • translation_too_long при объёме свыше 30 000 символов, translation_not_configured, когда в установке не настроен ИИ, 429 ai_quota_exceeded, когда рабочее пространство израсходовало действия ИИ на сегодня (лимит сбрасывается в полночь по UTC, и повтор не выполняется), translation_failed, когда провайдер не ответил. Ни в одном из этих случаев сообщение не отправляется без перевода в качестве запасного варианта.
  • Работает с template: переводится ОТРИСОВАННЫЙ результат, так что одно сохранённое тело обслуживает все языки, на которых читают ваши клиенты. Шаблон, отрисовывающий целый документ, сохраняет свой doctype, блоки <style> и правила @font-face: к модели уходит только тело, а остальное возвращается вокруг него. Его <title> остаётся нетронутым, поскольку его всё равно нигде не показывают.
  • Повтор не стоит ничего сверху. Перевод не входит в отпечаток идемпотентности (в него входит запрос, включая translate), так что повтор неотвеченной отправки с тем же Idempotency-Key воспроизводит уже существующее сообщение, а не переводит и отправляет второе.
  • Переведённое сообщение в очереди или в расписании сохраняет одобренный текст. emails->reschedule по-прежнему переносит его, а emails->update отклоняет новый текст с 409 translation_locked, поэтому, чтобы изменить содержание, придётся отменить отправку и отправить заново.

Вложения

content передаётся по сети в base64. Дайте клиенту то, что он может прочитать, и он закодирует байты за вас: ресурс потока из fopen, SplFileInfo либо поток PSR-7 или загруженный файл. Строка отправляется как есть, поэтому уже должна быть в base64, а именно это OpenEmail::toBase64() делает из байтов, которые у вас в памяти.

attachments.php
use OpenEmail\OpenEmail; $attachments = [    ['filename' => 'invoice.pdf', 'content' => OpenEmail::toBase64(file_get_contents('invoice.pdf')), 'contentType' => 'application/pdf'],    ['filename' => 'report.csv', 'content' => new \SplFileInfo('report.csv')],    ['filename' => 'contacts.csv', 'content' => fopen('contacts.csv', 'rb')],    ['fileId' => 'file_6bb640f5b99e47deb758f1f5'],]; $client->emails->send([    'from' => '[email protected]',    'to' => '[email protected]',    'subject' => 'Your documents',    'text' => 'All three are attached.',    'attachments' => $attachments,]);

Строковый content, который не является base64, выбрасывает OpenEmail\Exception\InvalidArgumentException ещё до отправки. Сырые байты, которые случайно читаются как base64, ушли бы искажёнными, поэтому никогда не передавайте байты файла как есть: оберните их в OpenEmail::toBase64() или передайте сам файл.

OpenEmail::toBase64() пригодится, если такая же кодировка нужна где-то ещё. Он принимает строку байтов, ресурс потока, SplFileInfo или поток PSR-7 и возвращает base64 без переводов строк.