Logotipo de Bitdoze

Envía Email desde un Sitio EmDash: Cloudflare Email Sending y SMTP

Configura el email de EmDash en Cloudflare Workers: incorpora el dominio remitente, añade el binding send_email, activa cloudflareEmail, más la ruta emdash-smtp y Brevo gratis.

Dragos

Actualizado Publicado 21 min de lectura

Envía Email desde un Sitio EmDash: Cloudflare Email Sending y SMTP

Un sitio EmDash envía correo saliente para sign-in con magic link, invitaciones de equipo, recuperación de cuenta y notificaciones de comentarios, más lo que un plugin necesite entregar (formularios de contacto, por ejemplo). Sin un proveedor configurado el admin sigue funcionando, pero cada flujo que envía correo devuelve 503 EMAIL_NOT_CONFIGURED, y los enlaces de invitación hay que copiarlos a mano. La guía de deploy en Cloudflare marcó esto y se saltó deliberadamente el arreglo. Esta es esa guía; si aún no has desplegado, esa guía y el tutorial de EmDash CMS van primero.

En Cloudflare Workers hay una ruta nativa sin cuenta externa y sin API key: el binding send_email respaldado por Cloudflare Email Sending, cableado a EmDash con el plugin oficial cloudflareEmail(). Es lo que corre en emdashhq.com, mi hub de EmDash para tutoriales, temas y plugins, y cada comando de abajo es uno que corrí ahí. También hay una segunda ruta, el plugin comunitario emdash-smtp, que es la respuesta real para cualquiera en Workers Free o fuera de Cloudflare por completo. Este artículo cubre ambas, y termina con el plugin de formularios de contacto que construí sobre el setup funcionando.

EmDash en GitHub

Email en Cloudflare en tres carriles

Cloudflare tiene varias funciones con “email” en el nombre que hacen trabajos distintos. Separarlas primero ahorra una hora de confusión después.

Email Routing recibe el correo dirigido a tu dominio y lo reenvía a otro sitio. [email protected] es una regla de routing: el correo enviado ahí aterriza en un buzón real detrás (una cuenta de Gmail, en mi caso). Routing es gratis, ilimitado, y disponible en cada plan. Cloudflare no aloja buzones, y el routing solo no puede enviar nada.

Email Sending a destinatarios arbitrarios es el servicio transaccional de salida, lo que un CMS necesita para magic links e invitaciones. Requiere Workers Paid (5 $/mes). Cada cuenta obtiene 3.000 emails salientes al mes incluidos en el plan, luego 0,35 $ por 1.000 más allá. Los mensajes que rebotan duro siguen contando contra la cuota; los rechazados en la frontera de la API (payload malo, lista de supresión) no.

Envío a direcciones de destino verificadas es la excepción que funciona en cada plan, Workers Free incluido. Una dirección de destino verificada es una que probaste poseer en los ajustes de Email Routing, las mismas direcciones a las que reenvían las reglas de routing. Antes de que cualquier dominio se incorpore a Email Sending, un binding send_email solo puede entregar a esas direcciones, y solo desde una dirección en un dominio de routing que poseas. Esos envíos siempre son gratis y nunca cuentan contra cuota ni límites diarios. Bien para pruebas y flujos de “envíame una copia”, inútil para un CMS que envía a usuarios arbitrarios.

Workers Free Workers Paid (5 $)
Recibir + reenviar correo entrante (Email Routing) Ilimitado Ilimitado
Enviar a direcciones de destino verificadas Gratis Gratis (no cuenta contra la cuota)
Enviar a cualquier destinatario (Email Sending) No disponible 3.000/mes incluidos, luego 0,35 $ por 1.000
Cuota diaria n/a Empieza conservadora, escala con la reputación del remitente

Los tutoriales de MailChannels están desactualizados

Los tutoriales viejos envían el email del Worker por MailChannels gratis. Esa integración terminó; MailChannels ya no es socio de Cloudflare y esas instrucciones fallan en silencio o con errores de binding hoy. Email Sending es el reemplazo, y en un plan de pago es más simple de lo que MailChannels jamás fue.

Cómo entrega EmDash el email

