All notable changes to SquatchMail are documented here. Format follows Keep a Changelog; SquatchMail follows Semantic Versioning once it reaches 1.0.

[Unreleased]

Nothing yet.

[0.1.1] — 2026-07-15

Fixed

  • SNS webhooks no longer 500 on every delivery. SNS posts to HTTP/S endpoints with Content-Type: text/plain, which a host's Plug.Parsers matches no parser for and passes through with the body unread — so the documented endpoint :body_reader never fired, conn.assigns[:raw_body] was never set, and every SubscriptionConfirmation/Notification fell through to {:unsupported_message_type, nil} and a 500. The webhook route now captures its own raw body via the new SquatchMail.SNS.RawBodyPlug (mounted by squatch_mail_dashboard), independent of the host's Plug.Parsers. Hosts no longer need to wire up a :body_reader at all; an already-captured conn.assigns[:raw_body] is still honored. The controller now logs an actionable error instead of silently fabricating a body when the raw bytes are missing.
  • Idempotent SES provisioning recognizes SESv2's HTTP-400 "already exists" conflict. create configuration set returns the conflict as HTTP 400 with a bare {"message":"… already exists."} body — no __type, so structured detection missed it and re-provisioning aborted. Error-code detection now also reads the x-amzn-errortype response header, plus a narrowly-scoped 400-with-"already exists"-message backstop on the create call only (fires only when no structured error code is present, so it can't false-match unrelated validation errors). The header fix mirrors to the "not found" topic-lookup path too.

Changed

  • mix squatch_mail.install no longer patches the host endpoint's Plug.Parsers (the raw-body capture is now handled by the router), removing a fragile AST-surgery step and its "signature verification will fail until you wire this by hand" notices. SquatchMail.SNS.RawBodyReader remains as an optional endpoint-side :body_reader.

[0.1.0] — 2026-07-09

First release published to Hex. See FEATURES.md for the full parity checklist against LaraSend.

Added

  • The dashboard. One router macro (squatch_mail_dashboard) mounts the full LiveView dashboard with self-contained, content-hashed assets and three-layer access control (host-owned auth via :on_mount, HTTP Basic Auth fallback, refuse-by-default otherwise — see README.md's "Keeping the Forest Safe"). Pages: the Trail Log (live activity feed with stats, filters, and CSV export), the per-email Sighting inspector (preview, headers, Footprint timeline, raw), the Do-Not-Disturb suppression registry, and Base Camp (SES connection, one-click provisioning, quota, identities, DNS guidance with live re-checks via SquatchMail.SES.check_dns/2).
  • Sightings, Bounces, and Complaints pages. The sidebar's three remaining nav items now have routes (/sightings, /bounces, /complaints) instead of 404ing. All three are served by SquatchMail.Web.Live.TrailLog via live_action page configs: the Sightings archive lists every email on record (all-time default range); Bounces and Complaints are the same table locked to status bounced/complained — the status select is hidden and a hand-typed ?status= param can't unlock it. CSV export links on each page carry the locked status and effective range, and the Sighting inspector's back link now returns to whichever page (and filter set) the user came from. Instead of the Trail Log's engagement column, Bounces shows a Reason column (SES bounceType · bounceSubType plus the recipient's diagnosticCode) and Complaints shows the ISP's complaintFeedbackType, backed by SquatchMail.Tracker.bounce_details/1 / complaint_details/1 — one latest-event-per-email query each, no per-row N+1.
  • Core data layer. SquatchMail.Migrations — versioned, host-owned migrations following the Oban/ErrorTracker pattern (up/1, down/1, migrated_version/1; version tracked via a Postgres COMMENT ON TABLE, not a separate tracking table). V01 creates the squatch_mail schema and all core tables: emails, email_recipients, email_attachments, email_events, suppressions, webhook_logs, sources. SquatchMail.Tracker is the context module the rest of the library reads/writes through: recording emails and their nested recipients/attachments in one transaction, event ingestion with message-id linking (including orphan events that arrive before their email does), status-rank tracking so a bounce or complaint can never be silently overwritten by a later, weaker event (next_status/2), suppression management with expiry, filtered activity queries, aggregate stats with prior-period deltas, and retention-based pruning (prune/0).
  • Swoosh telemetry capture. SquatchMail.Capture attaches to [:swoosh, :deliver | :deliver_many, :stop | :exception] at application boot and records every outgoing email the host sends through its existing Swoosh mailer — no adapter swap, no code changes to the send path. Handling is off the caller's process (GenServer.cast/2 to SquatchMail.Capture.Recorder) with a bounded queue, so a capture backlog degrades by dropping and logging rather than by blocking sends or growing unbounded.
  • SNS/SES event ingestion. SquatchMail.SNS.MessageVerifier hand-verifies inbound SNS message signatures (SigV1/SigV2) against :public_key with no new dependency, validating the SigningCertURL host/scheme before fetching it and caching parsed certificates in ETS. SquatchMail.SNS.Processor orchestrates constant-time webhook-token lookup, payload normalization (both eventType- and legacy notificationType-style SES payloads), Tracker.record_event/1 calls, bounce/complaint-driven suppression, and dedupe on (message_id, event_type, recipient, occurred_at) to survive SNS's at-least-once delivery. Every inbound payload is logged via Tracker.log_webhook/1 regardless of outcome.
  • SES integration. SquatchMail.SES wraps AWS.SESv2/AWS.SNS (via the shared SquatchMail.Finch pool, no hackney): one-click provisioning of a configuration set, SNS topic, HTTPS subscription, and event destination (provision/1,3); sending-quota sync with a six-hour cache (sync_quota/1,2, ensure_quota_synced/1); sending-identity listing with verification and DKIM status (list_identities/1, create_identity/1,2, recheck_identity/1,2); and pure DKIM/SPF/DMARC DNS record guidance (dns_records_for/1). AWS credentials resolve from the sources row either statically (stored keys) or ambiently (environment variables) — see SECURITY.md for the at-rest caveat on static credentials.
  • Guardrails and retention. SquatchMail.Guard checks every send against the suppression list and a complaint-rate circuit breaker (auto-pauses sending at a configurable complaint rate — 0.1% by default, matching SES's own account-suspension threshold — with a minimum-volume floor so a handful of early sends can't produce a false positive). SquatchMail.Adapters.Watchtower is an opt-in Swoosh adapter that enforces Guard ahead of a wrapped real adapter, all-or-nothing across deliver_many/2 batches, for hosts who want a suppressed recipient to actually block the send rather than only be recorded after the fact. SquatchMail.Pruner is a supervised timer that runs Tracker.prune/0 on a configurable interval (six hours by default), also pruning webhook_logs on a fixed 30-day window.
  • Igniter installer. mix igniter.install squatch_mail patches mix.exs, application config, the host router, and generates the migration in one step; a fully manual install path is documented in README.md for hosts not using igniter.
  • Project documentation: RESEARCH.md (architecture and ecosystem survey), FEATURES.md (feature inventory mapped against LaraSend, with P1/P2 phasing), DESIGN.md (dashboard visual design spec), SECURITY.md (vulnerability reporting and the dashboard/webhook/credentials security model), LICENSE (MIT), and this changelog.

Known gaps

  • credentials_mode: "static" stores access_key_id/secret_access_key as plaintext columns; encryption at rest is a tracked TODO, not yet done.
  • Ambient credentials read AWS_* environment variables only; EC2/ECS instance- and task-role resolution via the metadata service is a documented follow-up (see SquatchMail.SES).