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

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

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

emails.send

send-email.ts
const email = await openemail.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: pdfBytes }],  threadId: 'thread_…',  scheduledAt: 'PT1H',  tags: { order: '4021' },  tracking: { opens: true, clicks: true },})

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

Параметры

fromRecipientInputобязательно
Отправитель. Просто адрес, `Name <addr@host>` или объект. Должен быть одним из адресов, от которых может отправлять этот ключ. Запасного отправителя нет, потому что запасным вариантом был бы адрес рабочего пространства по умолчанию, а он меняется по мере того, как адреса появляются и исчезают.
toRecipientInput | RecipientInput[]обязательно
Один получатель или много; одиночный оборачивается за вас. Не более 50 суммарно по to, cc и bcc.
ccRecipientInput | RecipientInput[]
Засчитывается в лимит 50 получателей.
bccRecipientInput | RecipientInput[]
Никогда не называется в байтах, которые получает кто-либо ещё, потому что на каждого получателя передаётся свой конверт.
replyToRecipientInput
Один адрес, отправляемый как заголовок Reply-To.
subjectstring
Не длиннее 998 символов — предел строки по RFC 5322. По умолчанию пусто.
htmlstring
Требуется одно из html, text, draftId или template. HTML — это то, что видят получатели, когда заданы и html, и text.
textstring
Текстовая часть.
template{ id, version?, props?, slots? }
Отрисовать сохранённый шаблон на сервере. `version` закрепляет ревизию; опустите её, чтобы использовать то, что опубликовано в момент приёма запроса. Неизвестное или отсутствующее свойство — это 422, а не пустое место в сообщении.
draftIdstring
Отправить сохранённый черновик под этим конвертом.
headersRecord<string, string>
`X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority и Feedback-Id. Всё, что транспорт ставит сам, отклоняется, а не отбрасывается тихо.
attachmentsAttachmentInput[]
`{ filename, content, contentType? }` либо `{ fileId }`, называющий файл, уже находящийся в рабочем пространстве. Передайте байты в content, и они будут закодированы в base64 за вас. 20 файлов, причём встроенные ограничены 5 МБ в сумме после декодирования. Сохранённый файл может быть больше и уходит ссылкой на скачивание.
attachmentDeliveryAttachmentDeliveryMode
`mime`, `link` или `auto`. `auto` отправляет файлы ссылками на скачивание, когда они переваливают за 2 МБ на домене с активным доменом файлов, и внутри сообщения в остальных случаях. Если не указано, применяется настройка почтового ящика, а она по умолчанию `auto`.
threadIdstring
Ответить в существующую цепочку. Транспорт пишет In-Reply-To и References.
scheduledAtDate | string
Date, момент в ISO-8601 или длительность вроде `PT1H`. Не дальше года вперёд, никогда в прошлое. Нельзя сочетать с cancellableForSeconds.
cancellableForSecondsnumber
От 0 до 900. Окно отмены у немедленной отправки: механизм отмены из редактора, вынесенный наружу, а не зашитый.
tagsRecord<string, string>
До 10 меток, возвращаются обратно и доступны для фильтрации. Никогда не интерпретируются.
signatureboolean
Несёт ли это сообщение подпись адреса, с которого отправляется, — собственную подпись этого адреса либо ту, что задана для всех адресов. По умолчанию true, потому что подпись принадлежит адресу, а не тому клиенту, который отправил сообщение. Ставьте `false` для почты, которую программа отправляет от чьего-то имени: квитанции, сброса пароля или дайджеста — ни одному из них не нужна человеческая подпись внизу.
tracking{ opens?, clicks? }
Добавлять ли пиксель открытия и переписывать ли ссылки в этом сообщении. Включено, если владелец рабочего пространства не отключил трекинг для адреса, с которого идёт отправка, или для всех адресов, и любое указанное здесь поле решает судьбу одного сообщения независимо от настройки адреса.
translate{ to, from?, subject?, includeOriginal? }
Отправить на языке получателя. `to` принимает код, английское название или собственное название языка; `subject` и `includeOriginal` по умолчанию true. Разрешается при приёме запроса, так что запланированное сообщение несёт одобренные слова. Отклоняется вместе с `draftId`.

Ответ

idstring
Идентификатор отправки, `msg_…`. Используйте его для `get`, `cancel`, `reschedule` и `getTracking`.
statusEmailStatus
queued, scheduled, sending, sent, partial, cancelled или failed. Читайте это, а не сам факт, что промис разрешился. `partial` — самостоятельное состояние: у части получателей сообщение уже есть и его нельзя «разотправить», так что повтор будет ошибкой, а сообщение о неудаче — ложью.
mode'live' | 'test'
Каким видом ключа отправлено. Тестовая отправка записывается и никогда не передаётся.
fromstring
Адрес, который на самом деле был авторизован и поставлен на провод, а это не всегда тот, который запрашивали.
subjectstring | null
Как отправлено.
messageIdstring | null
Message-ID по RFC 5322. Null до появления MIME. Сервис отправки переписывает заголовок на выходе, так что ни один отбой или отчёт о доставке не несёт этого значения. С событием возвращается `id`.
threadIdstring | null
Цепочка, в которую оно попало.
transportstring | null
Как сообщение ушло. Null до отправки.
attemptsnumber
Сколько раз отправка была предпринята.
lastErrorstring | null
Почему последняя попытка не удалась, дословно.
scheduledAtstring | null
Момент в ISO, когда оно должно уйти.
cancellableUntilstring | null
Пока текущий момент раньше этого, отмена ещё работает.
sentAtstring | null
Момент в ISO, когда оно ушло.
tagsRecord<string, string>
То, что вы отправили, возвращённое обратно.
sourceEmailSource
composer, api, mcp, ai или queue: какая поверхность запросила. `api` — это данный клиент.
createdAtstring
Момент в ISO, когда была записана запись.
replayedboolean
True, когда Idempotency-Key совпал с уже существовавшей отправкой. Нового ничего не отправлено, и это исходное сообщение.
translationEmailTranslationResource | undefined
Присутствует только у переведённого сообщения и только там, где несётся весь сохранённый запрос: в этом ответе и в `get`. `{ language, languageName, detectedSourceLanguage, subject, includeOriginal }`, всё кодами, а не строками языков. В строке списка его не бывает никогда, так что его отсутствие там не говорит ничего ни в ту, ни в другую сторону.

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

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

translate.ts
const email = await openemail.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' },}) console.log(email.translation)// { language: 'de', languageName: 'German', detectedSourceLanguage: 'en', subject: true, includeOriginal: true }

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

preview-translation.ts
const preview = await openemail.emails.translate({  subject: 'Your September invoice',  html: '<p>Invoice attached. Payment is due on the 14th.</p>',  to: 'de',}) console.log(preview.language.native, preview.detectedSourceLanguage) const approved = await showToSomebody(preview) await openemail.emails.send({  from: '[email protected]',  to: '[email protected]',  subject: approved.subject,  html: approved.html,})
render-picker.ts
import { LANGUAGES, isRtlLanguage, languageByCode, openemail, resolveLanguage } from '@openemail/sdk' LANGUAGES.length // 200 const current = await openemail.languages.list() resolveLanguage('Deutsch')?.code // 'de'resolveLanguage('zh-TW')?.code // 'zh-Hant'languageByCode('DE')?.native // 'Deutsch'isRtlLanguage('ar') // true

Таблица входит в пакет, в порядке для списка выбора, так что список можно заполнить ещё до первого запроса. languages.list() разрешается в те же строки с провода, что и обычный массив, — для того, кто предпочитает текущие языки тем, с которыми вышла эта версия. resolveLanguage принимает код, английское название, эндоним или псевдоним (zh-TW — псевдоним кода, которого больше нет в списке), languageByCode сопоставляет точный код без учёта регистра, а шестнадцать строк — справа налево. Ищите по native, label и code вместе, показывайте native первым и храните код.

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

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

Вложения

content на проводе в base64. Передайте байты, и они будут закодированы за вас.

attachment.ts
attachments: [  { filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' },]

toBase64 экспортируется, если он понадобится вам в другом месте. Он работает по частям, чего btoa(String.fromCharCode(...bytes)) не делает. Тот падает на всём, что больше примерно 100 кБ, и падает на настоящем файле, а не на том, на котором вы тестировали.