EmDash delega el envío a un plugin proveedor activo. El pipeline es corto: una función (auth, invitaciones, comentarios, un plugin) produce un mensaje, los hooks de email pueden transformarlo o cancelarlo, y el handler email:deliver del plugin proveedor hace el envío.

  • Desarrollo. astro dev auto-activa un proveedor de consola integrado cuando no hay otro seleccionado. No envía nada; loguea cada mensaje al terminal y guarda los últimos 100 en memoria. Puedes listarlos en GET /_emdash/api/dev/emails con sesión iniciada. Los magic links funcionan en dev copiando la URL del log.
  • Producción. El proveedor de consola se compila fuera. Si ningún plugin proveedor está activo, Settings > Email en el admin no muestra proveedor y los envíos fallan hasta que uno se instala, activa y selecciona.
  • En Cloudflare. cloudflareEmail() de @emdash-cms/cloudflare/plugins entrega por el binding send_email del Worker. Las credenciales son el binding más el dominio incorporado; no hay token que guardar o rotar.
  • En cualquier otro sitio. La familia de plugins comunitarios emdash-smtp cubre SMTP genérico y proveedores alojados (SES, Brevo, Mailgun, Postmark, Resend, SendGrid, Zoho y más). Salta a la Opción 2.

El admin tiene un botón Send test email bajo Settings > Email que ejercita el pipeline entero y saca los errores del proveedor directamente. Es el paso de verificación más rápido de este artículo. El mecanismo completo también está documentado en la guía de email de EmDash.

Configurando Cloudflare Email Sending en emdashhq.com

Paso 1: incorpora el dominio remitente

Enviar requiere que el dominio del remitente esté incorporado a Email Service. La ruta del dashboard es Compute > Email Service > Email Sending > Onboard Domain. La CLI hace el mismo trabajo:

bash
npx wrangler email sending enable emdashhq.com

La incorporación crea registros DNS automáticamente:

Registro Nombre Propósito
MX x3 cf-bounce.emdashhq.com Devuelve los mensajes de rebote a Cloudflare
TXT (SPF) cf-bounce.emdashhq.com Autoriza a Cloudflare a enviar por el dominio
TXT (DKIM) cf-bounce._domainkey.emdashhq.com Firma el correo saliente
TXT (DMARC) _dmarc.emdashhq.com Registro de política (p=reject aquí)

Verifica el estado y los registros exactos que Cloudflare espera:

bash
npx wrangler email sending settings emdashhq.com
npx wrangler email sending dns get emdashhq.com

Y verifica lo que realmente está vivo en DNS:

bash
dig +short MX cf-bounce.emdashhq.com
dig +short TXT cf-bounce.emdashhq.com
dig +short TXT cf-bounce._domainkey.emdashhq.com
dig +short TXT _dmarc.emdashhq.com

En emdashhq.com los cuatro ya estaban presentes antes del cambio de código. El DNS en el resolver propio de Cloudflare normalmente propaga en minutos. Cualquier dirección en el dominio incorporado es un remitente aceptado; no registras hello@ ni dragos@ individualmente.

Ojo con los registros SPF duplicados

Dos registros TXT v=spf1 idénticos en el mismo hostname es una violación de RFC que produce un permerror en receptores estrictos. Email Routing añade un registro SPF al apex y Email Sending añade uno a cf-bounce. Si el apex acaba con un duplicado tras corridas de incorporación repetidas, borra el extra en el dashboard de DNS.

Paso 2: añade el binding

En wrangler.jsonc, junto a los bindings D1, R2 y KV existentes:

jsonc
"send_email": [
	{
		"name": "EMAIL"
	}
],

El nombre es lo que el plugin busca en el entorno del Worker. EMAIL es el default; un nombre distinto hay que pasarlo al plugin como su opción binding. La API builder completa tras el binding (adjuntos, headers personalizados, tope de 50 destinatarios) está en los docs de la Workers API.

Paso 3: registra el plugin

En astro.config.mjs:

js
import { cloudflareEmail } from "@emdash-cms/cloudflare/plugins";

emdash({
	// database, storage, objectCache, migrations...
	plugins: [
		siteScriptsPlugin(),
		cloudflareEmail({
			from: { email: "[email protected]", name: "EmDash HQ" },
			replyTo: "[email protected]",
		}),
	],
});

