Ir a la documentación
Python

Enviar un correo

`emails.send`: un mensaje, ahora o más tarde.

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 y bcc aceptan uno o varios destinatarios, y si es uno solo se envuelve por ti. Cada uno puede ser una dirección escueta, Name <addr@host> o {'email': ..., 'name': ...}.

Parámetros

fromRecipientInputobligatorio
El remitente. Una dirección escueta, `Name <addr@host>` o un diccionario. Debe ser una con la que esta clave pueda enviar. No hay remitente de reserva, así que un envío siempre nombra la dirección desde la que sale.
toRecipientInput | list[RecipientInput]obligatorio
Uno o varios destinatarios; si es uno solo, se envuelve por ti. Como máximo 50 entre to, cc y bcc en conjunto.
ccRecipientInput | list[RecipientInput]
Cuenta para el límite de 50 destinatarios.
bccRecipientInput | list[RecipientInput]
Nunca se nombra en los bytes que recibe nadie más, porque se transmite un sobre por destinatario.
replyToRecipientInput
Una única dirección, enviada como la cabecera Reply-To.
subjectstr
Como máximo 998 caracteres, el límite de línea de RFC 5322. Por defecto, vacío.
htmlstr
Se requiere uno de html, text, draftId o template. El HTML es lo que ven los destinatarios cuando se dan tanto html como text.
textstr
La parte de texto plano.
templateEmailSendTemplate
Renderiza una plantilla almacenada en el servidor. `version` la fija; omítelo para usar lo que esté publicado cuando se acepte la solicitud. Una prop desconocida o ausente es un 422 y no un hueco en blanco en el mensaje.
draftIdstr
Envía un borrador guardado bajo este sobre.
headersdict[str, str]
`X-*`, `List-*`, Reply-To, Precedence, Auto-Submitted, Importance, Priority y Feedback-Id. Todo lo que el transporte establece por sí mismo se rechaza en lugar de descartarse en silencio.
attachmentslist[AttachmentInput]
`{'filename': ..., 'content': ...}` con un `'contentType'` opcional, o `{'fileId': ...}` nombrando un archivo que ya está en el espacio de trabajo, como uno de `files.upload`. Pasa bytes como contenido y se codifican en base64 automáticamente. 20 archivos, con los archivos en línea limitados a 5 MB en total una vez descodificados. Un archivo almacenado puede ser mayor y viaja como enlace de descarga.
attachmentDeliveryAttachmentDeliveryMode
`mime`, `link` o `auto`. `auto` lleva los archivos como enlaces de descarga en cuanto superan 2 MB en un dominio con un dominio de archivos activo, y dentro del mensaje en caso contrario. Si se omite, se aplica la configuración del buzón, que por defecto es `auto`.
threadIdstr
Responde dentro de un hilo existente. El transporte escribe In-Reply-To y References.
scheduledAtdatetime | str
Un `datetime`, un instante ISO-8601 o una duración como `PT1H`. Hasta un año por delante, nunca en el pasado. No se puede combinar con cancellableForSeconds.
cancellableForSecondsint
De 0 a 900. Una ventana de deshacer en un envío inmediato: el mecanismo de deshacer del redactor, expuesto en lugar de fijado en el código.
tagsdict[str, str]
Hasta 10 etiquetas, devueltas tal cual y filtrables. Nunca se interpretan.
signaturebool
Si este mensaje lleva la firma de la dirección desde la que se envía: la suya, si no la del catch-all para una dirección que captó un catch-all, y si no el pie de OpenEmail, salvo que esa dirección lo haya desactivado. Si se omite, un cuerpo `html` sale exactamente como está escrito, sin firma, y un cuerpo solo `text` la lleva. Pon `False` en el correo que un programa envía en nombre de alguien, como un recibo, un restablecimiento de contraseña o un resumen, ninguno de los cuales quiere la firma de una persona debajo.
trackingTrackingRequest
Si añadir un píxel de apertura y reescribir los enlaces de este mensaje. Está desactivado salvo que se haya activado el seguimiento para la dirección desde la que se envía (o para el catch-all que la recogió), y cualquiera de los dos campos declarado aquí resuelve ese mensaje concreto sea cual sea la configuración de la dirección.
translateSendTranslateOptions
Envíalo en el idioma del destinatario. `to` acepta un código, un nombre en inglés o el nombre propio del idioma; `subject` e `includeOriginal` tienen ambos true por defecto. Se resuelve cuando se acepta la solicitud, así que un mensaje programado lleva las palabras que se aprobaron. Se rechaza junto con `draftId`.

Respuesta

