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

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

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

emails.send

send_email.py
from openemail import openemail email = 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': pdf_bytes}],    '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 | list[RecipientInput]обязательно
Один получатель или много; одиночный оборачивается за вас. Не более 50 суммарно по to, cc и bcc.
ccRecipientInput | list[RecipientInput]
Засчитывается в лимит 50 получателей.
bccRecipientInput | list[RecipientInput]
Никогда не называется в байтах, которые получает кто-либо ещё, потому что на каждого получателя передаётся свой конверт.
replyToRecipientInput
Один адрес, отправляемый как заголовок Reply-To.
subjectstr
Не длиннее 998 символов (предел строки по RFC 5322). По умолчанию пусто.
htmlstr
Требуется одно из html, text, draftId или template. Когда заданы и html, и text, получатели видят HTML.
textstr
Текстовая часть.
templateEmailSendTemplate
Отрисовать сохранённый шаблон на сервере. `version` закрепляет ревизию; опустите её, чтобы использовать то, что опубликовано в момент приёма запроса. Неизвестное или отсутствующее свойство даёт 422, а не пустое место в сообщении.
draftIdstr
Отправить сохранённый черновик под этим конвертом.
headersdict[str, str]
`X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority и Feedback-Id. Всё, что транспорт ставит сам, отклоняется, а не отбрасывается тихо.
attachmentslist[AttachmentInput]
`{'filename': ..., 'content': ...}` с необязательным `'contentType'` либо `{'fileId': ...}`, называющий файл, уже находящийся в рабочем пространстве, например загруженный через `files.upload`. Передайте байты в content, и они будут закодированы в base64 за вас. 20 файлов, причём встроенные ограничены 5 МБ в сумме после декодирования. Сохранённый файл может быть больше и уходит ссылкой на скачивание.
attachmentDeliveryAttachmentDeliveryMode
`mime`, `link` или `auto`. `auto` отправляет файлы ссылками на скачивание, когда они переваливают за 2 МБ на домене с активным доменом файлов, и внутри сообщения в остальных случаях. Если не указано, применяется настройка почтового ящика, а она по умолчанию `auto`.
threadIdstr
Ответить в существующую цепочку. Транспорт пишет In-Reply-To и References.
scheduledAtdatetime | str
`datetime`, момент в ISO-8601 или длительность вроде `PT1H`. Не дальше года вперёд, никогда в прошлом. Нельзя сочетать с cancellableForSeconds.
cancellableForSecondsint
От 0 до 900. Окно отмены у немедленной отправки: механизм отмены из редактора, вынесенный наружу, а не зашитый.
tagsdict[str, str]
До 10 меток, возвращаются обратно и доступны для фильтрации. Никогда не интерпретируются.
signaturebool
Несёт ли это письмо подпись адреса отправителя: его собственную, иначе подпись catch-all для адреса, принятого catch-all, иначе нижнюю строку OpenEmail, если этот адрес её не отключил. Если поле не указано, тело `html` уходит ровно в том виде, в каком написано, без подписи, а тело только `text` её несёт. Укажите `False` для писем, которые программа отправляет от чьего-то имени, например квитанции, сброса пароля или сводки: ни под одним из них не нужна подпись человека.
trackingTrackingRequest
Добавлять ли пиксель открытия и переписывать ли ссылки в этом сообщении. Выключено, если трекинг не включили для адреса, с которого идёт отправка (или для catch-all, который его поймал), и любое указанное здесь поле решает судьбу одного сообщения независимо от настройки адреса.
translateSendTranslateOptions
Отправить на языке получателя. `to` принимает код, английское название или собственное название языка; `subject` и `includeOriginal` по умолчанию true. Разрешается при приёме запроса, так что запланированное сообщение несёт одобренные слова. Отклоняется вместе с `draftId`.

Ответ

idstr
Идентификатор отправки, `msg_…`. Используйте его для `get`, `cancel`, `reschedule` и `get_tracking`.
statusEmailStatus
queued, scheduled, sending, sent, partial, bounced, cancelled или failed. Читайте это, а не сам факт, что вызов вернулся. `partial` является самостоятельным состоянием: у части получателей сообщение уже есть, и его нельзя «разотправить», так что повтор будет ошибкой, а сообщение о неудаче будет ложью.
modeApiKeyMode
Каким видом ключа отправлено. Тестовая отправка записывается и никогда не передаётся.
fromstr
Адрес, который на самом деле был авторизован и поставлен на провод, а это не всегда тот, который запрашивали.
subjectstr | None
Как отправлено.
messageIdstr | None
Message-ID по RFC 5322. Null до появления MIME. Сервис отправки переписывает заголовок на выходе, так что ни один отбой или отчёт о доставке не несёт этого значения. С событием возвращается `id`.
threadIdstr | None
Цепочка, в которую оно попало.
transportEmailTransport | str | None
Как сообщение ушло. Null до отправки.
attemptsint
Сколько раз отправка была предпринята.
lastErrorstr | None
Почему последняя попытка не удалась, дословно.
scheduledAtstr | None
Момент в ISO, когда оно должно уйти.
cancellableUntilstr | None
Пока текущий момент раньше этого, отмена ещё работает.
sentAtstr | None
Момент в ISO, когда оно ушло.
tagsdict[str, str]
То, что вы отправили, возвращённое обратно.
sourceEmailSource | str
composer, api, mcp, ai, oauth или form: какая поверхность запросила. `api` означает этот клиент с API-ключом, а `oauth` означает этот клиент с токеном доступа.
createdAtstr
Момент в ISO, когда была записана запись.
replayedbool
True, когда Idempotency-Key совпал с уже существовавшей отправкой. Нового ничего не отправлено, и это исходное сообщение.
translationNotRequired[EmailTranslationResource]
Присутствует только у переведённого сообщения и только там, где передаётся весь сохранённый запрос: в этом ответе и в `get`. Словарь из `language`, `languageName`, `detectedSourceLanguage`, `subject` и `includeOriginal`, всё кодами, а не строками языков. В строке списка его не бывает никогда, так что его отсутствие там ничего не говорит. Читайте его через `email.get('translation')`.

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

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

translate.py
from openemail import openemail email = 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'},}) print(email.get('translation'))

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

preview_translation.py
from openemail import openemail preview = openemail.emails.translate({    'subject': 'Your September invoice',    'html': '<p>Invoice attached. Payment is due on the 14th.</p>',    'to': 'de',}) print(preview['language']['native'], preview['detectedSourceLanguage'])print(preview['html']) approved_subject = input(f"Subject [{preview['subject']}]: ") or preview['subject'] or '' openemail.emails.send({    'from': '[email protected]',    'to': '[email protected]',    'subject': approved_subject,    'html': preview['html'] or '',})
render_picker.py
from openemail import LANGUAGES, is_rtl_language, language_by_code, openemail, resolve_language current = openemail.languages.list() german = resolve_language('Deutsch')traditional = resolve_language('zh-TW')upper = language_by_code('DE') assert len(LANGUAGES) == 200assert german is not None and german['code'] == 'de'assert traditional is not None and traditional['code'] == 'zh-Hant'assert upper is not None and upper['native'] == 'Deutsch'assert is_rtl_language('ar')

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

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

  • Язык, который ни во что не разрешается, даёт 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 воспроизводит уже существующее сообщение, а не переводит и отправляет второе.
  • Переведённое сообщение в состоянии queued или scheduled заморожено против изменений формулировок. emails.reschedule всё ещё может его перенести; изменить сказанное означает отменить и отправить заново.

Вложения

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

attachment.py
from pathlib import Path from openemail.types import AttachmentInput attachments: list[AttachmentInput] = [    {        'filename': 'invoice.pdf',        'content': Path('invoice.pdf').read_bytes(),        'contentType': 'application/pdf',    },]

to_base64 экспортируется, если он понадобится вам где-то ещё. Значение типа str в content отправляется как есть, поэтому оно уже должно быть в base64.

Справочник