MCP-native, IMAP-first email campaigns. Markdown in → thousands out. Drive everything from an AI agent (VS Code or any MCP client). Tracks opens/clicks, appends to your Sent via IMAP, and handles bounces/replies.
- Stack: Python 3.13 • Django 5+ • Celery 5+ • Postgres (recommended)
- Core surfaces: MCP tools (primary), Django Admin, CLI.
git clone <repo_url> mcpigeon
cd mcpigeon
python3 -m venv env && source env/bin/activate
pip install -r requirements.inMinimal settings in MCPigeon/settings.py:
# Public base for tracked URLs & pixel
CAMPAIGNS_PUBLIC_BASE_URL = "https://your.app" # required by template tags
# Celery (example: Redis)
CELERY_BROKER_URL = "redis://localhost:6379/0"
CELERY_RESULT_BACKEND = "redis://localhost:6379/0"
# MCP recipient ingest per-call cap (default 1000)
MCP_MAX_RECIPIENT_BATCH = 1000(Template tags build click/pixel URLs off CAMPAIGNS_PUBLIC_BASE_URL.)
Database & superuser:
python manage.py migrate
python manage.py createsuperuserRun web + worker:
python manage.py runserver 0.0.0.0:8000
# new shell
export DJANGO_SETTINGS_MODULE=MCPigeon.settings
celery -A campaigns.sender:app worker -l infoThe task pipeline is idempotent and retries on transient DB errors; it sends a failure report email when a run completes.
- Mailbox: SMTP/IMAP creds +
sent_folder,bounce_folder. - Campaign:
name,subject,mailbox,template_markdown,status. - Recipient: unique per (
campaign,email), optionalname,unsubscribed,meta. - MessageInstance: one per recipient send; holds
message_id, timestamps, click count. - Link / LinkClick / OpenEvent / DeliveryEvent: tracking artifacts.
This repo exposes MCP tools you can mount in your MCP host (VS Code, Claude Desktop, Cursor/Cline/Continue, etc.). Tools:
mailboxes— CRUD + verify creds + optional remote provisioning.campaigns— CRUD + send, status, list_recipients, add_recipient, clone, post_recipients (bulk).campaign_mailbox— assign/switch a campaign’s mailbox.
Mounting (typical local config):
- Command: your MCP host’s “add local tool/server” pointing at the Python that imports
campaigns.mcp(stdio). - Env:
DJANGO_SETTINGS_MODULE=MCPigeon.settings(and your Django env vars). - CWD: repo root.
Create a mailbox, then verify creds
{"tool":"mailboxes","action":"create","payload":{
"name":"Sales","from_name":"Sales","from_email":"sales@your.app",
"smtp_host":"smtp.your.app","smtp_port":587,"smtp_starttls":true,
"smtp_username":"sales@your.app","smtp_password":"***",
"imap_host":"imap.your.app","imap_port":993,"imap_ssl":true,
"imap_username":"sales@your.app","imap_password":"***",
"sent_folder":"Sent","bounce_folder":"INBOX"
}}{"tool":"mailboxes","action":"verify","payload":{"id":1}}(Verify attempts real SMTP/IMAP logins and returns pass/fail + errors.)
Create a campaign
{"tool":"campaigns","action":"create","payload":{
"name":"September Promo",
"subject":"Save big this month",
"mailbox_id":1,
"template_markdown":"Hey {{ recipient.name|default:\"there\" }} — check this out!"
}}Bulk-import recipients (strings or objects)
{"tool":"campaigns","action":"post_recipients","payload":{
"campaign_id":123,
"on_conflict":"update_name",
"recipients":[
"Ada Lovelace <ada@ex.com>",
{"email":"grace@ex.com","name":"Grace Hopper"},
"alan@ex.com"
]
}}- Accepts up to
MCP_MAX_RECIPIENT_BATCHper call; returnsremainingfor pagination. - Validates emails; dedupes within the batch; can update names on conflicts.
Send (queued via Celery)
{"tool":"campaigns","action":"send","payload":{"campaign_id":123}}Check progress:
{"tool":"campaigns","action":"status","payload":{"campaign_id":123}}(Status reports sent/opened/bounced/clicks + last event.)
Switch a campaign’s mailbox
{"tool":"campaign_mailbox","action":"assign","payload":{"campaign_id":123,"mailbox_id":2}}Implementation note: each recipient gets a stable RFC5322 Message-ID like
<uuid.campaignId.recipientId@domain>, which the IMAP sync uses to reconcile replies/bounces.
- Add a Mailbox with working SMTP/IMAP creds.
- Create a Campaign (Markdown body).
- Add Recipients (or use MCP bulk ingest).
- Enqueue/send from Admin actions or use MCP/CLI. (Status and events are visible via related models.)
Send (sync or enqueue)
python manage.py campaign_send --campaign 123
python manage.py campaign_send --campaign 123 --dry-run
python manage.py campaign_send --name "September Promo" --enqueue --batch-size 200 --sleep 1.5--enqueuesplits recipients and schedules chunk tasks.
Enqueue directly
python manage.py campaign_enqueue --campaign 123 --chunk-size 300(Uses send_campaign_chunk.delay per chunk.)
IMAP sync (bounces/replies)
python manage.py campaign_imap_sync- Logs into each campaign mailbox’s
bounce_folder, processes UNSEEN, marks\Seen. - Classifies BOUNCE/DEFERRED/REPLY; sets
bounced_atand recordsDeliveryEvent. - Matches messages by our
Message-IDpattern<campaignId.recipientId.rnd@domain>.
Enable the tags by keeping campaigns/templatetags/campaigns.py in the app. Use:
Hey {{ recipient.name|default:"there" }}!
[Open link]({% track_url campaign recipient "https://example.com" %})
<img src="{% tracking_pixel message %}" width="1" height="1" style="display:none" alt="">{% track_url %}rewrites to a tracked redirect underCAMPAIGNS_PUBLIC_BASE_URL.{% tracking_pixel %}emits the 1×1 open-tracking URL.
- Task
campaigns.send_campaignis idempotent; re-runs skip already-sent recipients. - Per-recipient failures don’t abort the run; a summary email is sent at the end.
- On DB hiccups, the task retries with backoff/jitter.
Set up SPF/DKIM/DMARC, warm up with smaller batches + sleeps, include unsubscribe, and honor bounces/unsubs. (Clicks/opens depend on client behavior.)
Apache-2.0
Mail stays where it belongs: your mailbox. Your agent runs the show.