idstr
El id de envío, `msg_…`. Úsalo para `get`, `cancel`, `reschedule` y `get_tracking`.
statusEmailStatus
queued, scheduled, sending, sent, partial, bounced, cancelled o failed. Lee este campo en lugar de fiarte de que la llamada haya terminado. `partial` es un estado propio: algunos destinatarios ya lo tienen y no se les puede quitar el envío, así que reintentar es un error e informar de un fallo es mentir.
modeApiKeyMode
Qué tipo de clave lo envió. Un envío de prueba se registra y nunca se transmite.
fromstr
La dirección realmente autorizada y puesta en la red, que no siempre es la que se pidió.
subjectstr | None
Tal como se envió.
messageIdstr | None
El Message-ID de RFC 5322. Null hasta que existe el MIME. El servicio de envío reescribe la cabecera a la salida, así que ningún rebote ni informe de entrega lleva este valor. `id` es aquello con lo que vuelve un evento.
threadIdstr | None
El hilo en el que aterrizó.
transportEmailTransport | str | None
Cómo salió el mensaje. Null hasta el despacho.
attemptsint
Cuántas veces se ha intentado el despacho.
lastErrorstr | None
Por qué falló el último intento, literalmente.
scheduledAtstr | None
Instante ISO en que está previsto que salga.
cancellableUntilstr | None
Mientras el momento actual sea anterior a este, la cancelación sigue funcionando.
sentAtstr | None
Instante ISO en que salió.
tagsdict[str, str]
Lo que enviaste, devuelto tal cual.
sourceEmailSource | str
composer, api, mcp, ai, oauth o form: qué superficie lo solicitó. `api` es este cliente con una clave de API, y `oauth` es este cliente con un token de acceso.
createdAtstr
Instante ISO en que se escribió el registro.
replayedbool
True cuando un Idempotency-Key coincidió con un envío que ya existía. No se envió nada nuevo, y este es el mensaje original.
translationNotRequired[EmailTranslationResource]
Presente solo en un mensaje que se tradujo, y solo donde se lleva la solicitud almacenada completa: esta respuesta y `get`. Un diccionario con `language`, `languageName`, `detectedSourceLanguage`, `subject` e `includeOriginal`, todos códigos en lugar de filas de idioma. Una fila de lista nunca lo tiene, así que su ausencia allí no dice nada en ningún sentido. Léelo con `email.get('translation')`.

En el idioma del destinatario

translate escribe el mensaje en el idioma de otra persona antes de que salga. El cuerpo, y el asunto salvo que lo desactives, se traducen cuando la API acepta la solicitud, y lo que salió de ahí es lo que sale: una traducción que no se pudo producir rechaza el envío en lugar de mandarlo en el idioma en que lo escribiste.

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'))

Nadie leyó eso antes de que saliera. emails.translate es el mismo viaje de ida y vuelta detenido un paso antes. Muéstraselo a una persona, deja que lo cambie y luego envía lo que aprobó sin translate en la llamada en absoluto. Pasarlo otra vez traduciría por segunda vez y tiraría sus ediciones a la basura.

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')

La tabla viene incluida en el paquete, en el orden del selector, así que se puede rellenar un selector antes de la primera solicitud. languages.list() devuelve las mismas filas obtenidas de la red como una lista simple, para quien prefiera las actuales en vez de las que trajo esta versión. resolve_language acepta un código, un nombre en inglés, un endónimo o un alias (zh-TW es un alias de un código que ya no se lista), language_by_code busca un código exacto sin distinguir mayúsculas, y dieciséis de las filas son de derecha a izquierda. Busca en native, label y code a la vez, muestra native primero y almacena el código.

emails.translate no se reintenta automáticamente. Consume llamadas al modelo y no escribe nada, así que no hay nada que hacer idempotente y un reintento tras una solicitud sin respuesta solo pagaría la misma respuesta dos veces.

  • Un idioma que no se resuelve en nada es un validation_error en translate.to, antes de enviar nada.
  • translation_too_long por encima de 30.000 caracteres, translation_not_configured cuando la instalación no tiene IA configurada, un 429 ai_quota_exceeded cuando el espacio de trabajo ha usado las acciones de IA de hoy (se restablece a medianoche UTC y no se reintenta), translation_failed cuando el proveedor no respondió. Ninguno de ellos envía el mensaje sin traducir como alternativa.
  • Funciona con template: lo que se traduce es la salida RENDERIZADA, así que un único cuerpo almacenado sirve para todos los idiomas en los que leen tus clientes. Una plantilla que renderiza un documento completo conserva su doctype, sus bloques <style> y sus reglas @font-face: solo el cuerpo va al modelo y el resto se vuelve a colocar alrededor. Su <title> se deja intacto, y de todos modos no lo muestra nada.
  • Un reintento no cuesta nada más. La traducción no forma parte de la huella de idempotencia (la solicitud sí, incluido translate), así que reintentar un envío sin respuesta con el mismo Idempotency-Key reproduce el mensaje que ya existe en lugar de traducir y enviar un segundo.
  • Un mensaje traducido que está en cola o programado queda congelado frente a cambios de redacción. emails.reschedule sigue pudiendo moverlo; cambiar lo que dice implica cancelarlo y enviarlo de nuevo.

Adjuntos

content viaja en base64 por la red. Pasa bytes y se codifican por ti.

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 se exporta por si lo necesitas en otro sitio. Un str en content se envía tal cual, así que ya tiene que estar en base64.

Referencia