Compare commits

...
Sign in to create a new pull request.

257 commits

Author SHA1 Message Date
b3b1a094de dmarc: refactor parseDMARCRecord to use shared tag parser and eliminate helper methods
All checks were successful
continuous-integration/drone/push Build is passing
Replace per-field regex extractor methods with a single parseDKIMTags call,
removing eight redundant private methods and unifying DMARC tag parsing with
the existing DKIM tag parser. Tests are updated to drive through parseDMARCRecord
instead of the removed helpers, and the NP scoring logic is corrected to award
+15/−15 symmetrically like the SP scoring path.
2026-05-18 21:02:53 +08:00
809bca02e4 dmarc: implement DMARCbis DNS Tree Walk and new tag support
Replace RFC 7489 PSL-based org-domain lookup and RFC 9091 PSD DMARC
fallback with the DMARCbis DNS Tree Walk algorithm (max 8 queries,
8-label shortcut, TLD records require psd=y). Add parsing for the new
t= (test mode), psd= (y/n/u), and deprecated tag detection (pct, rf,
ri). Update validateDMARC to accept p=-absent records with rua= per
DMARCbis §4.7. Score t=y by downgrading effective policy one level.

Surface user-facing advisories in DmarcRecordDisplay: deprecation
warnings for pct=/rf=/ri=, test mode explanation with per-policy
impact, and PSD/org-domain boundary notices.
2026-05-18 20:57:31 +08:00
1b8627ef86 dkim: expose algorithm, hash list, and key size in DKIM record analysis
Parse k=, h=, a= tags and derive RSA key bit-length from the public key
so consumers can detect weak configurations (SHA-1, short keys).
Scoring now penalises rsa-sha1 (cap 60), RSA <1024 bit (cap 25), and
RSA <2048 bit (cap 75); Ed25519 receives no penalty.

Fixes: #37
2026-05-18 20:57:31 +08:00
369a13526f analyzer: correct auth scoring weights, x-aligned-from penalty, and RBL divide-by-zero 2026-05-18 20:57:31 +08:00
3161e392e8 dmarc: add support for np= non-existent subdomain policy tag
Implements parsing, scoring, CLI output, and UI display for the DMARC
np= tag (DMARCbis draft-ietf-dmarc-dmarcbis), which controls policy for
NXDOMAIN subdomains independently of sp=. The score deducts 15 points
from the base and awards them back when np= is absent (good default) or
its strength is equal to or stricter than the effective sp=/p= policy.
2026-05-18 17:03:58 +08:00
1516991057 dmarc: implement RFC 7489 org-domain fallback and RFC 9091 PSD DMARC
DMARC lookup now follows the full RFC 7489 §6.6.3 fallback chain: exact
From domain → organizational domain (eTLD+1 via PSL) → public suffix
domain (RFC 9091, only when psd=y is present). DNS errors abort
immediately without triggering fallback; NXDOMAIN and missing v=DMARC1
records do trigger it. The found domain is exposed in the new
DMARCRecord.domain field for reporting purposes.

Also promote getOrganizationalDomain to a package-level function so both
HeaderAnalyzer and DNSAnalyzer can share it, and fix pre-existing
rbl_test.go compilation errors and stale score expectations.

Closes: #98
2026-05-18 17:03:58 +08:00
0de67af847 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-18 00:08:25 +00:00
e324e6cbf9 chore(deps): update module github.com/oapi-codegen/runtime to v1.4.0
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-16 11:17:18 +08:00
3e53fae713 fix(docker): install perl-cryptx via apk to fix arm64 build
All checks were successful
continuous-integration/drone/push Build is passing
2026-05-16 10:52:52 +08:00
b3137f7d37 fix(docker): explicitly install Mail::DKIM on arm64 builds
Some checks failed
continuous-integration/drone/push Build is failing
On arm64, cpanm does not automatically resolve Mail::DKIM as a
transitive dependency of Mail::Milter::Authentication, causing the
build to fail. Install it explicitly before Mail::Milter::Authentication.
2026-05-15 22:05:50 +08:00
bfe6ff81fa Add local unbound resolver for up-to-date DNS information
Some checks failed
continuous-integration/drone/push Build is failing
Fixes: #30
2026-05-15 21:24:24 +08:00
5ffc731297 Update go deps
Some checks are pending
continuous-integration/drone/push Build is running
2026-05-12 08:55:53 +08:00
454da476eb chore(deps): update module github.com/getkin/kin-openapi to v0.138.0
Some checks failed
continuous-integration/drone/push Build is failing
2026-05-10 14:25:17 +00:00
eaab446504 chore(deps): update module golang.org/x/net to v0.54.0
Some checks are pending
continuous-integration/drone/push Build is pending
2026-05-10 14:23:48 +00:00
cf63276a07 chore(deps): update dependency typescript to v6
Some checks failed
renovate/artifacts Artifact file update failure
continuous-integration/drone/push Build is pending
2026-05-05 22:07:20 +00:00
09d777634c Readd missing go deps
Some checks failed
continuous-integration/drone/push Build is failing
2026-05-04 11:15:53 +08:00
15120d8598 chore(deps): update module golang.org/x/net to v0.53.0
Some checks are pending
continuous-integration/drone/push Build is running
2026-05-04 03:14:35 +00:00
6f6211e833 chore(deps): update module github.com/oapi-codegen/oapi-codegen/v2 to v2.7.0
Some checks are pending
continuous-integration/drone/push Build is running
2026-05-04 03:14:23 +00:00
42cf6f450d chore(deps): update module github.com/jgltechnologies/gin-rate-limit to v1.5.8
Some checks are pending
continuous-integration/drone/push Build is running
2026-05-04 03:13:54 +00:00
31a27c120b Add instruction to use with new happyDomain checker
Some checks are pending
continuous-integration/drone/push Build is pending
2026-04-28 19:01:22 +07:00
396c51974a Extract OpenAPI schemas to separate file and move models to internal/model package
All checks were successful
continuous-integration/drone/push Build is passing
Split api/openapi.yaml schemas into api/schemas.yaml so structs can be
generated independently from the API server code. Models now generate
into internal/model/ via oapi-codegen, with the server referencing them
through import-mapping. Moved PtrTo helper to internal/utils and removed
storage.ReportSummary in favor of model.TestSummary.
2026-04-09 18:36:27 +07:00
3eec5ce966 Remove unused xAlignedFrom prop from HeaderAnalysisCard
Some checks are pending
continuous-integration/drone/push Build is pending
2026-04-09 18:05:11 +07:00
7422f6ed0a Add paginated test history listing with disable option
Add GET /tests endpoint returning lightweight test summaries (grade,
score, domain, date) with pagination, using database-level JSON
extraction to avoid loading full report blobs. The feature can be
disabled with --disable-test-list flag. Frontend includes a new
/tests/ page with table view and a conditional "History" navbar link.

Fixes: https://github.com/happyDomain/happydeliver/issues/12
2026-04-09 18:05:06 +07:00
e540377bd9 Don't penalize non iprev result nor aligned-from if non-existant
All checks were successful
continuous-integration/drone/push Build is passing
Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-27 17:57:48 +07:00
16b7dcb057 Incorporate DNSWL (whitelist) grade into blacklist scoring
All checks were successful
continuous-integration/drone/push Build is passing
CalculateScore now accepts a forWhitelist flag to handle whitelist
scoring logic separately. The final blacklist grade combines both
RBL and DNSWL results using MinGrade for a more accurate reputation
assessment.
2026-03-26 10:36:27 +07:00
dfa38e8a26 Fix RBL score: return A+ when not listed on any blocklist
Move the ListedCount check before scoringListCount calculation so we
return early with a perfect score when the IP/domain is not listed,
regardless of how many informational-only lists exist.
2026-03-26 10:36:25 +07:00
dee848d887 Rebalance authentication score: SPF/DKIM/DMARC as core, penalties for optional results
Some checks are pending
continuous-integration/drone/push Build is running
IPRev and X-Aligned-From now only penalize on failure instead of
contributing positively. Core authentication (SPF/DKIM/DMARC) rebalanced
to 30 points each, BIMI stays at 10, totaling 100 base points.

Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-26 10:13:37 +07:00
b158336451 Filter Received-SPF header by receiver hostname
Ensures parseLegacySPF only trusts Received-SPF headers where the
receiver= field matches the configured receiverHostname, preventing
incorrect SPF results from unrelated receivers.
2026-03-26 10:13:37 +07:00
a36824cf27 Fix DKIM headers retrieval
Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-26 10:13:28 +07:00
7d3009d7d0 Add rspamd symbol descriptions from embedded/API lookup
Embed rspamd-symbols.json in the binary to provide human-readable
descriptions for rspamd symbols in reports. Optionally fetch fresh
symbols from a configurable rspamd API URL (--rspamd-api-url flag),
falling back to the embedded list on error. Update the frontend to
display descriptions alongside symbol names and scores.
2026-03-26 09:51:45 +07:00
5c104f3c99 Merge RspamdSymbol into SpamTestDetail in OpenAPI spec
Add params field to SpamTestDetail, update RspamdResult.symbols to
reference SpamTestDetail instead of the now-removed RspamdSymbol schema,
and update Go code accordingly.
2026-03-26 08:58:13 +07:00
3c192f17fd Improve DKIM summary to distinguish missing records from invalid signatures
All checks were successful
continuous-integration/drone/push Build is passing
Use DNS records instead of authentication results to determine DKIM
presence, enabling a three-state display: passed (green), published but
invalid signature (yellow+red), or no DKIM at all (red).
2026-03-25 12:29:05 +07:00
35fc997390 Add warning banner when all authentication results are missing
All checks were successful
continuous-integration/drone/push Build is passing
Explains the two most common causes: the mail server not being
configured to verify email authentication, or a receiver hostname
mismatch with --receiver-hostname.

Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-25 12:12:08 +07:00
2fcee1b885 Return nil from spam analyzers when primary headers are missing
Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-25 12:12:08 +07:00
26025c96a2 Document --receiver-hostname flag and HAPPYDELIVER_RECEIVER_HOSTNAME env var
Explain how happyDeliver filters Authentication-Results headers by
hostname, how to find the correct authserv-id value, and when to
override it (especially when bypassing the embedded Postfix).

Bug: https://github.com/happyDomain/happydeliver/issues/1
Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-25 12:12:08 +07:00
76ee50a100 Make receiver hostname configurable via --receiver-hostname flag
Remove the package-level global hostname from parser.go.

Adds a log warning when the last Received hop doesn't match the
expected receiver hostname.

Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-25 12:12:08 +07:00
71e0832416 Parse DKIM-Signature headers directly in AnalyzeDNS
Remove authResults parameter from AnalyzeDNS, making it independent of
the authentication analysis step. Instead, parse DKIM-Signature headers
directly to extract domain and selector.

