Lightweight, extensible, stateful campaign mailer and delivery engine for MiyuLabs. Built to reliably email waitlists and product announcements from a local machine or a $5 VPS without Redis or external job queues, which is more than enough for up to tens of thousands of sends. Imports recipients, renders campaign-specific templates, and delivers through Resend with SQLite-backed durable job state, batched rate-limited sending, automatic retries with exponential backoff, and idempotent campaign delivery.
| Layer | Tech |
|---|---|
| Language | Python 3.10+ |
| Queue & State Store | SQLite 3 (WAL mode, foreign keys enabled) |
| Email Provider | Resend API (/emails/batch with permissive validation) |
| Templating Engine | Jinja2 (HTML + responsive typography + plain text) |
| Deliverability Verification | dnspython (Syntax + MX + RFC 7505 Null MX / RFC 5321 fallback) |
| Rate Limiter | Thread-safe Token Bucket (threading.Lock + monotonic clock) |
| Environment & Packaging | uv / pyproject.toml (Setuptools) |
- Embedded SQLite Job Queue: SQLite handles both persistent recipient records and durable send queues. Zero external brokers (no Redis, Celery, or RabbitMQ) needed.
- Idempotent by Design: Enforced by a
UNIQUE(recipient_id, campaign)schema constraint. Double-emailing someone for the same campaign is physically impossible. - Permissive Batch Delivery: Bundles up to 100 emails per HTTP request with
x-batch-validation: permissive. If 98 pass and 2 fail validation, Resend sends the 98 and Dispatch isolates the 2 failures in one single API call without looping one-by-one. - Bounce Protection via Enqueue Validation: Verifies email syntax, rejects RFC 7505 Null MX records (
. IN MX 0 .), and resolves domain MX records (with A-record fallback per RFC 5321) with in-process caching before queueing, keeping bounce rates safely below 4%. - Exponential Backoff with Full Jitter: Resilient retry scheduling with jitter (
random(0, min(cap, base * 2^attempts))) and dynamic adoption of providerretry-afterheader delay floors. - Automatic Crash & Interruption Recovery: Stale jobs interrupted in
sendingstate during unexpected shutdowns or restarts are automatically reclaimed and reset topendingon the next run. - Non-Destructive Dry Run: Renders, substitutes metadata, logs, and validates emails end-to-end without mutating queue records or calling provider endpoints.
Source.fetch() -> Recipient(s) -> StateStore (SQLite: recipients)
|
(Linked via campaign_recipients)
|
RecipientValidator (syntax + MX) at enqueue time
|
StateStore (SQLite: campaign_sends, the queue)
|
SendEngine claims a batch
|
Template.render(recipient) -> RenderedEmail, per job
|
EmailProvider.send_batch(...) (Token Bucket, permissive mode)
|
StateStore records sent / failed (+ backoff) / dead
| Abstraction | Interface | Current Implementation |
|---|---|---|
Source |
fetch() -> Iterable[Recipient] |
JSONFileSource (waitlist JSON array parser) |
RecipientValidator |
validate(recipient) -> ValidationResult |
SyntaxAndMXValidator (graceful fallback to SyntaxOnlyValidator) |
Template |
render(recipient) -> RenderedEmail |
LaunchTemplate (HTML + plain text pre-order discount) |
EmailProvider |
send(...), send_batch(...) |
ResendProvider (permissive batching + token bucket rate limit) |
StateStore |
Durable SQLite State & Queue | StateStore (WAL mode, atomic transactions, crash recovery) |
- Python 3.10+
uv(recommended) orpip
uv syncCopy the example and fill in your values:
cp .env.example .env| Variable | Description | Default |
|---|---|---|
RESEND_API_KEY |
Resend API key (re_...) |
Required |
FROM_EMAIL |
Verified sender email address | Required |
FROM_NAME |
Sender display name | MiyuLabs |
REPLY_TO |
Reply-To address (optional) | None |
DISPATCH_DB_PATH |
Path to the SQLite state database file | dispatch.db |
PRE_ORDER_URL |
Destination URL for the primary CTA button | https://miyulabs.in/ |
EXPLORE_URL |
Destination URL for secondary exploration link | https://miyulabs.in/explore |
RATE_LIMIT_PER_SECOND |
Max API calls/sec against Resend (batch calls count as 1) | 8 |
MAX_BATCH_SIZE |
Maximum recipients per batch request (Resend limit: 100) | 100 |
MAX_ATTEMPTS |
Maximum retry attempts for transient errors before job dies | 5 |
BASE_BACKOFF_SECONDS |
Initial backoff multiplier for retry jitter | 2 |
MAX_BACKOFF_SECONDS |
Maximum cap for exponential retry backoff | 600 |
VALIDATE_DELIVERABILITY |
Perform DNS MX check before queueing | true |
MX_LOOKUP_TIMEOUT_SECONDS |
DNS resolution timeout per domain | 3 |
Load environment variables into your active shell session:
set -a && source .env && set +aAll commands support --db <path> and -v / --verbose either before or after the subcommand.
| Command | Arguments | Description |
|---|---|---|
dispatch import |
--campaign <id>--file <path>[--source waitlist_json] |
Loads recipients from an export file into local state and links them to the specified campaign |
dispatch send |
--campaign <id>[--template <id>][--template-vars <json_or_file>][--dry-run][--watch][--max-wait <sec>][--skip-validation] |
Enqueues eligible recipients and executes email delivery for the given campaign |
dispatch status |
--campaign <id>[--limit <n>] |
Displays campaign stats, failures, and skipped records |
Loads and normalizes entries, deduplicating emails within the file and against existing database records, and linking them to a campaign:
uv run dispatch import --campaign launch-2026-09 --source waitlist_json --file data/waitlist.sample.jsonRenders every email using the generic --template-vars config, logs subject lines and recipients, and runs deliverability validation without calling Resend or modifying database queue states:
uv run dispatch send --campaign launch-2026-09 --template launch --template-vars '{"solo_price": "5,999", "partner_price": "11,999"}' --dry-runClaims batches, executes delivery via Resend, and records results:
uv run dispatch send --campaign launch-2026-09 --template launch --template-vars '{"solo_price": "5,999", "partner_price": "11,999"}'Inspect active progress, sent counts, retryable failures, and validation skips:
uv run dispatch status --campaign launch-2026-09If jobs enter backoff due to transient network or provider issues:
- One-off / Interactive: Pass
--watchto keep the process running until all retry timers expire and the queue drains:uv run dispatch send --campaign launch-2026-09 --watch --max-wait 1800
- Production / Scaled: Run
dispatch sendon a systemd timer or cron job every few minutes. Each invocation claims eligible retries and exits immediately when pending work is done:*/5 * * * * cd /opt/dispatch && set -a && source .env && set +a && uv run dispatch send --campaign launch-2026-09
Subclass dispatch.sources.base.Source and implement fetch() to yield Recipient models. Register it in SOURCES in dispatch/cli.py:
from dispatch.sources.base import Source
from dispatch.models import Recipient
class CsvSource(Source):
name = "csv"
def fetch(self):
# Read file, yield Recipient(email=..., metadata={...})
...Note: Any extra fields in source records are automatically preserved in Recipient.metadata.
LaunchTemplate.render() reads recipient.metadata.get("name") for personalized greetings. To branch copy (e.g. based on plan interest or user tier), subclass dispatch.templates.base.Template or modify template logic in dispatch/templates/:
class CustomTemplate(Template):
def render(self, recipient: Recipient) -> RenderedEmail:
plan = recipient.metadata.get("planInterest", "solo")
# Render custom HTML / subject based on plan
...To use Amazon SES, Postmark, or SMTP, subclass dispatch.providers.base.EmailProvider. Implement send() (single send) and optionally override send_batch() if the provider offers a bulk endpoint:
from dispatch.providers.base import EmailProvider, SendResult
class SesProvider(EmailProvider):
def send(self, *, to, subject, html, text, from_email, **kwargs) -> SendResult:
...Subclass dispatch.validation.RecipientValidator to add third-party validation APIs or reject disposable email providers:
from dispatch.validation import RecipientValidator, ValidationResult
class DisposableBlockerValidator(RecipientValidator):
def validate(self, recipient: Recipient) -> ValidationResult:
...- Resend Domain Verification: Verify your sending domain in the Resend dashboard before dispatching. Emails sent from unverified domains will fail immediately.
- DNS Deliverability Validation: Deliverability checking queries DNS MX and A records while actively rejecting RFC 7505 Null MX records (domains that explicitly publish that they accept no mail). Results are cached in-memory per unique domain. For large waitlists, enqueueing may take a few moments on the first run while unique domains are resolved. Disable with
--skip-validationorVALIDATE_DELIVERABILITY=falseif needed. - Resend Rate Limits: Resend enforces a default rate limit of 10 requests/second shared across the entire account. Because a batch of up to 100 emails counts as a single request, Dispatch can deliver up to 1,000 emails/second under this limit. Set
RATE_LIMIT_PER_SECONDto 8 or lower if other services share the same Resend API key. - Unsubscribe & Compliance: Dispatch is designed for opt-in waitlist announcements and does not include an automatic unsubscribe link handler out of the box. For recurring marketing broadcasts, include
List-Unsubscribeheaders or process opt-outs usingStateStore.mark_skipped().
Run the automated test suite covering state management, batching, and validation:
uv run --with pytest pytestGNU LGPL v2.1. Copyright (c) 2026 MiyuLabs.