Dos opciones importan:

  • from fija el remitente. Una cadena pelada o { email, name }. El dominio debe estar incorporado o cada envío falla con E_SENDER_NOT_VERIFIED. Una dirección estilo hello@ se lee mejor que un buzón personal en correo de sistema.
  • replyTo es a dónde van las respuestas humanas. Con un remitente hello@ o no-reply, apúntalo a un buzón que exista para que las respuestas no se pierdan. Un replyTo a nivel de mensaje lo pisa.

No hay paso wrangler secret. El binding es la credencial: solo tu Worker puede usarlo. Esto refleja la sección de email del doc de despliegue de EmDash en Cloudflare.

Paso 4: despliega

bash
npm run deploy

El script de deploy del sitio buildea, verifica el manifiesto de migración contra la D1 de producción, sube el Worker y calienta la caché de página. Un binding send_email no necesita aprovisionamiento; se adjunta a cualquier Worker que lo declare.

Si el deploy está bloqueado por migraciones de núcleo pendientes (el caso justo tras subir emdash), aplícalas primero:

bash
npx wrangler email sending settings emdashhq.com   # optional re-check
npx emdash migrate --status --wrangler-config wrangler.jsonc
npx emdash migrate --wrangler-config wrangler.jsonc
npm run deploy

Paso 5: activa y prueba en el admin

El registro hace el plugin disponible; aún no está activo.

  1. Abre https://emdashhq.com/_emdash/admin y ve a Extensions. Activa el plugin de email de Cloudflare.
  2. Abre Settings > Email. Si es el único proveedor, EmDash lo selecciona automáticamente; si no, elígelo.
  3. Haz clic en Send test email a una dirección que puedas comprobar.

Un resultado verde significa que todo el camino funciona: admin, pipeline de hooks, binding, Email Sending, DNS y los filtros del destinatario.

También puedes probar el binding sin involucrar a EmDash en absoluto, lo que aísla los problemas del lado Cloudflare de los del lado CMS:

bash
npx wrangler email sending send \
	--from "[email protected]" \
	--to "[email protected]" \
	--subject "Email Sending smoke test" \
	--text "If you can read this, the binding and domain are fine."

Paso 6 (opcional): verifica un flujo real

Solicita un magic link para una segunda cuenta de admin, o dispara una invitación desde la página Users. El correo debería llegar firmado por cf-bounce._domainkey.emdashhq.com con Reply-To puesto a la dirección configurada.

Límites y comportamiento que vale conocer

  • 50 destinatarios máximo por mensaje entre to, cc, bcc. El correo de sistema de EmDash es de un destinatario por vez, muy por debajo del tope.
  • 5 MiB de tamaño total de mensaje incluyendo adjuntos (25 MiB cuando se envía solo a direcciones de destino verificadas).
  • Las cuentas nuevas empiezan con una cuota diaria conservadora que crece automáticamente con el historial de envío y la reputación. Existe un formulario de aumento de límite para saltarse la cola.
  • Una lista de supresión bloquea direcciones que rebotaron duro o se quejaron. Los envíos a direcciones suprimidas se rechazan en la frontera de la API (y no facturan), o se descartan en silencio para los destinatarios restantes si Drop suppressed recipients está activado.
  • Los envíos salientes aparecen como dropped en el resumen del dashboard de Email Routing aunque hayan llegado. Esa vista es para entrantes. Sigue los envíos bajo observabilidad de Email Service en su lugar.
  • La entrega está a merced de que SPF/DKIM/DMARC pasen. La incorporación escribe los tres; si el correo aterriza en spam, comprueba esos registros primero, luego la reputación del remitente.
  • 30 dominios de Email Routing + Email Sending máximo por zona, 200 reglas de routing por dominio, 200 direcciones de destino verificadas por cuenta. Números completos en la página de límites y la página de precios.

Troubleshooting del email de EmDash

Síntoma, causa, arreglo. Las dos primeras filas son las que golpean a la mayoría.

