diff --git a/.gitignore b/.gitignore
index e943630..7ece05e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,5 +26,5 @@ logs/
*.sqlite3
# OpenAPI generated files
-internal/api/server.gen.go
-internal/model/types.gen.go
+internal/api/models.gen.go
+internal/api/server.gen.go
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 4568784..f6dc16a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -170,13 +170,7 @@ RUN chmod +x /entrypoint.sh
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
+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_RBL=zen.spamhaus.org,bl.spamcop.net,b.barracudacentral.org,dnsbl.sorbs.net,dnsbl-1.uceprotect.net,bl.mailspike.net
# Volume for persistent data
VOLUME ["/var/lib/happydeliver", "/var/log/happydeliver"]
diff --git a/README.md b/README.md
index 4010d7e..3c213cd 100644
--- a/README.md
+++ b/README.md
@@ -166,24 +166,7 @@ The server will start on `http://localhost:8080` by default.
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.
+Choose one of the following way to integrate happyDeliver in your existing setup:
#### Postfix LMTP Transport
@@ -279,33 +262,6 @@ 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:
diff --git a/api/config-models.yaml b/api/config-models.yaml
index aa2fb0e..9c3425c 100644
--- a/api/config-models.yaml
+++ b/api/config-models.yaml
@@ -1,9 +1,5 @@
-package: model
+package: api
generate:
models: true
- embedded-spec: true
-output: internal/model/types.gen.go
-output-options:
- skip-prune: true
-import-mapping:
- ./schemas.yaml: "-"
+ embedded-spec: false
+output: internal/api/models.gen.go
diff --git a/api/config-server.yaml b/api/config-server.yaml
index 347dbaf..20f8daf 100644
--- a/api/config-server.yaml
+++ b/api/config-server.yaml
@@ -1,8 +1,5 @@
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
diff --git a/api/openapi.yaml b/api/openapi.yaml
index 2dbf304..5c628fd 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -76,49 +76,6 @@ paths:
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:
- $ref: '#/components/schemas/Error'
-
/report/{id}:
get:
tags:
@@ -296,74 +253,1109 @@ paths:
components:
schemas:
Test:
- $ref: './schemas.yaml#/components/schemas/Test'
+ type: object
+ required:
+ - id
+ - email
+ - status
+ properties:
+ id:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ description: Unique test identifier (base32-encoded with hyphens)
+ example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a"
+ email:
+ type: string
+ format: email
+ description: Unique test email address
+ example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com"
+ status:
+ type: string
+ enum: [pending, analyzed]
+ description: Current test status (pending = no report yet, analyzed = report available)
+ example: "analyzed"
+
TestResponse:
- $ref: './schemas.yaml#/components/schemas/TestResponse'
+ type: object
+ required:
+ - id
+ - email
+ - status
+ properties:
+ id:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ description: Unique test identifier (base32-encoded with hyphens)
+ example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a"
+ email:
+ type: string
+ format: email
+ example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com"
+ status:
+ type: string
+ enum: [pending]
+ example: "pending"
+ message:
+ type: string
+ example: "Send your test email to the address above"
+
Report:
- $ref: './schemas.yaml#/components/schemas/Report'
+ type: object
+ required:
+ - id
+ - test_id
+ - score
+ - grade
+ - created_at
+ properties:
+ id:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ description: Report identifier (base32-encoded with hyphens)
+ test_id:
+ type: string
+ pattern: '^[a-z0-9-]+$'
+ description: Associated test ID (base32-encoded with hyphens)
+ score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Overall deliverability score as percentage (0-100)
+ example: 85
+ grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ summary:
+ $ref: '#/components/schemas/ScoreSummary'
+ authentication:
+ $ref: '#/components/schemas/AuthenticationResults'
+ spamassassin:
+ $ref: '#/components/schemas/SpamAssassinResult'
+ rspamd:
+ $ref: '#/components/schemas/RspamdResult'
+ dns_results:
+ $ref: '#/components/schemas/DNSResults'
+ blacklists:
+ type: object
+ additionalProperties:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlacklistCheck'
+ description: Map of IP addresses to their blacklist check results (array of checks per IP)
+ example:
+ "192.0.2.1":
+ - rbl: "zen.spamhaus.org"
+ listed: false
+ - rbl: "bl.spamcop.net"
+ listed: false
+ content_analysis:
+ $ref: '#/components/schemas/ContentAnalysis'
+ header_analysis:
+ $ref: '#/components/schemas/HeaderAnalysis'
+ raw_headers:
+ type: string
+ description: Raw email headers
+ created_at:
+ type: string
+ format: date-time
+
ScoreSummary:
- $ref: './schemas.yaml#/components/schemas/ScoreSummary'
+ type: object
+ required:
+ - dns_score
+ - dns_grade
+ - authentication_score
+ - authentication_grade
+ - spam_score
+ - spam_grade
+ - blacklist_score
+ - blacklist_grade
+ - header_score
+ - header_grade
+ - content_score
+ - content_grade
+ properties:
+ dns_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: DNS records score (in percentage)
+ example: 42
+ dns_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ authentication_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: SPF/DKIM/DMARC score (in percentage)
+ example: 28
+ authentication_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ spam_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Spam filter score (SpamAssassin + rspamd combined, in percentage)
+ example: 15
+ spam_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ blacklist_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Blacklist check score (in percentage)
+ example: 20
+ blacklist_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ header_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Header quality score (in percentage)
+ example: 9
+ header_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+ content_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Content quality score (in percentage)
+ example: 18
+ content_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score (A+ is best, F is worst)
+ example: "A"
+
ContentAnalysis:
- $ref: './schemas.yaml#/components/schemas/ContentAnalysis'
+ type: object
+ properties:
+ has_html:
+ type: boolean
+ description: Whether email contains HTML part
+ example: true
+ has_plaintext:
+ type: boolean
+ description: Whether email contains plaintext part
+ example: true
+ html_issues:
+ type: array
+ items:
+ $ref: '#/components/schemas/ContentIssue'
+ description: Issues found in HTML content
+ links:
+ type: array
+ items:
+ $ref: '#/components/schemas/LinkCheck'
+ description: Analysis of links found in the email
+ images:
+ type: array
+ items:
+ $ref: '#/components/schemas/ImageCheck'
+ description: Analysis of images in the email
+ text_to_image_ratio:
+ type: number
+ format: float
+ description: Ratio of text to images (higher is better)
+ example: 0.75
+ has_unsubscribe_link:
+ type: boolean
+ description: Whether email contains an unsubscribe link
+ example: true
+ unsubscribe_methods:
+ type: array
+ items:
+ type: string
+ enum: [link, mailto, list-unsubscribe-header, one-click]
+ description: Available unsubscribe methods
+ example: ["link", "list-unsubscribe-header"]
+
ContentIssue:
- $ref: './schemas.yaml#/components/schemas/ContentIssue'
+ type: object
+ required:
+ - type
+ - severity
+ - message
+ properties:
+ type:
+ type: string
+ enum: [broken_html, missing_alt, excessive_images, obfuscated_url, suspicious_link, dangerous_html]
+ description: Type of content issue
+ example: "missing_alt"
+ severity:
+ type: string
+ enum: [critical, high, medium, low, info]
+ description: Issue severity
+ example: "medium"
+ message:
+ type: string
+ description: Human-readable description
+ example: "3 images are missing alt attributes"
+ location:
+ type: string
+ description: Where the issue was found
+ example: "HTML body line 42"
+ advice:
+ type: string
+ description: How to fix this issue
+ example: "Add descriptive alt text to all images for better accessibility and deliverability"
+
LinkCheck:
- $ref: './schemas.yaml#/components/schemas/LinkCheck'
+ type: object
+ required:
+ - url
+ - status
+ properties:
+ url:
+ type: string
+ format: uri
+ description: The URL found in the email
+ example: "https://example.com/page"
+ status:
+ type: string
+ enum: [valid, broken, suspicious, redirected, timeout]
+ description: Link validation status
+ example: "valid"
+ http_code:
+ type: integer
+ description: HTTP status code received
+ example: 200
+ redirect_chain:
+ type: array
+ items:
+ type: string
+ description: URLs in the redirect chain, if any
+ example: ["https://example.com", "https://www.example.com"]
+ is_shortened:
+ type: boolean
+ description: Whether this is a URL shortener
+ example: false
+
ImageCheck:
- $ref: './schemas.yaml#/components/schemas/ImageCheck'
+ type: object
+ required:
+ - has_alt
+ properties:
+ src:
+ type: string
+ description: Image source URL or path
+ example: "https://example.com/logo.png"
+ has_alt:
+ type: boolean
+ description: Whether image has alt attribute
+ example: true
+ alt_text:
+ type: string
+ description: Alt text content
+ example: "Company Logo"
+ is_tracking_pixel:
+ type: boolean
+ description: Whether this appears to be a tracking pixel (1x1 image)
+ example: false
+
HeaderAnalysis:
- $ref: './schemas.yaml#/components/schemas/HeaderAnalysis'
+ type: object
+ properties:
+ has_mime_structure:
+ type: boolean
+ description: Whether body has a MIME structure
+ example: true
+ headers:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/HeaderCheck'
+ description: Map of header names to their check results (e.g., "from", "to", "dkim-signature")
+ example:
+ from:
+ present: true
+ value: "sender@example.com"
+ valid: true
+ importance: "required"
+ date:
+ present: true
+ value: "Mon, 1 Jan 2024 12:00:00 +0000"
+ valid: true
+ importance: "required"
+ received_chain:
+ type: array
+ items:
+ $ref: '#/components/schemas/ReceivedHop'
+ description: Chain of Received headers showing email path
+ domain_alignment:
+ $ref: '#/components/schemas/DomainAlignment'
+ issues:
+ type: array
+ items:
+ $ref: '#/components/schemas/HeaderIssue'
+ description: Issues found in headers
+
HeaderCheck:
- $ref: './schemas.yaml#/components/schemas/HeaderCheck'
+ type: object
+ required:
+ - present
+ properties:
+ present:
+ type: boolean
+ description: Whether the header is present
+ example: true
+ value:
+ type: string
+ description: Header value
+ example: "sender@example.com"
+ valid:
+ type: boolean
+ description: Whether the value is valid/well-formed
+ example: true
+ importance:
+ type: string
+ enum: [required, recommended, optional, newsletter]
+ description: How important this header is for deliverability
+ example: "required"
+ issues:
+ type: array
+ items:
+ type: string
+ description: Any issues with this header
+ example: ["Invalid date format"]
+
ReceivedHop:
- $ref: './schemas.yaml#/components/schemas/ReceivedHop'
+ type: object
+ properties:
+ from:
+ type: string
+ description: Sending server hostname
+ example: "mail.example.com"
+ by:
+ type: string
+ description: Receiving server hostname
+ example: "mx.receiver.com"
+ with:
+ type: string
+ description: Protocol used
+ example: "ESMTPS"
+ id:
+ type: string
+ description: Message ID at this hop
+ timestamp:
+ type: string
+ format: date-time
+ description: When this hop occurred
+ ip:
+ type: string
+ description: IP address of the sending server (IPv4 or IPv6)
+ example: "192.0.2.1"
+ reverse:
+ type: string
+ description: Reverse DNS (PTR record) for the IP address
+ example: "mail.example.com"
+
DKIMDomainInfo:
- $ref: './schemas.yaml#/components/schemas/DKIMDomainInfo'
+ type: object
+ required:
+ - domain
+ - org_domain
+ properties:
+ domain:
+ type: string
+ description: DKIM signature domain
+ example: "mail.example.com"
+ org_domain:
+ type: string
+ description: Organizational domain extracted from DKIM domain (using Public Suffix List)
+ example: "example.com"
+
DomainAlignment:
- $ref: './schemas.yaml#/components/schemas/DomainAlignment'
+ type: object
+ properties:
+ from_domain:
+ type: string
+ description: Domain from From header
+ example: "example.com"
+ from_org_domain:
+ type: string
+ description: Organizational domain extracted from From header (using Public Suffix List)
+ example: "example.com"
+ return_path_domain:
+ type: string
+ description: Domain from Return-Path header
+ example: "example.com"
+ return_path_org_domain:
+ type: string
+ description: Organizational domain extracted from Return-Path header (using Public Suffix List)
+ example: "example.com"
+ dkim_domains:
+ type: array
+ items:
+ $ref: '#/components/schemas/DKIMDomainInfo'
+ description: Domains from DKIM signatures with their organizational domains
+ aligned:
+ type: boolean
+ description: Whether all domains align (strict alignment - exact match)
+ example: true
+ relaxed_aligned:
+ type: boolean
+ description: Whether domains satisfy relaxed alignment (organizational domain match)
+ example: true
+ issues:
+ type: array
+ items:
+ type: string
+ description: Alignment issues
+ example: ["Return-Path domain does not match From domain"]
+
HeaderIssue:
- $ref: './schemas.yaml#/components/schemas/HeaderIssue'
+ type: object
+ required:
+ - header
+ - severity
+ - message
+ properties:
+ header:
+ type: string
+ description: Header name
+ example: "Date"
+ severity:
+ type: string
+ enum: [critical, high, medium, low, info]
+ description: Issue severity
+ example: "medium"
+ message:
+ type: string
+ description: Human-readable description
+ example: "Date header is in the future"
+ advice:
+ type: string
+ description: How to fix this issue
+ example: "Ensure your mail server clock is synchronized with NTP"
+
AuthenticationResults:
- $ref: './schemas.yaml#/components/schemas/AuthenticationResults'
+ type: object
+ properties:
+ spf:
+ $ref: '#/components/schemas/AuthResult'
+ dkim:
+ type: array
+ items:
+ $ref: '#/components/schemas/AuthResult'
+ dmarc:
+ $ref: '#/components/schemas/AuthResult'
+ bimi:
+ $ref: '#/components/schemas/AuthResult'
+ arc:
+ $ref: '#/components/schemas/ARCResult'
+ iprev:
+ $ref: '#/components/schemas/IPRevResult'
+ x_google_dkim:
+ $ref: '#/components/schemas/AuthResult'
+ description: Google-specific DKIM authentication result (x-google-dkim)
+ x_aligned_from:
+ $ref: '#/components/schemas/AuthResult'
+ description: X-Aligned-From authentication result (checks address alignment)
+
AuthResult:
- $ref: './schemas.yaml#/components/schemas/AuthResult'
+ type: object
+ required:
+ - result
+ properties:
+ result:
+ type: string
+ enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass]
+ 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
+
ARCResult:
- $ref: './schemas.yaml#/components/schemas/ARCResult'
+ type: object
+ required:
+ - result
+ properties:
+ result:
+ type: string
+ enum: [pass, fail, none]
+ description: Overall ARC chain validation result
+ example: "pass"
+ chain_valid:
+ type: boolean
+ description: Whether the ARC chain signatures are valid
+ example: true
+ chain_length:
+ type: integer
+ description: Number of ARC sets in the chain
+ example: 2
+ details:
+ type: string
+ description: Additional details about ARC validation
+ example: "ARC chain valid with 2 intermediaries"
+
IPRevResult:
- $ref: './schemas.yaml#/components/schemas/IPRevResult'
+ type: object
+ required:
+ - result
+ properties:
+ result:
+ type: string
+ enum: [pass, fail, temperror, permerror]
+ description: IP reverse DNS lookup result
+ example: "pass"
+ ip:
+ type: string
+ description: IP address that was checked
+ example: "195.110.101.58"
+ hostname:
+ type: string
+ description: Hostname from reverse DNS lookup (PTR record)
+ example: "authsmtp74.register.it"
+ details:
+ type: string
+ description: Additional details about the IP reverse lookup
+ example: "smtp.remote-ip=195.110.101.58 (authsmtp74.register.it)"
+
SpamAssassinResult:
- $ref: './schemas.yaml#/components/schemas/SpamAssassinResult'
+ type: object
+ required:
+ - score
+ - required_score
+ - is_spam
+ - test_details
+ properties:
+ deliverability_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: SpamAssassin deliverability score (0-100, higher is better)
+ example: 80
+ deliverability_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade for SpamAssassin deliverability score
+ example: "B"
+ version:
+ type: string
+ description: SpamAssassin version
+ example: "SpamAssassin 4.0.1"
+ 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"]
+ test_details:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/SpamTestDetail'
+ description: Map of test names to their detailed results
+ example:
+ BAYES_00:
+ name: "BAYES_00"
+ score: -1.9
+ description: "Bayes spam probability is 0 to 1%"
+ DKIM_SIGNED:
+ name: "DKIM_SIGNED"
+ score: 0.1
+ description: "Message has a DKIM or DK signature, not necessarily valid"
+ report:
+ type: string
+ description: Full SpamAssassin report
+
SpamTestDetail:
- $ref: './schemas.yaml#/components/schemas/SpamTestDetail'
+ type: object
+ required:
+ - name
+ - score
+ properties:
+ name:
+ type: string
+ description: Test name
+ example: "BAYES_00"
+ score:
+ type: number
+ format: float
+ description: Score contribution of this test
+ example: -1.9
+ description:
+ type: string
+ description: Human-readable description of what this test checks
+ example: "Bayes spam probability is 0 to 1%"
+
RspamdResult:
- $ref: './schemas.yaml#/components/schemas/RspamdResult'
+ type: object
+ required:
+ - score
+ - threshold
+ - is_spam
+ - symbols
+ properties:
+ deliverability_score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: rspamd deliverability score (0-100, higher is better)
+ example: 85
+ deliverability_grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade for rspamd deliverability score
+ example: "A"
+ score:
+ type: number
+ format: float
+ description: rspamd spam score
+ example: -3.91
+ threshold:
+ type: number
+ format: float
+ description: Score threshold for spam classification
+ example: 15.0
+ action:
+ type: string
+ description: rspamd action (no action, add header, rewrite subject, soft reject, reject)
+ example: "no action"
+ is_spam:
+ type: boolean
+ description: Whether message is classified as spam (action is reject or soft reject)
+ example: false
+ server:
+ type: string
+ description: rspamd server that processed the message
+ example: "rspamd.example.com"
+ symbols:
+ type: object
+ additionalProperties:
+ $ref: '#/components/schemas/RspamdSymbol'
+ description: Map of triggered rspamd symbols to their details
+ example:
+ BAYES_HAM:
+ name: "BAYES_HAM"
+ score: -1.9
+ params: "0.02"
+
+ RspamdSymbol:
+ type: object
+ required:
+ - name
+ - score
+ properties:
+ name:
+ type: string
+ description: Symbol name
+ example: "BAYES_HAM"
+ score:
+ type: number
+ format: float
+ description: Score contribution of this symbol
+ example: -1.9
+ params:
+ type: string
+ description: Symbol parameters or options
+ example: "0.02"
+
DNSResults:
- $ref: './schemas.yaml#/components/schemas/DNSResults'
+ type: object
+ required:
+ - from_domain
+ properties:
+ from_domain:
+ type: string
+ description: From Domain name
+ example: "example.com"
+ rp_domain:
+ type: string
+ description: Return Path Domain name
+ example: "example.com"
+ from_mx_records:
+ type: array
+ items:
+ $ref: '#/components/schemas/MXRecord'
+ description: MX records for the From domain
+ rp_mx_records:
+ type: array
+ items:
+ $ref: '#/components/schemas/MXRecord'
+ description: MX records for the Return-Path domain
+ spf_records:
+ type: array
+ items:
+ $ref: '#/components/schemas/SPFRecord'
+ description: SPF records found (includes resolved include directives)
+ dkim_records:
+ type: array
+ items:
+ $ref: '#/components/schemas/DKIMRecord'
+ description: DKIM records found
+ dmarc_record:
+ $ref: '#/components/schemas/DMARCRecord'
+ bimi_record:
+ $ref: '#/components/schemas/BIMIRecord'
+ ptr_records:
+ type: array
+ items:
+ type: string
+ description: PTR (reverse DNS) records for the sender IP address
+ example: ["mail.example.com", "smtp.example.com"]
+ ptr_forward_records:
+ type: array
+ items:
+ type: string
+ description: A or AAAA records resolved from the PTR hostnames (forward confirmation)
+ example: ["192.0.2.1", "2001:db8::1"]
+ errors:
+ type: array
+ items:
+ type: string
+ description: DNS lookup errors
+
MXRecord:
- $ref: './schemas.yaml#/components/schemas/MXRecord'
+ type: object
+ required:
+ - host
+ - priority
+ - valid
+ properties:
+ host:
+ type: string
+ description: MX hostname
+ example: "mail.example.com"
+ priority:
+ type: integer
+ format: uint16
+ description: MX priority (lower is higher priority)
+ example: 10
+ valid:
+ type: boolean
+ description: Whether the MX record is valid
+ example: true
+ error:
+ type: string
+ description: Error message if validation failed
+ example: "Failed to lookup MX records"
+
SPFRecord:
- $ref: './schemas.yaml#/components/schemas/SPFRecord'
+ type: object
+ required:
+ - valid
+ properties:
+ domain:
+ type: string
+ description: Domain this SPF record belongs to
+ example: "example.com"
+ record:
+ type: string
+ description: SPF record content
+ example: "v=spf1 include:_spf.example.com ~all"
+ valid:
+ type: boolean
+ description: Whether the SPF record is valid
+ example: true
+ all_qualifier:
+ type: string
+ enum: ["+", "-", "~", "?"]
+ description: "Qualifier for the 'all' mechanism: + (pass), - (fail), ~ (softfail), ? (neutral)"
+ example: "~"
+ error:
+ type: string
+ description: Error message if validation failed
+ example: "No SPF record found"
+
DKIMRecord:
- $ref: './schemas.yaml#/components/schemas/DKIMRecord'
+ type: object
+ required:
+ - selector
+ - domain
+ - valid
+ properties:
+ selector:
+ type: string
+ description: DKIM selector
+ example: "default"
+ domain:
+ type: string
+ description: Domain name
+ example: "example.com"
+ record:
+ type: string
+ description: DKIM record content
+ example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
+ valid:
+ type: boolean
+ description: Whether the DKIM record is valid
+ example: true
+ error:
+ type: string
+ description: Error message if validation failed
+ example: "No DKIM record found"
+
DMARCRecord:
- $ref: './schemas.yaml#/components/schemas/DMARCRecord'
+ type: object
+ required:
+ - valid
+ properties:
+ record:
+ type: string
+ description: DMARC record content
+ example: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
+ policy:
+ type: string
+ enum: [none, quarantine, reject, unknown]
+ description: DMARC policy
+ example: "quarantine"
+ subdomain_policy:
+ type: string
+ enum: [none, quarantine, reject, unknown]
+ description: DMARC subdomain policy (sp tag) - policy for subdomains if different from main policy
+ example: "quarantine"
+ percentage:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Percentage of messages subjected to filtering (pct tag, default 100)
+ example: 100
+ spf_alignment:
+ type: string
+ enum: [relaxed, strict]
+ description: SPF alignment mode (aspf tag)
+ example: "relaxed"
+ dkim_alignment:
+ type: string
+ enum: [relaxed, strict]
+ description: DKIM alignment mode (adkim tag)
+ example: "relaxed"
+ valid:
+ type: boolean
+ description: Whether the DMARC record is valid
+ example: true
+ error:
+ type: string
+ description: Error message if validation failed
+ example: "No DMARC record found"
+
BIMIRecord:
- $ref: './schemas.yaml#/components/schemas/BIMIRecord'
+ type: object
+ required:
+ - selector
+ - domain
+ - valid
+ properties:
+ selector:
+ type: string
+ description: BIMI selector
+ example: "default"
+ domain:
+ type: string
+ description: Domain name
+ example: "example.com"
+ record:
+ type: string
+ description: BIMI record content
+ example: "v=BIMI1; l=https://example.com/logo.svg"
+ logo_url:
+ type: string
+ format: uri
+ description: URL to the brand logo (SVG)
+ example: "https://example.com/logo.svg"
+ vmc_url:
+ type: string
+ format: uri
+ description: URL to Verified Mark Certificate (optional)
+ example: "https://example.com/vmc.pem"
+ valid:
+ type: boolean
+ description: Whether the BIMI record is valid
+ example: true
+ error:
+ type: string
+ description: Error message if validation failed
+ example: "No BIMI record found"
+
BlacklistCheck:
- $ref: './schemas.yaml#/components/schemas/BlacklistCheck'
+ type: object
+ required:
+ - rbl
+ - listed
+ properties:
+ 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"
+ error:
+ type: string
+ description: RBL error if any
+
Status:
- $ref: './schemas.yaml#/components/schemas/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
+
Error:
- $ref: './schemas.yaml#/components/schemas/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
+
DomainTestRequest:
- $ref: './schemas.yaml#/components/schemas/DomainTestRequest'
+ type: object
+ required:
+ - domain
+ properties:
+ domain:
+ type: string
+ pattern: '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
+ description: Domain name to test (e.g., example.com)
+ example: "example.com"
+
DomainTestResponse:
- $ref: './schemas.yaml#/components/schemas/DomainTestResponse'
+ type: object
+ required:
+ - domain
+ - score
+ - grade
+ - dns_results
+ properties:
+ domain:
+ type: string
+ description: The tested domain name
+ example: "example.com"
+ score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Overall domain configuration score (0-100)
+ example: 85
+ grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score
+ example: "A"
+ dns_results:
+ $ref: '#/components/schemas/DNSResults'
+
BlacklistCheckRequest:
- $ref: './schemas.yaml#/components/schemas/BlacklistCheckRequest'
+ type: object
+ required:
+ - ip
+ properties:
+ ip:
+ type: string
+ description: IPv4 or IPv6 address to check against blacklists
+ example: "192.0.2.1"
+ pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$'
+
BlacklistCheckResponse:
- $ref: './schemas.yaml#/components/schemas/BlacklistCheckResponse'
- TestSummary:
- $ref: './schemas.yaml#/components/schemas/TestSummary'
- TestListResponse:
- $ref: './schemas.yaml#/components/schemas/TestListResponse'
+ type: object
+ required:
+ - ip
+ - checks
+ - listed_count
+ - score
+ - grade
+ properties:
+ ip:
+ type: string
+ description: The IP address that was checked
+ example: "192.0.2.1"
+ checks:
+ type: array
+ items:
+ $ref: '#/components/schemas/BlacklistCheck'
+ description: List of blacklist check results
+ listed_count:
+ type: integer
+ description: Number of blacklists that have this IP listed
+ example: 0
+ score:
+ type: integer
+ minimum: 0
+ maximum: 100
+ description: Blacklist score (0-100, higher is better)
+ example: 100
+ grade:
+ type: string
+ enum: [A+, A, B, C, D, E, F]
+ description: Letter grade representation of the score
+ example: "A+"
diff --git a/api/schemas.yaml b/api/schemas.yaml
deleted file mode 100644
index df0b416..0000000
--- a/api/schemas.yaml
+++ /dev/null
@@ -1,1173 +0,0 @@
-openapi: 3.0.3
-info:
- title: happyDeliver Schemas
- description: Shared schema definitions for happyDeliver
- version: 0.1.0
-
-paths: {}
-
-components:
- schemas:
- Test:
- type: object
- required:
- - id
- - email
- - status
- properties:
- id:
- type: string
- pattern: '^[a-z0-9-]+$'
- description: Unique test identifier (base32-encoded with hyphens)
- example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a"
- email:
- type: string
- format: email
- description: Unique test email address
- example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com"
- status:
- type: string
- enum: [pending, analyzed]
- description: Current test status (pending = no report yet, analyzed = report available)
- example: "analyzed"
-
- TestResponse:
- type: object
- required:
- - id
- - email
- - status
- properties:
- id:
- type: string
- pattern: '^[a-z0-9-]+$'
- description: Unique test identifier (base32-encoded with hyphens)
- example: "krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a"
- email:
- type: string
- format: email
- example: "test-krfwg4z-amrqw4z-zmorsw2-djmfzgk-3a@example.com"
- status:
- type: string
- enum: [pending]
- example: "pending"
- message:
- type: string
- example: "Send your test email to the address above"
-
- Report:
- type: object
- required:
- - id
- - test_id
- - score
- - grade
- - created_at
- properties:
- id:
- type: string
- pattern: '^[a-z0-9-]+$'
- description: Report identifier (base32-encoded with hyphens)
- test_id:
- type: string
- pattern: '^[a-z0-9-]+$'
- description: Associated test ID (base32-encoded with hyphens)
- score:
- type: integer
- minimum: 0
- maximum: 100
- description: Overall deliverability score as percentage (0-100)
- example: 85
- grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- summary:
- $ref: '#/components/schemas/ScoreSummary'
- authentication:
- $ref: '#/components/schemas/AuthenticationResults'
- spamassassin:
- $ref: '#/components/schemas/SpamAssassinResult'
- rspamd:
- $ref: '#/components/schemas/RspamdResult'
- dns_results:
- $ref: '#/components/schemas/DNSResults'
- blacklists:
- type: object
- additionalProperties:
- type: array
- items:
- $ref: '#/components/schemas/BlacklistCheck'
- description: Map of IP addresses to their blacklist check results (array of checks per IP)
- example:
- "192.0.2.1":
- - rbl: "zen.spamhaus.org"
- listed: false
- - rbl: "bl.spamcop.net"
- listed: false
- whitelists:
- type: object
- additionalProperties:
- type: array
- items:
- $ref: '#/components/schemas/BlacklistCheck'
- description: Map of IP addresses to their DNS whitelist check results (informational only)
- example:
- "192.0.2.1":
- - rbl: "list.dnswl.org"
- listed: false
- - rbl: "swl.spamhaus.org"
- listed: false
- content_analysis:
- $ref: '#/components/schemas/ContentAnalysis'
- header_analysis:
- $ref: '#/components/schemas/HeaderAnalysis'
- raw_headers:
- type: string
- description: Raw email headers
- created_at:
- type: string
- format: date-time
-
- ScoreSummary:
- type: object
- required:
- - dns_score
- - dns_grade
- - authentication_score
- - authentication_grade
- - spam_score
- - spam_grade
- - blacklist_score
- - blacklist_grade
- - header_score
- - header_grade
- - content_score
- - content_grade
- properties:
- dns_score:
- type: integer
- minimum: 0
- maximum: 100
- description: DNS records score (in percentage)
- example: 42
- dns_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- authentication_score:
- type: integer
- minimum: 0
- maximum: 100
- description: SPF/DKIM/DMARC score (in percentage)
- example: 28
- authentication_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- spam_score:
- type: integer
- minimum: 0
- maximum: 100
- description: Spam filter score (SpamAssassin + rspamd combined, in percentage)
- example: 15
- spam_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- blacklist_score:
- type: integer
- minimum: 0
- maximum: 100
- description: Blacklist check score (in percentage)
- example: 20
- blacklist_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- header_score:
- type: integer
- minimum: 0
- maximum: 100
- description: Header quality score (in percentage)
- example: 9
- header_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
- content_score:
- type: integer
- minimum: 0
- maximum: 100
- description: Content quality score (in percentage)
- example: 18
- content_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score (A+ is best, F is worst)
- example: "A"
-
- ContentAnalysis:
- type: object
- properties:
- has_html:
- type: boolean
- description: Whether email contains HTML part
- example: true
- has_plaintext:
- type: boolean
- description: Whether email contains plaintext part
- example: true
- html_issues:
- type: array
- items:
- $ref: '#/components/schemas/ContentIssue'
- description: Issues found in HTML content
- links:
- type: array
- items:
- $ref: '#/components/schemas/LinkCheck'
- description: Analysis of links found in the email
- images:
- type: array
- items:
- $ref: '#/components/schemas/ImageCheck'
- description: Analysis of images in the email
- text_to_image_ratio:
- type: number
- format: float
- description: Ratio of text to images (higher is better)
- example: 0.75
- has_unsubscribe_link:
- type: boolean
- description: Whether email contains an unsubscribe link
- example: true
- unsubscribe_methods:
- type: array
- items:
- type: string
- enum: [link, mailto, list-unsubscribe-header, one-click]
- description: Available unsubscribe methods
- example: ["link", "list-unsubscribe-header"]
-
- ContentIssue:
- type: object
- required:
- - type
- - severity
- - message
- properties:
- type:
- type: string
- enum: [broken_html, missing_alt, excessive_images, obfuscated_url, suspicious_link, dangerous_html]
- description: Type of content issue
- example: "missing_alt"
- severity:
- type: string
- enum: [critical, high, medium, low, info]
- description: Issue severity
- example: "medium"
- message:
- type: string
- description: Human-readable description
- example: "3 images are missing alt attributes"
- location:
- type: string
- description: Where the issue was found
- example: "HTML body line 42"
- advice:
- type: string
- description: How to fix this issue
- example: "Add descriptive alt text to all images for better accessibility and deliverability"
-
- LinkCheck:
- type: object
- required:
- - url
- - status
- properties:
- url:
- type: string
- format: uri
- description: The URL found in the email
- example: "https://example.com/page"
- status:
- type: string
- enum: [valid, broken, suspicious, redirected, timeout]
- description: Link validation status
- example: "valid"
- http_code:
- type: integer
- description: HTTP status code received
- example: 200
- redirect_chain:
- type: array
- items:
- type: string
- description: URLs in the redirect chain, if any
- example: ["https://example.com", "https://www.example.com"]
- is_shortened:
- type: boolean
- description: Whether this is a URL shortener
- example: false
-
- ImageCheck:
- type: object
- required:
- - has_alt
- properties:
- src:
- type: string
- description: Image source URL or path
- example: "https://example.com/logo.png"
- has_alt:
- type: boolean
- description: Whether image has alt attribute
- example: true
- alt_text:
- type: string
- description: Alt text content
- example: "Company Logo"
- is_tracking_pixel:
- type: boolean
- description: Whether this appears to be a tracking pixel (1x1 image)
- example: false
-
- HeaderAnalysis:
- type: object
- properties:
- has_mime_structure:
- type: boolean
- description: Whether body has a MIME structure
- example: true
- headers:
- type: object
- additionalProperties:
- $ref: '#/components/schemas/HeaderCheck'
- description: Map of header names to their check results (e.g., "from", "to", "dkim-signature")
- example:
- from:
- present: true
- value: "sender@example.com"
- valid: true
- importance: "required"
- date:
- present: true
- value: "Mon, 1 Jan 2024 12:00:00 +0000"
- valid: true
- importance: "required"
- received_chain:
- type: array
- items:
- $ref: '#/components/schemas/ReceivedHop'
- description: Chain of Received headers showing email path
- domain_alignment:
- $ref: '#/components/schemas/DomainAlignment'
- issues:
- type: array
- items:
- $ref: '#/components/schemas/HeaderIssue'
- description: Issues found in headers
-
- HeaderCheck:
- type: object
- required:
- - present
- properties:
- present:
- type: boolean
- description: Whether the header is present
- example: true
- value:
- type: string
- description: Header value
- example: "sender@example.com"
- valid:
- type: boolean
- description: Whether the value is valid/well-formed
- example: true
- importance:
- type: string
- enum: [required, recommended, optional, newsletter]
- description: How important this header is for deliverability
- example: "required"
- issues:
- type: array
- items:
- type: string
- description: Any issues with this header
- example: ["Invalid date format"]
-
- ReceivedHop:
- type: object
- properties:
- from:
- type: string
- description: Sending server hostname
- example: "mail.example.com"
- by:
- type: string
- description: Receiving server hostname
- example: "mx.receiver.com"
- with:
- type: string
- description: Protocol used
- example: "ESMTPS"
- id:
- type: string
- description: Message ID at this hop
- timestamp:
- type: string
- format: date-time
- description: When this hop occurred
- ip:
- type: string
- description: IP address of the sending server (IPv4 or IPv6)
- example: "192.0.2.1"
- reverse:
- type: string
- description: Reverse DNS (PTR record) for the IP address
- example: "mail.example.com"
-
- DKIMDomainInfo:
- type: object
- required:
- - domain
- - org_domain
- properties:
- domain:
- type: string
- description: DKIM signature domain
- example: "mail.example.com"
- org_domain:
- type: string
- description: Organizational domain extracted from DKIM domain (using Public Suffix List)
- example: "example.com"
-
- DomainAlignment:
- type: object
- properties:
- from_domain:
- type: string
- description: Domain from From header
- example: "example.com"
- from_org_domain:
- type: string
- description: Organizational domain extracted from From header (using Public Suffix List)
- example: "example.com"
- return_path_domain:
- type: string
- description: Domain from Return-Path header
- example: "example.com"
- return_path_org_domain:
- type: string
- description: Organizational domain extracted from Return-Path header (using Public Suffix List)
- example: "example.com"
- dkim_domains:
- type: array
- items:
- $ref: '#/components/schemas/DKIMDomainInfo'
- description: Domains from DKIM signatures with their organizational domains
- aligned:
- type: boolean
- description: Whether all domains align (strict alignment - exact match)
- example: true
- relaxed_aligned:
- type: boolean
- description: Whether domains satisfy relaxed alignment (organizational domain match)
- example: true
- issues:
- type: array
- items:
- type: string
- description: Alignment issues
- example: ["Return-Path domain does not match From domain"]
-
- HeaderIssue:
- type: object
- required:
- - header
- - severity
- - message
- properties:
- header:
- type: string
- description: Header name
- example: "Date"
- severity:
- type: string
- enum: [critical, high, medium, low, info]
- description: Issue severity
- example: "medium"
- message:
- type: string
- description: Human-readable description
- example: "Date header is in the future"
- advice:
- type: string
- description: How to fix this issue
- example: "Ensure your mail server clock is synchronized with NTP"
-
- AuthenticationResults:
- type: object
- properties:
- spf:
- $ref: '#/components/schemas/AuthResult'
- dkim:
- type: array
- items:
- $ref: '#/components/schemas/AuthResult'
- dmarc:
- $ref: '#/components/schemas/AuthResult'
- bimi:
- $ref: '#/components/schemas/AuthResult'
- arc:
- $ref: '#/components/schemas/ARCResult'
- iprev:
- $ref: '#/components/schemas/IPRevResult'
- x_google_dkim:
- $ref: '#/components/schemas/AuthResult'
- description: Google-specific DKIM authentication result (x-google-dkim)
- x_aligned_from:
- $ref: '#/components/schemas/AuthResult'
- description: X-Aligned-From authentication result (checks address alignment)
-
- AuthResult:
- type: object
- required:
- - result
- properties:
- result:
- type: string
- enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass, skipped]
- 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
-
- ARCResult:
- type: object
- required:
- - result
- properties:
- result:
- type: string
- enum: [pass, fail, none]
- description: Overall ARC chain validation result
- example: "pass"
- chain_valid:
- type: boolean
- description: Whether the ARC chain signatures are valid
- example: true
- chain_length:
- type: integer
- description: Number of ARC sets in the chain
- example: 2
- details:
- type: string
- description: Additional details about ARC validation
- example: "ARC chain valid with 2 intermediaries"
-
- IPRevResult:
- type: object
- required:
- - result
- properties:
- result:
- type: string
- enum: [pass, fail, temperror, permerror]
- description: IP reverse DNS lookup result
- example: "pass"
- ip:
- type: string
- description: IP address that was checked
- example: "195.110.101.58"
- hostname:
- type: string
- description: Hostname from reverse DNS lookup (PTR record)
- example: "authsmtp74.register.it"
- details:
- type: string
- description: Additional details about the IP reverse lookup
- example: "smtp.remote-ip=195.110.101.58 (authsmtp74.register.it)"
-
- SpamAssassinResult:
- type: object
- required:
- - score
- - required_score
- - is_spam
- - test_details
- properties:
- deliverability_score:
- type: integer
- minimum: 0
- maximum: 100
- description: SpamAssassin deliverability score (0-100, higher is better)
- example: 80
- deliverability_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade for SpamAssassin deliverability score
- example: "B"
- version:
- type: string
- description: SpamAssassin version
- example: "SpamAssassin 4.0.1"
- 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"]
- test_details:
- type: object
- additionalProperties:
- $ref: '#/components/schemas/SpamTestDetail'
- description: Map of test names to their detailed results
- example:
- BAYES_00:
- name: "BAYES_00"
- score: -1.9
- description: "Bayes spam probability is 0 to 1%"
- DKIM_SIGNED:
- name: "DKIM_SIGNED"
- score: 0.1
- description: "Message has a DKIM or DK signature, not necessarily valid"
- report:
- type: string
- description: Full SpamAssassin report
-
- SpamTestDetail:
- type: object
- required:
- - name
- - score
- properties:
- name:
- type: string
- description: Test name
- example: "BAYES_00"
- score:
- type: number
- format: float
- description: Score contribution of this test
- example: -1.9
- params:
- type: string
- description: Symbol parameters or options
- example: "0.02"
- description:
- type: string
- description: Human-readable description of what this test checks
- example: "Bayes spam probability is 0 to 1%"
-
- RspamdResult:
- type: object
- required:
- - score
- - threshold
- - is_spam
- - symbols
- properties:
- deliverability_score:
- type: integer
- minimum: 0
- maximum: 100
- description: rspamd deliverability score (0-100, higher is better)
- example: 85
- deliverability_grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade for rspamd deliverability score
- example: "A"
- score:
- type: number
- format: float
- description: rspamd spam score
- example: -3.91
- threshold:
- type: number
- format: float
- description: Score threshold for spam classification
- example: 15.0
- action:
- type: string
- description: rspamd action (no action, add header, rewrite subject, soft reject, reject)
- example: "no action"
- is_spam:
- type: boolean
- description: Whether message is classified as spam (action is reject or soft reject)
- example: false
- server:
- type: string
- description: rspamd server that processed the message
- example: "rspamd.example.com"
- symbols:
- type: object
- additionalProperties:
- $ref: '#/components/schemas/SpamTestDetail'
- description: Map of triggered rspamd symbols to their details
- example:
- BAYES_HAM:
- name: "BAYES_HAM"
- score: -1.9
- params: "0.02"
- report:
- type: string
- description: Full rspamd report (raw X-Spamd-Result header)
-
-
- DNSResults:
- type: object
- required:
- - from_domain
- properties:
- from_domain:
- type: string
- description: From Domain name
- example: "example.com"
- rp_domain:
- type: string
- description: Return Path Domain name
- example: "example.com"
- from_mx_records:
- type: array
- items:
- $ref: '#/components/schemas/MXRecord'
- description: MX records for the From domain
- rp_mx_records:
- type: array
- items:
- $ref: '#/components/schemas/MXRecord'
- description: MX records for the Return-Path domain
- spf_records:
- type: array
- items:
- $ref: '#/components/schemas/SPFRecord'
- description: SPF records found (includes resolved include directives)
- dkim_records:
- type: array
- items:
- $ref: '#/components/schemas/DKIMRecord'
- description: DKIM records found
- dmarc_record:
- $ref: '#/components/schemas/DMARCRecord'
- bimi_record:
- $ref: '#/components/schemas/BIMIRecord'
- ptr_records:
- type: array
- items:
- type: string
- description: PTR (reverse DNS) records for the sender IP address
- example: ["mail.example.com", "smtp.example.com"]
- ptr_forward_records:
- type: array
- items:
- type: string
- description: A or AAAA records resolved from the PTR hostnames (forward confirmation)
- example: ["192.0.2.1", "2001:db8::1"]
- errors:
- type: array
- items:
- type: string
- description: DNS lookup errors
-
- MXRecord:
- type: object
- required:
- - host
- - priority
- - valid
- properties:
- host:
- type: string
- description: MX hostname
- example: "mail.example.com"
- priority:
- type: integer
- format: uint16
- description: MX priority (lower is higher priority)
- example: 10
- valid:
- type: boolean
- description: Whether the MX record is valid
- example: true
- error:
- type: string
- description: Error message if validation failed
- example: "Failed to lookup MX records"
-
- SPFRecord:
- type: object
- required:
- - valid
- properties:
- domain:
- type: string
- description: Domain this SPF record belongs to
- example: "example.com"
- record:
- type: string
- description: SPF record content
- example: "v=spf1 include:_spf.example.com ~all"
- valid:
- type: boolean
- description: Whether the SPF record is valid
- example: true
- all_qualifier:
- type: string
- enum: ["+", "-", "~", "?"]
- description: "Qualifier for the 'all' mechanism: + (pass), - (fail), ~ (softfail), ? (neutral)"
- example: "~"
- error:
- type: string
- description: Error message if validation failed
- example: "No SPF record found"
-
- DKIMRecord:
- type: object
- required:
- - selector
- - domain
- - valid
- properties:
- selector:
- type: string
- description: DKIM selector
- example: "default"
- domain:
- type: string
- description: Domain name
- example: "example.com"
- record:
- type: string
- description: DKIM record content
- example: "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA..."
- valid:
- type: boolean
- description: Whether the DKIM record is valid
- example: true
- error:
- type: string
- description: Error message if validation failed
- example: "No DKIM record found"
-
- DMARCRecord:
- type: object
- required:
- - valid
- properties:
- record:
- type: string
- description: DMARC record content
- example: "v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com"
- policy:
- type: string
- enum: [none, quarantine, reject, unknown]
- description: DMARC policy
- example: "quarantine"
- subdomain_policy:
- type: string
- enum: [none, quarantine, reject, unknown]
- description: DMARC subdomain policy (sp tag) - policy for subdomains if different from main policy
- example: "quarantine"
- percentage:
- type: integer
- minimum: 0
- maximum: 100
- description: Percentage of messages subjected to filtering (pct tag, default 100)
- example: 100
- spf_alignment:
- type: string
- enum: [relaxed, strict]
- description: SPF alignment mode (aspf tag)
- example: "relaxed"
- dkim_alignment:
- type: string
- enum: [relaxed, strict]
- description: DKIM alignment mode (adkim tag)
- example: "relaxed"
- valid:
- type: boolean
- description: Whether the DMARC record is valid
- example: true
- error:
- type: string
- description: Error message if validation failed
- example: "No DMARC record found"
-
- BIMIRecord:
- type: object
- required:
- - selector
- - domain
- - valid
- properties:
- selector:
- type: string
- description: BIMI selector
- example: "default"
- domain:
- type: string
- description: Domain name
- example: "example.com"
- record:
- type: string
- description: BIMI record content
- example: "v=BIMI1; l=https://example.com/logo.svg"
- logo_url:
- type: string
- format: uri
- description: URL to the brand logo (SVG)
- example: "https://example.com/logo.svg"
- vmc_url:
- type: string
- format: uri
- description: URL to Verified Mark Certificate (optional)
- example: "https://example.com/vmc.pem"
- valid:
- type: boolean
- description: Whether the BIMI record is valid
- example: true
- error:
- type: string
- description: Error message if validation failed
- example: "No BIMI record found"
-
- BlacklistCheck:
- type: object
- required:
- - rbl
- - listed
- properties:
- 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"
- error:
- type: string
- description: RBL error if any
-
- 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
-
- 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
-
- DomainTestRequest:
- type: object
- required:
- - domain
- properties:
- domain:
- type: string
- pattern: '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
- description: Domain name to test (e.g., example.com)
- example: "example.com"
-
- DomainTestResponse:
- type: object
- required:
- - domain
- - score
- - grade
- - dns_results
- properties:
- domain:
- type: string
- description: The tested domain name
- example: "example.com"
- score:
- type: integer
- minimum: 0
- maximum: 100
- description: Overall domain configuration score (0-100)
- example: 85
- grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score
- example: "A"
- dns_results:
- $ref: '#/components/schemas/DNSResults'
-
- BlacklistCheckRequest:
- type: object
- required:
- - ip
- properties:
- ip:
- type: string
- description: IPv4 or IPv6 address to check against blacklists
- example: "192.0.2.1"
- pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$'
-
- BlacklistCheckResponse:
- type: object
- required:
- - ip
- - blacklists
- - listed_count
- - score
- - grade
- properties:
- ip:
- type: string
- description: The IP address that was checked
- example: "192.0.2.1"
- blacklists:
- type: array
- items:
- $ref: '#/components/schemas/BlacklistCheck'
- description: List of blacklist check results
- listed_count:
- type: integer
- description: Number of blacklists that have this IP listed
- example: 0
- score:
- type: integer
- minimum: 0
- maximum: 100
- description: Blacklist score (0-100, higher is better)
- example: 100
- grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade representation of the score
- example: "A+"
- whitelists:
- type: array
- items:
- $ref: '#/components/schemas/BlacklistCheck'
- description: List of DNS whitelist check results (informational only)
-
- TestSummary:
- type: object
- required:
- - test_id
- - score
- - grade
- - created_at
- properties:
- test_id:
- type: string
- pattern: '^[a-z0-9-]+$'
- description: Test identifier (base32-encoded with hyphens)
- score:
- type: integer
- minimum: 0
- maximum: 100
- description: Overall deliverability score (0-100)
- grade:
- type: string
- enum: [A+, A, B, C, D, E, F]
- description: Letter grade
- from_domain:
- type: string
- description: Sender domain extracted from the report
- created_at:
- type: string
- format: date-time
-
- TestListResponse:
- type: object
- required:
- - tests
- - total
- - offset
- - limit
- properties:
- tests:
- type: array
- items:
- $ref: '#/components/schemas/TestSummary'
- total:
- type: integer
- description: Total number of tests
- offset:
- type: integer
- description: Current offset
- limit:
- type: integer
- description: Current limit
diff --git a/docker/README.md b/docker/README.md
index 2199eeb..3769365 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -110,38 +110,14 @@ Default configuration for the Docker environment:
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
+Note that the hostname of the container is used to filter the authentication tests results.
-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):
+Example:
```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:**
diff --git a/docker/spamassassin/local.cf b/docker/spamassassin/local.cf
index ce9a31c..c248ef6 100644
--- a/docker/spamassassin/local.cf
+++ b/docker/spamassassin/local.cf
@@ -48,14 +48,3 @@ rbl_timeout 5
# Don't use user-specific rules
user_scores_dsn_timeout 3
user_scores_sql_override 0
-
-# Disable Validity network rules
-dns_query_restriction deny sa-trusted.bondedsender.org
-dns_query_restriction deny sa-accredit.habeas.com
-dns_query_restriction deny bl.score.senderscore.com
-score RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0
-score RCVD_IN_VALIDITY_RPBL_BLOCKED 0
-score RCVD_IN_VALIDITY_SAFE_BLOCKED 0
-score RCVD_IN_VALIDITY_CERTIFIED 0
-score RCVD_IN_VALIDITY_RPBL 0
-score RCVD_IN_VALIDITY_SAFE 0
\ No newline at end of file
diff --git a/generate.go b/generate.go
index 324c52c..d1ee5ab 100644
--- a/generate.go
+++ b/generate.go
@@ -21,5 +21,5 @@
package main
-//go:generate go tool oapi-codegen -config api/config-models.yaml api/schemas.yaml
+//go:generate go tool oapi-codegen -config api/config-models.yaml api/openapi.yaml
//go:generate go tool oapi-codegen -config api/config-server.yaml api/openapi.yaml
diff --git a/go.mod b/go.mod
index bcf45d7..d44d5cc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,15 +1,15 @@
module git.happydns.org/happyDeliver
-go 1.25.0
+go 1.24.6
require (
- github.com/JGLTechnologies/gin-rate-limit v1.5.8
+ github.com/JGLTechnologies/gin-rate-limit v1.5.6
github.com/emersion/go-smtp v0.24.0
- github.com/getkin/kin-openapi v0.135.0
- github.com/gin-gonic/gin v1.12.0
+ github.com/getkin/kin-openapi v0.133.0
+ github.com/gin-gonic/gin v1.11.0
github.com/google/uuid v1.6.0
- github.com/oapi-codegen/runtime v1.3.0
- golang.org/x/net v0.53.0
+ github.com/oapi-codegen/runtime v1.1.2
+ golang.org/x/net v0.50.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -50,31 +50,30 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
- github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 // indirect
- github.com/oasdiff/yaml v0.0.9 // indirect
- github.com/oasdiff/yaml3 v0.0.9 // indirect
+ github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect
+ github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
+ github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
- github.com/redis/go-redis/v9 v9.18.0 // indirect
- github.com/speakeasy-api/jsonpath v0.6.3 // indirect
- github.com/speakeasy-api/openapi v1.19.2 // indirect
+ github.com/redis/go-redis/v9 v9.17.2 // indirect
+ github.com/speakeasy-api/jsonpath v0.6.0 // indirect
+ github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect
github.com/woodsbury/decimal128 v1.4.0 // indirect
- go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
- go.uber.org/atomic v1.11.0 // indirect
- go.yaml.in/yaml/v3 v3.0.4 // indirect
+ go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.23.0 // indirect
- golang.org/x/crypto v0.50.0 // indirect
- golang.org/x/mod v0.34.0 // indirect
- golang.org/x/sync v0.20.0 // indirect
- golang.org/x/sys v0.43.0 // indirect
- golang.org/x/text v0.36.0 // indirect
- golang.org/x/tools v0.43.0 // indirect
+ golang.org/x/crypto v0.48.0 // indirect
+ golang.org/x/mod v0.32.0 // indirect
+ golang.org/x/sync v0.19.0 // indirect
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/text v0.34.0 // indirect
+ golang.org/x/tools v0.41.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/go.sum b/go.sum
index 872377c..717c4ff 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-github.com/JGLTechnologies/gin-rate-limit v1.5.8 h1:KiaHIEbpYxHpDvjhpjIif8fnVmjdw/afCMdGoN1AsB0=
-github.com/JGLTechnologies/gin-rate-limit v1.5.8/go.mod h1:t9eLOUxikPI0TzKy0VYRbZJr7hBP2Qg9E3JigoxF70g=
+github.com/JGLTechnologies/gin-rate-limit v1.5.6 h1:BrL2wXrF7SSqmB88YTGFVKMGVcjURMUeKqwQrlmzweI=
+github.com/JGLTechnologies/gin-rate-limit v1.5.6/go.mod h1:fwUuBegxLKm8+/4ST0zDFssRFTFaVZ7bH3ApK7iNZww=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
@@ -22,9 +22,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
-github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dprotaso/go-yit v0.0.0-20191028211022-135eb7262960/go.mod h1:9HQzr9D/0PGwMEbC3d5AB7oi67+h4TsQqItC1GVYG58=
@@ -39,12 +38,12 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
-github.com/getkin/kin-openapi v0.135.0 h1:751SjYfbiwqukYuVjwYEIKNfrSwS5YpA7DZnKSwQgtg=
-github.com/getkin/kin-openapi v0.135.0/go.mod h1:6dd5FJl6RdX4usBtFBaQhk9q62Yb2J0Mk5IhUO/QqFI=
+github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
+github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
-github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
-github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
+github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
+github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
@@ -131,14 +130,14 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
-github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 h1:/8daqIYZfwnsHEAZdHUu9m0D5LA+5DoJCP7zLlT5Cs0=
-github.com/oapi-codegen/oapi-codegen/v2 v2.7.0/go.mod h1:qzFy6iuobJw/hD1aRILee4G87/ShmhR0xYCwcUtZMCw=
-github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA=
-github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY=
-github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
-github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
-github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
-github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
+github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 h1:5vHNY1uuPBRBWqB2Dp0G7YB03phxLQZupZTIZaeorjc=
+github.com/oapi-codegen/oapi-codegen/v2 v2.5.1/go.mod h1:ro0npU1BWkcGpCgGD9QwPp44l5OIZ94tB3eabnT7DjQ=
+github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI=
+github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
@@ -155,23 +154,22 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
-github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
-github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
-github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
-github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
-github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
+github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
+github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/speakeasy-api/jsonpath v0.6.3 h1:c+QPwzAOdrWvzycuc9HFsIZcxKIaWcNpC+xhOW9rJxU=
-github.com/speakeasy-api/jsonpath v0.6.3/go.mod h1:2cXloNuQ+RSXi5HTRaeBh7JEmjRXTiaKpFTdZiL7URI=
-github.com/speakeasy-api/openapi v1.19.2 h1:md90tE71/M8jS3cuRlsuWP5Aed4xoG5PSRvXeZgCv/M=
-github.com/speakeasy-api/openapi v1.19.2/go.mod h1:UfKa7FqE4jgexJZuj51MmdHAFGmDv0Zaw3+yOd81YKU=
+github.com/speakeasy-api/jsonpath v0.6.0 h1:IhtFOV9EbXplhyRqsVhHoBmmYjblIRh5D1/g8DHMXJ8=
+github.com/speakeasy-api/jsonpath v0.6.0/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw=
+github.com/speakeasy-api/openapi-overlay v0.10.2 h1:VOdQ03eGKeiHnpb1boZCGm7x8Haj6gST0P3SGTX95GU=
+github.com/speakeasy-api/openapi-overlay v0.10.2/go.mod h1:n0iOU7AqKpNFfEt6tq7qYITC4f0yzVVdFw0S7hukemg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -196,26 +194,18 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN
github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc=
github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
-github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
-github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
-go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
-go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
-go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
-go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
-go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
-golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
+golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
+golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
-golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -223,13 +213,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
-golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
-golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
+golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
+golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
+golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -245,21 +235,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
-golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
+golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
-golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
+golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
+golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
-golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
-golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index de2d5df..80c8f9a 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -31,7 +31,6 @@ import (
openapi_types "github.com/oapi-codegen/runtime/types"
"git.happydns.org/happyDeliver/internal/config"
- "git.happydns.org/happyDeliver/internal/model"
"git.happydns.org/happyDeliver/internal/storage"
"git.happydns.org/happyDeliver/internal/utils"
"git.happydns.org/happyDeliver/internal/version"
@@ -41,8 +40,8 @@ import (
// This interface breaks the circular dependency with pkg/analyzer
type EmailAnalyzer interface {
AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error)
- AnalyzeDomain(domain string) (dnsResults *model.DNSResults, score int, grade string)
- CheckBlacklistIP(ip string) (checks []model.BlacklistCheck, whitelists []model.BlacklistCheck, listedCount int, score int, grade string, err error)
+ AnalyzeDomain(domain string) (dnsResults *DNSResults, score int, grade string)
+ CheckBlacklistIP(ip string) (checks []BlacklistCheck, listedCount int, score int, grade string, err error)
}
// APIHandler implements the ServerInterface for handling API requests
@@ -80,11 +79,11 @@ func (h *APIHandler) CreateTest(c *gin.Context) {
)
// Return response
- c.JSON(http.StatusCreated, model.TestResponse{
+ c.JSON(http.StatusCreated, TestResponse{
Id: base32ID,
Email: openapi_types.Email(email),
- Status: model.TestResponseStatusPending,
- Message: utils.PtrTo("Send your test email to the given address"),
+ Status: TestResponseStatusPending,
+ Message: stringPtr("Send your test email to the given address"),
})
}
@@ -94,10 +93,10 @@ func (h *APIHandler) GetTest(c *gin.Context, id string) {
// Convert base32 ID to UUID
testUUID, err := utils.Base32ToUUID(id)
if err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_id",
Message: "Invalid test ID format",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -105,20 +104,20 @@ func (h *APIHandler) GetTest(c *gin.Context, id string) {
// Check if a report exists for this test ID
reportExists, err := h.storage.ReportExists(testUUID)
if err != nil {
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to check test status",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
// Determine status based on report existence
- var apiStatus model.TestStatus
+ var apiStatus TestStatus
if reportExists {
- apiStatus = model.TestStatusAnalyzed
+ apiStatus = TestStatusAnalyzed
} else {
- apiStatus = model.TestStatusPending
+ apiStatus = TestStatusPending
}
// Generate test email address using Base32-encoded UUID
@@ -128,7 +127,7 @@ func (h *APIHandler) GetTest(c *gin.Context, id string) {
h.config.Email.Domain,
)
- c.JSON(http.StatusOK, model.Test{
+ c.JSON(http.StatusOK, Test{
Id: id,
Email: openapi_types.Email(email),
Status: apiStatus,
@@ -141,10 +140,10 @@ func (h *APIHandler) GetReport(c *gin.Context, id string) {
// Convert base32 ID to UUID
testUUID, err := utils.Base32ToUUID(id)
if err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_id",
Message: "Invalid test ID format",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -152,16 +151,16 @@ func (h *APIHandler) GetReport(c *gin.Context, id string) {
reportJSON, _, err := h.storage.GetReport(testUUID)
if err != nil {
if err == storage.ErrNotFound {
- c.JSON(http.StatusNotFound, model.Error{
+ c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Report not found",
})
return
}
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve report",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -176,10 +175,10 @@ func (h *APIHandler) GetRawEmail(c *gin.Context, id string) {
// Convert base32 ID to UUID
testUUID, err := utils.Base32ToUUID(id)
if err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_id",
Message: "Invalid test ID format",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -187,16 +186,16 @@ func (h *APIHandler) GetRawEmail(c *gin.Context, id string) {
_, rawEmail, err := h.storage.GetReport(testUUID)
if err != nil {
if err == storage.ErrNotFound {
- c.JSON(http.StatusNotFound, model.Error{
+ c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Email not found",
})
return
}
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve raw email",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -210,10 +209,10 @@ func (h *APIHandler) ReanalyzeReport(c *gin.Context, id string) {
// Convert base32 ID to UUID
testUUID, err := utils.Base32ToUUID(id)
if err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_id",
Message: "Invalid test ID format",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -222,16 +221,16 @@ func (h *APIHandler) ReanalyzeReport(c *gin.Context, id string) {
_, rawEmail, err := h.storage.GetReport(testUUID)
if err != nil {
if err == storage.ErrNotFound {
- c.JSON(http.StatusNotFound, model.Error{
+ c.JSON(http.StatusNotFound, Error{
Error: "not_found",
Message: "Email not found",
})
return
}
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to retrieve email",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -239,20 +238,20 @@ func (h *APIHandler) ReanalyzeReport(c *gin.Context, id string) {
// Re-analyze the email using the current analyzer
reportJSON, err := h.analyzer.AnalyzeEmailBytes(rawEmail, testUUID)
if err != nil {
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "analysis_error",
Message: "Failed to re-analyze email",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
// Update the report in storage
if err := h.storage.UpdateReport(testUUID, reportJSON); err != nil {
- c.JSON(http.StatusInternalServerError, model.Error{
+ c.JSON(http.StatusInternalServerError, Error{
Error: "internal_error",
Message: "Failed to update report",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -268,24 +267,24 @@ func (h *APIHandler) GetStatus(c *gin.Context) {
uptime := int(time.Since(h.startTime).Seconds())
// Check database connectivity by trying to check if a report exists
- dbStatus := model.StatusComponentsDatabaseUp
+ dbStatus := StatusComponentsDatabaseUp
if _, err := h.storage.ReportExists(uuid.New()); err != nil {
- dbStatus = model.StatusComponentsDatabaseDown
+ dbStatus = StatusComponentsDatabaseDown
}
// Determine overall status
- overallStatus := model.Healthy
- if dbStatus == model.StatusComponentsDatabaseDown {
- overallStatus = model.Unhealthy
+ overallStatus := Healthy
+ if dbStatus == StatusComponentsDatabaseDown {
+ overallStatus = Unhealthy
}
- mtaStatus := model.StatusComponentsMtaUp
- c.JSON(http.StatusOK, model.Status{
+ mtaStatus := StatusComponentsMtaUp
+ c.JSON(http.StatusOK, Status{
Status: overallStatus,
Version: version.Version,
Components: &struct {
- Database *model.StatusComponentsDatabase `json:"database,omitempty"`
- Mta *model.StatusComponentsMta `json:"mta,omitempty"`
+ Database *StatusComponentsDatabase `json:"database,omitempty"`
+ Mta *StatusComponentsMta `json:"mta,omitempty"`
}{
Database: &dbStatus,
Mta: &mtaStatus,
@@ -297,14 +296,14 @@ func (h *APIHandler) GetStatus(c *gin.Context) {
// TestDomain performs synchronous domain analysis
// (POST /domain)
func (h *APIHandler) TestDomain(c *gin.Context) {
- var request model.DomainTestRequest
+ var request DomainTestRequest
// Bind and validate request
if err := c.ShouldBindJSON(&request); err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_request",
Message: "Invalid request body",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
@@ -313,28 +312,28 @@ func (h *APIHandler) TestDomain(c *gin.Context) {
dnsResults, score, grade := h.analyzer.AnalyzeDomain(request.Domain)
// Convert grade string to DomainTestResponseGrade enum
- var responseGrade model.DomainTestResponseGrade
+ var responseGrade DomainTestResponseGrade
switch grade {
case "A+":
- responseGrade = model.DomainTestResponseGradeA
+ responseGrade = DomainTestResponseGradeA
case "A":
- responseGrade = model.DomainTestResponseGradeA1
+ responseGrade = DomainTestResponseGradeA1
case "B":
- responseGrade = model.DomainTestResponseGradeB
+ responseGrade = DomainTestResponseGradeB
case "C":
- responseGrade = model.DomainTestResponseGradeC
+ responseGrade = DomainTestResponseGradeC
case "D":
- responseGrade = model.DomainTestResponseGradeD
+ responseGrade = DomainTestResponseGradeD
case "E":
- responseGrade = model.DomainTestResponseGradeE
+ responseGrade = DomainTestResponseGradeE
case "F":
- responseGrade = model.DomainTestResponseGradeF
+ responseGrade = DomainTestResponseGradeF
default:
- responseGrade = model.DomainTestResponseGradeF
+ responseGrade = DomainTestResponseGradeF
}
// Build response
- response := model.DomainTestResponse{
+ response := DomainTestResponse{
Domain: request.Domain,
Score: score,
Grade: responseGrade,
@@ -347,79 +346,37 @@ func (h *APIHandler) TestDomain(c *gin.Context) {
// CheckBlacklist checks an IP address against DNS blacklists
// (POST /blacklist)
func (h *APIHandler) CheckBlacklist(c *gin.Context) {
- var request model.BlacklistCheckRequest
+ var request BlacklistCheckRequest
// Bind and validate request
if err := c.ShouldBindJSON(&request); err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_request",
Message: "Invalid request body",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
// Perform blacklist check using analyzer
- checks, whitelists, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip)
+ checks, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip)
if err != nil {
- c.JSON(http.StatusBadRequest, model.Error{
+ c.JSON(http.StatusBadRequest, Error{
Error: "invalid_ip",
Message: "Invalid IP address",
- Details: utils.PtrTo(err.Error()),
+ Details: stringPtr(err.Error()),
})
return
}
// Build response
- response := model.BlacklistCheckResponse{
+ response := BlacklistCheckResponse{
Ip: request.Ip,
- Blacklists: checks,
- Whitelists: &whitelists,
+ Checks: checks,
ListedCount: listedCount,
Score: score,
- Grade: model.BlacklistCheckResponseGrade(grade),
+ Grade: BlacklistCheckResponseGrade(grade),
}
c.JSON(http.StatusOK, response)
}
-
-// ListTests returns a paginated list of test summaries
-// (GET /tests)
-func (h *APIHandler) ListTests(c *gin.Context, params ListTestsParams) {
- if h.config.DisableTestList {
- c.JSON(http.StatusForbidden, model.Error{
- Error: "feature_disabled",
- Message: "Test listing is disabled on this instance",
- })
- return
- }
-
- offset := 0
- limit := 20
- if params.Offset != nil {
- offset = *params.Offset
- }
- if params.Limit != nil {
- limit = *params.Limit
- if limit > 100 {
- limit = 100
- }
- }
-
- tests, total, err := h.storage.ListReportSummaries(offset, limit)
- if err != nil {
- c.JSON(http.StatusInternalServerError, model.Error{
- Error: "internal_error",
- Message: "Failed to list tests",
- Details: utils.PtrTo(err.Error()),
- })
- return
- }
-
- c.JSON(http.StatusOK, model.TestListResponse{
- Tests: tests,
- Total: int(total),
- Offset: offset,
- Limit: limit,
- })
-}
diff --git a/internal/utils/ptr.go b/internal/api/helpers.go
similarity index 91%
rename from internal/utils/ptr.go
rename to internal/api/helpers.go
index 748d6ba..cce306a 100644
--- a/internal/utils/ptr.go
+++ b/internal/api/helpers.go
@@ -1,5 +1,5 @@
// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2026 happyDomain
+// Copyright (c) 2025 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
@@ -19,7 +19,11 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see
{#if hop.with}
diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte
index 73c39e8..b26b492 100644
--- a/web/src/lib/components/HeaderAnalysisCard.svelte
+++ b/web/src/lib/components/HeaderAnalysisCard.svelte
@@ -11,7 +11,7 @@
headerScore?: number;
}
- let { dmarcRecord, headerAnalysis, headerGrade, headerScore }: Props = $props();
+ let { dmarcRecord, headerAnalysis, headerGrade, headerScore, xAlignedFrom }: Props = $props();
-
-
-
-
-
-
- {#each tests as test}
- Grade
- Score
- Domain
- Date
-
- goto(`/test/${test.test_id}`)}>
-
- {/each}
-
-
-
-
- {test.score}%
-
-
- {#if test.from_domain}
-
- {test.from_domain}
- {:else}
- -
- {/if}
-
- {formatDate(test.created_at)}
-
-
-
-
- {ip}
- {ip}
-
+
rspamd Analysis
@@ -75,7 +76,7 @@
@@ -87,14 +88,7 @@
? "table-success"
: ""}
>
- Symbol
Score
- Description
+ Parameters
- {symbolName}
- {#if symbol.params}
-
- {symbol.params}
-
- {/if}
-
+ {symbolName}
0
@@ -106,7 +100,7 @@
{symbol.score > 0 ? "+" : ""}{symbol.score.toFixed(2)}
- {symbol.description ?? ""}
+ {symbol.params ?? ""}
{/each}
@@ -114,32 +108,10 @@
Raw Report
- {rspamd.report}
-