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