Síntoma Causa probable Arreglo
503 EMAIL_NOT_CONFIGURED Ningún proveedor activo Activa el plugin en Extensions, selecciónalo en Settings > Email
E_SENDER_NOT_VERIFIED / E_SENDER_DOMAIN_NOT_AVAILABLE Dominio from no incorporado, o DNS aún propagando wrangler email sending enable <dominio>; re-comprueba con dns get y dig
E_RECIPIENT_NOT_ALLOWED Dominio no incorporado y el destinatario no es una dirección verificada Incorpora el dominio, o envía solo a direcciones verificadas
E_DAILY_LIMIT_EXCEEDED / E_RATE_LIMIT_EXCEEDED Cuota de cuenta nueva o límite de ráfaga Espera a la ventana rodante; pide un aumento para volumen de producción
E_RECIPIENT_SUPPRESSED La dirección está en la lista de supresión (rebotó o se quejó antes) Quítala de la lista de supresión en los ajustes de Email Service
Envíos funcionan en dev, fallan en prod Dev usó el proveedor de consola El proveedor de consola nunca corre en producción; termina el Paso 5
El correo aterriza en spam Problema de SPF/DKIM/DMARC o reputación de remitente fresca dig los cuatro registros; calienta el dominio con correo real de bajo volumen
Registros SPF duplicados en el apex Corridas de incorporación repetidas Borra el TXT extra en el dashboard de DNS
Envíos de formulario guardados pero nunca enviados El plugin guarda primero, envía por el proveedor después Arregla el proveedor; reenvía desde la página de submissions

Opción 2: SMTP y proveedores alojados con emdash-smtp

El binding no es la única vía. La familia de plugins emdash-smtp mantenida por la comunidad enruta el correo de EmDash por SMTP genérico o la API de un proveedor alojado. Necesita EmDash 1.0.1 o posterior y se envía en dos paquetes:

  • emdash-smtp es la instalación confiable. Cubre el catálogo completo de proveedores más SMTP genérico y sendmail local.
  • emdash-smtp-marketplace es la variante segura-para-sandbox para instalaciones de registro/marketplace. Corre bajo sandboxed: y necesita un sandboxRunner (sandbox() de @emdash-cms/cloudflare, más el binding LOADER) en la config de EmDash.

Proveedores

La mayoría de proveedores corren sobre HTTPS, así que funcionan igual en Workers y Node: Amazon SES, Brevo, Elastic Email, Emailit, Mailchimp Transactional, MailerSend, Mailgun, Mailjet, Postmark, Resend, SendGrid, SMTP2GO, SparkPost. Google/Gmail, Microsoft 365 y Zoho Mail usan sus APIs con tokens OAuth (token directo, o client credential más refresh token, que el plugin refresca al enviar). SMTP genérico y sendmail local solo existen en el paquete confiable y corren sobre transports de Node, así que pertenecen a deploys VPS/autoalojados; no hay transport SMTP genérico en Workers, ni gratis ni de pago.

Setup

bash
npm install emdash-smtp
js
import { emdashSmtp } from "emdash-smtp";

emdash({
	// database, storage...
	plugins: [emdashSmtp()],
});

Despliega, luego en el admin abre los ajustes del plugin (Plugins > SMTP Providers), elige un proveedor y pega sus credenciales. No se necesitan variables de entorno ni entradas wrangler secret para el propio proveedor: EmDash guarda las credenciales como ajustes de plugin en D1, así que rotar una clave es una edición en el admin, no un redeploy. El único secret que tiene que existir ya es EMDASH_ENCRYPTION_KEY, que cifra esos ajustes en reposo; cualquier deploy de plantilla lo tiene (la guía de deploy cubre npx emdash secrets generate y wrangler secret put si el tuyo no). Selecciona SMTP bajo Settings > Email y usa Send test email como antes.

La opción de SMTP gratis: Brevo

Brevo es la elección pragmática cuando el coste importa más que quedarse dentro de Cloudflare. Su plan Free es gratis para siempre, sin tarjeta, e incluye email transaccional por SMTP y API a 300 envíos al día (unas 9.000/mes si se reparte parejo). Pasado el tope diario, el correo transaccional se encola en un buffer de reintento de hasta 1.000 en vez de fallar duro. Eso eclipsa la necesidad real de un CMS: magic links, invitaciones y avisos de comentarios son correos de un dígito al día en la mayoría de sitios. Los números actuales están en la página de límites de Brevo.

