Cloudflare Email Service — Implementacion Completa
Folio SaaS · Junio 2026
Folio — Gestion Documental para Despachos de Abogados ✓ folioapp.es activo ⚡ Workers binding Python REST API 4 flujos transaccionales

Estado del proyecto

✓ Dominio onboarded
✓ Workers binding OK
✓ SPF configurado
✓ DKIM activo
⚠ DMARC pendiente
× Python env vars

Arquitectura de Email para Folio SaaS

4 flujos transaccionales · 2 metodos de envio · dominio folioapp.es

4
Tipos de email transaccional
2
Metodos de envio
0€
Coste adicional
Tipo de emailTriggerMetodoAdjunto
BienvenidaActivacion de cuentaWorkers bindingNo
Invitacion colaboradorAdmin invita abogadoWorkers bindingNo
Alerta de seguridadIP desconocida detectadaPython REST APINo
Confirmacion firma digitalFirma completadaWorkers bindingPDF documento

1. Setup del dominio — folioapp.es

Habilitar Email Sending y configurar wrangler.jsonc

Antes de enviar cualquier email, el dominio debe estar registrado. El binding de Workers elimina la necesidad de API keys para envios desde edge.

1

Verificar y habilitar el dominio

bashterminal
# Ver dominios activos npx wrangler email sending list # Si folioapp.es no aparece: npx wrangler email sending enable folioapp.es
2

Configurar wrangler.jsonc con el binding

