Отправка письма
`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: Pathname("invoice.pdf")}], threadId: "CAHk7pQ2x9LmZ4-mail.example.com", scheduledAt: "PT1H", tags: {order: "4021"}, tracking: {opens: true, clicks: true}) puts email[:id], email[:status]to, cc и bcc принимают одного получателя или Array получателей, а одиночный оборачивается за вас. Каждый может быть голым адресом, Name <addr@host> или Hash с email и name.
Сообщение передаётся именованными аргументами или одним Hash. Именованные аргументы рядом с Hash сливаются с ним и имеют приоритет, если оба задают одно поле, поэтому client.emails.send(message, subject: "Re: your invoice") меняет одно поле в сообщении, собранном ранее. Ключи сохраняют имена из API, поэтому replyTo и scheduledAt остаются в camelCase, тогда как idempotency_key: и api_key: являются параметрами вызова и никогда не входят в сообщение.
Параметры
fromString or Hashобязательно- Отправитель. Голый адрес, `Name <addr@host>` или Hash с `email` и `name`. Должен быть адресом, от имени которого этому ключу разрешено отправлять, иначе вызов выбрасывает 403 `from_address_forbidden`. Запасного отправителя нет, поэтому отправка всегда называет адрес, от имени которого уходит.
toString, Hash or Arrayобязательно- Один получатель или Array получателей, а одиночный оборачивается за вас. Не более 50 в сумме по `to`, `cc` и `bcc`, а больше даёт 422 `too_many_recipients`.
ccString, Hash or Array- Засчитывается в лимит 50 получателей.
bccString, Hash or Array- Никогда не упоминается в байтах, которые получает кто-либо другой, потому что на каждого получателя передаётся отдельный конверт. Тоже учитывается в пределе 50.
replyToString or Hash- Один адрес, отправляемый как заголовок Reply-To.
subjectString- Не более 998 символов, предел строки по RFC 5322. По умолчанию пусто, а при пустой теме используется тема шаблона или черновика.
htmlString- Требуется одно из `html`, `text`, `draftId` или `template`. Когда заданы и `html`, и `text`, получатели видят HTML. Не более 1 000 000 символов.
textString- Текстовая часть, не более 1 000 000 символов.
templateHash- Отрисовать сохранённый шаблон на сервере: Hash с `id`, который принимает идентификатор или слаг, и необязательными `version` (Integer), `props` и `slots`. `version` закрепляет ревизию. Опустите его, чтобы использовать то, что опубликовано на момент принятия запроса. Неизвестный или отсутствующий prop даёт 422, а не пустое место в сообщении.
draftIdString- Отправить сохранённый черновик под этим конвертом в том виде, в каком он написан. Нельзя сочетать с `template` или `translate`.
headersHash- Имя заголовка, сопоставленное значению String, только для `X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority и Feedback-ID. Всё, что транспорт задаёт сам, отклоняется с 422 `reserved_header`, а не отбрасывается молча.
attachmentsArray<Hash>- Каждое вложение является Hash с `filename`, `content` и необязательным `contentType` или Hash только с `fileId`, который называет файл, уже находящийся в рабочем пространстве, например загруженный через `files.upload`. Передайте байты в `content`, и они будут закодированы в base64 за вас. Не более 20 файлов, причём встроенные файлы ограничены 5 МБ в сумме после декодирования. Сохранённый файл может быть больше и передаётся как ссылка для скачивания.
attachmentDeliveryString- `mime`, `link` или `auto`. `auto` отправляет файлы ссылками на скачивание, когда они переваливают за 2 МБ на домене с активным доменом файлов, и внутри сообщения в остальных случаях. Если не указано, применяется настройка почтового ящика, а она по умолчанию `auto`.
threadIdString- Ответить в существующую цепочку. Транспорт пишет In-Reply-To и References.
scheduledAtTime, DateTime or String- Time или DateTime, которые отправляются как момент ISO 8601 в UTC, момент ISO 8601 в виде String или длительность вроде `PT1H`. Не дальше чем на год вперёд и никогда в прошлом. Нельзя сочетать с `cancellableForSeconds`. Date из Ruby отправляется как голая дата, которую API читает как полночь UTC в этот день, поэтому передавайте Time, когда важен час.
cancellableForSecondsInteger- От 0 до 900. Окно отмены у немедленной отправки: механизм отмены из редактора, вынесенный наружу, а не зашитый.
tagsHash- До 10 меток с ключами от 1 до 64 символов из букв, цифр, `_` или `-` и значениями String до 256 символов. Возвращаются при каждом чтении и никогда не интерпретируются.
signatureBoolean- Несёт ли это сообщение подпись адреса, с которого оно отправлено: собственную подпись этого адреса, иначе подпись catch-all для адреса, принятого catch-all, иначе нижнюю строку OpenEmail, если этот адрес её не отключил. Если параметр не указан, тело `html` уходит ровно в написанном виде без подписи, а тело только с `text` её несёт. Задайте `false` для писем, которые программа отправляет от чьего-то имени, например чека, сброса пароля или дайджеста: ни одному из них не нужна подпись человека. Отправки по шаблону и зашифрованные отправки никогда её не несут.
trackingHash- Hash с необязательными Boolean `opens` и `clicks`: добавлять ли пиксель открытия и переписывать ли ссылки в этом сообщении. Выключено, если трекинг не включён для адреса, с которого идёт отправка (или для catch-all, который его поймал), а любой из ключей, указанный здесь, решает судьбу этого одного сообщения независимо от настройки адреса.
translateHash- Отправить на языке получателя: Hash с `to` и необязательными `from`, `subject` и `includeOriginal`. `to` принимает код, английское название или самоназвание языка, а `subject` и `includeOriginal` по умолчанию равны true. Фиксируется при принятии запроса, поэтому запланированное сообщение несёт одобренный текст. Отклоняется вместе с `draftId`.
idempotency_keyString- Ваш собственный ключ для этой отправки, от 1 до 255 символов из букв, цифр, `_`, `.`, `:` или `-`. Без него клиент генерирует ключ для каждого вызова, поэтому его собственные повторы никогда не отправляют дважды, а с ним отправка, запущенная снова в другом процессе, воспроизводится, а не повторяется.
api_keyString- Отправляет с этим ключом вместо ключа клиента. Для процесса, который отправляет от имени нескольких рабочих пространств.
Ответ
Hash с ключами типа Symbol, поэтому email[:status] читает статус.
idString- Идентификатор отправки: `msg_` и 24 шестнадцатеричных символа. Используйте его для `get`, `cancel`, `reschedule` и `get_tracking`.
statusString- queued, scheduled, sending, sent, partial, bounced, cancelled или failed. Читайте это поле, а не сам факт возврата из вызова: немедленная отправка выполняется внутри запроса и обычно возвращается как `sent`, `partial` или `failed`, а отложенная возвращается как `queued` или `scheduled`. `partial` является самостоятельным состоянием: у части получателей сообщение уже есть, и отменить его отправку нельзя, поэтому повтор будет ошибкой, а сообщение о сбое будет неправдой.
modeString- `live` или `test`: какой вид ключа его отправил. Тестовая отправка записывается и никогда не передаётся. Её статус `sent`, а `transport` равен `test`, поэтому проверяйте ответ, а не почтовый ящик.
fromString- Адрес, который на самом деле был авторизован и поставлен на провод, а это не всегда тот, который запрашивали.
subjectString or nil- Как отправлено.
messageIdString or nil- Message-ID по RFC 5322. nil, пока не существует MIME. Сервис отправки переписывает заголовок на выходе, поэтому ни один отказ или отчёт о доставке не несёт этого значения. Событие возвращается с `id`.
threadIdString or nil- Цепочка, в которую оно попало.
transportString or nil- Каким путём ушло сообщение. nil до отправки.
attemptsInteger- Сколько раз отправка была предпринята.
lastErrorString or nil- Почему последняя попытка не удалась, дословно.
scheduledAtString or nil- Момент ISO 8601, когда оно должно уйти.
cancellableUntilString or nil- Пока текущее время раньше этого момента, `cancel` ещё работает.
sentAtString or nil- Момент ISO 8601, когда оно ушло.
tagsHash- То, что вы отправили, возвращённое обратно.
sourceString- composer, api, mcp, ai или queue: какая поверхность запросила. `api` означает данный клиент.
createdAtString- Момент ISO 8601, когда была создана запись.
replayedBoolean- True, когда Idempotency-Key совпал с уже существующей отправкой. Ничего нового не отправлено, а это исходное сообщение в его текущем состоянии.
translationHash- Присутствует только у переведённого сообщения и только там, где передаётся весь сохранённый запрос: в этом ответе и в `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"}) p email[:translation]Тогда email[:translation] содержит {language: "de", languageName: "German", detectedSourceLanguage: "en", subject: true, 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") puts preview.dig(:language, :native), preview[:subject], preview[:html]print "Send it as it is? [y/N] " if $stdin.gets.to_s.strip.casecmp?("y") client.emails.send( from: "[email protected]", to: "[email protected]", subject: preview[:subject], html: preview[:html] )endp OpenEmail::LANGUAGES.size current = client.languages.listp current.size p OpenEmail.resolve_language("Deutsch")&.fetch(:code)p OpenEmail.resolve_language("zh-TW")&.fetch(:code)p OpenEmail.language_by_code("DE")&.fetch(:native)p OpenEmail.rtl_language?("ar")Эти строки печатают 200, число строк, с которыми поставляется эта версия, затем сколько их сейчас в API, затем "de", "zh-Hant", "Deutsch" и true. Таблица встроена в гем в порядке списка выбора как OpenEmail::LANGUAGES, замороженный Array из Hash с code, label, native, flag и rtl, поэтому список выбора можно заполнить до первого запроса. languages.list возвращает те же строки по сети как обычный Array для тех, кому нужны текущие строки, а не те, с которыми вышла эта версия. OpenEmail.resolve_language принимает код, английское название, самоназвание или псевдоним (zh-TW является псевдонимом кода, которого больше нет в списке) и возвращает nil, если ничего не совпало, OpenEmail.language_by_code ищет точное совпадение кода без учёта регистра, а шестнадцать строк пишутся справа налево. Ищите по 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. Передайте байты, и они будут закодированы за вас: двоичную String, например ту, что возвращает File.binread, IO, например открытый File, или Pathname, который будет прочитан за вас.
attachments = [ {filename: "invoice.pdf", content: File.binread("invoice.pdf"), contentType: "application/pdf"}, {filename: "report.pdf", content: Pathname("report.pdf")}, {fileId: "file_6bb640f5b99e47deb758f1f5"}] client.emails.send( from: "[email protected]", to: "[email protected]", subject: "Your documents", text: "Both are attached.", attachments:)String с текстовой кодировкой, например та, что возвращает File.read, считается уже закодированной в base64, а строка, которая не является base64, выбрасывает ArgumentError ещё до отправки. Читайте файлы через File.binread или вызывайте .b на байтах, которые пришли с текстовой кодировкой.
OpenEmail.to_base64 пригодится, если такая же кодировка нужна где-то ещё. Он принимает двоичную String, IO или Pathname и возвращает строгий base64 без переводов строк.