Como Brevo va por su API HTTP en emdash-smtp, también funciona en Workers Free. Ese es el desbloqueo real: un sitio en el plan de 0 $ no puede usar send_email a destinatarios arbitrarios en absoluto, pero el tier gratis de Brevo cubre toda la carga de correo del CMS sin tocar la factura de Workers.

Esquema del setup:

  1. Crea una cuenta Brevo y añade emdashhq.com como dominio remitente. Brevo emite sus propios registros SPF/DKIM para añadir en Cloudflare DNS; coexisten con los registros de Email Sending ya que Brevo usa su propio selector DKIM y entradas de hostname.
  2. Genera una API key (SMTP & API > API Keys). En Workers usa siempre el proveedor Brevo, que habla con la API HTTP de Brevo; las credenciales crudas smtp-relay.brevo.com:587 solo son usables por el transport Generic SMTP en Node.
  3. Pégala en los ajustes del proveedor Brevo del plugin, selecciona SMTP en Settings > Email, envía una prueba.

Ponlo a trabajar: formularios de contacto que sí envían

Una vez hay un proveedor activo, cada función que llama a ctx.email.send() lo monta. El ejemplo concreto en emdashhq.com es @bitdoze.com/emdashhq-contact-forms, un plugin de formularios de contacto que publiqué al registro de EmDash. Nunca habla con un servicio de email él mismo; entrega los mensajes al proveedor que hayas configurado arriba, que es como un plugin de EmDash debería comportarse. El código vive en el repo de emdashhq.com.

Lo que obtienes:

  • Una página de admin Forms: crea y edita formularios, gestiona campos (text, email, tel, url, number, textarea, select, checkbox), reordena, activa/desactiva.
  • Una página Submissions: cada entrada guardada con página, IP, país y user-agent; filtra por formulario/estado/resultado de email, marca leído o archiva, reenvía la notificación, copia como CSV.
  • Una página Email log: cada intento de envío con estado, duración y el mensaje de error del proveedor literal. Esto dobla como vista de depuración para todo lo de este artículo; los errores E_* de la tabla de troubleshooting aparecen aquí.
  • Un widget de dashboard (conteo de no leídos, formularios activos, submissions recientes), anti-spam (honeypot, trampa de tiempo-mínimo-de-relleno, límite de tasa por IP por hora) y poda de retención opcional vía el cron.

Es un plugin sandboxed, así que corre en un Worker aislado y necesita el mismo sandbox runner que cualquier plugin de marketplace: sandboxRunner: sandbox() más el binding LOADER y una clase PluginBridge exportada, según los docs de plugin sandbox. Eso significa Workers Paid, el mismo muro que cubre la guía de deploy. Si tomaste la ruta Brevo porque estás en Workers Free, este plugin tampoco es alcanzable; es un límite de sandboxing, no de email.

Instalar desde el registro

La ruta recomendada. Con un sandbox runner configurado, abre Registry en el admin, busca @bitdoze.com/emdashhq-contact-forms y selecciona Install. El plugin declara la capacidad email:send, que EmDash te muestra para revisar antes de instalar nada.

Instalar desde npm

bash
npm install emdashhq-contact-forms

Registra el descriptor generado en astro.config.mjs bajo sandboxed, no plugins:

js
import { sandbox } from "@emdash-cms/cloudflare";
import contactForms from "emdashhq-contact-forms";

emdash({
	sandboxRunner: sandbox(),
	sandboxed: [
		contactForms, // descriptor object, not a factory call
	],
});

Actívalo bajo Plugins.

Luego cablea el bloque de página. Los tipos de bloque de EmDash viven en la semilla del sitio, un plugin no puede inyectar uno, así que dos archivos vienen en el directorio site/ del paquete para copiar:

  1. site/block-type.json va a blockTypes en seed/seed.json, más "contact_form" en el validation.allowedTypes del campo de página (sáltate eso si el campo no tiene allowedTypes, todo está ya permitido).
  2. site/ContactForm.astro va a src/components/blocks/, mapeado en tu mapa de componentes de bloque como contact_form: ContactForm.

Resiembra para que el tipo de bloque llegue a la base de datos:

bash
npx emdash seed seed/seed.json --database <dev-db> --no-content --on-conflict=update

