big-mailer
big-mailer 📬
Рассылки, серии писем и транзакционные письма в одном Cloudflare Worker. Самостоятельно размещаемая замена платному ESP, где список, отправка и данные о вовлечённости остаются у вас.
Статус: функционально завершён и запускается локально, но ещё не развёрнут. Он создан для одного оператора и не является мультитенантным — это осознанное решение, а не упущение. См. Перед развёртыванием для честного списка того, что осталось.

Панель после загрузки демо-данных. Согласие по областям — это панель, которая имеет значение: два человека вышли из отдельной серии и остались в списке. В обычном ESP эти два числа были бы одинаковыми.
Идея 💡
Каждый ESP рассматривает отписку как один переключатель. Кто-то заканчивает вашу приветственную серию, нажимает «отписаться», чтобы остановить её, и тихо навсегда покидает вашу рассылку. Вы никогда об этом не узнаете. Просто число уменьшается.
Здесь согласие ограничено областью. Выход из одной серии убирает вас из этой серии. Выход из рассылки не отменяет серию, на которую вы сознательно подписались. Только явное «отписаться от всего», жёсткий отказ или жалоба на спам полностью удаляют человека.
Именно эта асимметрия и является причиной существования проекта, и всё остальное в кодовой базе устроено так, чтобы её нельзя было случайно нарушить.
Related MCP server: Resend MCP Server
Запуск 🚀
bun install
bun run db:migrate # applies migrations to the local D1 database
bun run dev # http://localhost:8787Откройте http://localhost:8787 и нажмите Загрузить демо-данные: двенадцать человек, две активные серии, отправленная рассылка. Затем откройте Исходящие, чтобы прочитать письма, которые «ушли».
Ничего не покидает ваш компьютер. EMAIL_PROVIDER=console — это локальное значение по
умолчанию, которое записывает полностью отрендеренные письма во встроенные «Исходящие»
вместо их отправки. Никаких API-ключей не нужно, и невозможно случайно отправить письмо
реальному человеку, пока вы экспериментируете.
Попробуйте то, для чего это создано 🎯
Подписчики → выберите кого-нибудь → Откройте его центр предпочтений
Добавьте
?scope=sequence:2к этому URL. Так выглядит ссылка внутри письма из серии.Нажмите Остановить только эту серию
Вернитесь на страницу подписчика: он всё ещё
active, всё ещё в рассылке, но вышел ровно из одной серииСогласие показывает всех, кто покинул одну серию, по сравнению с (пустым) списком полностью ушедших людей
После этого отправьте рассылку — они её всё ещё получат. В этом весь аргумент.
Задержки в сериях указываются в днях, и первый шаг по умолчанию равен 0 (приходит при подписке), а последующие шаги по умолчанию равны 1. Это означает, что созданная серия не завершится, пока вы за ней наблюдаете, поэтому на панели есть Перемотать время вперёд (только локально): он переносит все ожидающие шаги на текущий момент и выполняет тик. Воспользуйтесь этим, и вы увидите, как шаг 2 пропускает людей, покинувших эту серию.
Что здесь 🗂
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Примерно 16 тысяч строк TypeScript. bun run typecheck проверяет Worker и браузерный
бандл по отдельности и проходит без ошибок.
Архитектура в двух словах 🧱
Cloudflare Workers · D1 (SQLite) через Drizzle · Queues для фан-аута отправки · Cron Triggers
для планирования · R2 для медиа · Hono + JSX серверный рендеринг админки · Cloudflare Access
для аутентификации · подключаемый порт EmailProvider с адаптерами console и Resend.
Решения, о которых стоит знать и почему:
Каждая строка messages материализуется до того, как будет отправлено хоть одно письмо.
Рассылка заранее формирует полный список получателей, записывает по строке на каждое
предполагаемое письмо и только потом отправляет в очередь. Это делает рассылку
возобновляемой после сбоя, идемпотентной при повторах в очереди и проверяемой впоследствии.
Ленивое разрешение получателей во время отправки дешевле, но превращает любой сбой в
середине рассылки в невосстановимый беспорядок.
Согласие проверяется непосредственно перед вызовом провайдера, а не при постановке в очередь. Очередь может доставить сообщение через несколько минут после его создания, и за это время кто-то может отписаться. Проверка при постановке в очередь привела бы к тому, что письмо всё равно бы ушло.
Параллельность очереди зафиксирована на 6. Один батч — это один запрос к провайдеру, поэтому параллельность батчей и есть частота запросов. Если оставить значение по умолчанию, Cloudflare Queues автоматически масштабируется до 250 одновременных потребителей, заваливает лимит Resend в 10 запросов/с ошибками 429, сжигает все три повтора и отправляет в мёртвую очередь вполне нормальные письма. Шесть батчей по 100 оставляют запас примерно в 600 писем/с, оставаясь в пределах лимита.
Подавление привязано к адресу электронной почты, а не к подписчику. У транзакционных
получателей и отскочивших адресов часто вообще нет строки подписчика, поэтому флаг
subscribers.status молча бы их пропустил.
Всё, что можно наблюдать, — это строка в D1, а не запись в логе. Логи Workers
хранятся 3–7 дней. Аудит-трейл с недельным хранением — это не аудит. mcp_calls
записывает каждое действие агента, включая отказы; sync_runs записывает каждое
извлечение из Stripe.
В админ-консоли нет пароля. Cloudflare Access завершает идентификацию на периметре,
а src/web/auth.ts правильно проверяет пересланный JWT: подпись по живому JWKS команды
(кэшируется на изолят, с принудительным повторным запросом при неизвестном key id),
alg зафиксирован как RS256, плюс аудитория, издатель, exp и nbf. Наличие заголовка
ничего не доказывает и никогда не считается доказательством. При неправильной настройке
промежуточное ПО отказывает в закрытом режиме и блокирует всех, включая вас. Это
правильное направление отказа.
HTML-рендерер писем написан вручную (core/render-doc.ts) вместо использования
@tiptap/html, чья серверная точка входа требует happy-dom и не работает внутри
workerd. В итоге это оказалось лучшим решением: обходчик инлайнит все стили (Gmail
удаляет <style>) и генерирует вложенные таблицы для кнопок (Outlook игнорирует
padding у <a>), чего не сделала бы обычная HTML-сериализация.
Полный журнал решений, включая отклонённые альтернативы и причины, — в
docs/MEMORY.md.
Модель согласия 🔐
Три независимые области. Узкий выбор никогда не перерастает в широкий.
Область | Где хранится | Эффект |
Серия | строка | Выход из этой серии. Всё остальное продолжается. |
Рассылка |
| Выход из рассылки. Серии продолжают идти. |
Глобально | строка | Выход из всего. Юридический запасной выход. |
Только явное «отписаться от всего», жёсткий отказ или жалоба создают глобальную подавление.
Отправка серий намеренно игнорирует status = 'unsubscribed', потому что этот флаг
относится к рассылке: человек, покинувший рассылку, всё равно получит приветственную
серию, на которую подписался. Транзакционные письма (чеки, загрузки) полностью
игнорируют маркетинговое согласие и блокируются только мёртвым адресом или жалобой
на спам. Чек — это не маркетинг, и отписавшемуся клиенту всё равно нужна его загрузка.
Редактор ✍️
Блочный форматированный текст, TipTap v3, ванильный (без React). Откройте черновик «Черновик: всё, что умеет редактор», чтобы увидеть всё.
/в строке → меню блоков: заголовки, списки, чек-листы, цитаты, код, таблицы, переключатели, разделители, изображения, YouTube, кнопка CTA@→ поля персонализации как настоящие узлы, так чтоfirst_nameневозможно написать с ошибкойПеретащите маркер на левом поле, чтобы изменить порядок; shift-клик выделяет несколько блоков
Перетащите или вставьте изображение в любом месте → загружается в R2, вставляется, когда URL будет готов
Блоки кода подсвечиваются синтаксисом (15 языков, включая Ruby, Elixir, TS, SQL)
Выделите текст для всплывающего меню; выберите кнопку — и всплывающее меню станет редактором её URL и цвета
Кнопки и merge-теги — это пользовательские узлы, созданные специально для
электронной почты. CTA отображается как вложенная таблица, и все стили инлайновые.
Merge-тег — это узел, а не сырой текст {{first_name}}, потому что опечатка в
тексте отправит «Привет, {{frist_name}}» всему списку.
Тела хранятся как TipTap JSON в body_json. Устаревшая разметка в body_md
по-прежнему рендерится и конвертируется в момент открытия в редакторе. Ничего не
мигрирует массово, потому что массовая миграция, пошедшая не так, утащит за собой
и архив.
Клиентский бандл — ~226 КБ в gzip и загружается только на двух экранах, где составляются письма. Всё остальное в админ-консоли рендерится на сервере без единой строчки JavaScript.
Есть браузерный смоук-тест (bun run smoke, 33 проверки), управляющий настоящим
Chromium, потому что переименованный параметр расширения молча не работает в
браузере, и поле тела просто не сохраняется. Ничто на сервере не может это поймать.
Управление из Claude Code 🤖
Worker предоставляет MCP-сервер на POST /mcp/<secret>: 93 инструмента,
покрывающих весь почтовик, так что агент может нарезать сегменты, составлять и
отправлять рассылки, строить серии, читать эффективность кампаний и сверять 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"Три уровня защиты, от дешёвого к дорогому. Секрет в пути, который невозможно
угадать, сравнивается за константное время (промах возвращает 404, а не 403,
потому что URL, который никто не угадал, должен выглядеть так, будто его не
существует), затем bearer-ключ с правами admin (транзакционные send-ключи до
него не допускаются), а затем поштучные проверки инструментов. Каждый вызов
попадает в mcp_calls, включая отказы.
Необратимые отправки требуют предварительной проверки. broadcast_send
отказывается работать без токена от broadcast_preflight: одноразовый, срок
действия 10 минут, аннулируется при любом изменении контента или аудитории.
То же самое для sequence_activate. Кроме того, MCP_ALLOW_SEND имеет значение
"false" в продакшене, поэтому MCP может читать и составлять что угодно, но не
может отправить письмо, пока вы намеренно не переключите его. Обратное
переключение — это мгновенный аварийный выключатель.
Агенты должны прочитать bigmailer://conventions, прежде чем касаться согласия.
Ограниченная отписка — это не та форма, которую ожидает кто-то, обученный на
обычных ESP, и ошибиться здесь — ровно тот сбой, которого этот проект и призван
избежать.
Stripe → атрибуция кампаний 💳
Установите STRIPE_SECRET_KEY (ограниченный, только чтение по платежам/возвратам/
клиентам). Ежедневный cron в 09:17 UTC подтягивает новые платежи и относит каждый
к последнему касанию атрибуции покупателя или к metadata.campaign, если платёж
его содержит. Идемпотентность по идентификатору платежа Stripe, поэтому повторные
запуски и перекрывающиеся бэкфиллы безвредны.
stripe_sync_preview выполняет пробный прогон, sales_unattributed — это список
задач, которые эвристика не смогла разместить, а sync_runs_list доказывает, что
ночная задача действительно выполняется.
Команды ▶️
| Собрать клиентский бандл, затем запустить сервер на :8787 |
| Пересобрать бандл редактора при изменении (вместе с |
| Браузерный смоук-тест редактора. Требует запущенного |
| Применить миграции к локальной D1 |
| Сгенерировать миграцию после редактирования |
| Drizzle Studio для локальной базы данных |
| Проверка типов Worker и браузерного бандла по отдельности |
| Сборка, затем |
Настоящая отправка 📮
Скопируйте .dev.vars.example в .dev.vars, добавьте ключ Resend и установите
EMAIL_PROVIDER=resend. Укажите вебхук Resend на /webhooks/resend, чтобы отказы и
жалобы корректно подавлялись. Без этого вебхука плохие адреса никогда не будут
подавляться, и ваша репутация отправителя будет тихо деградировать — это медленный
путь к потере доставляемости для всего домена.
Секреты хранятся в .dev.vars (в gitignore) или через wrangler secret put. Никогда
в wrangler.jsonc и никогда в .dev.vars.example.
⚠️ Перед развёртыванием
Ещё не развёрнуто, и между этим состоянием и продакшеном есть реальная настройка:
DEV_AUTH_BYPASS=trueнаходится в переменных верхнего уровняwrangler.jsonc, чтобы приложение можно было запускать локально.--env productionустанавливает его в false. Голыйwrangler deployпубликует неаутентифицированную админ-консоль, именно поэтомуbun run deployжёстко прописывает--env production. Не обходите это.database_id— это заглушка. Создайте настоящую базу D1 с помощьюwrangler d1 create.Создайте бакет R2 (
big-mailer-media) и две очереди (big-mailer-send,big-mailer-dlq). Для очередей нужен платный план Workers за $5/мес.Установите
PUBLIC_URLна реальный хост. Он встраивается в трекинговые ссылки и URL изображений в момент отправки, поэтому неверное значение навсегда отправляет сломанные письма уже доставленной почте. Это нельзя исправить постфактум.MCP_PATH_SECRETдолжен быть настоящим секретом (wrangler secret put), а не переменной. Если он не задан, эндпоинт MCP возвращает 404 — это безопасное поведение по умолчанию. Включайте его осознанно.Cloudflare Access требует одно приложение Allow и несколько приложений Bypass. Защита всего хоста одной политикой Allow также защищает трекинговый пиксель, формы регистрации, центр предпочтений, вебхуки и MCP, а это значит, что каждый трекинговый пиксель в каждом отправленном письме будет навсегда перенаправляться на экран входа — для уже доставленной почты. Access сопоставляет наиболее конкретный путь первым, поэтому
/t,/f,/p,/api,/webhooksи/mcp— каждому нужен свой Bypass-апп. Каждый из них либо несёт собственную аутентификацию, либо публичен по замыслу.Нет серверного набора тестов.
bun run smokeпокрывает редактор.docs/SPEC.mdнаписан как нумерованные, тестируемые требования, сформированные так, чтобы их можно было исполнять.
Документация 📚
Проблема, для кого это, и что явно вне рамок | |
Системный дизайн, модель данных и конвейер отправки | |
Нумерованные поведенческие требования. Эталон ожидаемого поведения | |
Журнал решений: что выбрано, что отклонено и почему |
Участие 🤝
Баг-репорты, исправления корректности и исправления рендеринга в почтовых клиентах
очень приветствуются. Мультитенантность, конструктор с перетаскиванием и собственный
SMTP намеренно вне рамок. См. CONTRIBUTING.md перед открытием
PR и SECURITY.md, если вы нашли уязвимость (пожалуйста, сообщите
о ней приватно, а не как issue).
Форкинг действительно поощряется. Этот проект достаточно мал, чтобы прочитать его целиком и сделать своим. Участие регулируется Кодексом поведения.
Лицензия 📄
MIT © Rob Conery
This server cannot be installed
Maintenance
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
- AlicenseCqualityBmaintenanceMCP 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.54113MIT
- AlicenseAqualityFmaintenanceAn MCP server for the Resend email API, enabling AI assistants to send emails, manage contacts, audiences, and domains through natural language.1844MIT

xmit-mcpofficial
FlicenseNot gradedqualityDmaintenanceRemote MCP server for the Transmit email platform, enabling email sending, contact management, template and campaign operations via natural language.- FlicenseCqualityDmaintenanceComprehensive 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.1001
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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