jsoncwrangler.jsonc
{ "name": "folio-workers", "main": "src/index.ts", "compatibility_date": "2026-06-01", // Email Sending binding (sin API key) "send_email": [ { "name": "EMAIL" } ], "vars": { "APP_URL": "https://app.folioapp.es" } }
folioapp.es verificado y activo Puede enviar desde cualquier subdireccion: notificaciones@folioapp.es, noreply@folioapp.es, soporte@folioapp.es.

2. Envio desde Workers — Bienvenida e Invitacion

env.EMAIL.send() sin API keys · campo "email" en from (no "address")

La diferencia clave entre Workers binding y REST API: en Workers from.email (camelCase); en REST API from.address (snake_case). Siempre incluir text junto a html.

Email de Bienvenida

typescriptsrc/emails/welcome.ts
export async function sendWelcome( env: Env, user: { name: string; email: string; activationUrl: string; } ) { await env.EMAIL.send({ to: user.email, from: { // Workers: "email" (no "address") email: "notificaciones@folioapp.es", name: "Folio — Tu Despacho" }, subject: `Bienvenido/a, ${user.name}`, html: welcomeHtml(user), // Siempre incluir text text: `Hola ${user.name},\n\n` + `Activa tu cuenta:\n${user.activationUrl}` }); }

Invitacion a Colaborador

typescriptsrc/emails/invite.ts
export async function sendInvite( env: Env, data: { inviteeEmail: string; inviterName: string; despacho: string; inviteUrl: string; } ) { await env.EMAIL.send({ to: data.inviteeEmail, from: { email: "notificaciones@folioapp.es", name: "Folio" }, replyTo: // camelCase en Workers `${data.inviterName} <noreply@folioapp.es>`, subject: `Invitacion a ${data.despacho}`, html: inviteHtml(data), text: `Te han invitado a Folio.\n` + `Acepta aqui: ${data.inviteUrl}` }); }

Email con Adjunto PDF — Confirmacion de Firma

typescriptsrc/emails/firma.ts
export async function sendFirmaConfirmacion( env: Env, data: { abogadoEmail: string; documentoNombre: string; pdfBytes: ArrayBuffer; firmadoPor: string; fechaFirma: string; } ) { // Convertir PDF a base64 para adjunto const pdfBase64 = btoa( String.fromCharCode(...new Uint8Array(data.pdfBytes)) ); await env.EMAIL.send({ to: data.abogadoEmail, from: { email: "notificaciones@folioapp.es", name: "Folio" }, subject: `Firma completada: ${data.documentoNombre}`, html: firmaHtml(data), text: `Firma completada por ${data.firmadoPor} el ${data.fechaFirma}.\n` + `Documento: ${data.documentoNombre}`, attachments: [ { filename: `${data.documentoNombre}-firmado.pdf`, content: pdfBase64, encoding: "base64", contentType: "application/pdf" } ] }); }
🐍

3. Alerta de Seguridad — Python REST API

Envio desde FastAPI externo · "address" en from (no "email") · snake_case

Cuando el sistema de deteccion de intrusiones (Python/FastAPI) detecta una IP desconocida, envia la alerta via REST API. Diferencias respecto a Workers: from.address, reply_to (snake_case), respuesta con delivered/queued.

Pendiente: configurar variables de entorno en FastAPI CF_ACCOUNT_ID y CF_EMAIL_TOKEN deben estar en el archivo .env del servidor Python. NUNCA hardcodear tokens en el codigo.
pythonapp/services/email_service.py
import httpx import os from datetime import datetime class CloudflareEmailService: BASE_URL = "https://api.cloudflare.com/client/v4" def __init__(self): self.account_id = os.environ["CF_ACCOUNT_ID"] self.token = os.environ["CF_EMAIL_TOKEN"] async def send_security_alert( self, abogado_email: str, abogado_nombre: str, ip_address: str, documento: str, timestamp: datetime ) -> dict: url = ( f"{self.BASE_URL}/accounts/{self.account_id}" "/email/sending/send" ) payload = { # REST API: "address" (no "email") "from": { "address": "notificaciones@folioapp.es", "name": "Folio Seguridad" }, "to": [{ "address": abogado_email }], "subject": f"Alerta: acceso desde IP desconocida — {documento}", "html": self._build_alert_html( abogado_nombre, ip_address, documento, timestamp ), "text": ( f"Hola {abogado_nombre},\n\n" f"Se ha detectado acceso al documento '{documento}'\n" f"desde la IP {ip_address} a las {timestamp}.\n\n" "Si no fuiste tu, cambia tu contrasena inmediatamente.\n" ), # REST API: snake_case "reply_to": { "address": "soporte@folioapp.es" } } async with httpx.AsyncClient() as client: response = await client.post( url, json=payload, headers={ "Authorization": f"Bearer {self.token}", "Content-Type": "application/json" } ) response.raise_for_status() data = response.json() # Respuesta: {"delivered":[], "queued":[], "permanent_bounces":[]} if data.get("permanent_bounces"): log_bounce(abogado_email, data["permanent_bounces"]) return data
🔒

5. Entregabilidad — Registros DNS para folioapp.es

SPF activo, DKIM activo, DMARC pendiente de configurar

Para que los emails no caigan en spam, los tres registros de autenticacion deben estar configurados. Cloudflare genera automaticamente los registros SPF y DKIM al habilitar Email Sending.

TipoNombreValorEstado
TXT folioapp.es v=spf1 include:_spf.cloudflare.com ~all ✓ Activo
CNAME cf2024._domainkey cf2024._domainkey.folioapp.es.dkim.cloudflare.net ✓ Activo
TXT _dmarc.folioapp.es v=DMARC1; p=quarantine; rua=mailto:dmarc@folioapp.es ⚠ Pendiente
Accion requerida: anadir registro DMARC Sin DMARC, algunos proveedores (Gmail, Outlook) pueden desconfiar de los emails. Anade el registro TXT indicado arriba en el DNS de Cloudflare.

Gestionar supresiones y bounces

bashwrangler CLI
# Ver supresiones activas (emails que no recibiron) npx wrangler email sending suppressions list # Eliminar una supresion (si el usuario quiere reactivar) npx wrangler email sending suppressions delete usuario@despacho.es # Ver estadisticas de envio del dominio npx wrangler email sending stats --domain folioapp.es

6. Errores Comunes — Folio SaaS

Los 6 errores mas frecuentes al integrar Cloudflare Email Service

× Error: Using "address" in Workers from field
Workers binding usa from.email, no from.address. El REST API usa address. Confundirlos da error 400.
× Error: Enviando sin campo "text"
Los emails sin version de texto plano tienen peor deliverability y fallan en clientes de solo texto. Siempre incluir text junto a html.
× Error: Token CF_EMAIL_TOKEN hardcodeado en Python
Los tokens en codigo se commitean y se filtran. Usar os.environ["CF_EMAIL_TOKEN"] y guardarlo en el servidor de secretos.
× Error: Leyendo message.raw dos veces en routing handler
El stream raw es de un solo uso. Bufferizar primero: const raw = await new Response(message.raw).arrayBuffer()
× Error: Usar Cloudflare Email para newsletters
Email Service es SOLO para emails transaccionales. Para newsletters usar una plataforma dedicada (Resend, SendGrid, Mailchimp).
× Error: DMARC no configurado
Sin DMARC, Gmail y Outlook pueden marcar emails como spam. Anadir el registro _dmarc.folioapp.es con politica quarantine.