me.squibble.email — Feature Overview
me.squibble.email is a secure, rate-limited, token-based email gateway for
AI agents and applications. It provides inbound IMAP inbox access and a
full outbound email pipeline — from API submission through SMTP delivery,
open/click tracking, and automated bounce handling.
Feature Matrix
Outbound sending
| Capability | Detail |
|---|---|
| Send endpoint | POST /api/v1/messages/send — returns 202 Accepted with per-recipient UUIDs |
| Per-recipient rows | One outbound_messages row per recipient; enables individual tracking, bounce attribution, and suppression |
| HTML auto-enhancement | Tracking pixel injected, all <a href> links rewritten to the tracking gateway |
| Sender binding | From, Sender, Reply-To, Return-Path are always the authenticated mailbox’s address — no caller override. The From display name is operator-configurable via mailboxes:update --display-name. |
| Input caps | ≤100 combined recipients, ≤1 MiB body, ≤998-octet subject |
| Idempotency | Idempotency-Key header with ≥24 h dedup window prevents duplicate sends on network retries |
| Per-token quotas | send_limits (hourly/daily) and allowed_recipient_domains on each JWT token |
Delivery queue
| Capability | Detail |
|---|---|
| PostgreSQL queue | SELECT FOR UPDATE SKIP LOCKED — no external broker needed |
| At-least-once delivery | claim → deliver → finalize pattern; reaper loop recovers crashed workers |
| TLS enforcement | Per-mailbox smtp_tls_mode: implicit (port 465) or starttls_required (port 587, fail-closed) |
| Retry policy | Exponential back-off up to 10 attempts; SMTP 5xx fast-fails immediately |
| PII scrubbing | Recipient addresses in SMTP error responses are redacted before error_log storage |
| One-click unsubscribe | List-Unsubscribe + List-Unsubscribe-Post headers injected automatically (RFC 8058 / Gmail/Yahoo 2024 requirements) |
Open & click tracking
| Capability | Detail |
|---|---|
| Open tracking | GET /track/open/{signed_token}.gif — 1×1 GIF, updates opened_at once |
| Click tracking | GET /track/click/{signed_token}?url=…&sig=… — 302 redirect, updates clicked_at once |
| HMAC-signed tokens | Raw UUIDs are never in tracking URLs; invalid signatures rejected without DB access |
| Rate limiting | 120 req/min per IP on tracking endpoints |
Bounce processing
| Capability | Detail |
|---|---|
| VERP authentication | Return-Path is bounce+{msg_id}.{hmac16}@domain; forged DSNs are quarantined |
| RFC 3463 classification | transient (4.x.x) / permanent (5.x.x) / unknown |
| Suppression list | Permanent bounces auto-insert to suppressions; future sends to that address get 422 |
| Suppression enforcement | Single batch query — no per-recipient round-trip at send time |
| Separate process | Bounce cron runs as a one-shot sync CLI (bounces:poll-once), scheduled externally |
Security & compliance
| Capability | Detail |
|---|---|
| Credential encryption | SMTP and IMAP passwords encrypted with MultiFernet (supports key rotation) |
| JWT scopes | messages:send scope required; tokens carry per-token recipient domain allowlists and send-rate caps |
| DKIM / SPF / DMARC | Handled at the MTA layer (see docs/runbooks/deliverability.md) |
| No open relay | Sender identity bound to the issuing mailbox at all times |
Observability
| Metric | Description |
|---|---|
outbound_messages_enqueued_total | Messages accepted by the send endpoint |
outbound_messages_sent_total | Successful SMTP deliveries |
outbound_messages_failed_total{reason} | Permanent failures by error class |
outbound_messages_bounced_total{type} | Bounces by classification |
smtp_delivery_duration_seconds | SMTP round-trip histogram |
queue_depth / queue_oldest_age_seconds | Queue health (sync collector at scrape time) |
tracking_opens_total / tracking_clicks_total | Engagement events |
bounce_cron_*_total | Bounce cron polled / rejected / suppressions inserted |
Admin CLI
mailboxes:create / :update / :destroy / :validate
tokens:issue / :revoke / :list
messages:list / :retry / :cancel
suppressions:list / :remove
bounces:poll-once
db:init / :migrate / :current / :history / :backup
Architecture at a glance
Agent / App
│
▼
POST /api/v1/messages/send ──► outbound_messages (status=queued)
│
async worker polls
│
SMTP delivery
│
┌─────────┴──────────┐
success failure
│ │
status=sent 5xx → status=failed
4xx → retry (backoff)
Recipient opens email
▼
GET /track/open/{token}.gif → opened_at set
Recipient clicks link
▼
GET /track/click/{token}?url=…&sig=… → clicked_at set → 302 to destination
MTA returns DSN
▼
bounces:poll-once (sync CLI, every 5 min)
│
├── HMAC valid + permanent → status=bounced, suppressions INSERT
├── HMAC valid + transient → status=bounced (no suppression)
└── HMAC invalid → Rejected-Bounces IMAP folder
Reference docs
| Doc | Purpose |
|---|---|
README.md | Getting started, local dev, CLI reference |
CONTRIBUTING.md | Coding standards and invariants (formerly AGENTS.md) |
docs/architecture.md | Technical deep-dive |
docs/adr/ | Architecture decisions (scope change, bounce topology, fan-out model) |
docs/runbooks/deliverability.md | DKIM/SPF/DMARC setup, bulk-sender compliance |
docs/runbooks/backup-posture.md | Backup strategy, restore procedures |
docs/runbooks/key-rotation.md | Fernet key rotation — three-phase procedure with re-encryption script |
docs/features/01_database_and_config.md | Phase 1: schema & CLI |
docs/features/02_outbound_api_and_html_parsing.md | Phase 2: send endpoint & HTML parsing |
docs/features/03_async_worker_queue.md | Phase 3: delivery worker |
docs/features/04_tracking_endpoints.md | Phase 4: open & click tracking |
docs/features/05_imap_bounce_processing.md | Phase 5: bounce processing & suppressions |