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

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

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

emails.send

send_email.rb
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 пишет сообщение на чужом языке перед его уходом. Тело, а также тема, если вы это не отключите, переводятся в момент приёма запроса, и что получилось, то и уходит: перевод, который не удалось получить, отклоняет отправку, а не отправляет письмо на том языке, на котором вы его написали.

translate.rb
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_translation.rb
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]  )end
languages.rb
p 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, когда в установке не настроен ИИ, 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. Передайте байты, и они будут закодированы за вас: двоичную String, например ту, что возвращает File.binread, IO, например открытый File, или Pathname, который будет прочитан за вас.

attachments.rb
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 без переводов строк.