En el admin: pon un email de notificación por defecto en Plugins > Contact forms > Settings, crea un formulario con un slug (contact), luego añade un bloque Contact form en el editor de página y teclea el slug. Las plantillas de asunto aceptan {form}, {site} y cualquier {field_key} enviado; las respuestas al correo de notificación van al primer campo email del visitante vía replyTo. Sin proveedor configurado, los submissions igualmente se guardan, solo que no se envían, que es el modo de fallo correcto.

Contact Forms en el registro de EmDash Código del plugin en GitHub

Qué ruta de email elegir

  • En Workers Paid, bajo volumen, quieres cero terceros. Binding send_email más cloudflareEmail(). Un binding, una llamada de plugin, nada que rotar.
  • En Workers Free, o quieres tracking de entrega, plantillas, o una asignación gratis mayor. emdash-smtp con Brevo (gratis, 300/día) u otro proveedor HTTP.
  • Alto volumen. Compara 0,35 $ por 1.000 sobre los 3.000/mes incluidos de Cloudflare contra los tiers de proveedores. IPs dedicadas y warmup son una cuestión de procurement aparte en cualquier caso.
  • Fuera de Cloudflare. emdash-smtp es la única ruta; no hay binding send_email en Node.

FAQ: email de EmDash

¿Puede EmDash enviar email en Workers Free?

Solo a direcciones de destino verificadas, que cubre pruebas y flujos de “envíame una copia”, no magic links a usuarios arbitrarios. La ruta gratis usable es emdash-smtp apuntado a un proveedor HTTP como Brevo, cuyo tier gratis (300 envíos/día) cubre la carga de correo real de un CMS sin tocar la factura de Workers.

¿Cuál es la diferencia entre Email Routing y Email Sending?

Routing es entrante: recibe correo en tu dominio y lo reenvía a un buzón que posees. Sending es saliente: entrega correo transaccional desde un Worker a destinatarios arbitrarios. Se configuran en sitios distintos y se cobran distinto. Routing es gratis en cada plan; Sending a cualquiera necesita Workers Paid.

¿Por qué los magic links devuelven 'Email is not configured'?

Los Workers de producción no tienen entrega de email por defecto, así que los flujos de auth devuelven 503 EMAIL_NOT_CONFIGURED hasta que un plugin proveedor está instalado, activado y seleccionado en Settings > Email. En dev el proveedor de consola lo maneja en silencio, por eso solo se rompe tras el deploy. Los passkeys y los copy-links de invitación funcionan mientras tanto.

¿Puedo seguir usando MailChannels para email gratis del Worker?

No. La integración de MailChannels terminó cuando lo hizo la asociación, y los tutoriales construidos sobre ella ahora fallan en silencio o con errores de binding. Cloudflare Email Sending es el reemplazo en pago; un proveedor HTTP gratis por emdash-smtp es el reemplazo en gratis.

¿El plugin Contact Forms funciona en Workers Free?

No, por una razón distinta a la del email. Es un plugin sandboxed, así que necesita el binding LOADER y sandboxRunner, y Dynamic Workers es solo Workers Paid. En un plan gratis la ruta npm emdash-smtp igualmente arregla tu correo de sistema (magic links, invitaciones); el plugin de formularios específicamente es una función del plan de pago.

Mi envío aparece como 'dropped' en el dashboard de Email Routing. ¿Falló?

Probablemente no. Ese resumen es la vista de entrantes, y los envíos salientes se cuentan como dropped ahí aunque hayan llegado bien. Sigue los envíos reales bajo la observabilidad de Email Service en el dashboard, o en el email log del plugin si usas Contact Forms.

Cierre

En Workers Paid, el email de EmDash es un trabajo de tres partes: incorpora el dominio, añade send_email a wrangler.jsonc, registra cloudflareEmail() y actívalo en el admin. Sin secrets, sin cuenta de terceros, y 3.000 envíos al mes antes de pagar un céntimo sobre el plan de 5 $. En Workers Free o fuera de Cloudflare, emdash-smtp más el tier gratis de Brevo cubre la misma carga de correo del CMS por 0 $. En cualquier caso la prueba es la misma: instala Contact Forms, envía el formulario, mira el email log.

Esa es la parte 5 de la serie EmDash CMS: email saliente, de las dos formas.

Vuelve a la guía de deploy en Cloudflare