Skip to main content
Glama
robconery

big-mailer

by robconery

big-mailer 📬

Boletines, secuencias de goteo y correo transaccional en un solo Cloudflare Worker. Un sustituto autoalojado de un ESP de pago, donde la lista, el envío y los datos de interacción siguen siendo tuyos.

CI License: MIT TypeScript Cloudflare Workers

Estado: funcionalmente completo y ejecutable en local, aún no desplegado. Está construido para un operador único y no es multiinquilino, por diseño y no por omisión. Consulta Antes de desplegar para ver la lista honesta de lo que queda.

El panel de big-mailer, mostrando el consentimiento desglosado por ámbito

El panel después de sembrar los datos de demostración. Consentimiento, por ámbito es el panel que importa: dos personas abandonaron una serie individual y siguen en la lista. En un ESP normal esos dos números son el mismo número.


La idea 💡

Todo ESP trata la baja como un único interruptor. Alguien termina tu serie de incorporación, hace clic en "darse de baja" para detener eso, y desaparece silenciosamente de tu boletín para siempre. Nunca te enteras. El número simplemente baja.

Aquí, el consentimiento está delimitado por ámbito. Abandonar una secuencia te quita de esa secuencia. Abandonar el boletín no cancela una serie en la que te apuntaste deliberadamente. Solo una baja explícita de "todo", un rebote duro o una queja por spam elimina a alguien por completo.

Esa asimetría es la razón de que esto exista, y todo lo demás en el código está organizado para que no se pueda romper por accidente.


Related MCP server: Resend MCP Server

Ejecútalo 🚀

bun install
bun run db:migrate     # applies migrations to the local D1 database
bun run dev            # http://localhost:8787

Abre http://localhost:8787 y haz clic en Sembrar datos de demostración: doce personas, dos series activas, un boletín enviado. Luego abre la Bandeja de salida para leer el correo que "salió".

Nada sale de tu máquina. EMAIL_PROVIDER=console es el valor predeterminado local y escribe correo completamente renderizado en la Bandeja de salida de la aplicación en lugar de enviarlo. No se necesita clave API, y no hay forma de enviar correo accidentalmente a una persona real mientras experimentas.

Prueba aquello para lo que está hecho 🎯

  1. Suscriptores → elige a alguien → Abre su centro de preferencias

  2. Añade ?scope=sequence:2 a esa URL. Así es como se ve un enlace dentro de un correo de secuencia.

  3. Pulsa Detener solo esta serie

  4. Vuelve a su página de suscriptor: sigue active, sigue en el boletín, fuera de exactamente una serie

  5. Consentimiento muestra a todos los que abandonaron una sola serie frente a la lista (vacía) de personas que se fueron por completo

Envía un boletín después y aun así lo recibirán. Ese es todo el argumento.

Los retrasos de secuencia están en días, y el primer paso tiene como valor predeterminado 0 (llega al unirse) mientras que los pasos posteriores tienen como valor predeterminado 1. Lo que significa que una serie sembrada no terminará mientras la observas, así que el panel tiene Adelantar el reloj (solo local): mueve cada paso pendiente al momento actual y ejecuta un tick. Úsalo y verás cómo el paso 2 se salta a las personas que abandonaron esa serie.


Qué hay aquí 🗂

src/
  worker.tsx      fetch + scheduled + queue handlers — the whole entry point, 151 lines
  core/           domain logic: consent, sending, sequences, segments, rendering
  db/             Drizzle schema (24 tables) and the D1 client
  web/            server-rendered admin console (Hono + JSX, no frontend framework)
  api/            transactional send API, signup forms, media upload, bearer-key auth
  mcp/            MCP server — 93 tools, 4 resources, 4 prompts
  providers/      EmailProvider port + console and Resend adapters
  client/         the only browser JS in the project: the TipTap editor bundle
migrations/       drizzle-kit generated, applied by wrangler
docs/             problem brief, architecture, spec, and a decision log

Aproximadamente 16 mil líneas de TypeScript. bun run typecheck cubre el Worker y el paquete del navegador por separado y está limpio.


Arquitectura de un vistazo 🧱

Cloudflare Workers · D1 (SQLite) mediante Drizzle · Queues para la distribución de envíos · Cron Triggers para la programación · R2 para medios · Hono + JSX renderizado en servidor para la administración · Cloudflare Access para autenticación · un puerto EmailProvider conectable con adaptadores console y Resend.

Las decisiones que merece la pena conocer, y por qué:

