Ir a la documentación
SDK

Enviar un correo

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

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 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 objeto. Debe ser una con la que esta clave pueda enviar. No hay remitente de reserva, porque la reserva sería la dirección predeterminada del espacio de trabajo, que cambia según entran y salen direcciones.
toRecipientInput | 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 | RecipientInput[]
Cuenta para el límite de 50 destinatarios.
bccRecipientInput | 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.
subjectstring
Como máximo 998 caracteres, el límite de línea de RFC 5322. Por defecto, vacío.
htmlstring
Se requiere uno de html, text, draftId o template. El HTML es lo que ven los destinatarios cuando se dan tanto html como text.
textstring
La parte de texto plano.
template{ id, version?, props?, slots? }
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.
draftIdstring
Envía un borrador guardado bajo este sobre.
headersRecord<string, string>
`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.
attachmentsAttachmentInput[]
`{ filename, content, contentType? }`, o `{ fileId }` nombrando un archivo que ya está en el espacio de trabajo. Pasa bytes como contenido y se codifican en base64 por ti. 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`.
threadIdstring
Responde dentro de un hilo existente. El transporte escribe In-Reply-To y References.
scheduledAtDate | string
Un Date, 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.
cancellableForSecondsnumber
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.
tagsRecord<string, string>
Hasta 10 etiquetas, devueltas tal cual y filtrables. Nunca se interpretan.
signatureboolean
Si este mensaje lleva la firma de la dirección desde la que se envía, que es la firma propia de esa dirección o, en su defecto, la configurada para Todas las direcciones. Por defecto es true, porque una firma pertenece a la dirección y no al cliente que envió el mensaje. Ponlo en `false` para 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 rúbrica de una persona debajo.
tracking{ opens?, clicks? }
Si añadir un píxel de apertura y reescribir los enlaces de este mensaje. Está activado salvo que el propietario del espacio de trabajo haya desactivado el seguimiento para la dirección desde la que se envía o para Todas las direcciones, y cualquiera de los dos campos declarado aquí resuelve ese mensaje concreto sea cual sea la configuración de la dirección.
translate{ to, from?, subject?, includeOriginal? }
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

idstring
El id de envío, `msg_…`. Úsalo para `get`, `cancel`, `reschedule` y `getTracking`.
statusEmailStatus
queued, scheduled, sending, sent, partial, cancelled o failed. Lee esto en lugar del hecho de que la promesa se resolviera. `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.
mode'live' | 'test'
Qué tipo de clave lo envió. Un envío de prueba se registra y nunca se transmite.
fromstring
La dirección realmente autorizada y puesta en la red, que no siempre es la que se pidió.
subjectstring | null
Tal como se envió.
messageIdstring | null
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.
threadIdstring | null
El hilo en el que aterrizó.
transportstring | null
Cómo salió el mensaje. Null hasta el despacho.
attemptsnumber
Cuántas veces se ha intentado el despacho.
lastErrorstring | null
Por qué falló el último intento, literalmente.
scheduledAtstring | null
Instante ISO en que está previsto que salga.
cancellableUntilstring | null
Mientras el momento actual sea anterior a este, la cancelación sigue funcionando.
sentAtstring | null
Instante ISO en que salió.
tagsRecord<string, string>
Lo que enviaste, devuelto tal cual.
sourceEmailSource
composer, api, mcp, ai o queue: qué superficie lo solicitó. `api` es este cliente.
createdAtstring
Instante ISO en que se escribió el registro.
replayedboolean
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.
translationEmailTranslationResource | undefined
Presente solo en un mensaje que se tradujo, y solo donde se lleva la solicitud almacenada completa: esta respuesta y `get`. `{ language, languageName, detectedSourceLanguage, subject, 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.

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.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 }

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.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

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() se resuelve en las mismas filas obtenidas de la red que un array simple, para quien prefiera las actuales en vez de las que trajo esta versión. resolveLanguage 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), languageByCode 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 y 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.ts
attachments: [  { filename: 'invoice.pdf', content: pdfBytes, contentType: 'application/pdf' },]

toBase64 se exporta por si lo necesitas en otro sitio. Trocea, cosa que btoa(String.fromCharCode(...bytes)) no hace. Ese falla con cualquier cosa que pase de unos 100 kB, y falla con el archivo real en lugar de con el que usaste para probar.