Envía el correo de tu propia app
Un correo transaccional es el que tu app le debe a una persona: un recibo, un enlace de acceso, un restablecimiento de contraseña. Sale de un buzón reservado para eso, o de una dirección de remitente que envía por nuestros servidores de correo, exactamente como lo escribiste. Empieza leyendo lo que tu app ya envió. Esto no envía nada.
import { Colding } from "colding";
const colding = new Colding("cd_your_key");
const page = await colding.sends.list({ kind: "transactional" });
for (const send of page.items) console.log(send.to, send.status);Cambia la clave por la tuya. kind: "transactional" deja fuera todos los demás tipos de correo. La lista está vacía hasta que tu app envíe el primero.
[email protected] sent
Qué necesitas antes
Usa una clave de la primera guía. Cada permiso cubre un tipo de acción, y uno no incluye al otro.
- El permiso
transactionalen la clave. Una clave solo consendno envía estos correos, y una clave solo contransactionalno envía campañas. Para seguir un correo después, la clave también necesitaread. - De dónde sale: un buzón reservado para los correos de tu app, con
purpose: "transactional"(la siguiente sección muestra cómo definirlo), o una dirección de remitente (la sección Direcciones de remitente). - Un plan con correo transaccional. El plan gratuito no lo tiene, y la llamada responde 402
PLAN_GATE. - El dueño del workspace debe haber aceptado la política de uso aceptable. Hasta entonces, el envío responde 403
AUP_REQUIRED. - Una dirección confirmada desde la que enviar. Que los datos de acceso funcionen prueba la contraseña, no que la dirección sea tuya. Por eso un dueño confirma la dirección de un buzón en la consola. Hasta entonces, la llamada responde 409. Una dirección de remitente no lo necesita: los registros de su dominio ya la prueban.
Mira cómo se conectan los buzones
Un plan de pago deja que un buzón envíe de 200 a 1.000 de estos correos al día, según el plan. La cuenta empieza de nuevo a medianoche, en la zona horaria del workspace.
Reserva un buzón para esto
Todo buzón tiene un solo propósito a la vez, el purpose: cold para campañas y mensajes, o transactional para los correos de tu app. Un buzón transaccional no envía campañas, y el otro no envía los correos de tu app. Si intentas enviar el tipo equivocado desde un buzón, la llamada responde 409.
Define el propósito con mailboxes.update. La clave necesita operate y transactional, y el plan debe incluir correo transaccional:
const mailbox = await colding.mailboxes.update({
mailboxId,
purpose: "transactional",
});
console.log(mailbox.purpose);Imprime transactional. Volver a cold es la misma llamada, y también pide transactional. Un buzón transaccional sigue recibiendo correo, y puedes seguir respondiendo desde él.
Un buzón desde el que salen campañas, o que tiene correos programados, no puede pasar a transaccional. La llamada responde 409, y details.campaigns lista esas campañas con ID y nombre. Quita el buzón de ellas, deja salir sus correos programados o cancélalos, y llama de nuevo.
Un buzón solo de envío no tiene bandeja de entrada. Son los datos de acceso SMTP de un servicio de envío, y siempre es transaccional. Un dueño lo conecta en la consola. Un agente pide esa conexión con un handoff.
Mira cómo se conecta un buzón solo de envío
Pon los correos de tu app en su propio subdominio, como mail.example.com, aparte del dominio de tus campañas. Así es menos probable que los rebotes y las denuncias de spam de una campaña lleguen a tus recibos. La consola lo sugiere cuando los dos tipos comparten un dominio. Es un consejo, no una regla.
Direcciones de remitente
Las direcciones de remitente todavía no están en ningún plan a la venta. Hasta que nuestro equipo active el envío desde nuestros servidores de correo para tu workspace, crear una responde 402 PLAN_GATE con el messageKey serverErrors.hostedTransactionalClosed. Lo activamos para una empresa a la vez: escribe a [email protected].
Una dirección de remitente es la dirección From del correo de tu app, como [email protected]. No tiene contraseña ni inicio de sesión, y está en un dominio de correo tuyo preparado para correo transaccional. No recibe correo.
Una clave no puede crearlas. El dueño las crea en la consola, en Dominios, cuando el dominio está Activo. Un agente se lo pide a un dueño con un handoff manage_sender_addresses. Detrás de la consola hay tres operaciones que solo el dueño puede llamar: create_sender_address, update_sender_address y delete_sender_address. Una clave recibe 403 en ellas.
Toda dirección de remitente tiene un replyTo, la dirección a la que van las respuestas. Nosotros no recibimos el correo del dominio, así que una respuesta a la propia dirección de remitente no llegaría a nadie. Indica una dirección que alguien lea, como la de tu soporte. No puede ser la propia dirección de remitente.
Una clave lee direcciones de remitente con senderAddresses.list y senderAddresses.get. Cambia la clave por la tuya.
const page = await colding.senderAddresses.list();
for (const sender of page.items) console.log(sender.id, sender.address, sender.replyTo);Cada una tiene id, address, domainId, displayName, replyTo, createdAt y createdBy. El id es el mailboxId que nombra un envío. Acota la lista con domainId. La lista va paginada: devuelve nextCursor como cursor. Una dirección de remitente eliminada no aparece en la lista, y pedirla responde 404.
Para pedir una, crea un handoff, entrega el enlace al dueño y lee el resultado. Cuando el dueño crea la dirección, el handoff queda completed y resultMailboxId es el ID de la nueva dirección de remitente.
const request = await colding.handoffs.create({
kind: "manage_sender_addresses",
note: "Add [email protected]. Replies go to [email protected].",
});
console.log(request.url);
// Later, after the owner has opened the link:
const done = await colding.handoffs.get({ handoffId: request.id });
if (done.resultMailboxId) {
const sender = await colding.senderAddresses.get({ senderAddressId: done.resultMailboxId });
console.log(sender.address, sender.replyTo);
}Una dirección desde la que tu app ya envía, como buzón conectado o como buzón solo de envío, no puede ser también una dirección de remitente. Para cambiarla, desconecta primero el buzón. Eso elimina los mensajes guardados aquí para él (el correo en tu proveedor se queda), y la dirección de remitente recibe un ID nuevo, que tu app debe usar en los envíos. Hasta que cambies el ID en la configuración de tu app, un envío que nombre el ID anterior responde 404.
Para enviar desde una dirección de remitente, pasa su id como mailboxId.
Envía un correo
transactional.send envía un correo a una persona, desde un buzón o una dirección de remitente:
const send = await colding.transactional.send({
mailboxId: "66666666-6666-4666-8666-666666666666",
to: "[email protected]",
subject: "Your sign-in link",
bodyText: "Open this link to sign in: https://app.example.com/in/abc123",
intentKey: "sign-in-link-user-42-2030-01-15",
expiresAt: "2030-01-15T10:15:00Z",
});
console.log(send.id, send.status);La llamada devuelve el correo con status reserved, que aparece como A punto de salir en la consola. Ya cuenta entre los envíos de hoy, pero todavía no salió. El worker, nuestro proceso de envío en segundo plano, se encarga de enviarlo.
Envía bodyHtml, bodyText o los dos. El correo sale exactamente como lo escribiste: sin firma, sin línea para darse de baja, sin seguimiento de aperturas ni de clics, sin espera entre correos y sin ventana para deshacer. toName define el nombre en la línea Para, y replyTo dice adónde van las respuestas.
Define expiresAt para un código o enlace que deja de servir, con fecha, hora y desfase horario. Si el correo no empezó a salir para entonces, se cancela con el errorCode expired y nunca se envía. Una hora que ya pasó responde 400.
Puedes cancelar un correo que aún no empezó a salir con sends.cancel. Pide el permiso transactional. Una vez que el correo empezó a salir, ya no se puede cancelar, y la llamada responde 409.
Ponle nombre a cada correo
El intentKey es obligatorio. Es tu nombre para un correo, de hasta 255 caracteres, como receipt-order-1042. Usa algo que tu app ya tenga, como el número del pedido.
Envía la misma petición otra vez y recibes el mismo correo de vuelta. No se envía nada más. Esto vale para siempre, no solo durante 24 horas.
const request = {
mailboxId: "66666666-6666-4666-8666-666666666666",
to: "[email protected]",
subject: "Receipt for order 1042",
bodyText: "Thanks for your order. The receipt is in your account.",
intentKey: "receipt-order-1042",
};
const first = await colding.transactional.send(request);
const again = await colding.transactional.send(request);
console.log(first.id === again.id);true
La petición es el buzón, la dirección, el nombre, el asunto, los dos cuerpos, el expiresAt y el replyTo. Arma la petición una vez y envía esa misma copia en cada reintento. Guarda el intentKey antes de enviar, para que una caída no lo pierda.
El mismo intentKey con otra petición se rechaza con 422 IDEMPOTENCY_KEY_REUSED, y no se envía nada. Para enviar un segundo correo a propósito, como un recibo otra vez, usa un intentKey nuevo.
Un reintento se responde antes de comprobar el plan, el pago y el expiresAt. Así que un reintento que llega tarde sigue recibiendo su correo, aunque el pago haya fallado mientras tanto.
Los nombres que empiezan con draft:, warmup: o campaign: son nuestros y se rechazan.
Desde nuestros servidores
Pasa el id de una dirección de remitente como mailboxId. Nada más cambia en la llamada: el mismo intentKey, el mismo cuerpo, el mismo expiresAt. El correo sale de nuestros servidores de correo, así que tu app no necesita un buzón propio para enviarlo.
const send = await colding.transactional.send({
mailboxId: "77777777-7777-4777-8777-777777777777",
to: "[email protected]",
subject: "Your sign-in link",
bodyText: "Open this link to sign in: https://app.example.com/in/abc123",
intentKey: "sign-in-link-user-42-2030-01-16",
});
console.log(send.status, send.viaOurServers);reserved true
La llamada responde como cualquier envío, con status reserved. viaOurServers es true cuando el correo sale por nuestros servidores y false cuando sale por un buzón que conectaste.
Aquí, sent significa que nuestro servidor de correo recibió el correo y lo está pasando al servidor de la persona. No significa que llegó a la bandeja de entrada. Cuando el servidor de la persona responde, send.delivered avisa de que lo aceptó, o send.bounced avisa de que lo rechazó para siempre. El rebote llega después de sent, y el estado sigue siendo sent.
Hay dos límites. expiresAt solo detiene un correo que nuestro servidor todavía no recibió. Una vez que lo recibe, sigue intentando hasta que el servidor de la persona responda, así que un código puede llegar después de vencido. Y, en casos raros, el servidor de la persona acepta el correo pero su respuesta se pierde por el camino, y nuestro servidor lo envía otra vez: la persona lo recibe dos veces.
Cuando nuestros servidores dicen que no
Una dirección de remitente puede rechazarse por más motivos que un buzón. Ninguno reserva nada, así que la misma petición puede enviarse de nuevo. resetAt dice cuándo.
402 hostedTransactionalClosed- Tu workspace todavía no está habilitado para enviar correo desde nuestros servidores. Nuestro equipo lo habilita para una empresa a la vez: escribe a [email protected].
429 hostedTransactionalDay- Tu workspace ya envió hoy todos los correos que puede enviar desde nuestros servidores. Ese número es solo de tu workspace. Espera hasta
resetAt, la medianoche en la zona horaria del workspace. 429 domainRamp- Un dominio nuevo en nuestros servidores empieza con pocos correos al día y puede enviar un poco más cada día que envía.
details.limites el número de hoy. Vuelve a empezar enresetAt, a la medianoche UTC. 429 ipCeiling429 ipShare- Estos son límites de nuestros servidores, no de tu plan. Otras empresas envían por los mismos servidores, y cada una tiene un espacio reservado para sus propios correos. Vuelven a empezar en
resetAt, a la medianoche UTC. 429 receiverPace- El proveedor de correo de la persona pidió a nuestro servidor que fuera más despacio, o nuestro equipo detuvo por un tiempo el correo hacia ese proveedor.
details.receiverdice cuál. Vuelve a enviar después deresetAt. 409 senderIpNotReady409 mailIpHeld- El problema es de nuestro lado: todavía no hay un servidor nuestro habilitado para correo de apps, o el servidor por el que sale tu correo está en espera. No se envió nada. Envía la misma petición de nuevo más tarde.
409 mailDomainNotSigning- Los registros DNS de tu dominio no pasaron nuestra comprobación, así que no podemos firmar su correo. No sale nada por él hasta que los registros vuelvan a pasar. Corrígelos en la consola, en Dominios.
409 senderDomainUnderReview- Nuestro equipo puso en espera el dominio de la dirección de remitente porque demasiados correos suyos rebotaron o se marcaron como spam. No hay
resetAt: no sale nada por el dominio hasta que nuestro equipo lo active de nuevo.details.caseNumberes el número que debes citar. Más abajo en esta página verás cuándo ponemos un dominio en espera y cómo termina la espera.
El número diario de tu workspace vuelve a empezar a la medianoche en la zona horaria del workspace. El límite diario de un dominio nuevo y los límites de nuestros servidores vuelven a empezar a la medianoche UTC.
Con domainRamp, ipCeiling o ipShare, si tu app ya tiene otra dirección confirmada y conectada para correo transaccional, envía el mismo contenido desde ella, con su propio intentKey. El destinatario verá esa otra dirección como remitente. Si no la tienes, vuelve a enviar la misma petición después de resetAt. Con hostedTransactionalDay, espera hasta resetAt: ese número es solo de tu workspace.
Configura otra forma de enviar antes de necesitarla
Todavía no se pueden adjuntar archivos a un envío.
Configura otra forma de enviar antes de necesitarla
- Antes de necesitarla, conecta otra dirección para correo transaccional: un buzón reservado para eso o el acceso de tu servicio de envío. Confirma la dirección del remitente. No puede ser la dirección de remitente que usas desde nuestros servidores.
- Guarda su
mailboxIden la configuración de tu app como segundo remitente. - Úsala solo después de un 429 que no reservó nada, con
details.scopeigual adomainRamp,ipCeilingoipShare. Envía el mismo contenido desde esemailboxId, con su propiointentKey. Se rechaza un correo distinto con la misma clave. Si se acepta este envío, no lo vuelvas a intentar por la primera vía. Nunca envíes una segunda copia de un correo que ya estásentounknown. - El destinatario verá la segunda dirección como remitente, no tu dirección de remitente en nuestros servidores.
hostedTransactionalDayes el número diario solo de tu workspace. En ese caso, espera hastaresetAt.
Síguelo hasta el final
sends.get responde por un correo. sends.list responde por varios, del más nuevo al más antiguo, y kind: "transactional" deja solo estos. Los dos piden read. El estado solo avanza. Estos correos no se pueden programar, así que scheduled nunca aparece:
reserved- A punto de salir. Ya cuenta entre los envíos de hoy y espera su turno.
sending- Saliendo ahora.
sent- El servidor de correo por el que salió lo aceptó: el del proveedor del buzón que conectaste o, en una dirección de remitente, el nuestro. Eso no significa que llegó a la bandeja de entrada.
failed- No salió. El
errorCodedice el motivo. unknown- La conexión se cortó mientras salía, así que no sabemos si el servidor lo recibió. Cuenta como enviado y nunca se envía de nuevo.
cancelled- Se detuvo antes de salir: una persona o una clave lo canceló, o una comprobación lo rechazó cuando llegó su hora. El
errorCodedice el motivo.
Para saber cómo terminó cada correo sin preguntar, lee los eventos. Todo envío termina en send.sent, send.failed, send.unknown, send.cancelled o send.bounced, y los correos de campaña también, así que el ejemplo comprueba el kind:
const page = await colding.events.list({
types: ["send.sent", "send.failed", "send.unknown", "send.cancelled", "send.bounced"],
});
for (const event of page.items) {
if ("kind" in event.data && event.data.kind === "transactional")
console.log(event.type, event.subjectId);
}El subjectId es el ID del correo. Estos correos no tienen send.opened ni send.clicked, porque no llevan seguimiento.
sent significa que el servidor de correo por el que salió lo aceptó: el de tu proveedor, en un buzón que conectaste, o el nuestro, en una dirección de remitente. Ninguno de los dos dice que llegó a la bandeja de entrada. Un rebote puede llegar después. Entonces se dispara send.bounced, y bouncedAt dice cuándo.
Las respuestas llegan a la bandeja de entrada, o a replyTo si defines uno. Un buzón solo de envío no tiene bandeja de entrada, y una dirección de remitente tampoco: las respuestas van a replyTo o, si no lo defines, a la dirección de respuesta con la que se creó la dirección de remitente.
Cuando se rechaza o se detiene
Una llamada rechazada no envía nada. Estas son las respuestas que puede recibir un envío:
400 VALIDATION_ERROR- La petición está mal: sin cuerpo, una dirección inválida o un
expiresAtque ya pasó. 402 PLAN_GATE- El plan no tiene correo transaccional. Cambiar de plan lo resuelve, y
details.upgradeTodice qué plan lo tiene. En una dirección de remitente, también puede ser que nuestro equipo todavía no haya habilitado nuestros servidores para el workspace. 402 PAYMENT_REQUIRED- El último pago falló. Pagar lo resuelve.
403 FORBIDDEN- La clave no tiene
transactional, o un dueño la limitó: a algunos buzones, a un tipo de correo o a un total de correos.details.scopedice cuál. 403 AUP_REQUIRED- El dueño todavía no aceptó la política de uso aceptable.
404 NOT_FOUND- No existe ese buzón ni esa dirección de remitente en este workspace. Uno de otro workspace responde lo mismo.
409 CONFLICT- Algo del buzón, de la dirección o del workspace bloquea este envío. El mensaje dice qué: el buzón no es transaccional, su dirección no está confirmada, el destinatario detuvo los correos de tu app o, en una dirección de remitente, un servidor nuestro no está listo o nuestro equipo puso su dominio en espera.
422 IDEMPOTENCY_KEY_REUSED- Este
intentKeyya nombra otro correo. 429 QUOTA_EXCEEDED- El buzón ya envió todos los correos de la app de hoy, o la clave llegó a su límite diario. En una dirección de remitente también puede ser el espacio de nuestros servidores: la sección Desde nuestros servidores lista cada caso.
details.scopedice cuál, yresetAtdice cuándo se puede volver a enviar. 429 RATE_LIMIT_EXCEEDED- El proveedor de correo de la persona pidió a nuestro servidor que fuera más despacio. Esto solo ocurre en una dirección de remitente.
details.scopeesreceiverPace, y la sección Desde nuestros servidores lo explica.
La primera guía lista todos los códigos de error y qué hacer con cada uno.
Detenido antes de salir
Una llamada puede salir bien y el correo aun así no salir. Poco antes de entregarlo, el worker mira una vez más. Si algo cambió, cancela el correo con un errorCode y no envía nada:
expired- El
expiresAtpasó antes de que el correo empezara a salir. recipient_suppressed- La dirección entró en la lista de supresión de los correos de la app después de tu llamada.
authority_revoked- La clave o la persona que lo envió ya no puede enviar correos de la app.
key_stream_scopekey_mailbox_scope- Un dueño limitó la clave después de tu llamada, y ya no puede enviar este tipo de correo ni enviar desde este buzón.
payment_failed- Un pago falló después de tu llamada.
workspace_posture- Limitamos el workspace antes de que el correo saliera, o la cuenta que lo envió ya no puede enviar.
workspace_closing- El dueño pidió eliminar el workspace.
sender_unconfirmed- La dirección del buzón no estaba confirmada como del workspace.
mailbox_disconnected- El buzón se desconectó, o la dirección de remitente se eliminó, después de tu llamada.
mail_ip_held- El servidor nuestro por el que saldría el correo quedó en espera después de tu llamada.
domain_not_signing- El dominio de la dirección de remitente no pasó nuestra comprobación después de tu llamada, así que no podemos firmar su correo.
mailbox_paused- El buzón se pausó, o nuestro equipo puso en espera el dominio de la dirección de remitente, después de tu llamada.
failed es distinto: el correo se intentó, y el servidor o el buzón lo rechazó. El errorCode dice el motivo. Para enviarlo de nuevo, usa un intentKey nuevo, porque el anterior sigue nombrando el correo que falló.
relay_refused es cosa nuestra: nuestro servidor de correo rechazó un correo de una dirección de remitente. No es un rebote, nadie entra en la lista de supresión y el correo no cuenta en el número de hoy. Envíalo de nuevo más tarde, con un intentKey nuevo.
Cuándo ponemos un dominio de correo en espera
Vigilamos el correo de tu app que sale por nuestros servidores, un dominio de correo a la vez. Si demasiados correos rebotan o se marcan como spam, ponemos el dominio en espera. Todas sus direcciones de remitente dejan de enviar hasta que nuestro equipo vuelva a activarlas. Los demás dominios de correo y los buzones que conectaste siguen enviando.
Contamos los últimos 7 días. Un rebote cuenta cuando el servidor de correo de la persona rechazó la dirección. Un rebote sin motivo no cuenta, y un retraso de nuestra parte tampoco. Un correo que la persona marca como spam cuenta una vez.
Ponemos un dominio en espera cuando 0,5% de sus correos se marcaron como spam o 10% rebotaron. Con menos de 50 correos en esos días, ninguna tasa cuenta. Para el spam se piden además al menos 3 personas distintas que marcaron un correo, para que una sola persona no pueda poner tu dominio en espera por sí sola. Para los rebotes se piden además al menos 10 rebotes hacia 3 o más dominios de destinatario distintos (lo que va después de la @), para que el servidor de una sola empresa no pueda poner tu dominio en espera por sí solo. Estos son los números de hoy y pueden cambiar.
Un servidor de destino también puede rechazar tu correo por no deseado. Solo ponemos el dominio en espera por eso cuando otros remitentes de nuestros servidores no reciben el mismo rechazo de ese servidor.
Mientras dura la espera, un envío desde cualquier dirección de remitente del dominio responde 409 con senderDomainUnderReview. details.caseNumber es el número de nuestro caso. details.ground dice qué regla puso el dominio en espera: complaint_rate, bounce_rate, policy_bounce_rate o policy_abuse_burst. Un correo que ya esperaba para salir se cancela con mailbox_paused. Tampoco puedes agregar una dirección de remitente en el dominio.
Solo nuestro equipo vuelve a activar los envíos, y el conteo empieza de cero cuando lo hace. Escribe a [email protected] y cita el número del caso.
El workspace recibe una alerta mail_domain_problem cuando empieza la espera (under_review) y otra cuando termina (review_ended), y cada dirección de remitente dispara mailbox.review_changed. La página del dominio en la consola muestra los últimos 7 días y la espera.
También leemos el correo de tu app como un grupo aparte cuando juzgamos el workspace completo. Las tasas son las mismas, pero solo cuentan a partir de 200 correos en esos días. Las marcas de spam siguen pidiendo 3 personas distintas, y los rebotes siguen pidiendo 3 o más dominios de destinatario. Si tu correo las supera, o si un dominio queda en espera en varios días distintos, podemos limitar el workspace. Entonces posture.reasonCode trae transactional_bounce_rate, transactional_complaint_rate o sending_pauses.
Quién deja de recibirlo
Los correos de tu app tienen su propia lista de supresión. Una dirección puede detener campañas y mensajes (cold en la API), los correos de tu app (transactional) o ambos. Qué detiene a qué depende de lo que volvió y de qué correo se trata:
- La dirección rebotó, en cualquier correo
- Ambos
- Una persona marcó como spam un correo de campaña o mensaje
- Solo campañas y mensajes
- Una persona marcó como spam un correo de tu app
- Ambos
- Una persona respondió pidiendo que paren, a un correo de campaña o mensaje
- Solo campañas y mensajes
- Una persona respondió pidiendo que paren, a un correo de tu app
- Ambos
Una baja de tus campañas nunca detiene los correos de tu app.
Ninguna comprobación de la dirección va antes, porque la persona pidió este correo. Solo se lee la lista de supresión, dos veces: cuando haces la llamada y de nuevo poco antes de que salga el correo.
Mira qué detuvo los correos de tu app:
const page = await colding.suppressions.list({ stream: "transactional" });
for (const row of page.items) console.log(row.value, row.source, row.twinSource);source dice por qué está cada fila. twinSource dice por qué la misma dirección está en la otra lista, o es null cuando no está.
suppressions.add agrega una dirección con stream: "transactional" y pide operate. Quitar una de la lista le toca a un dueño o admin con sesión iniciada en la consola. Ninguna clave puede.
suppression.added se dispara por cada fila nueva, con su stream y su source.
Informes de un servicio de envío
Un buzón solo de envío no tiene bandeja de entrada, así que un rebote no vuelve como correo. El servicio de envío avisa de otra manera: un informe enviado a una dirección nuestra, el webhook de informes del buzón. Leemos Mailgun, Postmark, Resend, SendGrid y Amazon SES. Un informe solo cuenta cuando el servicio demuestra que es suyo y nombra un correo que este buzón envió.
El dueño activa el webhook en la consola y copia su dirección en el servicio. Una clave no puede, porque el webhook guarda o muestra un secreto. La clave lee el resto:
const hook = await colding.mailboxes.reportHook({ mailboxId });
console.log(hook.service, hook.on, hook.lastReportAt);
const reports = await colding.mailboxes.reports({ mailboxId });
for (const report of reports) console.log(report.kind, report.outcome);reportHook dice si leemos este servicio (supported) y si el webhook está activo (on). También trae la hora del último informe (lastReportAt) y cuántos llegaron en los últimos 30 días, por resultado (counts). Nunca muestra el secreto.
Amazon SES no tiene secreto. El dueño nos da el tema de SNS. El webhook dice cuándo se confirmó la suscripción de AWS y cuál fue el último mensaje que rechazamos, con el motivo (refusal).
reports lista lo que llegó, del más nuevo al más antiguo: 20 por defecto, hasta 100. Cada uno dice si fue un rebote o una denuncia y qué pasó con él. No trae la dirección ni las palabras del servicio. Los informes se guardan 30 días.
El resultado dice qué hicimos:
suppressed- El informe dijo que la dirección ya no existe, o una persona denunció. El destinatario de ese correo está en la lista de supresión.
stamped- Un fallo permanente que no dice que la dirección ya no existe, como un rechazo sobre el mensaje o sobre nosotros. El correo queda marcado como rebotado, y nadie entra en la lista.
no_send- Ningún correo de este buzón coincide con el informe. No cambió nada.
other_recipient- Nombra a alguien a quien el correo no fue. No cambió nada.
not_final- Una demora, o un fallo que el servicio va a reintentar. No cambió nada.
Solo el destinatario del correo al que se refiere el informe entra en la lista. Una dirección nombrada dentro de un informe nunca entra.
El código que lee los informes de cada servicio se escribió a partir de su documentación y todavía no se probó con una cuenta real. Revisa el primer informe con reports antes de confiar en él.
Tres protecciones contra el envío doble
Un timeout no te dice si el correo salió. Tres protecciones separadas impiden un segundo. Cada una actúa en un punto distinto, y ninguna reemplaza a las otras.
Una respuesta guardada: la clave de la petición HTTP
Cada escritura del SDK lleva una Idempotency-Key. Durante 24 horas, el mismo workspace, operación, clave y datos reciben la respuesta guardada en lugar de una segunda ejecución. Si falla guardar la respuesta, o si el primer intento queda 15 minutos sin respuesta, la llamada puede ejecutarse de nuevo. Esta protección sola no garantiza un único correo.
La intención de envío: un nombre para un correo
Por eso el intentKey es obligatorio aquí: es la protección que vale para siempre. La misma clave con la misma petición devuelve siempre el mismo correo, mucho después de que la respuesta guardada desaparezca.
El worker toma el correo para sí
Antes de entregar, el worker marca el registro del correo como suyo, una sola vez, así que dos workers no pueden enviarlo a la vez. Si la conexión se corta al final del envío, el correo queda como unknown, cuenta como enviado y nunca se envía de nuevo.
Ninguna de las tres demuestra que el correo llegó. sent significa que el servidor de correo por el que salió lo aceptó: el de tu proveedor o, en una dirección de remitente, el nuestro.
La guía de envío muestra las mismas tres protecciones para borradores