Bug: https://github.com/happyDomain/happydeliver/issues/11
2026-03-25 12:12:08 +07:00
c96a8b92b8 Readd missing go deps 2026-03-25 12:12:08 +07:00
b1c18a3894 chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-23 02:25:53 +00:00
c8e28c31ee chore(deps): update module github.com/oapi-codegen/runtime to v1.3.0 2026-03-23 02:25:53 +00:00
1d8ee637da chore(deps): update module github.com/oapi-codegen/runtime to v1.3.0
Some checks are pending
continuous-integration/drone/push Build is running
2026-03-23 09:25:48 +07:00
968f42761f Readd missing go dep
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-19 11:56:06 +07:00
2b70115834 chore(deps): update module golang.org/x/net to v0.52.0
Some checks are pending
continuous-integration/drone/push Build is running
2026-03-19 04:07:17 +00:00
d65840000a chore(deps): update module github.com/gin-gonic/gin to v1.12.0
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-19 03:28:34 +00:00
61503a1c1f chore(deps): lock file maintenance
Some checks are pending
continuous-integration/drone/push Build is running
2026-03-19 03:13:22 +00:00
26025644b0 chore(deps): update dependency @sveltejs/vite-plugin-svelte to v7
Some checks failed
continuous-integration/drone/push Build is failing
2026-03-19 03:00:49 +00:00
bd02b8f9ba chore(deps): update dependency vite to v8
Some checks are pending
continuous-integration/drone/push Build is pending
2026-03-19 03:00:40 +00:00
a3b539179e chore(deps): update eslint monorepo to v10
Some checks are pending
continuous-integration/drone/push Build is pending
2026-03-19 03:00:26 +00:00
8b6154c183 feat: add whitelist checks to IP blacklist endpoint and rename checks to blacklists
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-09 16:01:54 +07:00
56e6494a75 rbl: parallelize IP checks against blacklists using goroutines 2026-03-09 15:34:38 +07:00
0176c3803d docker: split ENV declarations for readability and remove default RBL list 2026-03-09 15:22:36 +07:00
21e16fd847 rbl: remove SpamRats entries from default RBL list
Those RBLs requires an API key
2026-03-09 14:08:34 +07:00
edfe498b27 Improve responsiveness 2026-03-09 14:08:34 +07:00
27650a3496 feat: add raw report display to rspamd card
Add a collapsible Raw Report section to RspamdCard, storing the raw
X-Spamd-Result header value and displaying it like SpamAssassin's report.
2026-03-09 14:08:34 +07:00
d9b9ea87c6 refactor: extract email path into standalone card component
Move the received chain display out of BlacklistCard into EmailPathCard,
giving it its own card styling and placing it as a dedicated section on
the report page.
2026-03-09 13:09:11 +07:00
bb47bb7c29 fix: handle nested brackets in rspamd symbol params 2026-03-09 12:52:15 +07:00
da93d6d706 Add rspamd tests 2026-03-09 12:47:24 +07:00
2a2bfe46a8 fix: various small fixes and improvements
- Add 'skipped' to authentication result enum in OpenAPI spec
- Fix optional chaining on bimiResult.details check
- Add rbls field to AppConfig interface
- Restrict theme storage to valid 'light'/'dark' values only
- Fix null coalescing for blacklist result data
- Fix survey source to use domain instead of ip
2026-03-09 12:46:30 +07:00
55e9bcd3d0 refactor: handle DNS whitelists
Introduce a single DNSListChecker struct with flags to avoid code
duplication with already existing RBL checker.
2026-03-09 12:46:16 +07:00
28424729a5 rbl: support informational-only RBL entries
Add DefaultInformationalRBLs (UCEPROTECT L2/L3) and track listings
separately via RelevantListedCount so these broader lists are displayed
but excluded from the deliverability score calculation.
2026-03-07 14:24:35 +07:00
3cc39c9c54 rbl: add more RBL providers
Add 8 new RBL providers (SpamRats, PSBL, DroneBL, Mailspike, RBL-DNS
and NSZones).
2026-03-07 14:23:51 +07:00
f9c5c815d1 spamassassin: disable Validity network rules scoring
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-07 12:23:17 +07:00
4245f93ce4 Add MIME-Version recommended header check
Validate MIME-Version header value equals "1.0" and subtract 5 points
from the score if the header is present but invalid. Absence is not
penalized.
2026-03-07 12:14:53 +07:00
9679b381c7 fix: mark Message-ID as invalid when multiple headers are present
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-07 12:05:38 +07:00
7b9c45fb68 summary: color SPF error in red
All checks were successful
continuous-integration/drone/push Build is passing
2026-03-07 11:42:28 +07:00
b619ebf8c3 Display permerror (SPF test) as error: text-danger
Some checks are pending
continuous-integration/drone/push Build is running
2026-03-07 11:38:09 +07:00
a146940a65 Improve FCrDNS UI: hide non-matching IPs when match exists
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
Closes: https://github.com/happyDomain/happydeliver/issues/4
2026-02-23 04:25:48 +07:00
e811d02b3b Add rspamd as a second spam filter alongside SpamAssassin
Some checks are pending
continuous-integration/drone/push Build is running
Closes: #36
2026-02-23 04:01:10 +07:00
8fda7746a1 Add one-click unsubscribe detection and warning
All checks were successful
continuous-integration/drone/push Build is passing
Detect the List-Unsubscribe-Post: List-Unsubscribe=One-Click header
(RFC 8058) and expose it as the 'one-click' unsubscribe method in the
content analysis. When unsubscribe methods are present but one-click is
absent, the summary card now shows a warning nudging senders to adopt it.
2026-02-23 00:15:17 +07:00
96e83ff70d Add multilingual unsubscribe keywords for link detection
The list comes from github.com/knadh/listmonk i18n strings

