Un buzón buscable con Cloudflare Email Workers y D1 en unas 30 líneas de TypeScript
Email Routing reenvía el correo y no guarda copia. La receta para tener un buzón consultable son unas 30 líneas de TypeScript, un Worker y una base D1, o clonar uno de los tres proyectos ya hechos.

Cloudflare Email Routing hace su trabajo y se olvida del mensaje: acepta la sesión SMTP, aplica las reglas y entrega los bytes al destino, sin quedarse copia. Si quieres un buzón que puedas consultar después, la salida es apuntar la regla a un Worker que guarde el correo en D1 y lo reenvíe. Son unas 30 líneas de TypeScript, y ya hay tres proyectos autoalojados con esa misma forma.
El motivo de que no exista un buzón integrado es que Cloudflare vende borde, no almacenamiento. Guardar correo implica retención, búsqueda, cuotas y gestión de abuso, y nada de eso está en el producto. El hueco lo tapa que un destino pueda ser un Worker, que es justo lo que hacen los tres proyectos.
El handler y lo que conviene saber
El Worker exporta un método email() junto a —o en lugar de— fetch(), y la documentación de Email Workers cubre el contrato del mensaje. El núcleo queda así:
import PostalMime from "postal-mime";
export default {
async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext) {
const raw = await new Response(message.raw).arrayBuffer();
const parsed = await PostalMime.parse(raw);
await env.DB.prepare(
"INSERT INTO messages (id, from_addr, to_addr, subject, text_body, received_at) VALUES (?,?,?,?,?,?)"
)
.bind(crypto.randomUUID(), message.from, message.to, parsed.subject ?? "", parsed.text ?? "", new Date().toISOString())
.run();
ctx.waitUntil(message.forward("you@personal.example"));
},
};
Tres detalles antes del primer despliegue. message.raw es un ReadableStream, no una cadena: hay que envolverlo en un Response para leerlo una vez, porque a la segunda vuelve vacío. forward() solo acepta destinos verificados, los mismos de la lista del panel, y una dirección sin verificar lanza excepción con rebote para el remitente. Una regla mapea a un destino o a un Worker, así que el reparto a varias personas obliga a llamar a forward() una vez por dirección. Existe además setReject(reason): si el Worker decide que un mensaje es spam, corta la sesión SMTP con un 5xx y no se guarda ni se reenvía nada.
Esquema, límites y regla de enrutado
CREATE TABLE messages (
id TEXT PRIMARY KEY,
from_addr TEXT NOT NULL,
to_addr TEXT NOT NULL,
subject TEXT NOT NULL DEFAULT '',
text_body TEXT NOT NULL DEFAULT '',
received_at TEXT NOT NULL
);
CREATE INDEX messages_received ON messages (received_at DESC);
El wrangler.toml solo declara el binding de D1; la regla de enrutado se crea a mano en el panel (Compute, Email Service, Email Routing, Routing rules) o vía API, porque no hay comando de wrangler para ella. Un detalle que muerde: renombrar el Worker rompe el binding, así que renombra primero y vuelve a apuntar la regla después.
Con el plan gratuito de D1 —5 millones de filas leídas y 100.000 escritas al día— una dirección de soporte que reciba 100 mensajes diarios consume el 0,1% del presupuesto de escritura. El índice sobre received_at es lo que evita que un "últimos 50" recorra la tabla entera. Los adjuntos van a R2, no a D1; para correo de soporte solo texto, con D1 basta para empezar.
Los tres buzones ya hechos
- cloudflare/agentic-inbox, Apache-2.0, monta Workers y D1 como una bandeja que los agentes de IA pueden leer y sobre la que pueden actuar.
- HQBase/hqbase, AGPL-3.0, añade R2 y Queues y envía mediante el binding
send_emailde Cloudflare. mirza-rizvi/ResolveHQ, source-available y no open source, combina Email Routing con Resend, D1, R2 y Queues.
Autoalojar cualquiera de ellos significa que las migraciones de D1, el bucket de R2, la ruta de respuesta y el aviso cuando algo se cae son tuyos. Si eso entra en tus planes y alguno encaja, clonar sale más barato que escribir el handler.
Quien publica el código avisa de que no lo ha probado en producción: su support@ sigue reenviando a un buzón normal, y el handler sigue la documentación y los tres proyectos, que sí lo corren. Lo que te llevas es un Worker que hace de buzón: cada mensaje guardado, consultable con SQL y reenviado igual al móvil. Lo que no hay es interfaz, hilos, un "quién lleva esto" ni forma de responder desde support@ una hora más tarde. Eso lo ponen los repositorios de arriba, y la versión alojada de todo esto es hoy una página de lista de espera, no un producto.

