Отправка письма
`emails->send`: одно сообщение, сейчас или позже.
emails->send
$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 пишет сообщение на чужом языке перед его уходом. Тело, а также тема, если вы это не отключите, переводятся в момент приёма запроса, и что получилось, то и уходит: перевод, который не удалось получить, отклоняет отправку, а не отправляет письмо на том языке, на котором вы его написали.
$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 = $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'], ]);}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, когда в установке не настроен ИИ, 429ai_quota_exceeded, когда рабочее пространство израсходовало действия ИИ на сегодня (лимит сбрасывается в полночь по UTC, и повтор не выполняется),translation_failed, когда провайдер не ответил. Ни в одном из этих случаев сообщение не отправляется без перевода в качестве запасного варианта.- Работает с
template: переводится ОТРИСОВАННЫЙ результат, так что одно сохранённое тело обслуживает все языки, на которых читают ваши клиенты. Шаблон, отрисовывающий целый документ, сохраняет свой doctype, блоки<style>и правила@font-face: к модели уходит только тело, а остальное возвращается вокруг него. Его<title>остаётся нетронутым, поскольку его всё равно нигде не показывают. - Повтор не стоит ничего сверху. Перевод не входит в отпечаток идемпотентности (в него входит запрос, включая
translate), так что повтор неотвеченной отправки с тем жеIdempotency-Keyвоспроизводит уже существующее сообщение, а не переводит и отправляет второе. - Переведённое сообщение в очереди или в расписании сохраняет одобренный текст.
emails->rescheduleпо-прежнему переносит его, аemails->updateотклоняет новый текст с 409translation_locked, поэтому, чтобы изменить содержание, придётся отменить отправку и отправить заново.
Вложения
content передаётся по сети в base64. Дайте клиенту то, что он может прочитать, и он закодирует байты за вас: ресурс потока из fopen, SplFileInfo либо поток PSR-7 или загруженный файл. Строка отправляется как есть, поэтому уже должна быть в base64, а именно это OpenEmail::toBase64() делает из байтов, которые у вас в памяти.
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 без переводов строк.