Bug: https://github.com/happyDomain/happydeliver/issues/8
2026-02-23 00:15:17 +07:00
6b983f0506 Use List-Unsubscribe header URLs for unsubscribe link detection
Bug: https://github.com/happyDomain/happydeliver/issues/8
2026-02-23 00:15:17 +07:00
c50e18a347 Use modern Go slices.Contains and switch instead of if/else if 2026-02-23 00:15:17 +07:00
054cd8ae25 chore(deps): update module golang.org/x/net to v0.50.0 2026-02-23 00:15:17 +07:00
c2917f8580 chore(deps): lock file maintenance
Some checks are pending
continuous-integration/drone/push Build is pending
2026-02-02 01:14:33 +00:00
b39a9dc625 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-26 01:14:47 +00:00
88553cd3c8 Moved perl-net-idn-encode from testing to community
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2026-01-25 09:52:53 +08:00
8a10eef2f5 Add custom logo URL configuration option
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/tag Build is failing
Bug: https://github.com/happyDomain/happydeliver/issues/6
2026-01-24 21:42:42 +08:00
64ba6932f7 Use io instead of deprecated ioutil 2026-01-24 21:36:32 +08:00
5453c09420 Use slimmer footer by default
Bug: https://github.com/happyDomain/happydeliver/issues/6
2026-01-24 21:29:09 +08:00
6b4ca126b0 Add colors to css 2026-01-24 21:23:40 +08:00
ac9b567025 web: Format code files 2026-01-24 19:18:26 +08:00
035e864de4 Update go modules 2026-01-24 18:47:48 +08:00
a6efd7710e chore(deps): update module github.com/quic-go/quic-go to v0.57.0 [security]
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-24 08:47:19 +00:00
e6746a1382 chore(deps): update module golang.org/x/net to v0.49.0
Some checks are pending
continuous-integration/drone/push Build is running
2026-01-24 08:47:04 +00:00
d1e48b9885 chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-05 01:15:53 +00:00
9ac3e165fa Readd missing go dep
All checks were successful
continuous-integration/drone/push Build is passing
2026-01-03 12:18:07 +07:00
dc21b72f52 chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2026-01-02 12:58:55 +00:00
1ba35c6f9f chore(deps): update dependency globals to v17
Some checks are pending
continuous-integration/drone/push Build is running
2026-01-01 19:16:13 +00:00
0fda0f88c1 chore(deps): update dependency @eslint/compat to v2
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-26 06:18:56 +00:00
57a3774d28 chore(deps): update module golang.org/x/net to v0.48.0
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-26 02:24:48 +00:00
11d46de033 chore(deps): update dependency prettier-plugin-svelte to v3.4.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-12-26 02:24:25 +00:00
6081e486bf chore(deps): update dependency svelte-check to v4.3.5
Some checks are pending
continuous-integration/drone/push Build is pending
2025-12-26 02:24:24 +00:00
528a65ca04 chore(deps): lock file maintenance
Some checks are pending
continuous-integration/drone/push Build is running
2025-12-08 01:15:59 +00:00
926796b79e chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-02 05:15:50 +00:00
5d02070100 chore(deps): update dependency prettier to v3.7.3
Some checks failed
continuous-integration/drone/push Build is failing
2025-12-02 03:58:31 +00:00
5701070cc1 chore(deps): update dependency vite to v7.2.6
Some checks are pending
continuous-integration/drone/push Build is pending
2025-12-01 07:14:52 +00:00
954cbe29fc chore(deps): update module golang.org/x/crypto to v0.45.0 [security]
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-28 21:15:11 +00:00
ca2ac3df7c chore(deps): update dependency prettier to v3.7.2
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-28 21:15:01 +00:00
016ed7180e Simplify docker usage, HOSTNAME variable is taken from container hostname
All checks were successful
continuous-integration/drone/push Build is passing
Bug: https://github.com/happyDomain/happydeliver/issues/3
2025-11-23 19:42:20 +07:00
3e76692448 chore(deps): lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-18 07:38:17 +00:00
e23afcc77c Add container options to use certificates in postfix
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-18 14:37:39 +07:00
d81ff1731c Fix tests 2025-11-17 10:31:04 +07:00
eef6480e75 Refactor DNS resolution: create an interface to have multiple implementations 2025-11-17 10:15:55 +07:00
f2261adb54 Update go dependancies 2025-11-17 10:15:11 +07:00
3bcbb5814d chore(deps): lock file maintenance
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-14 13:12:20 +00:00
5ac0e2a8bf chore(deps): update dependency vite to v7.2.2
Some checks failed
continuous-integration/drone/push Build is failing
2025-11-14 12:56:23 +00:00
a1e8dd35bd Update dependency @types/node to v24.9.2
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 12:56:11 +00:00
e194fcc5b1 Fix calculateTextPlainConsistency algorithm 2025-11-14 12:56:11 +00:00
c19f545df0 chore(deps): update dependency typescript-eslint to v8.46.4
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 12:56:00 +00:00
03b58b6f19 Update module github.com/oapi-codegen/oapi-codegen/v2 to v2.5.1
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 12:55:50 +00:00
a3ca8ffb48 Fix calculateTextPlainConsistency algorithm 2025-11-14 12:55:50 +00:00
27d5220687 Update dependency globals to v16.5.0
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 12:55:22 +00:00
723bec622a Fix calculateTextPlainConsistency algorithm 2025-11-14 12:55:22 +00:00
ee9fa59dbc Update eslint monorepo to v9.39.0
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 12:55:16 +00:00
e05c6d0bc2 Fix calculateTextPlainConsistency algorithm 2025-11-14 12:55:16 +00:00
04d8b150b4 chore(deps): update module golang.org/x/net to v0.47.0
Some checks are pending
continuous-integration/drone/push Build is running
2025-11-14 09:11:24 +00:00
e28a96508d Respond with HTTP 200 on blacklist, domain and test pages
All checks were successful
continuous-integration/drone/push Build is passing
Bug: https://github.com/happyDomain/happydeliver/issues/2
2025-11-14 15:34:42 +07:00
ea71074cc8 chore(deps): update dependency svelte-check to v4.3.4
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-13 03:55:11 +00:00
644dfda223 Don't stop polling report if response is not ok
Some checks are pending
continuous-integration/drone/push Build is running
Bug: https://github.com/happyDomain/happydeliver/issues/2
2025-11-13 10:54:50 +07:00
447a666ae7 Fix Domain Alignment align issue when error messages
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-07 17:07:31 +07:00
2172603ad5 content: Add spaces behind each node to reduce gap with plain text
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-11-07 15:14:15 +07:00
c91ab96642 Include the HEALTHCHECK command in Dockerfile
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-07 14:23:29 +07:00
18c8622513 Don't require docker-compose to build the image, use docker hub published 2025-11-07 14:22:58 +07:00
deb9fd4f51 Handle RFC6652
All checks were successful
continuous-integration/drone/push Build is passing
Closes: https://framagit.org/happyDomain/happydeliver/-/issues/1
2025-11-07 14:09:05 +07:00
c52a3aa8a7 Improve DMARC description
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-03 15:00:14 +07:00
5b179e7b93 Domain alignment checks for DKIM 2025-11-03 14:58:48 +07:00
465da6d16a Don't look at original DKIM keys headers 2025-11-03 14:58:23 +07:00
d870fc8130 Add backup/restore commands
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-03 11:53:43 +07:00
1c4eb0653e Don't alert on missing -all on included SPF records
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-01 17:57:57 +07:00
372c9c5153 Handle all options of x-aligned-from 2025-11-01 17:52:28 +07:00
3b301a415f Protonmail is now the best mailbox provider I tested
All checks were successful
continuous-integration/drone/push Build is passing
2025-11-01 15:46:48 +07:00
7231669362 Add survey on RBL report and Domain report page
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-31 11:15:15 +07:00
bc6a6397ad New route to check blacklist only 2025-10-31 11:15:15 +07:00
e166e75e42 Update dependency @eslint/compat to v1.4.1 2025-10-31 11:15:15 +07:00
d3f69630c9 Update dependency eslint-plugin-svelte to v3.13.0 2025-10-31 11:15:15 +07:00
9e9e76cf42 Update dependency @hey-api/openapi-ts to v0.86.10 2025-10-31 11:15:15 +07:00
65c8e9a528 Update Node.js to v24 2025-10-31 11:15:15 +07:00
718b624fb8 Add domain only tests 2025-10-31 11:15:15 +07:00
099965c1f9 Report BIMI issues 2025-10-31 11:06:43 +07:00
90dda126ad Don't consider mailto as suspiscious, search domain alignment 2025-10-30 14:10:42 +07:00
3a8a25ddeb Add info title on non-standard authentication tests 2025-10-30 14:10:42 +07:00
b01ca9b38c Report invalid records in summary 2025-10-30 14:10:42 +07:00
20fe4e5b97 Improve SPF record validation and include error message
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-30 10:34:24 +07:00
706dc6eed9 Include report ID in survey response 2025-10-30 10:31:59 +07:00
164b2a98ab DKIM result can be invalid 2025-10-30 10:31:59 +07:00
dccf75b238 Don't show BIMI hint if result is skipped
Some checks are pending
continuous-integration/drone/push Build is running
2025-10-30 10:06:53 +07:00
f0dbc29da4 Handle multiple dkim authentication-results 2025-10-30 10:03:46 +07:00
8769514f1c Don't deduce point on weak SPF all qualifier, when DMARC is configured
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-28 11:42:23 +07:00
871f4e62f6 Fix content scoring error
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-27 17:56:06 +07:00
86ec7a6100 By default, only check the first IP against RBL, not all chain 2025-10-27 17:56:06 +07:00
2d3316eaaf Lock file maintenance
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-27 04:13:14 +00:00
730b43cad1 Fix SurveyURL property name
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-10-27 11:12:48 +07:00
ff5ac0fe1f Create a package to store the version number
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2025-10-26 21:54:00 +07:00
b95e5d6732 Add a favicon
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-26 21:42:58 +07:00
0325139461 Add a dark mode 2025-10-26 21:42:58 +07:00
07c7e63ee7 Create a stores directory 2025-10-26 21:42:58 +07:00
faf860f4a1 New option to configure ratelimit 2025-10-26 21:42:58 +07:00
6d2a59dd7b Update dependency @hey-api/openapi-ts to v0.86.4 2025-10-26 21:42:58 +07:00
39185f82bd Update PTR description 2025-10-26 21:42:58 +07:00
dd14d7a814 Update eslint monorepo to v9.38.0 2025-10-26 21:42:58 +07:00
924c54912a Update dependency @types/node to v22.18.12 2025-10-26 21:42:58 +07:00
20513bfc00 Update dependency vite to v7.1.12 2025-10-26 21:42:58 +07:00
b98988c153 Update dependency typescript-eslint to v8.46.2 2025-10-26 21:42:58 +07:00
386951b1eb Update module github.com/getkin/kin-openapi to v0.133.0 2025-10-26 21:42:58 +07:00
1a373d1942 Update dependency @hey-api/openapi-ts to v0.86.3 2025-10-26 21:42:58 +07:00
426fad9856 Add network config 2025-10-26 21:42:58 +07:00
2bedd0ed75 BIMI differenciate declined in summary 2025-10-26 21:42:58 +07:00
cdfdeb74fc Add a sample report in README.md 2025-10-26 21:42:58 +07:00
3c5bd5cbbd Add OpenGraph image and headers 2025-10-26 21:42:58 +07:00
c5bb9e46ac Add a background report sample in hero on home page
Why use a report from icloud.com to illustrate this project?

Among all the common email providers I tested, it achieved the best results.
2025-10-26 21:42:58 +07:00
4c71dd1d53 Handle errors on test page 2025-10-26 21:42:58 +07:00
76de5f60d6 Add survey capability 2025-10-26 21:42:58 +07:00
82373cdaac Redo cli_analyzer 2025-10-26 21:42:58 +07:00
edb172c4bc Update features on home page 2025-10-26 21:42:58 +07:00
53a48cba07 Fix typescript/svelte checks 2025-10-26 21:42:58 +07:00
08c6e0eef2 Rate limit API requests 2025-10-25 03:36:34 +07:00
932bc981b5 Filter Authentication-Results to keep only local ones 2025-10-25 03:36:34 +07:00
b2dc479a79 Update label associated with grade 2025-10-25 03:36:34 +07:00
6e2e403873 Improve pending state with clearer messages 2025-10-25 03:36:34 +07:00
eb28499dfd Monitor testId to update the page on test change 2025-10-25 03:36:25 +07:00
ff1a958220 Add a menu to results page 2025-10-25 03:36:25 +07:00
38b2be58fe Ensure latest created report is fetch 2025-10-25 03:31:29 +07:00
52f43c6bc5 Add x-align-form authentication test 2025-10-25 03:31:29 +07:00
3ea958b2fd Split dns.go in one file per check 2025-10-25 03:31:29 +07:00
a700db0873 Split authentication.go in one file per check 2025-10-25 03:31:29 +07:00
115da72874 Handle x-google-dkim authentication result 2025-10-25 03:31:29 +07:00
255027d00b Start bad spam score to B 2025-10-25 03:31:29 +07:00
9970e957d5 Handle SPF redirect 2025-10-25 03:31:29 +07:00
8fe8581b78 Handle declined auth result 2025-10-25 03:31:29 +07:00
aa35ab223d Align no DMARC authentication with on DMARC DNS record: grade C 2025-10-25 03:31:29 +07:00
29cb2cf1f9 Headers value for Date and email related are now parsed 2025-10-25 03:31:29 +07:00
7ed347c86e Improve test display in some circonstancies 2025-10-25 03:31:29 +07:00
4bbba66a81 Handle local postfix delivery 2025-10-25 03:31:29 +07:00
8b3ab541ba Add a summary after score 2025-10-25 03:31:29 +07:00
3588af3267 Add links to section 2025-10-25 03:31:29 +07:00
c1063cb4aa Add iprev check 2025-10-25 03:31:29 +07:00
3d03bfc4fa Handle relaxed domain match 2025-10-25 03:31:29 +07:00
84a504d668 Add reverse lookup and forward confirmation 2025-10-25 03:31:29 +07:00
7bc7e7b7a2 Reuse domain extractes from headers 2025-10-25 03:28:44 +07:00
326abc0744 Detect SPF all mechanism 2025-10-25 03:28:44 +07:00
a6448a1533 Split DnsRecordsCard in several components 2025-10-25 03:28:44 +07:00
e5c678174c Comprehensive DMARC record checks 2025-10-25 03:28:44 +07:00
5d335c6a6c Add email-path checks 2025-10-25 03:28:44 +07:00
a97729fea6 Tests design and descriptions 2025-10-24 09:56:35 +07:00
4149a5de92 Truncate DKIM record 2025-10-24 09:56:35 +07:00
8ca4bed875 SPF check return-path 2025-10-24 09:56:35 +07:00
f6a1ea73a2 Check SPF include 2025-10-24 09:56:35 +07:00
a64b866cfa Add grades 2025-10-24 09:56:35 +07:00
33d394a27b Improve content analyzing and reporting 2025-10-23 18:09:23 +07:00
41013d8af2 Improve headers reporting 2025-10-23 18:09:23 +07:00
e77bffb04f Improve spamassassin report 2025-10-23 18:09:23 +07:00
abfd1f0155 Add a score to DNS 2025-10-23 18:09:23 +07:00
fdb43533cd Prepend Return-Path header when receiving email 2025-10-23 18:09:23 +07:00
eb210e7bed Align spamassassin report 2025-10-23 18:09:23 +07:00
c51f8e5904 Improve authentication results 2025-10-23 18:09:23 +07:00
866cf2e5db Refactor spam score 2025-10-23 18:09:23 +07:00
eadc7ff8ca docker: Use spamass-milter 2025-10-23 18:09:23 +07:00
0581e0cf6b Use authentication_milter instead of opendkim and opendmarc 2025-10-23 18:09:23 +07:00
ec1ab7886e Rework DNS results 2025-10-23 10:40:12 +07:00
d87b0cbcb0 Remove checks 2025-10-23 10:40:12 +07:00
954a9d705e Change RBL test to return map of ips 2025-10-21 14:42:18 +07:00
1be917136c Handle RBL error report 2025-10-21 12:52:38 +07:00
74aee54432 Score as percentages 2025-10-21 12:52:20 +07:00
dfc0eeb323 New route to perform a new analysis of the email
All checks were successful
continuous-integration/drone/push Build is passing
2025-10-20 19:33:23 +07:00
0ac51ac06d Use grade instead of numeral notation 2025-10-20 19:25:48 +07:00
e3d89dc953 Add colors on scores 2025-10-20 15:39:12 +07:00
8bf3b500d9 Create a new test when visiting /test/ 2025-10-20 15:30:45 +07:00
0107858ee6 Simplify copy of the address 2025-10-20 15:30:45 +07:00
4d637214de Improve responsiveness 2025-10-20 15:30:45 +07:00
849bdb53c5 Use base32 encoded UUID to reduce address size 2025-10-20 15:30:45 +07:00
3f5e2c6dd4 Add option to add custom content before </head> and </body> 2025-10-20 15:13:53 +07:00
0084fd9660 Indicate when the next inbox check will be done 2025-10-20 15:13:53 +07:00
a687f5cb6b Improve branding 2025-10-20 15:13:53 +07:00
243ca4ba11 Add a banner in README.md 2025-10-20 15:13:53 +07:00
3c58f5ccd5 Lock file maintenance 2025-10-20 15:13:53 +07:00
1fa7af4c2b Fix spamassassin report details 2025-10-20 15:13:53 +07:00
30f774c1fb Expose analyzer 2025-10-20 15:13:53 +07:00
cd40b7c3ea Refactor authentication.go 2025-10-20 15:13:53 +07:00
8313fd7d98 Implement ARC header check 2025-10-20 15:08:45 +07:00
6097eb54c6 Implement BIMI checks 2025-10-20 15:08:45 +07:00
7e603ddf4a go mod tidy 2025-10-20 15:08:45 +07:00
e7aa80bef4 Update module golang.org/x/net to v0.46.0 2025-10-20 15:08:45 +07:00
b2bbf0ee78 Add an auto-cleanup worker 2025-10-20 15:08:45 +07:00
6096e043c6 Update dependency @hey-api/openapi-ts to v0.85.2 2025-10-20 15:08:45 +07:00
9ff2ca30cc Add CI/CD 2025-10-20 15:08:45 +07:00
4304784796 Add an auto-cleanup worker 2025-10-20 15:08:45 +07:00
6565c6fda4 Update dependency @hey-api/openapi-ts to v0.85.2 2025-10-20 15:08:44 +07:00
079dc6a813 Update module github.com/quic-go/quic-go to v0.54.1 [SECURITY] 2025-10-20 15:08:44 +07:00
16a0f3a158 Add renovate.json 2025-10-20 15:08:44 +07:00
20f5b37e5e Create test on email arrival 2025-10-20 15:08:44 +07:00
3867fa36a2 Add LMTP server 2025-10-20 15:02:14 +07:00
18c2f95112 Refactor main.go 2025-10-20 15:02:14 +07:00
4b9733531e Implement web ui 2025-10-20 15:02:14 +07:00
682ca6bb20 Web UI setup 2025-10-20 15:02:14 +07:00
3d823dedd8 Add AIO Dockerfile 2025-10-20 15:02:14 +07:00
62bb85ebec Glue things together 2025-10-20 15:02:14 +07:00
449a8a2c67 Handle config, parse flags 2025-10-19 15:47:38 +07:00
171 changed files with 35902 additions and 6734 deletions