Cada fila de messages se materializa antes de que salga un solo envío. Un boletín resuelve su lista completa de destinatarios de antemano, escribe una fila por envío previsto, y solo entonces se distribuye a la cola. Eso hace que un boletín sea reanudable tras un fallo, idempotente entre reintentos de cola y auditable después. Resolver destinatarios de forma diferida en el momento del envío es más barato y convierte cualquier fallo a mitad de boletín en un desastre irrecuperable.

El consentimiento se vuelve a comprobar inmediatamente antes de la llamada al proveedor, no en el momento de ponerlo en cola. Una cola puede entregar minutos después de que se creara el mensaje, y alguien puede darse de baja entretanto. Comprobarlo al poner en cola les enviaría el correo de todos modos.

La concurrencia de la cola está fijada en 6. Un lote es una solicitud al proveedor, así que la concurrencia de lotes es la tasa de solicitudes. Si se deja sin configurar, Cloudflare Queues escala automáticamente a 250 consumidores concurrentes, entierra el límite de 10 req/s de Resend bajo 429s, agota los tres reintentos y deja en la cola de mensajes muertos correo perfectamente válido. Seis lotes de 100 dejan ~600 correos/seg de margen mientras se permanece bajo el límite.

La supresión se basa en la dirección de correo, no en el suscriptor. Los destinatarios transaccionales y las direcciones rebotadas a menudo no tienen ninguna fila de suscriptor, así que un indicador subscribers.status pasaría por alto silenciosamente.

Cualquier cosa observable es una fila en D1, nunca una línea de registro. Los registros de Workers caducan en 3–7 días. Un rastro de auditoría con una retención de una semana no es tal cosa. mcp_calls registra cada acción del agente, incluidas las denegaciones; sync_runs registra cada extracción de Stripe.

La consola de administración no tiene contraseña. Cloudflare Access termina la identidad en el borde, y src/web/auth.ts verifica el JWT reenviado correctamente: la firma contra el JWKS en vivo del equipo (almacenado en caché por aislado, con una recuperación forzada ante un id de clave desconocido), alg fijado a RS256, más audiencia, emisor, exp y nbf. La presencia de la cabecera no prueba nada y nunca se trata como prueba. Si se configura mal, el middleware falla en modo cerrado y bloquea a todos, incluido a ti. Esa es la dirección correcta en la que fallar.

El renderizador de HTML de correo está escrito a mano (core/render-doc.ts) en lugar de usar @tiptap/html, cuyo punto de entrada de servidor necesita happy-dom y no se ejecuta dentro de workerd. Resultó ser la mejor respuesta de todos modos: el recorredor inserta en línea cada estilo (Gmail elimina <style>) y emite tablas anidadas para los botones (Outlook ignora el padding en <a>), algo que la serialización HTML genérica no haría.

El registro completo de decisiones, incluidas las alternativas que se rechazaron y por qué, está en docs/MEMORY.md.


Modelo de consentimiento 🔐

Tres ámbitos independientes. Una elección restrictiva nunca escala a una amplia.

Ámbito

Almacenado como

Efecto

Secuencia

fila sequence_optouts

Fuera de esa serie. Todo lo demás continúa.

Boletín

subscribers.status

Fuera del boletín. Las series siguen activas.

Global

fila suppressions

Fuera de todo. La vía de escape legal.

Solo una baja explícita de "todo", un rebote duro o una queja escribe una supresión global.

Los envíos de secuencia ignoran deliberadamente status = 'unsubscribed', porque ese indicador tiene ámbito de boletín: alguien que abandonó el boletín sigue recibiendo la serie de incorporación que solicitó. El correo transaccional (recibos, descargas) ignora por completo el consentimiento de marketing y solo se bloquea por una dirección muerta o una queja por spam. Un recibo no es marketing, y un cliente dado de baja sigue necesitando su descarga.


El editor ✍️

Texto enriquecido por bloques, TipTap v3, vanilla (sin React). Abre el borrador sembrado "Borrador: todo lo que el editor puede hacer" para verlo todo.

  • / en una línea → menú de bloques: encabezados, listas, lista de verificación, cita, código, tabla, alternar, divisor, imagen, YouTube, botón CTA

  • @ → campos de personalización como nodos reales, así que first_name no se puede escribir mal

  • Arrastra el asa en el margen izquierdo para reordenar; mayús-clic selecciona varios bloques

  • Suelta o pega una imagen en cualquier lugar → se sube a R2, se inserta cuando la URL regresa

  • Los bloques de código tienen resaltado de sintaxis (15 idiomas, incl. Ruby, Elixir, TS, SQL)

  • Selecciona texto para el menú de burbuja; selecciona un botón y el menú de burbuja se convierte en su URL y selector de color

