Skip to content

Releases: mohamedboukari/bunmail

v0.10.0

Choose a tag to compare

@github-actions github-actions released this 18 Oct 17:37

What's Changed

Full Changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 09 Aug 22:24

What's Changed

Full Changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 19 Jul 10:40

Full Changelog: v0.7.0...v0.8.0

v0.7.0

Choose a tag to compare

@github-actions github-actions released this 18 Jun 22:26

What's Changed

Full Changelog: v0.6.1...v0.7.0

v0.6.1

Choose a tag to compare

@github-actions github-actions released this 29 May 14:03

What's Changed

  • ci(deps): bump the github-actions group with 5 updates by @dependabot[bot] in #103
  • fix(dashboard): timestamps render in viewer machine time, never date-only by @mohamedboukari in #105

Full Changelog: v0.6.0...v0.6.1

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 25 May 02:36

What's Changed

  • ci(deps): bump github/codeql-action from 4.35.3 to 4.35.4 in the github-actions group by @dependabot[bot] in #77
  • chore(deps): bump @commitlint/config-conventional from 20.5.3 to 21.0.0 by @dependabot[bot] in #78
  • chore(deps): bump @commitlint/cli from 20.5.3 to 21.0.0 by @dependabot[bot] in #79
  • chore(deps): bump lint-staged from 16.4.0 to 17.0.4 by @dependabot[bot] in #80
  • ci: auto-sync bun.lock on Dependabot PRs by @mohamedboukari in #81
  • ci(deps): bump github/codeql-action from 4.35.4 to 4.35.5 in the github-actions group by @dependabot[bot] in #82
  • feat(dashboard): add bounced status filter to emails list by @mohamedboukari in #88
  • feat(dashboard): show full timestamp in emails list Created column by @mohamedboukari in #90
  • fix(docker): parameterise PORT and SMTP_PORT in docker-compose.yml by @mohamedboukari in #94
  • fix(inbound): make SMTP receiver loudly opt-in instead of silently disabled by @mohamedboukari in #95
  • fix(emails): per-MX connection throttling on outbound queue by @mohamedboukari in #96
  • fix(emails): multi-MX delivery for cross-domain CC/BCC (Phase 1 of #87) by @mohamedboukari in #98
  • feat(dashboard): Gmail-style chip input for CC / BCC by @mohamedboukari in #99
  • feat(dashboard): suppressions list page + explicit "Sending as" key picker by @mohamedboukari in #100
  • feat(dashboard): reply to inbound emails by @mohamedboukari in #101
  • feat(emails): per-group retry state for multi-MX delivery (Phase 2 of #87) by @mohamedboukari in #102

Full Changelog: v0.5.0...v0.6.0

v0.5.0 — deliverability + reliability + observability

Choose a tag to compare

@github-actions github-actions released this 10 May 00:41

[0.5.0] - 2026-05-10

Theme: deliverability + reliability + observability. Bounce handling becomes end-to-end — DSN parsing (#24), per-API-key suppression list (#25), and auto-suppress on inline SMTP 5xx (#68) close the "stop sending to dead recipients" loop that protects IP reputation. DMARC aggregate reports are parsed and surfaced in the dashboard (#41). Webhook delivery becomes durable with persistence + replay (#30) so consumer outages can't drop events. Email tombstones (#34) preserve audit trails past trash purge so late complaints / bounces can be traced. The queue's race condition under concurrent workers is fixed (#20) — multi-replica deploys are now safe. DKIM private keys are encrypted at rest (#23). A new integration test tier (#70) lifted overall coverage from 62% to 90%+.

Breaking change — DKIM encryption. Operators upgrading from 0.4.0 must set DKIM_ENCRYPTION_KEY=$(openssl rand -base64 32) in .env before pulling this version. Boot fails loudly otherwise. Existing rows are auto-encrypted on first boot. See #23 / SECURITY.md for rotation.

Changed

  • Consolidate the duplicate ### Added sections under [Unreleased] (cosmetic, keeps Keep-a-Changelog format consistent). (#70)

Added

  • Email tombstones — post-purge audit trail (#34). Hard-deleted emails (whether by the periodic trash purge sweep, the per-row permanent API, or the bulk empty trash) now leave behind a forensic snapshot in a new email_tombstones table — id (matches the original msg_…), apiKeyId, messageId, fromAddress, toAddress, subject, status, sentAt, deletedAt, purgedAt. Body / html / text are deliberately not preserved — the whole point is to drop sensitive payload past the trash retention window while keeping enough identifiers to answer "did we send the message that just bounced / generated this complaint?" weeks later. All five hard-delete code paths in the codebase route through one deleteEmailsWithTombstones helper that wraps the snapshot INSERT + the DELETE in a single transaction. Tombstones have no foreign keys — they survive the api_key being revoked + cascade-deleted, which is exactly the audit-trail use case. Tombstones themselves are aged out by a new sweep on the existing 6h trash purge cadence, default TOMBSTONE_RETENTION_DAYS=90. New REST endpoints GET /api/v1/emails/tombstones?messageId= (paginated, with-and-without angle-bracket-wrap matching) and GET /api/v1/emails/tombstones/:id. Matching dashboard at /dashboard/emails/tombstones with a Message-ID search box. Outbound only — inbound tombstones are out of scope. (#34)
  • Persisted webhook delivery queue with replay (#30). The webhook dispatcher used to retry in-memory (3 attempts: 1s/2s/4s) — a server restart mid-retry would silently drop the event and a consumer outage longer than ~7s would burn through every retry before recovery. The retry loop is now durable: every dispatch INSERTs a row into the new webhook_deliveries table at status='pending', and a worker poll loop (webhook-delivery-worker.service.ts, 5s tick) atomically claims due rows via FOR UPDATE SKIP LOCKED (same pattern as #20 — concurrency-safe from day 1), POSTs each one with a freshly-signed timestamp, and updates the row to delivered on 2xx, reschedules on failure per the new schedule (1m / 5m / 15m / 1h / 6h = 5 attempts over ~7h), or terminates at failed after the cap. Three new endpoints surface the queue: GET /api/v1/webhooks/:id/deliveries (paginated, optional ?status=pending|delivered|failed), GET /api/v1/webhooks/deliveries/:deliveryId (full attempt detail with body bytes for signature debugging), and POST /api/v1/webhooks/deliveries/:deliveryId/replay (resets a row to pending so the worker re-tries it). Matching dashboard pages at /dashboard/webhooks/:id/deliveries and /dashboard/webhooks/deliveries/:deliveryId with a Replay button. New WEBHOOK_DELIVERY_RETENTION_DAYS env var (default 30) controls when delivered rows are reaped by an hourly cleanup task; failed rows are kept indefinitely for forensics. CASCADE on webhooks delete reaps deliveries automatically. See docs/webhooks.md. (#30)

Tests

  • SMTP receiver + queue concurrency coverage. Backfill of the gating-now-shipped acceptance criteria from #35: 15 unit tests in test/unit/inbound-malformed-input.test.ts feeding adversarial inputs (binary garbage, truncated MIME, control characters, BOM, conflicting headers, 500KB single-line input) through parseBounce and simpleParser to lock down "malformed RFC 822 doesn't crash the receiver"; 4 integration tests in test/integration/inbound-bounce-flow.integration.test.ts exercising the full DSN end-to-end chain (parseBounce → handleParsedBounce) against real Postgres + a captured fetch mock — covers hard-bounce → suppression row + email row marked bounced + signed email.bounced webhook payload, the soft-bounce time-windowed variant, the soft → hard escalation rule from #24, and the dropped-no-original safety case. Concurrent-workers exactly-once was already covered by test/integration/queue-claim.integration.test.ts from #20. The 50MB-stream-rejection bullet remains partial: the protocol-level cap is enforced by the smtp-server library's SIZE extension (advertised via new SMTPServer({ size: MAX_MESSAGE_BYTES })); the in-handler chunk guard is belt-and-suspenders that would require a real SMTPServer boot or a closure-extraction refactor to test cleanly — deferred. (#35)

Fixed

  • Queue race condition under concurrent workers. The poll loop's queued → sending transition was a SELECT followed by a separate per-row UPDATE (queue.service.ts), so two workers running it at the same time could pick up the same rows and double-send the same email. The transition is now a single atomic statement: UPDATE emails SET status='sending', attempts = attempts + 1 WHERE id IN (SELECT id … FOR UPDATE SKIP LOCKED) RETURNING *. FOR UPDATE SKIP LOCKED makes a concurrent caller skip rows another transaction has locked rather than waiting on them or grabbing duplicates, so two workers always see disjoint result sets. We're single-worker today so this wasn't manifesting in production, but it was the blocker for horizontal scaling — running a second BunMail instance behind a load balancer is now safe. New integration test (test/integration/queue-claim.integration.test.ts) seeds 30 queued rows, fires 6 concurrent claim calls, asserts each row is claimed by exactly one caller. (#20)

Added

  • DMARC rua aggregate report ingest. When BunMail's inbound SMTP receives a DMARC aggregate report (the daily XML attachment that Microsoft / Google / Yahoo / Mail.ru send to the rua=mailto: address you publish in _dmarc), the new module (src/modules/dmarc-reports/) detects it via a heuristic on sender + subject + content-type, decompresses the attachment (gzip / zip / raw — sniffed by magic bytes, not filename), parses the RFC 7489 XML via fast-xml-parser, and stores the report in two new tables: dmarc_reports (one row per report, with rawXml kept for forensics) + dmarc_records (one row per source IP, FK with ON DELETE CASCADE). Re-receipt of the same report is deduped via UNIQUE (org_email, report_id) + ON CONFLICT DO NOTHING. DMARC-shaped messages don't pollute the inbound_emails table — they branch off in smtp-receiver.service.ts after the bounce branch. Two new REST endpoints — GET /api/v1/dmarc-reports (paginated, optional ?domain=) and GET /api/v1/dmarc-reports/:id (with per-source-IP detail) — and matching dashboard pages at /dashboard/dmarc-reports. The detail view computes alignment totals and highlights misaligned source IPs in amber so spoofing attempts and unauthorised third-party senders are visually obvious. No webhook event for v1 (consumers can poll); no ruf / forensic report support; no backfill. See docs/dmarc-reports.md. (#41)
  • Integration test tier against real Postgres + service-level unit tests. Two-pronged test coverage uplift: (a) New test/integration/ directory with 40 tests across 5 services (suppression, email-create, queue-recover, domain, webhook-dispatch) running against a dedicated bunmail_test database. test/integration/_preload.ts overrides DATABASE_URL before any service module loads; each test wraps in beforeEach(truncateAll) for isolation. Catches ON DELETE CASCADE / SET NULL behaviour, ON CONFLICT DO UPDATE upserts, encryption round-trips, schema drift between TS models and the live DB. (b) New unit tests for every previously-uncovered service (mailer, dns-verification, template, webhook, webhook-dispatch, api-key, domain) using mocked dependencies — covers the orchestration code paths even without a DB. Plus 20 JSX render-smoke tests for every dashboard page + presentational component. Overall coverage uplift: 61.7% → 89.8% functions, 66.1% → 93.0% lines. bunfig.toml threshold raised to function: 0.85, line: 0.9 so future PRs can't regress. CI provisions a postgres:16 service container and runs all three tiers on every PR. New scripts: test:unit, test:e2e, test:integration, test:integration:setup, test:all, test:coverage. See docs/testing.md. (#70)

Added

  • Auto-suppress on inline SMTP 5xx rejections. When a recipient's MX rejects an outbound send during the SMTP transaction with a 550 5.1.1 (the way Gmail / Outlook / Yahoo handle obviously-bad addresses today), the queue's failure path now cl...
Read more

v0.4.0

Choose a tag to compare

@github-actions github-actions released this 03 May 10:32

What's Changed

Full Changelog: v0.3.0...v0.4.0

v0.3.0

Choose a tag to compare

@github-actions github-actions released this 26 Apr 01:43

What's Changed

  • feat: gmail-style trash for emails + inbound, fix domain delete by @mohamedboukari in #17

Full Changelog: v0.2.1...v0.3.0

v0.2.1

Choose a tag to compare

@github-actions github-actions released this 24 Apr 21:28

What's Changed

Full Changelog: v0.2.0...v0.2.1