27
.dockerignore Normal file
View file

@ -0,0 +1,27 @@
# Git files
.git
.gitignore
# Documentation
*.md
!README.md
# Build artifacts
happyDeliver
*.db
*.sqlite
*.sqlite3
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# Logs files
logs/
# Test files
*_test.go
testdata/

22
.drone-manifest.yml Normal file
View file

@ -0,0 +1,22 @@
image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
variant: v8
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux
variant: v7

156
.drone.yml Normal file
View file

@ -0,0 +1,156 @@
---
kind: pipeline
type: docker
name: build-arm64
platform:
os: linux
arch: arm64
steps:
- name: frontend
image: node:24-alpine
commands:
- cd web
- npm install --network-timeout=100000
- npm run generate:api
- npm run build
- name: backend-commit
image: golang:1-alpine
commands:
- apk add --no-cache git
- go generate ./...
- go build -tags netgo -ldflags '-w -X git.happydns.org/happyDeliver/internal/version.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDeliver
- ln happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydeliver
environment:
CGO_ENABLED: 0
when:
event:
exclude:
- tag
- name: backend-tag
image: golang:1-alpine
commands:
- apk add --no-cache git
- go generate ./...
- go build -tags netgo -ldflags '-w -X git.happydns.org/happyDeliver/internal/version.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
- ln happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydeliver
environment:
CGO_ENABLED: 0
when:
event:
- tag
- name: build-commit macOS
image: golang:1-alpine
commands:
- apk add --no-cache git
- go build -tags netgo -ldflags '-w -X "git.happydns.org/happyDeliver/internal/version.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
environment:
CGO_ENABLED: 0
GOOS: darwin
GOARCH: arm64
when:
event:
exclude:
- tag
- name: build-tag macOS
image: golang:1-alpine
commands:
- apk add --no-cache git
- go build -tags netgo -ldflags '-w -X "git.happydns.org/happyDeliver/internal/version.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
environment:
CGO_ENABLED: 0
GOOS: darwin
GOARCH: arm64
when:
event:
- tag
- name: publish on Docker Hub
image: plugins/docker
settings:
repo: happydomain/happydeliver
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
trigger:
branch:
exclude:
- renovate/*
event:
- cron
- push
- tag
---
kind: pipeline
type: docker
name: build-amd64
platform:
os: linux
arch: amd64
steps:
- name: publish on Docker Hub
image: plugins/docker
settings:
repo: happydomain/happydeliver
auto_tag: true
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
dockerfile: Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
trigger:
branch:
exclude:
- renovate/*
event:
- cron
- push
- tag
---
kind: pipeline
name: docker-manifest
platform:
os: linux
arch: arm64
steps:
- name: publish on Docker Hub
image: plugins/manifest
settings:
auto_tag: true
ignore_missing: true
spec: .drone-manifest.yml
username:
from_secret: docker_username
password:
from_secret: docker_password
trigger:
branch:
exclude:
- renovate/*
event:
- cron
- push
- tag
depends_on:
- build-amd64
- build-arm64

5
.gitignore vendored
View file

@ -17,11 +17,14 @@ vendor/
.env.local
*.local
# Logs files
logs/
# Database files
*.db
*.sqlite
*.sqlite3
# OpenAPI generated files
internal/api/models.gen.go
internal/api/server.gen.go
internal/model/types.gen.go

193
Dockerfile Normal file
View file

@ -0,0 +1,193 @@
# Multi-stage Dockerfile for happyDeliver with integrated MTA
# Stage 1: Build the Svelte application
FROM node:24-alpine AS nodebuild
WORKDIR /build
COPY api/ api/
COPY web/ web/
RUN yarn --cwd web install && \
yarn --cwd web run generate:api && \
yarn --cwd web --offline build
# Stage 2: Build the Go application
FROM golang:1-alpine AS builder
WORKDIR /build
# Install build dependencies
RUN apk add --no-cache ca-certificates git gcc musl-dev
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
COPY --from=nodebuild /build/web/build/ ./web/build/
# Build the application
RUN go generate ./... && \
CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o happyDeliver ./cmd/happyDeliver
# Stage 3: Prepare perl and spamass-milt
FROM alpine:3 AS pl
RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
apk add --no-cache \
build-base \
libmilter-dev \
musl-obstack-dev \
openssl \
openssl-dev \
perl-app-cpanminus \
perl-alien-libxml2 \
perl-class-load-xs \
perl-cpanel-json-xs \
perl-crypt-openssl-rsa \
perl-crypt-openssl-random \
perl-crypt-openssl-verify \
perl-crypt-openssl-x509 \
perl-cryptx \
perl-dbd-sqlite \
perl-dbi \
perl-email-address-xs \
perl-json-xs \
perl-list-moreutils \
perl-moose \
perl-net-idn-encode@edge \
perl-net-ssleay \
perl-netaddr-ip \
perl-package-stash \
perl-params-util \
perl-params-validate \
perl-proc-processtable \
perl-sereal-decoder \
perl-sereal-encoder \
perl-socket6 \
perl-sub-identify \
perl-variable-magic \
perl-xml-libxml \
perl-dev \
spamassassin-client \
zlib-dev \
&& \
ln -s /usr/bin/ld /bin/ld
RUN cpanm --notest Mail::SPF && \
cpanm --notest Mail::DKIM && \
cpanm --notest Mail::Milter::Authentication
RUN wget https://download.savannah.nongnu.org/releases/spamass-milt/spamass-milter-0.4.0.tar.gz && \
tar xzf spamass-milter-0.4.0.tar.gz && \
cd spamass-milter-0.4.0 && \
./configure && make install
# Stage 4: Runtime image with Postfix and all filters
FROM alpine:3
# Install all required packages
RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
apk add --no-cache \
bash \
ca-certificates \
libmilter \
openssl \
perl \
perl-alien-libxml2 \
perl-class-load-xs \
perl-cpanel-json-xs \
perl-crypt-openssl-rsa \
perl-crypt-openssl-random \
perl-crypt-openssl-verify \
perl-crypt-openssl-x509 \
perl-cryptx \
perl-dbd-sqlite \
perl-dbi \
perl-email-address-xs \
perl-json-xs \
perl-list-moreutils \
perl-moose \
perl-net-idn-encode@edge \
perl-net-ssleay \
perl-netaddr-ip \
perl-package-stash \
perl-params-util \
perl-params-validate \
perl-proc-processtable \
perl-sereal-decoder \
perl-sereal-encoder \
perl-socket6 \
perl-sub-identify \
perl-variable-magic \
perl-xml-libxml \
postfix \
postfix-pcre \
rspamd \
spamassassin \
spamassassin-client \
supervisor \
sqlite \
tzdata \
&& rm -rf /var/cache/apk/*
# Copy Mail::Milter::Authentication and its dependancies
COPY --from=pl /usr/local/ /usr/local/
# Create happydeliver user and group
RUN addgroup -g 1000 happydeliver && \
adduser -D -u 1000 -G happydeliver happydeliver
# Create necessary directories
RUN mkdir -p /etc/happydeliver \
/var/lib/happydeliver \
/var/log/happydeliver \
/var/cache/authentication_milter \
/var/lib/authentication_milter \
/var/spool/postfix/authentication_milter \
/var/spool/postfix/spamassassin \
/var/spool/postfix/rspamd \
&& chown -R happydeliver:happydeliver /var/lib/happydeliver /var/log/happydeliver \
&& chown -R mail:mail /var/spool/postfix/authentication_milter /var/spool/postfix/spamassassin \
&& chown rspamd:mail /var/spool/postfix/rspamd \
&& chmod 750 /var/spool/postfix/rspamd
# Copy the built application
COPY --from=builder /build/happyDeliver /usr/local/bin/happyDeliver
RUN chmod +x /usr/local/bin/happyDeliver
# Copy configuration files
COPY docker/postfix/ /etc/postfix/
COPY docker/authentication_milter/authentication_milter.json /etc/authentication_milter.json
COPY docker/spamassassin/ /etc/mail/spamassassin/
COPY docker/rspamd/local.d/ /etc/rspamd/local.d/
COPY docker/supervisor/ /etc/supervisor/
COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Expose ports
# 25 - SMTP
# 8080 - API server
EXPOSE 25 8080
# Default configuration
ENV HAPPYDELIVER_DATABASE_TYPE=sqlite \
HAPPYDELIVER_DATABASE_DSN=/var/lib/happydeliver/happydeliver.db \
HAPPYDELIVER_DOMAIN=happydeliver.local \
HAPPYDELIVER_ADDRESS_PREFIX=test- \
HAPPYDELIVER_DNS_TIMEOUT=5s \
HAPPYDELIVER_HTTP_TIMEOUT=10s \
HAPPYDELIVER_RSPAMD_API_URL=http://127.0.0.1:11334
# Volume for persistent data
VOLUME ["/var/lib/happydeliver", "/var/log/happydeliver"]
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/api/status || exit 1
# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"]
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]

329
README.md Normal file
View file

@ -0,0 +1,329 @@
# happyDeliver - Email Deliverability Tester
![banner](banner.webp)
An open-source email deliverability testing platform that analyzes test emails and provides detailed deliverability reports with scoring.
## Features
- **Complete Email Analysis**: Analyzes SPF, DKIM, DMARC, BIMI, ARC, SpamAssassin and rspamd scores, DNS records, blacklist status, content quality, and more
- **REST API**: Full-featured API for creating tests and retrieving reports
- **LMTP Server**: Built-in LMTP server for seamless MTA integration
- **Scoring System**: Gives A to F grades and scoring with weighted factors across dns, authentication, spam, blacklists, content, and headers
- **Database Storage**: SQLite or PostgreSQL support
- **Configurable**: via environment or config file for all settings
![A sample deliverability report](web/static/img/report.webp)
## Quick Start
### With Docker (Recommended)
The easiest way to run happyDeliver is using the all-in-one Docker container that includes Postfix, authentication_milter, SpamAssassin, and the happyDeliver application.
#### What's included in the Docker container:
- **Postfix MTA**: Receives emails on port 25
- **authentication_milter**: Entreprise grade email authentication
- **SpamAssassin**: Spam scoring and analysis
- **rspamd**: Second spam filter for cross-validated scoring
- **happyDeliver API**: REST API server on port 8080
- **SQLite Database**: Persistent storage for tests and reports
#### 1. Using docker-compose
```bash
# Clone the repository
git clone https://git.nemunai.re/happyDomain/happyDeliver.git
cd happydeliver
# Edit docker-compose.yml to set your domain
# Change HAPPYDELIVER_DOMAIN environment variable and hostname
# Build and start
docker-compose up -d
# View logs
docker-compose logs -f
# Stop
docker-compose down
```
The API will be available at `http://localhost:8080` and SMTP at `localhost:25`.
#### 2. Using docker build directly
```bash
# Build the image
docker build -t happydeliver:latest .
# Run the container
docker run -d \
--name happydeliver \
-p 25:25 \
-p 8080:8080 \
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
--hostname mail.yourdomain.com \
-v $(pwd)/data:/var/lib/happydeliver \
-v $(pwd)/logs:/var/log/happydeliver \
happydeliver:latest
```
#### 3. Configure TLS Certificates (Optional but Recommended)
To enable TLS encryption for incoming SMTP connections, you can configure Postfix to use your SSL/TLS certificates. This is highly recommended for production deployments.
##### Using docker-compose
Add the certificate paths to your `docker-compose.yml`:
```yaml
environment:
- POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt
- POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key
volumes:
- /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro
- /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro
```
##### Using docker run
```bash
docker run -d \
--name happydeliver \
-p 25:25 \
-p 8080:8080 \
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
-e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \
-e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \
--hostname mail.yourdomain.com \
-v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \
-v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \
-v $(pwd)/data:/var/lib/happydeliver \
-v $(pwd)/logs:/var/log/happydeliver \
happydeliver:latest
```
**Notes:**
- The certificate file should contain the full certificate chain (certificate + intermediate CAs)
- The private key file must be readable by the postfix user inside the container
- TLS is configured with `smtpd_tls_security_level = may`, which means it's opportunistic (STARTTLS supported but not required)
- If both environment variables are not set, Postfix will run without TLS support
#### 4. Configure Network and DNS
##### Open SMTP Port
Port 25 (SMTP) must be accessible from the internet to receive test emails:
```bash
# Check if port 25 is listening
netstat -ln | grep :25
# Allow port 25 through firewall (example with ufw)
sudo ufw allow 25/tcp
# For iptables
sudo iptables -A INPUT -p tcp --dport 25 -j ACCEPT
```
**Note:** Many ISPs and cloud providers block port 25 by default to prevent spam. You may need to request port 25 to be unblocked through your provider's support.
##### Configure DNS Records
Point your domain to the server's IP address.
```
yourdomain.com. IN A 203.0.113.10
yourdomain.com. IN AAAA 2001:db8::10
```
Replace `yourdomain.com` with the value you set for `HAPPYDELIVER_DOMAIN` and IPs accordingly.
There is no need for an MX record here since the same host will serve both HTTP and SMTP.
### Manual Build
#### 1. Build
```bash
go generate
go build -o happyDeliver ./cmd/happyDeliver
```
### 2. Run the API Server
```bash
./happyDeliver server
```
The server will start on `http://localhost:8080` by default.
#### 3. Integrate with your existing e-mail setup
It is expected your setup annotate the email with eg. opendkim, spamassassin, rspamd, ...
happyDeliver will not perform thoses checks, it relies instead on standard software to have real world annotations.
#### Receiver Hostname
happyDeliver filters `Authentication-Results` headers by hostname to only trust headers added by your MTA (and not headers that may have been injected by the sender). By default, it uses the system hostname (`os.Hostname()`).
If your MTA's `authserv-id` (the hostname at the beginning of `Authentication-Results` headers) differs from the machine running happyDeliver, you must set it explicitly:
```bash
./happyDeliver server -receiver-hostname mail.example.com
```
Or via environment variable:
```bash
HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ./happyDeliver server
```
**How to find the correct value:** look at the `Authentication-Results` headers in a received email. They start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...` — in this case, use `mail.example.com`.
If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname.
#### Postfix LMTP Transport
You'll obtain the best results with a custom [transport rule](https://www.postfix.org/transport.5.html) using LMTP.
1. Start the happyDeliver server with LMTP enabled (default listens on `127.0.0.1:2525`):
```bash
./happyDeliver server
```
You can customize the LMTP address with the `-lmtp-addr` flag or in the config file.
2. Create the file `/etc/postfix/transport_happydeliver` with the following content:
```
# Transport map - route test emails to happyDeliver LMTP server
# Pattern: test-<base32-uuid>@yourdomain.com -> LMTP on localhost:2525
/^test-[a-zA-Z2-7-]{26,30}@yourdomain\.com$/ lmtp:inet:127.0.0.1:2525
```
3. Append the created file to `transport_maps` in your `main.cf`:
```diff
-transport_maps = texthash:/etc/postfix/transport
+transport_maps = texthash:/etc/postfix/transport, pcre:/etc/postfix/transport_happydeliver
```
If your `transport_maps` option is not set, just append this line:
```
transport_maps = pcre:/etc/postfix/transport_happydeliver
```
Note: to use the `pcre:` type, you need to have `postfix-pcre` installed.
4. Reload Postfix configuration:
```bash
postfix reload
```
#### 4. Create a Test
```bash
curl -X POST http://localhost:8080/api/test
```
Response:
```json
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "test-kfauqaao-ukj2if3n-fgrfkiafaa@localhost",
"status": "pending",
"message": "Send your test email to the address above"
}
```
#### 5. Send Test Email
Send a test email to the address provided (you'll need to configure your MTA to route emails to the analyzer - see MTA Integration below).
#### 6. Get Report
```bash
curl http://localhost:8080/api/report/550e8400-e29b-41d4-a716-446655440000
```
## API Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/api/test` | POST | Create a new deliverability test |
| `/api/test/{id}` | GET | Get test metadata and status |
| `/api/report/{id}` | GET | Get detailed analysis report |
| `/api/report/{id}/raw` | GET | Get raw annotated email |
| `/api/status` | GET | Service health and status |
## Email Analyzer (CLI Mode)
For manual testing or debugging, you can analyze emails from the command line:
```bash
cat email.eml | ./happyDeliver analyze
```
Or specify recipient explicitly:
```bash
cat email.eml | ./happyDeliver analyze -recipient test-uuid@yourdomain.com
```
**Note:** In production, emails are delivered via LMTP (see integration instructions above).
## Use with happyDomain
happyDeliver can be driven by [happyDomain](https://happydomain.org) through
the [`checker-happydeliver`](https://git.nemunai.re/happyDomain/checker-happydeliver)
plugin, so the deliverability of a domain you manage is monitored alongside
its DNS and inbound SMTP posture.
How it works:
1. Attach the **Outbound deliverability** checker to the mail service of a zone
in happyDomain. Point it at a happyDeliver instance via `happydeliver_url`;
operators can configure a default instance globally.
2. On each run, the checker calls `POST /api/test` to allocate a fresh
recipient address, prompts the user (or an automated sender) to mail it from
the tested domain, then polls `GET /api/test/{id}` until the report is
ready.
3. The structured report from `GET /api/report/{id}` is translated into
happyDomain rule states: CRIT/WARN/INFO on SPF, DKIM, DMARC, alignment, spam
score, blacklists and headers, plus an overall score threshold
(`min_score`/`warn_score`).
4. Runs repeat on a configurable interval so a regression in deliverability (a
new RBL listing, a DKIM key rotation gone wrong, a broken SPF include, ...)
surfaces as a domain-level alert in happyDomain.
See the [`checker-happydeliver` repository](https://git.nemunai.re/happyDomain/checker-happydeliver)
for build instructions and the full list of run options.
## Scoring System
The deliverability score is calculated from A to F based on:
- **DNS**: Step-by-step analysis of PTR, Forward-Confirmed Reverse DNS, MX, SPF, DKIM, DMARC and BIMI records
- **Authentication**: IPRev, SPF, DKIM, DMARC, BIMI and ARC validation
- **Blacklist**: RBL/DNSBL checks
- **Headers**: Required headers, MIME structure, Domain alignment
- **Spam**: SpamAssassin and rspamd scores (combined 50/50)
- **Content**: HTML quality, links, images, unsubscribe
## Funding
This project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/happyDomain).
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/core)
## License
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)

View file

@ -1,5 +1,9 @@
package: api
package: model
generate:
models: true
embedded-spec: false
output: internal/api/models.gen.go
embedded-spec: true
output: internal/model/types.gen.go
output-options:
skip-prune: true
import-mapping:
./schemas.yaml: "-"

View file

@ -1,5 +1,8 @@
package: api
generate:
gin-server: true
models: true
embedded-spec: true
output: internal/api/server.gen.go
import-mapping:
./schemas.yaml: git.happydns.org/happyDeliver/internal/model

View file

@ -31,11 +31,11 @@ paths:
tags:
- tests
summary: Create a new deliverability test
description: Generates a unique test email address for sending test emails
description: Generates a unique test email address for sending test emails. No database record is created until an email is received.
operationId: createTest
responses:
'201':
description: Test created successfully
description: Test email address generated successfully
content:
application/json:
schema:
@ -51,8 +51,8 @@ paths:
get:
tags:
- tests
summary: Get test metadata
description: Retrieve test status and metadata
summary: Get test status
description: Check if a report exists for the given test ID (base32-encoded). Returns pending if no report exists, analyzed if a report is available.
operationId: getTest
parameters:
- name: id
@ -60,16 +60,60 @@ paths:
required: true
schema:
type: string
format: uuid
pattern: '^[a-z0-9-]+$'
description: Base32-encoded test ID (with hyphens)
responses:
'200':
description: Test metadata retrieved successfully
description: Test status retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Test'
'404':
description: Test not found
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/tests:
get:
tags:
- tests
summary: List all tests
description: Returns a paginated list of test summaries with scores and grades. Can be disabled via server configuration.
operationId: listTests
parameters:
- name: offset
in: query
schema:
type: integer
minimum: 0
default: 0
description: Number of items to skip
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
description: Maximum number of items to return
responses:
'200':
description: List of test summaries
content:
application/json:
schema:
$ref: '#/components/schemas/TestListResponse'
'403':
description: Test listing is disabled
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
@ -88,7 +132,8 @@ paths:
required: true
schema:
type: string
format: uuid
pattern: '^[a-z0-9-]+$'
description: Base32-encoded test ID (with hyphens)
responses:
'200':
description: Report retrieved successfully
@ -116,7 +161,8 @@ paths:
required: true
schema:
type: string
format: uuid
pattern: '^[a-z0-9-]+$'
description: Base32-encoded test ID (with hyphens)
responses:
'200':
description: Raw email retrieved successfully
@ -131,6 +177,107 @@ paths:
schema:
$ref: '#/components/schemas/Error'
/report/{id}/reanalyze:
post:
tags:
- reports
summary: Reanalyze email and regenerate report
description: Re-run the analysis on the stored raw email to regenerate the report with the latest analyzer version. This is useful after analyzer improvements or bug fixes.
operationId: reanalyzeReport
parameters:
- name: id
in: path
required: true
schema:
type: string
pattern: '^[a-z0-9-]+$'
description: Base32-encoded test ID (with hyphens)
responses:
'200':
description: Report regenerated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/Report'
'404':
description: Email not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error during reanalysis
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/domain:
post:
tags:
- tests
summary: Test a domain's email configuration
description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately.
operationId: testDomain
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DomainTestRequest'
responses:
'200':
description: Domain test completed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/DomainTestResponse'
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/blacklist:
post:
tags:
- tests
summary: Check an IP address against DNS blacklists
description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately.
operationId: checkBlacklist
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BlacklistCheckRequest'
responses:
'200':
description: Blacklist check completed successfully
content:
application/json:
schema:
$ref: '#/components/schemas/BlacklistCheckResponse'
'400':
description: Invalid request
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: Internal server error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/status:
get:
tags:
@ -149,359 +296,74 @@ paths:
components:
schemas:
Test:
type: object
required:
- id
- email
- status
- created_at
properties:
id:
type: string
format: uuid
description: Unique test identifier
example: "550e8400-e29b-41d4-a716-446655440000"
email:
type: string
format: email
description: Unique test email address
example: "test-550e8400@example.com"
status:
type: string
enum: [pending, received, analyzed, failed]
description: Current test status
example: "analyzed"
created_at:
type: string
format: date-time
description: Test creation timestamp
updated_at:
type: string
format: date-time
description: Last update timestamp
$ref: './schemas.yaml#/components/schemas/Test'
TestResponse:
type: object
required:
- id
- email
- status
properties:
id:
type: string
format: uuid
example: "550e8400-e29b-41d4-a716-446655440000"
email:
type: string
format: email
example: "test-550e8400@example.com"
status:
type: string
enum: [pending]
example: "pending"
message:
type: string
example: "Send your test email to the address above"
$ref: './schemas.yaml#/components/schemas/TestResponse'
Report:
type: object
required:
- id
- test_id
- score
- checks
- created_at
properties:
id:
type: string
format: uuid
description: Report identifier
test_id:
type: string
format: uuid
description: Associated test ID
score:
type: number
format: float
minimum: 0
maximum: 10
description: Overall deliverability score (0-10)
example: 8.5
summary:
$ref: '#/components/schemas/ScoreSummary'
checks:
type: array
items:
$ref: '#/components/schemas/Check'
authentication:
$ref: '#/components/schemas/AuthenticationResults'
spamassassin:
$ref: '#/components/schemas/SpamAssassinResult'
dns_records:
type: array
items:
$ref: '#/components/schemas/DNSRecord'
blacklists:
type: array
items:
$ref: '#/components/schemas/BlacklistCheck'
raw_headers:
type: string
description: Raw email headers
created_at:
type: string
format: date-time
$ref: './schemas.yaml#/components/schemas/Report'
ScoreSummary:
type: object
required:
- authentication_score
- spam_score
- blacklist_score
- content_score
- header_score
properties:
authentication_score:
type: number
format: float
minimum: 0
maximum: 3
description: SPF/DKIM/DMARC score (max 3 pts)
example: 2.8
spam_score:
type: number
format: float
minimum: 0
maximum: 2
description: SpamAssassin score (max 2 pts)
example: 1.5
blacklist_score:
type: number
format: float
minimum: 0
maximum: 2
description: Blacklist check score (max 2 pts)
example: 2.0
content_score:
type: number
format: float
minimum: 0
maximum: 2
description: Content quality score (max 2 pts)
example: 1.8
header_score:
type: number
format: float
minimum: 0
maximum: 1
description: Header quality score (max 1 pt)
example: 0.9
Check:
type: object
required:
- category
- name
- status
- score
- message
properties:
category:
type: string
enum: [authentication, dns, content, blacklist, headers, spam]
description: Check category
example: "authentication"
name:
type: string
description: Check name
example: "DKIM Signature"
status:
type: string
enum: [pass, fail, warn, info, error]
description: Check result status
example: "pass"
score:
type: number
format: float
description: Points contributed to total score
example: 1.0
message:
type: string
description: Human-readable result message
example: "DKIM signature is valid"
details:
type: string
description: Additional details (may be JSON)
severity:
type: string
enum: [critical, high, medium, low, info]
description: Issue severity
example: "info"
advice:
type: string
description: Remediation advice
example: "Your DKIM configuration is correct"
$ref: './schemas.yaml#/components/schemas/ScoreSummary'
ContentAnalysis:
$ref: './schemas.yaml#/components/schemas/ContentAnalysis'
ContentIssue:
$ref: './schemas.yaml#/components/schemas/ContentIssue'
LinkCheck:
$ref: './schemas.yaml#/components/schemas/LinkCheck'
ImageCheck:
$ref: './schemas.yaml#/components/schemas/ImageCheck'
HeaderAnalysis:
$ref: './schemas.yaml#/components/schemas/HeaderAnalysis'
HeaderCheck:
$ref: './schemas.yaml#/components/schemas/HeaderCheck'
ReceivedHop:
$ref: './schemas.yaml#/components/schemas/ReceivedHop'
DKIMDomainInfo:
$ref: './schemas.yaml#/components/schemas/DKIMDomainInfo'
DomainAlignment:
$ref: './schemas.yaml#/components/schemas/DomainAlignment'
HeaderIssue:
$ref: './schemas.yaml#/components/schemas/HeaderIssue'
AuthenticationResults:
type: object
properties:
spf:
$ref: '#/components/schemas/AuthResult'
dkim:
type: array
items:
$ref: '#/components/schemas/AuthResult'
dmarc:
$ref: '#/components/schemas/AuthResult'
$ref: './schemas.yaml#/components/schemas/AuthenticationResults'
AuthResult:
type: object
required:
- result
properties:
result:
type: string
enum: [pass, fail, none, neutral, softfail, temperror, permerror]
description: Authentication result
example: "pass"
domain:
type: string
description: Domain being authenticated
example: "example.com"
selector:
type: string
description: DKIM selector (for DKIM only)
example: "default"
details:
type: string
description: Additional details about the result
$ref: './schemas.yaml#/components/schemas/AuthResult'
ARCResult:
$ref: './schemas.yaml#/components/schemas/ARCResult'
IPRevResult:
$ref: './schemas.yaml#/components/schemas/IPRevResult'
SpamAssassinResult:
type: object
required:
- score
- required_score
- is_spam
properties:
score:
type: number
format: float
description: SpamAssassin spam score
example: 2.3
required_score:
type: number
format: float
description: Threshold for spam classification
example: 5.0
is_spam:
type: boolean
description: Whether message is classified as spam
example: false
tests:
type: array
items:
type: string
description: List of triggered SpamAssassin tests
example: ["BAYES_00", "DKIM_SIGNED"]
report:
type: string
description: Full SpamAssassin report
DNSRecord:
type: object
required:
- domain
- record_type
- status
properties:
domain:
type: string
description: Domain name
example: "example.com"
record_type:
type: string
enum: [MX, SPF, DKIM, DMARC]
description: DNS record type
example: "SPF"
status:
type: string
enum: [found, missing, invalid]
description: Record status
example: "found"
value:
type: string
description: Record value
example: "v=spf1 include:_spf.example.com ~all"
$ref: './schemas.yaml#/components/schemas/SpamAssassinResult'
SpamTestDetail:
$ref: './schemas.yaml#/components/schemas/SpamTestDetail'
RspamdResult:
$ref: './schemas.yaml#/components/schemas/RspamdResult'
DNSResults:
$ref: './schemas.yaml#/components/schemas/DNSResults'
MXRecord:
$ref: './schemas.yaml#/components/schemas/MXRecord'
SPFRecord:
$ref: './schemas.yaml#/components/schemas/SPFRecord'
DKIMRecord:
$ref: './schemas.yaml#/components/schemas/DKIMRecord'
DMARCRecord:
$ref: './schemas.yaml#/components/schemas/DMARCRecord'
BIMIRecord:
$ref: './schemas.yaml#/components/schemas/BIMIRecord'
BlacklistCheck:
type: object
required:
- ip
- rbl
- listed
properties:
ip:
type: string
description: IP address checked
example: "192.0.2.1"
rbl:
type: string
description: RBL/DNSBL name
example: "zen.spamhaus.org"
listed:
type: boolean
description: Whether IP is listed
example: false
response:
type: string
description: RBL response code or message
example: "127.0.0.2"
$ref: './schemas.yaml#/components/schemas/BlacklistCheck'
Status:
type: object
required:
- status
- version
properties:
status:
type: string
enum: [healthy, degraded, unhealthy]
description: Overall service status
example: "healthy"
version:
type: string
description: Service version
example: "0.1.0-dev"
components:
type: object
properties:
database:
type: string
enum: [up, down]
example: "up"
mta:
type: string
enum: [up, down]
example: "up"
uptime:
type: integer
description: Service uptime in seconds
example: 3600
$ref: './schemas.yaml#/components/schemas/Status'
Error:
type: object
required:
- error
- message
properties:
error:
type: string
description: Error code
example: "not_found"
message:
type: string
description: Human-readable error message
example: "Test not found"
details:
type: string
description: Additional error details
$ref: './schemas.yaml#/components/schemas/Error'
DomainTestRequest:
$ref: './schemas.yaml#/components/schemas/DomainTestRequest'
DomainTestResponse:
$ref: './schemas.yaml#/components/schemas/DomainTestResponse'
BlacklistCheckRequest:
$ref: './schemas.yaml#/components/schemas/BlacklistCheckRequest'
BlacklistCheckResponse:
$ref: './schemas.yaml#/components/schemas/BlacklistCheckResponse'
TestSummary:
$ref: './schemas.yaml#/components/schemas/TestSummary'
TestListResponse:
$ref: './schemas.yaml#/components/schemas/TestListResponse'

1221
api/schemas.yaml Normal file

File diff suppressed because it is too large Load diff

BIN
banner.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -22,31 +22,50 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"git.happydns.org/happyDeliver/internal/app"
"git.happydns.org/happyDeliver/internal/config"
"git.happydns.org/happyDeliver/internal/version"
)
func main() {
fmt.Println("Mail Tester - Email Deliverability Testing Platform")
fmt.Println("Version: 0.1.0-dev")
fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform")
fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version)
if len(os.Args) < 2 {
printUsage()
os.Exit(1)
cfg, err := config.ConsolidateConfig()
if err != nil {
log.Fatal(err.Error())
}
command := os.Args[1]
command := flag.Arg(0)
switch command {
case "server":
log.Println("Starting API server...")
// TODO: Start API server
if err := app.RunServer(cfg); err != nil {
log.Fatalf("Server error: %v", err)
}
case "analyze":
log.Println("Starting email analyzer...")
// TODO: Start email analyzer (LMTP/pipe mode)
if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil {
log.Fatalf("Analyzer error: %v", err)
}
case "backup":
if err := app.RunBackup(cfg); err != nil {
log.Fatalf("Backup error: %v", err)
}
case "restore":
inputFile := ""
if len(flag.Args()) >= 2 {
inputFile = flag.Args()[1]
}
if err := app.RunRestore(cfg, inputFile); err != nil {
log.Fatalf("Restore error: %v", err)
}
case "version":
fmt.Println("0.1.0-dev")
fmt.Println(version.Version)
default:
fmt.Printf("Unknown command: %s\n", command)
printUsage()
@ -55,8 +74,12 @@ func main() {
}
func printUsage() {
fmt.Println("\nUsage:")
fmt.Println(" mailtester server - Start the API server")
fmt.Println(" mailtester analyze - Start the email analyzer (MDA mode)")
fmt.Println(" mailtester version - Print version information")
fmt.Println("\nCommand availables:")
fmt.Println(" happyDeliver server - Start the API server")
fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal")
fmt.Println(" happyDeliver backup - Backup database to stdout as JSON")
fmt.Println(" happyDeliver restore [file] - Restore database from JSON file or stdin")
fmt.Println(" happyDeliver version - Print version information")
fmt.Println("")
flag.Usage()
}

78
docker-compose.yml Normal file
View file

@ -0,0 +1,78 @@
services:
unbound:
image: alpinelinux/unbound
restart: unless-stopped
configs:
- source: unbound_conf
target: /etc/unbound/unbound.conf
uid: "100"
gid: "101"
networks:
default:
ipv4_address: 172.28.0.53
happydeliver:
build:
context: .
dockerfile: Dockerfile
image: happydomain/happydeliver:latest
container_name: happydeliver
# Set a hostname
hostname: mail.happydeliver.local
environment:
# Set your domain
HAPPYDELIVER_DOMAIN: happydeliver.local
ports:
# SMTP port
- "25:25"
# API port
- "8080:8080"
volumes:
# Persistent database storage
- ./data:/var/lib/happydeliver
# Log files
- ./logs:/var/log/happydeliver
dns:
- 172.28.0.53
restart: unless-stopped
configs:
unbound_conf:
content: |
server:
verbosity: 1
interface: 0.0.0.0
port: 53
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
access-control: 127.0.0.0/8 allow
access-control: 172.28.0.0/24 allow
# Short cache for a testing resolver
cache-max-ttl: 60
# Buffers: let the system decide
so-sndbuf: 0
so-rcvbuf: 0
# Trust anchor (static, ships with the image)
trust-anchor-file: "/etc/unbound/root.key"
volumes:
data:
logs:
networks:
default:
ipam:
config:
- subnet: 172.28.0.0/24

189
docker/README.md Normal file
View file

@ -0,0 +1,189 @@
# happyDeliver Docker Configuration
This directory contains all configuration files for the all-in-one Docker container.
## Architecture
The Docker container integrates multiple components:
- **Postfix**: Mail Transfer Agent (MTA) that receives emails on port 25
- **OpenDKIM**: DKIM signature verification
- **OpenDMARC**: DMARC policy validation
- **SpamAssassin**: Spam scoring and content analysis
- **happyDeliver**: Go application (API server + email analyzer)
- **Supervisor**: Process manager that runs all services
## Directory Structure
```
docker/
├── postfix/
│ ├── main.cf # Postfix main configuration
│ ├── master.cf # Postfix service definitions
│ └── transport_maps # Email routing rules
├── opendkim/
│ └── opendkim.conf # DKIM verification config
├── opendmarc/
│ └── opendmarc.conf # DMARC validation config
├── spamassassin/
│ └── local.cf # SpamAssassin rules and scoring
├── supervisor/
│ └── supervisord.conf # Supervisor service definitions
├── entrypoint.sh # Container initialization script
└── config.docker.yaml # happyDeliver default config
```
## Configuration Details
### Postfix (postfix/)
**main.cf**: Core Postfix settings
- Configures hostname, domain, and network interfaces
- Sets up milter integration for OpenDKIM and OpenDMARC
- Configures SPF policy checking
- Routes emails through SpamAssassin content filter
- Uses transport_maps to route test emails to happyDeliver
**master.cf**: Service definitions
- Defines SMTP service with content filtering
- Sets up SPF policy service (postfix-policyd-spf-perl)
- Configures SpamAssassin content filter
- Defines happydeliver pipe for email analysis
**transport_maps**: PCRE-based routing
- Matches test-UUID@domain emails
- Routes them to the happydeliver pipe
### OpenDKIM (opendkim/)
**opendkim.conf**: DKIM verification settings
- Operates in verification-only mode
- Adds Authentication-Results headers
- Socket communication with Postfix via milter
- 5-second DNS timeout
### OpenDMARC (opendmarc/)
**opendmarc.conf**: DMARC validation settings
- Validates DMARC policies
- Adds results to Authentication-Results headers
- Does not reject emails (analysis mode only)
- Socket communication with Postfix via milter
### SpamAssassin (spamassassin/)
**local.cf**: Spam detection rules
- Enables network tests (RBL checks)
- SPF and DKIM checking
- Required score: 5.0 (standard threshold)
- Adds detailed spam report headers
- 5-second RBL timeout
### Supervisor (supervisor/)
**supervisord.conf**: Service orchestration
- Runs all services as daemons
- Start order: OpenDKIM → OpenDMARC → SpamAssassin → Postfix → API
- Automatic restart on failure
- Centralized logging
### Entrypoint Script (entrypoint.sh)
Initialization script that:
1. Creates required directories and sets permissions
2. Replaces configuration placeholders with environment variables
3. Initializes Postfix (aliases, transport maps)
4. Updates SpamAssassin rules
5. Starts Supervisor to launch all services
### happyDeliver Config (config.docker.yaml)
Default configuration for the Docker environment:
- API server on 0.0.0.0:8080
- SQLite database at /var/lib/happydeliver/happydeliver.db
- Configurable domain for test emails
- RBL servers for blacklist checking
- Timeouts for DNS and HTTP checks
## Environment Variables
The container accepts these environment variables:
- `HAPPYDELIVER_DOMAIN`: Email domain for test addresses (default: happydeliver.local)
- `HAPPYDELIVER_RECEIVER_HOSTNAME`: Hostname used to filter `Authentication-Results` headers (see below)
- `POSTFIX_CERT_FILE` / `POSTFIX_KEY_FILE`: TLS certificate and key paths for Postfix SMTP
### Receiver Hostname
happyDeliver filters `Authentication-Results` headers by hostname to only trust results from the expected MTA. By default, it uses the system hostname (i.e., the container's `--hostname`).
In the all-in-one Docker container, the container hostname is also used as the `authserv-id` in the embedded Postfix and authentication_milter, so everything matches automatically.
**When bypassing the embedded Postfix** (e.g., routing emails from your own MTA via LMTP), your MTA's `authserv-id` will likely differ from the container hostname. In that case, set `HAPPYDELIVER_RECEIVER_HOSTNAME` to your MTA's hostname:
```bash
docker run -d \
-e HAPPYDELIVER_DOMAIN=example.com \
-e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com \
...
```
To find the correct value, look at the `Authentication-Results` headers in a received email — they start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...`.
If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname.
Example (all-in-one, no override needed):
```bash
docker run -e HAPPYDELIVER_DOMAIN=example.com --hostname mail.example.com ...
```
Example (external MTA integration):
```bash
docker run -e HAPPYDELIVER_DOMAIN=example.com -e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ...
```
## Volumes
**Required volumes:**
- `/var/lib/happydeliver`: Database and persistent data
- `/var/log/happydeliver`: Log files from all services
**Optional volumes:**
- `/etc/happydeliver/config.yaml`: Custom configuration file
## Ports
- **25**: SMTP (Postfix)
- **8080**: HTTP API (happyDeliver)
## Service Startup Order
Supervisor ensures services start in the correct order:
1. **OpenDKIM** (priority 10): DKIM verification milter
2. **OpenDMARC** (priority 11): DMARC validation milter
3. **SpamAssassin** (priority 12): Spam scoring daemon
4. **Postfix** (priority 20): MTA that uses the above services
5. **happyDeliver API** (priority 30): REST API server
## Email Processing Flow
1. Email arrives at Postfix on port 25
2. Postfix sends to OpenDKIM milter
- Verifies DKIM signature
- Adds `Authentication-Results: ... dkim=pass/fail`
3. Postfix sends to OpenDMARC milter
- Validates DMARC policy
- Adds `Authentication-Results: ... dmarc=pass/fail`
4. Postfix routes through SpamAssassin content filter
- Checks SPF record
- Scores email for spam
- Adds `X-Spam-Status` and `X-Spam-Report` headers
5. Postfix checks transport_maps
- If recipient matches test-UUID pattern, route to happydeliver pipe
6. happyDeliver analyzer receives email
- Extracts test ID from recipient
- Parses all headers added by filters
- Performs additional analysis (DNS, RBL, content)
- Generates deliverability score
- Stores report in database

View file

@ -0,0 +1,75 @@
{
"logtoerr" : "1",
"error_log" : "",
"connection" : "unix:/var/spool/postfix/authentication_milter/authentication_milter.sock",
"umask" : "0007",
"runas" : "mail",
"rungroup" : "mail",
"authserv_id" : "__HOSTNAME__",
"connect_timeout" : 30,
"command_timeout" : 30,
"content_timeout" : 300,
"dns_timeout" : 10,
"dns_retry" : 2,
"handlers" : {
"Sanitize" : {
"hosts_to_remove" : [
"__HOSTNAME__"
],
"extra_auth_results_types" : [
"X-Spam-Status",
"X-Spam-Report",
"X-Spam-Level",
"X-Spam-Checker-Version"
]
},
"SPF" : {
"hide_none" : 0
},
"DKIM" : {
"hide_none" : 0,
},
"XGoogleDKIM" : {
"hide_none" : 1,
},
"ARC" : {
"hide_none" : 0,
},
"DMARC" : {
"hide_none" : 0,
"detect_list_id" : "1"
},
"BIMI" : {},
"PTR" : {},
"SenderID" : {
"hide_none" : 1
},
"IPRev" : {},
"Auth" : {},
"AlignedFrom" : {},
"LocalIP" : {},
"TrustedIP" : {
"trusted_ip_list" : []
},
"!AddID" : {},
"ReturnOK" : {}
}
}

View file

@ -0,0 +1,58 @@
; This is YOU. DMARC reports include information about the reports. Enter it here.
[organization]
domain = example.com
org_name = My Company Limited
email = admin@example.com
extra_contact_info = http://example.com
; aggregate DMARC reports need to be stored somewhere. Any database
; with a DBI module (MySQL, SQLite, DBD, etc.) should work.
; SQLite and MySQL are tested.
; Default is sqlite.
[report_store]
backend = SQL
;dsn = dbi:SQLite:dbname=dmarc_reports.sqlite
dsn = dbi:mysql:database=dmarc_reporting_database;host=localhost;port=3306
user = authmilterusername
pass = authmiltpassword
; backend can be perl or libopendmarc
[dmarc]
backend = perl
[dns]
timeout = 5
public_suffix_list = share/public_suffix_list
[smtp]
; hostname is the external FQDN of this MTA
hostname = mx1.example.com
cc = dmarc.copy@example.com
; list IP addresses to whitelist (bypass DMARC reject/quarantine)
; see sample whitelist in share/dmarc_whitelist
whitelist = /path/to/etc/dmarc_whitelist
; By default, we attempt to email directly to the report recipient.
; Set these to relay via a SMTP smart host.
smarthost = mx2.example.com
smartuser = dmarccopyusername
smartpass = dmarccopypassword
[imap]
server = mail.example.com
user =
pass =
; the imap folder where new dmarc messages will be found
folder = dmarc
; the folders to store processed reports (a=aggregate, f=forensic)
f_done = dmarc.forensic
a_done = dmarc.aggregate
[http]
port = 8080
[https]
port = 8443
ssl_crt =
ssl_key =

74
docker/entrypoint.sh Normal file
View file

@ -0,0 +1,74 @@
#!/bin/bash
set -e
echo "Starting happyDeliver container..."
# Get environment variables with defaults
[ -n "${HOSTNAME}" ] || HOSTNAME=$(hostname)
HAPPYDELIVER_DOMAIN="${HAPPYDELIVER_DOMAIN:-happydeliver.local}"
echo "Hostname: $HOSTNAME"
echo "Domain: $HAPPYDELIVER_DOMAIN"
# Create socket directories
mkdir -p /var/spool/postfix/authentication_milter
chown mail:mail /var/spool/postfix/authentication_milter
chmod 750 /var/spool/postfix/authentication_milter
mkdir -p /var/spool/postfix/rspamd
chown rspamd:mail /var/spool/postfix/rspamd
chmod 750 /var/spool/postfix/rspamd
# Create log directory
mkdir -p /var/log/happydeliver /var/cache/authentication_milter /var/spool/authentication_milter /var/lib/authentication_milter /run/authentication_milter
chown happydeliver:happydeliver /var/log/happydeliver
chown mail:mail /var/cache/authentication_milter /run/authentication_milter /var/spool/authentication_milter /var/lib/authentication_milter
# Replace placeholders in Postfix configuration
echo "Configuring Postfix..."
sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/postfix/main.cf
sed -i "s/__DOMAIN__/${HAPPYDELIVER_DOMAIN}/g" /etc/postfix/main.cf
# Add certificates to postfix
[ -n "${POSTFIX_CERT_FILE}" ] && [ -n "${POSTFIX_KEY_FILE}" ] && {
cat <<EOF >> /etc/postfix/main.cf
smtpd_tls_cert_file = ${POSTFIX_CERT_FILE}
smtpd_tls_key_file = ${POSTFIX_KEY_FILE}
smtpd_tls_security_level = may
EOF
}
# Replace placeholders in configurations
sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/authentication_milter.json
# Initialize Postfix aliases
if [ -f /etc/postfix/aliases ]; then
echo "Initializing Postfix aliases..."
postalias /etc/postfix/aliases || true
fi
# Compile transport maps
if [ -f /etc/postfix/transport_maps ]; then
echo "Compiling transport maps..."
postmap /etc/postfix/transport_maps
fi
# Update SpamAssassin rules
echo "Updating SpamAssassin rules..."
sa-update || echo "SpamAssassin rules update failed (might be first run)"
# Compile SpamAssassin rules
sa-compile || echo "SpamAssassin compilation skipped"
# Initialize database if it doesn't exist
if [ ! -f /var/lib/happydeliver/happydeliver.db ]; then
echo "Database will be initialized on first API startup..."
fi
# Set proper permissions
chown -R happydeliver:happydeliver /var/lib/happydeliver
echo "Configuration complete, starting services..."
# Execute the main command (supervisord)
exec "$@"

10
docker/postfix/aliases Normal file
View file

@ -0,0 +1,10 @@
# Postfix aliases for happyDeliver
# This file is processed by postalias to create aliases.db
# Standard aliases
postmaster: root
abuse: root
mailer-daemon: postmaster
# Root mail can be redirected if needed
# root: admin@example.com

40
docker/postfix/main.cf Normal file
View file

@ -0,0 +1,40 @@
# Postfix main configuration for happyDeliver
# This configuration receives emails and routes them through authentication filters
# Basic settings
compatibility_level = 3.6
myhostname = __HOSTNAME__
mydomain = __DOMAIN__
myorigin = $mydomain
inet_interfaces = all
inet_protocols = ipv4
# Recipient settings
mydestination = localhost.$mydomain, localhost
mynetworks = 127.0.0.0/8 [::1]/128
# Relay settings - accept mail for our test domain
relay_domains = $mydomain
# Queue and size limits
message_size_limit = 10485760
mailbox_size_limit = 0
queue_minfree = 50000000
# Transport maps - route test emails to happyDeliver analyzer
transport_maps = pcre:/etc/postfix/transport_maps
# Authentication milters
# OpenDKIM for DKIM verification
milter_default_action = accept
milter_protocol = 6
smtpd_milters = unix:/var/spool/postfix/authentication_milter/authentication_milter.sock unix:/var/spool/postfix/spamassassin/spamass-milter.sock unix:/var/spool/postfix/rspamd/rspamd-milter.sock
non_smtpd_milters = $smtpd_milters
# SPF policy checking
smtpd_recipient_restrictions =
permit_mynetworks,
reject_unauth_destination
# Logging
debug_peer_level = 2

78
docker/postfix/master.cf Normal file
View file

@ -0,0 +1,78 @@
# Postfix master process configuration for happyDeliver
# SMTP service
smtp inet n - n - - smtpd
# Pickup service
pickup unix n - n 60 1 pickup
# Cleanup service
cleanup unix n - n - 0 cleanup
# Queue manager
qmgr unix n - n 300 1 qmgr
# Rewrite service
rewrite unix - - n - - trivial-rewrite
# Bounce service
bounce unix - - n - 0 bounce
# Defer service
defer unix - - n - 0 bounce
# Trace service
trace unix - - n - 0 bounce
# Verify service
verify unix - - n - 1 verify
# Flush service
flush unix n - n 1000? 0 flush
# Proxymap service
proxymap unix - - n - - proxymap
# Proxywrite service
proxywrite unix - - n - 1 proxymap
# SMTP client
smtp unix - - n - - smtp
# Relay service
relay unix - - n - - smtp
# Showq service
showq unix n - n - - showq
# Error service
error unix - - n - - error
# Retry service
retry unix - - n - - error
# Discard service
discard unix - - n - - discard
# Local delivery
local unix - n n - - local
# Virtual delivery
virtual unix - n n - - virtual
# LMTP delivery
lmtp unix - - n - - lmtp
# Anvil service
anvil unix - - n - 1 anvil
# Scache service
scache unix - - n - 1 scache
# Maildrop service
maildrop unix - n n - - pipe
flags=DRXhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
# SpamAssassin content filter
spamassassin unix - n n - - pipe
user=mail argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}

View file

@ -0,0 +1,4 @@
# Transport map - route test emails to happyDeliver LMTP server
# Pattern: test-<base32-uuid>@domain.com -> LMTP on localhost:2525
/^test-[a-zA-Z2-7-]{26,30}@.*$/ lmtp:inet:127.0.0.1:2525