Los botones y las etiquetas de combinación son nodos personalizados construidos específicamente para correo. Un CTA se renderiza como una tabla anidada, y cada estilo se inserta en línea. Una etiqueta de combinación es un nodo en lugar de texto {{first_name}} sin procesar, porque un error tipográfico en texto sin procesar envía "Hola {{frist_name}}" a toda la lista.

Los cuerpos se almacenan como JSON de TipTap en body_json. El markdown heredado en body_md aún se renderiza y se convierte en el momento en que lo abres en el editor. Nada se migra en masa, porque una migración masiva que sale mal se lleva el archivo consigo.

El paquete del cliente tiene ~226KB comprimidos con gzip y se carga solo en las dos pantallas que componen correo. Todo lo demás en la consola de administración se renderiza en el servidor con cero JavaScript.

Hay una prueba de humo del navegador (bun run smoke, 33 comprobaciones) que maneja Chromium real, porque una opción de extensión renombrada falla silenciosamente en el navegador y el campo del cuerpo simplemente nunca se guarda. Nada del lado del servidor puede detectar eso.


Condúcelo desde Claude Code 🤖

El Worker sirve un servidor MCP en POST /mcp/<secret>: 93 herramientas que cubren todo el mailer, así que un agente puede segmentar, redactar y enviar boletines, construir secuencias, leer el rendimiento de las campañas y conciliar Stripe.

# 1. a path secret (this is what makes the endpoint exist at all)
openssl rand -hex 24                      # → put in .dev.vars as MCP_PATH_SECRET

# 2. an admin-scoped key — POST /seed prints one, or use apikey_create

# 3. point Claude Code at it
claude mcp add --transport http --scope local \
  --header "Authorization: Bearer $BIG_MAILER_KEY" \
  big-mailer "http://localhost:8787/mcp/$MCP_PATH_SECRET"

Tres puertas, la más barata primero. Un secreto de ruta imposible de adivinar comparado en tiempo constante (un fallo devuelve 404, no 403, porque una URL que nadie adivinó debería parecer que no hay nada ahí), luego una clave portadora con ámbito admin (las claves de send transaccional no pueden alcanzarla), y luego guardas por herramienta. Cada llamada aterriza en mcp_calls, incluidas las denegaciones.

Los envíos irreversibles necesitan una verificación previa. broadcast_send se niega sin un token de broadcast_preflight: de un solo uso, caducidad de 10 minutos, invalidado por cualquier edición del contenido o de la audiencia. Igual para sequence_activate. Además, MCP_ALLOW_SEND es "false" en producción, así que MCP puede leer y redactar todo pero no puede poner correo en el cable hasta que lo actives deliberadamente. Volver a apagarlo es el interruptor de apagado instantáneo.

Los agentes deberían leer bigmailer://conventions antes de tocar el consentimiento. La baja con ámbito no es la forma que espera nada entrenado con ESP normales, y hacerlo mal es exactamente el fallo que este proyecto se construyó para evitar.


Stripe → atribución de campaña 💳

Configura STRIPE_SECRET_KEY (restringida, de solo lectura en cargos/reembolsos/clientes). Un cron diario a las 09:17 UTC extrae los nuevos cargos y acredita cada uno al último toque de atribución del comprador, o a metadata.campaign cuando el cargo lleva uno. Idempotente con el id de cargo de Stripe, así que las re-ejecuciones y los rellenos superpuestos son inofensivos.

stripe_sync_preview lo prueba en seco, sales_unattributed es la lista de trabajo de lo que la heurística no pudo ubicar, y sync_runs_list demuestra que el trabajo nocturno está realmente ejecutándose.


Comandos ▶️

bun run dev

Compila el paquete del cliente y luego sirve en :8787

bun run watch:client

Recompila el paquete del editor al cambiar (junto con dev)

bun run smoke

Prueba de humo del editor en el navegador. Necesita dev en ejecución

bun run db:migrate

Aplica migraciones al D1 local

bun run db:generate

Genera una migración después de editar src/db/schema.ts

bun run db:studio

Drizzle Studio contra la base de datos local

bun run typecheck

Comprueba tipos del Worker y del paquete del navegador por separado

bun run deploy

Compila y luego wrangler deploy --env production. Lee la sección de abajo primero


Enviar de verdad 📮

