Repository navigation
Releases: mohamedboukari/bunmail
Release list
v0.10.0
What's Changed
- ci(deps): bump the github-actions group with 5 updates by @dependabot[bot] in #156
- ci(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #157
- fix(security): remediate CVE-2026-40345, refresh deps, pin Trivy v0.74.0 by @mohamedboukari in #164
- build: migrate to TypeScript 7 and replace TS-API-dependent tooling by @mohamedboukari in #165
- ci(deps): bump the github-actions group across 1 directory with 7 updates by @dependabot[bot] in #160
- ci(deps): bump the github-actions group across 1 directory with 7 updates by @dependabot[bot] in #194
- docs: fix CLAUDE.md stack (#168), remove em dashes, lint test/ in CI by @mohamedboukari in #195
- ci(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #196
- fix(security): patch two HIGH nodemailer advisories and refresh deps by @mohamedboukari in #198
- fix(security): check public 172.x addresses against the DNSBL (#166) by @mohamedboukari in #197
- ci: gate formatting, dead code, coverage and lint warnings (#171) by @mohamedboukari in #199
- chore(security): remove 15 stale CVE suppressions from .trivyignore (#170) by @mohamedboukari in #200
- feat: Reply-To support on the send API, SMTP submission and dashboard (#169) by @mohamedboukari in #201
- fix(security): reject CR/LF in dashboard send cc, bcc and subject (#202) by @mohamedboukari in #203
- chore: add project Claude Code tooling (skills, hooks, Context7, graphify) by @mohamedboukari in #204
- fix(security): run the container as non-root (#167) by @mohamedboukari in #207
- fix: make the Docker healthcheck probe PORT instead of hardcoded 3000 (#206) by @mohamedboukari in #208
- fix(security): bind the dev Postgres publish to 127.0.0.1 (#209) by @mohamedboukari in #210
- test: make rate-limit, session and sign-payload tests exercise real code (#174) by @mohamedboukari in #211
- fix: bound pagination on every list route (#172) by @mohamedboukari in #212
- fix: serialize concurrent migration runs with a Postgres advisory lock (#175) by @mohamedboukari in #213
- fix: scope webhook dispatch to the API key that owns the email by @mohamedboukari in #226
- fix: escape API-writable data on the templates and suppressions pages by @mohamedboukari in #227
- fix: enforce the per-API-key rate limit by @mohamedboukari in #234
- fix: linear-time bounce parsing and recipient check for bounces by @mohamedboukari in #235
- fix: refuse MX hosts on private addresses and pin the checked address by @mohamedboukari in #237
- fix: bound webhook response reads and count attempts when claimed by @mohamedboukari in #238
- fix: revoking an API key also cuts off its webhooks and SMTP sessions by @mohamedboukari in #239
- fix: make the DMARC report endpoints admin-only by @mohamedboukari in #240
- fix: remove webhook deliveries that reached the wrong API key by @mohamedboukari in #252
- fix: cap request bodies, bound template rendering, map validation errors by @mohamedboukari in #253
- fix: SMTP submission delivers only to envelope recipients by @mohamedboukari in #254
- fix: retry temporary failures with backoff and recover rows with leases by @mohamedboukari in #263
- fix: sign List-Unsubscribe-Post with DKIM by @mohamedboukari in #264
- fix: refuse to start in production without a real MAIL_HOSTNAME by @mohamedboukari in #265
- fix: store and allow editing domain unsubscribe settings by @mohamedboukari in #266
- fix: sender policy blocks no longer suppress recipients by @mohamedboukari in #267
- test: authorization matrix for every API route by @mohamedboukari in #268
- ci: name exact release tags on pinned actions, drop persisted token by @mohamedboukari in #270
- ci: run CodeQL with the security-extended query suite by @mohamedboukari in #269
- ci: dependency review on pull requests and a Dependabot cooldown by @mohamedboukari in #271
- ci: close the xss-scan blind spots (ternary true branch, attributes) by @mohamedboukari in #272
- fix: match DMARC reporter senders on the parsed address, not substrings by @mohamedboukari in #273
- ci(deps): bump the github-actions group with 4 updates by @dependabot[bot] in #274
- fix: linear recipient address checks, 254-char cap, parser time budgets by @mohamedboukari in #275
- feat: accounts schema for the invite-only dashboard login (#214) by @mohamedboukari in #276
- feat: owner bootstrap from BUNMAIL_OWNER_EMAIL with a one-time setup link (#215) by @mohamedboukari in #278
- feat: account login with hashed, rotating server-side sessions (#216) by @mohamedboukari in #283
- feat: correlation IDs, per-request log lines and central secret redaction (#284) by @mohamedboukari in #285
- feat: role checks on every dashboard route, deny by default (#222) by @mohamedboukari in #286
- feat: bind one-time secret reveals to their creator (#280) by @mohamedboukari in #287
- feat: invite-only onboarding with email invitations and an accept page (#217) by @mohamedboukari in #288
- feat: Users page with role changes, deactivation, removal and ownership transfer (#218) by @mohamedboukari in #289
- feat: admin-issued password reset links, sessions bound to their credential (#277) by @mohamedboukari in #290
- feat: API keys, templates, domains and webhooks belong to their creator (#219) by @mohamedboukari in #292
- fix: connect webhook deliveries to the address the SSRF guard validated by @mohamedboukari in #294
- chore: prepare the v0.10.0 changelog and docs by @mohamedboukari in #295
Full Changelog: v0.9.0...v0.10.0
v0.9.0
What's Changed
- ci(deps): bump the github-actions group across 1 directory with 6 updates by @dependabot[bot] in #136
- chore(deps): bump bumpp from 11.1.0 to 12.0.0 by @dependabot[bot] in #135
- fix(deps): bump linkify-it to 5.0.2 — CVE-2026-59887 (HIGH) by @mohamedboukari in #139
- feat(api-keys): admin vs restricted key roles — gate the management plane (#130) by @mohamedboukari in #138
- fix(webhooks): SSRF guard on webhook URLs — create + delivery (#128) by @mohamedboukari in #140
- chore: bump Bun 1.3.10 → 1.3.14 (Dockerfile + CI) by @mohamedboukari in #141
- chore(deps): refresh dependencies (ncu) + drop redundant overrides by @mohamedboukari in #142
- ci: harden workflows — least-privilege permissions, concurrency, timeouts (#145) by @mohamedboukari in #146
- ci: build-provenance attestation for GHCR image + fix trivy-action pin comment (#145) by @mohamedboukari in #147
- fix(dashboard): one-time secret reveal — stop leaking keys/secrets in URLs (#132) by @mohamedboukari in #148
- fix(security): dashboard & API hardening bundle (#133) by @mohamedboukari in #149
- fix(dashboard): escape attacker-controlled DMARC report fields — stored XSS (#131) by @mohamedboukari in #151
- fix(dmarc): bound inbound report parsing — decompression bomb + entity expansion (#129) by @mohamedboukari in #152
- feat(dashboard): emails filters by source (API vs SMTP) + by API key (#137) by @mohamedboukari in #153
- ci: zizmor workflow-security audit + fix a template-injection (#145) by @mohamedboukari in #154
- fix(security): escape all dashboard JSX + add xss-scan CI gate (#150) by @mohamedboukari in #155
Full Changelog: v0.8.0...v0.9.0
v0.8.0
v0.7.0
What's Changed
- feat(inbound): per-domain inbound notification email on receive by @mohamedboukari in #107
- ci(deps): bump the github-actions group with 3 updates by @dependabot[bot] in #108
- feat(dashboard): rate-limit login to block password brute-force (#109) by @mohamedboukari in #110
- ci(deps): bump github/codeql-action from 4.36.1 to 4.36.2 in the github-actions group by @dependabot[bot] in #111
- feat(dashboard): live HTML preview in the template editor (#112) by @mohamedboukari in #113
- fix(deps): bump nodemailer to 9.0.1 to patch GHSA-p6gq-j5cr-w38f (HIGH) by @mohamedboukari in #115
Full Changelog: v0.6.1...v0.7.0
v0.6.1
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
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
[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.envbefore pulling this version. Boot fails loudly otherwise. Existing rows are auto-encrypted on first boot. See #23 /SECURITY.mdfor rotation.
Changed
- Consolidate the duplicate
### Addedsections 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
permanentAPI, or the bulkempty trash) now leave behind a forensic snapshot in a newemail_tombstonestable —id(matches the originalmsg_…),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 onedeleteEmailsWithTombstoneshelper 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, defaultTOMBSTONE_RETENTION_DAYS=90. New REST endpointsGET /api/v1/emails/tombstones?messageId=(paginated, with-and-without angle-bracket-wrap matching) andGET /api/v1/emails/tombstones/:id. Matching dashboard at/dashboard/emails/tombstoneswith 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_deliveriestable atstatus='pending', and a worker poll loop (webhook-delivery-worker.service.ts, 5s tick) atomically claims due rows viaFOR UPDATE SKIP LOCKED(same pattern as #20 — concurrency-safe from day 1), POSTs each one with a freshly-signed timestamp, and updates the row todeliveredon 2xx, reschedules on failure per the new schedule (1m / 5m / 15m / 1h / 6h = 5 attempts over ~7h), or terminates atfailedafter 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), andPOST /api/v1/webhooks/deliveries/:deliveryId/replay(resets a row topendingso the worker re-tries it). Matching dashboard pages at/dashboard/webhooks/:id/deliveriesand/dashboard/webhooks/deliveries/:deliveryIdwith a Replay button. NewWEBHOOK_DELIVERY_RETENTION_DAYSenv var (default 30) controls when delivered rows are reaped by an hourly cleanup task; failed rows are kept indefinitely for forensics. CASCADE onwebhooksdelete 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
parseBounceandsimpleParserto 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 capturedfetchmock — covers hard-bounce → suppression row + email row marked bounced + signedemail.bouncedwebhook 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 vianew 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 → sendingtransition was aSELECTfollowed by a separate per-rowUPDATE(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 LOCKEDmakes 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
ruaaggregate report ingest. When BunMail's inbound SMTP receives a DMARC aggregate report (the daily XML attachment that Microsoft / Google / Yahoo / Mail.ru send to therua=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 viafast-xml-parser, and stores the report in two new tables:dmarc_reports(one row per report, withrawXmlkept for forensics) +dmarc_records(one row per source IP, FK withON DELETE CASCADE). Re-receipt of the same report is deduped viaUNIQUE (org_email, report_id)+ON CONFLICT DO NOTHING. DMARC-shaped messages don't pollute theinbound_emailstable — they branch off in smtp-receiver.service.ts after the bounce branch. Two new REST endpoints —GET /api/v1/dmarc-reports(paginated, optional?domain=) andGET /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); noruf/ 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 dedicatedbunmail_testdatabase.test/integration/_preload.tsoverridesDATABASE_URLbefore any service module loads; each test wraps inbeforeEach(truncateAll)for isolation. CatchesON DELETE CASCADE/SET NULLbehaviour,ON CONFLICT DO UPDATEupserts, 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.tomlthreshold raised tofunction: 0.85, line: 0.9so future PRs can't regress. CI provisions apostgres:16service 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...
v0.4.0
What's Changed
- fix: validate LOG_LEVEL at config load (#39) by @mohamedboukari in #44
- fix: cleanup expired entries in HTTP rate-limit map (#22) by @mohamedboukari in #45
- fix: cap email html/text payloads at 5MB (#26) by @mohamedboukari in #46
- fix: enable opportunistic STARTTLS on outbound delivery (#21) by @mohamedboukari in #47
- fix: dedupe auth hash + DB lookup per request (#27) by @mohamedboukari in #48
- fix: throttle api_keys.last_used_at writes (#28) by @mohamedboukari in #49
- fix: extend webhook event union with email.received + email.complained (#31) by @mohamedboukari in #50
- fix: refuse to start in production with empty DASHBOARD_PASSWORD (#19) by @mohamedboukari in #51
- fix: harden inbound SMTP against open-relay abuse (#18) by @mohamedboukari in #52
- docs: add THREAT_MODEL.md (#38) by @mohamedboukari in #53
- ci: add Trivy fs scan + SHA-pin all actions (#54) by @mohamedboukari in #55
- fix: webhook signature replay protection (#43) by @mohamedboukari in #57
- feat: emit List-Unsubscribe headers on every outbound message (#40) by @mohamedboukari in #58
- feat: redact email addresses in log records (#33) by @mohamedboukari in #59
- refactor: drop
as typeof contextcasts across plugins (#36) by @mohamedboukari in #61
Full Changelog: v0.3.0...v0.4.0
v0.3.0
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
What's Changed
- chore(deps): bump @types/nodemailer from 7.0.11 to 8.0.0 by @dependabot[bot] in #12
- ci(deps): bump softprops/action-gh-release from 2 to 3 by @dependabot[bot] in #14
- chore(deps): bump knip from 5.88.1 to 6.4.1 by @dependabot[bot] in #15
- ci: pin Bun to 1.3.10 by @mohamedboukari in #16
Full Changelog: v0.2.0...v0.2.1