Copia .dev.vars.example a .dev.vars, añade una clave de Resend y establece EMAIL_PROVIDER=resend. Apunta el webhook de Resend a /webhooks/resend para que los rebotes y las quejas se supriman correctamente. Sin ese webhook, las direcciones incorrectas nunca se suprimen y tu reputación de envío se degrada silenciosamente, que es la forma lenta de perder la entregabilidad de todo el dominio.

Los secretos van en .dev.vars (ignorado por git) o mediante wrangler secret put. Nunca en wrangler.jsonc, y nunca en .dev.vars.example.

⚠️ Antes de desplegar

Aún no está desplegado, y hay configuración real entre aquí y la producción:

  • DEV_AUTH_BYPASS=true se encuentra en las variables de wrangler.jsonc de nivel superior para que la aplicación se pueda ejecutar localmente. --env production lo establece en false. Un wrangler deploy sin más publica una consola de administración sin autenticar, por eso bun run deploy tiene --env production codificado. No intentes saltártelo.

  • database_id es un marcador de posición. Crea una base de datos D1 real con wrangler d1 create.

  • Crea el bucket de R2 (big-mailer-media) y las dos colas (big-mailer-send, big-mailer-dlq). Las colas necesitan el plan de pago de Workers de 5 $/mes.

  • Establece PUBLIC_URL al host real. Se incrusta en los enlaces de seguimiento y las URL de imágenes en el momento del envío, por lo que un valor incorrecto hace que se envíe correo permanentemente roto a mensajes ya entregados. No se puede corregir a posteriori.

  • MCP_PATH_SECRET debe ser un secreto real (wrangler secret put), no una variable. Si no está definido, el endpoint MCP devuelve 404, que es el comportamiento seguro por defecto. Actívalo deliberadamente.

  • Cloudflare Access necesita una aplicación Allow y varias aplicaciones Bypass. Proteger todo el nombre de host con una única política Allow también protege el píxel de seguimiento, los formularios de registro, el centro de preferencias, los webhooks y el MCP, lo que significa que cada píxel de seguimiento de cada correo que envías redirige a una pantalla de inicio de sesión, permanentemente, para el correo ya entregado. Access aplica primero la ruta más específica, por lo que /t, /f, /p, /api, /webhooks y /mcp necesitan cada uno su propia aplicación Bypass. Cada una de ellas lleva su propia autenticación o es pública por diseño.

  • No hay suite de pruebas del lado del servidor. bun run smoke cubre el editor. docs/SPEC.md está escrito como requisitos numerados y comprobables, diseñados para poder hacerse ejecutables.


Documentación 📚

docs/PROJECT.md

El problema, a quién va dirigido y qué queda explícitamente fuera de alcance

docs/ARCHITECTURE.md

Diseño del sistema, modelo de datos y el proceso de envío

docs/SPEC.md

Requisitos de comportamiento numerados. La referencia para el comportamiento previsto

docs/MEMORY.md

Registro de decisiones: qué se eligió, qué se rechazó y por qué


Contribuciones 🤝

Los informes de errores, las correcciones de correctitud y las correcciones de renderizado en clientes de correo son muy bienvenidos. La multitenencia, el constructor de arrastrar y soltar y el SMTP autogestionado están fuera de alcance a propósito. Consulta CONTRIBUTING.md antes de abrir un PR, y SECURITY.md si has encontrado una vulnerabilidad (por favor, repórtala de forma privada, no como un issue).

Hacer fork está realmente recomendado. Es lo suficientemente pequeño como para leerlo de principio a fin y hacerlo tuyo. La participación está cubierta por el Código de Conducta.

Licencia 📄

MIT © Rob Conery

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    C
    quality
    B
    maintenance
    MCP server that exposes the complete Libredesk REST API (54 endpoints) as tools, enabling natural language management of conversations, contacts, agents, teams, and more for the open-source customer support desk.
    54
    11
    3
    MIT
  • A
    license
    A
    quality
    F
    maintenance
    An MCP server for the Resend email API, enabling AI assistants to send emails, manage contacts, audiences, and domains through natural language.
    18
    44
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    Remote MCP server for the Transmit email platform, enabling email sending, contact management, template and campaign operations via natural language.
  • F
    license
    C
    quality
    D
    maintenance
    Comprehensive MCP server for Mailchimp Marketing API v3.0 with over 104 tools and 15+ React UI apps, enabling management of campaigns, audiences, ecommerce, automations, reports, and more via natural language.
    100
    1

View all related MCP servers

Related MCP Connectors

  • GibsonAI MCP server: manage your databases with natural language

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

  • Markdown-first MCP server for Notion API with 8 composite tools and 39 actions.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/robconery/big-mailer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server