diff --git a/api/schemas.yaml b/api/schemas.yaml
index 042a3b3..53aa297 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -434,29 +434,6 @@ components:
type: string
description: Reverse DNS (PTR record) for the IP address
example: "mail.example.com"
- tls:
- $ref: '#/components/schemas/TLSInfo'
- description: TLS details of the connection for this hop, if encrypted
-
- TLSInfo:
- type: object
- properties:
- version:
- type: string
- description: TLS protocol version
- example: "TLSv1.3"
- cipher:
- type: string
- description: Cipher suite used
- example: "TLS_AES_256_GCM_SHA384"
- bits:
- type: integer
- description: Cipher strength in bits
- example: 256
- verified:
- type: boolean
- description: Whether the peer certificate was verified/trusted
- example: true
DKIMDomainInfo:
type: object
@@ -560,14 +537,6 @@ components:
x_aligned_from:
$ref: '#/components/schemas/AuthResult'
description: X-Aligned-From authentication result (checks address alignment)
- x_ptr:
- $ref: '#/components/schemas/XPtrResult'
- description: X-Ptr result (HELO hostname vs reverse DNS consistency check)
- x_tls:
- $ref: '#/components/schemas/AuthResult'
- description: >-
- Transport TLS encryption of the inbound connection (x-tls).
- Synthesized from the inbound Received hop when no x-tls header is present.
AuthResult:
type: object
@@ -637,29 +606,6 @@ components:
description: Additional details about the IP reverse lookup
example: "smtp.remote-ip=195.110.101.58 (authsmtp74.register.it)"
- XPtrResult:
- type: object
- required:
- - result
- properties:
- result:
- type: string
- enum: [pass, fail, none, temperror, permerror]
- description: HELO/PTR consistency check result
- example: "fail"
- helo:
- type: string
- description: HELO/EHLO hostname announced by the sending server (smtp.helo)
- example: "relay.example.org"
- ptr:
- type: string
- description: Reverse DNS (PTR) hostname of the sender IP (policy.ptr)
- example: "mail.example.com"
- details:
- type: string
- description: Additional details about the x-ptr check
- example: "smtp.helo=relay.example.org policy.ptr=mail.example.com"
-
SpamAssassinResult:
type: object
required:
@@ -850,56 +796,12 @@ components:
type: string
description: A or AAAA records resolved from the PTR hostnames (forward confirmation)
example: ["192.0.2.1", "2001:db8::1"]
- helo_hostname:
- type: string
- description: HELO/EHLO hostname announced by the sending server (from the first Received hop)
- example: "mail.example.com"
- helo_ptr_match:
- type: boolean
- description: Whether the announced HELO hostname matches one of the sender's PTR records (case-insensitive)
- return_ok:
- $ref: '#/components/schemas/ReturnOK'
errors:
type: array
items:
type: string
description: DNS lookup errors
- ReturnOK:
- type: object
- description: Whether the sender domains can receive replies and bounces (MX, with A/AAAA fallback)
- properties:
- from:
- $ref: '#/components/schemas/ReturnOKDomain'
- return_path:
- $ref: '#/components/schemas/ReturnOKDomain'
-
- ReturnOKDomain:
- type: object
- required:
- - domain
- - status
- properties:
- domain:
- type: string
- description: Domain that was evaluated
- example: "example.com"
- status:
- type: string
- enum: [pass, warn, fail]
- x-go-type: string
- description: pass = MX present, warn = only A/AAAA records (implicit MX), fail = no records
- has_mx:
- type: boolean
- description: Whether the domain has at least one MX record
- has_address:
- type: boolean
- description: Whether the domain has an A or AAAA record (implicit MX fallback)
- org_domain:
- type: string
- description: Organizational domain used as fallback when the domain itself had no records
- example: "example.com"
-
MXRecord:
type: object
required:
diff --git a/docker/authentication_milter/authentication_milter.json b/docker/authentication_milter/authentication_milter.json
index cd3bd03..5db3bbc 100644
--- a/docker/authentication_milter/authentication_milter.json
+++ b/docker/authentication_milter/authentication_milter.json
@@ -52,8 +52,6 @@
"PTR" : {},
- "TLS" : {},
-
"SenderID" : {
"hide_none" : 1
},
diff --git a/docker/postfix/main.cf b/docker/postfix/main.cf
index 9f09396..5a73fb3 100644
--- a/docker/postfix/main.cf
+++ b/docker/postfix/main.cf
@@ -7,7 +7,7 @@ myhostname = __HOSTNAME__
mydomain = __DOMAIN__
myorigin = $mydomain
inet_interfaces = all
-inet_protocols = all
+inet_protocols = ipv4
# Recipient settings
mydestination = localhost.$mydomain, localhost
@@ -36,8 +36,5 @@ smtpd_recipient_restrictions =
permit_mynetworks,
reject_unauth_destination
-# TLS - record the negotiated cipher/protocol in the Received: header
-smtpd_tls_received_header = yes
-
# Logging
debug_peer_level = 2
diff --git a/docker/postfix/master.cf b/docker/postfix/master.cf
index 822d56e..9c2ac57 100644
--- a/docker/postfix/master.cf
+++ b/docker/postfix/master.cf
@@ -3,9 +3,6 @@
# SMTP service
smtp inet n - n - - smtpd
-# TLS session cache and PRNG manager (required for STARTTLS)
-tlsmgr unix - - n 1000? 1 tlsmgr
-
# Pickup service
pickup unix n - n 60 1 pickup
diff --git a/go.mod b/go.mod
index c638f4a..a975215 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
github.com/gin-gonic/gin v1.12.0
github.com/google/uuid v1.6.0
github.com/oapi-codegen/runtime v1.4.0
- golang.org/x/net v0.55.0
+ golang.org/x/net v0.54.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -36,7 +36,7 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
- github.com/jackc/pgx/v5 v5.9.2 // indirect
+ github.com/jackc/pgx/v5 v5.8.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -69,10 +69,10 @@ require (
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.23.0 // indirect
- golang.org/x/crypto v0.52.0 // indirect
+ golang.org/x/crypto v0.51.0 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sync v0.20.0 // indirect
- golang.org/x/sys v0.45.0 // indirect
+ golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
diff --git a/go.sum b/go.sum
index f467434..f4c8d28 100644
--- a/go.sum
+++ b/go.sum
@@ -93,8 +93,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
-github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
-github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
+github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
+github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -215,8 +215,8 @@ 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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
-golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
+golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
+golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
@@ -227,8 +227,8 @@ 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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
-golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
+golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
+golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
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=
@@ -249,8 +249,8 @@ 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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
+golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
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=
diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go
index 666f9ee..bd8880d 100644
--- a/pkg/analyzer/authentication.go
+++ b/pkg/analyzer/authentication.go
@@ -140,20 +140,6 @@ func (a *AuthenticationAnalyzer) parseAuthenticationResultsHeader(header string,
results.XAlignedFrom = a.parseXAlignedFromResult(part)
}
}
-
- // Parse x-ptr
- if strings.HasPrefix(part, "x-ptr=") {
- if results.XPtr == nil {
- results.XPtr = a.parseXPtrResult(part)
- }
- }
-
- // Parse x-tls
- if strings.HasPrefix(part, "x-tls=") {
- if results.XTls == nil {
- results.XTls = a.parseXTLSResult(part)
- }
- }
}
}
@@ -190,9 +176,6 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *model.Aut
// Penalty-only: X-Aligned-From (up to -5 points on failure)
score += 5 * a.calculateXAlignedFromScore(results) / 100
- // Penalty-only: X-TLS / transport encryption (-10 points when not encrypted)
- score += 10 * a.calculateXTLSScore(results) / 100
-
// Ensure score doesn't exceed 100
if score > 100 {
score = 100
diff --git a/pkg/analyzer/authentication_x_ptr.go b/pkg/analyzer/authentication_x_ptr.go
deleted file mode 100644
index 93ecd03..0000000
--- a/pkg/analyzer/authentication_x_ptr.go
+++ /dev/null
@@ -1,61 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025-2026 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "regexp"
- "strings"
-
- "git.happydns.org/happyDeliver/internal/model"
- "git.happydns.org/happyDeliver/internal/utils"
-)
-
-// parseXPtrResult parses the x-ptr result from Authentication-Results.
-// Example: x-ptr=fail smtp.helo=relay.example.org policy.ptr=mail.example.com
-func (a *AuthenticationAnalyzer) parseXPtrResult(part string) *model.XPtrResult {
- result := &model.XPtrResult{}
-
- // Extract result (pass, fail, none, temperror, permerror)
- re := regexp.MustCompile(`x-ptr=(\w+)`)
- if matches := re.FindStringSubmatch(part); len(matches) > 1 {
- resultStr := strings.ToLower(matches[1])
- result.Result = model.XPtrResultResult(resultStr)
- }
-
- // Extract announced HELO hostname (smtp.helo)
- heloRe := regexp.MustCompile(`smtp\.helo=([^\s;()]+)`)
- if matches := heloRe.FindStringSubmatch(part); len(matches) > 1 {
- helo := matches[1]
- result.Helo = &helo
- }
-
- // Extract reverse DNS hostname (policy.ptr)
- ptrRe := regexp.MustCompile(`policy\.ptr=([^\s;()]+)`)
- if matches := ptrRe.FindStringSubmatch(part); len(matches) > 1 {
- ptr := matches[1]
- result.Ptr = &ptr
- }
-
- result.Details = utils.PtrTo(strings.TrimPrefix(part, "x-ptr="))
-
- return result
-}
diff --git a/pkg/analyzer/authentication_x_ptr_test.go b/pkg/analyzer/authentication_x_ptr_test.go
deleted file mode 100644
index 7015951..0000000
--- a/pkg/analyzer/authentication_x_ptr_test.go
+++ /dev/null
@@ -1,81 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025-2026 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "testing"
-
- "git.happydns.org/happyDeliver/internal/model"
- "git.happydns.org/happyDeliver/internal/utils"
-)
-
-func TestParseXPtrResult(t *testing.T) {
- a := NewAuthenticationAnalyzer("receiver.com")
-
- tests := []struct {
- name string
- part string
- expectedResult model.XPtrResultResult
- expectedHelo *string
- expectedPtr *string
- }{
- {
- name: "x-ptr fail with helo and ptr",
- part: "x-ptr=fail smtp.helo=relay.example.org policy.ptr=mail.example.com",
- expectedResult: model.XPtrResultResultFail,
- expectedHelo: utils.PtrTo("relay.example.org"),
- expectedPtr: utils.PtrTo("mail.example.com"),
- },
- {
- name: "x-ptr pass",
- part: "x-ptr=pass smtp.helo=mail.example.com policy.ptr=mail.example.com",
- expectedResult: model.XPtrResultResultPass,
- expectedHelo: utils.PtrTo("mail.example.com"),
- expectedPtr: utils.PtrTo("mail.example.com"),
- },
- {
- name: "x-ptr none without ptr",
- part: "x-ptr=none smtp.helo=relay.example.org",
- expectedResult: model.XPtrResultResultNone,
- expectedHelo: utils.PtrTo("relay.example.org"),
- expectedPtr: nil,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- result := a.parseXPtrResult(tt.part)
- if result == nil {
- t.Fatal("expected non-nil result")
- }
- if result.Result != tt.expectedResult {
- t.Errorf("Result = %q, want %q", result.Result, tt.expectedResult)
- }
- if !equalStrPtr(result.Helo, tt.expectedHelo) {
- t.Errorf("Helo = %v, want %v", result.Helo, tt.expectedHelo)
- }
- if !equalStrPtr(result.Ptr, tt.expectedPtr) {
- t.Errorf("Ptr = %v, want %v", result.Ptr, tt.expectedPtr)
- }
- })
- }
-}
diff --git a/pkg/analyzer/authentication_x_tls.go b/pkg/analyzer/authentication_x_tls.go
deleted file mode 100644
index 440f806..0000000
--- a/pkg/analyzer/authentication_x_tls.go
+++ /dev/null
@@ -1,154 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "fmt"
- "regexp"
- "strings"
-
- "git.happydns.org/happyDeliver/internal/model"
- "git.happydns.org/happyDeliver/internal/utils"
-)
-
-// parseXTLSResult parses the x-tls result from Authentication-Results.
-// Example: x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256
-func (a *AuthenticationAnalyzer) parseXTLSResult(part string) *model.AuthResult {
- result := &model.AuthResult{}
-
- // Extract result (pass, fail, none, ...)
- re := regexp.MustCompile(`x-tls=(\w+)`)
- if matches := re.FindStringSubmatch(part); len(matches) > 1 {
- result.Result = model.AuthResultResult(strings.ToLower(matches[1]))
- }
-
- result.Details = utils.PtrTo(formatTLSDetails(
- submatch(part, `smtp\.version=([^\s;()]+)`),
- submatch(part, `smtp\.cipher=([^\s;()]+)`),
- submatch(part, `smtp\.bits=(\d+)`),
- ))
-
- return result
-}
-
-// calculateXTLSScore returns a penalty for a negative transport-TLS result.
-// pass (or absent) does not alter the score; any other result is penalized.
-func (a *AuthenticationAnalyzer) calculateXTLSScore(results *model.AuthenticationResults) (score int) {
- if results.XTls != nil {
- switch results.XTls.Result {
- case model.AuthResultResultPass:
- // pass: don't alter the score
- default:
- return -100
- }
- }
-
- return 0
-}
-
-// ReconcileXTLS fills in the x-tls result from the inbound connection's parsed TLS
-// information when no x-tls Authentication-Results header was present. The inbound
-// connection is the most recent hop (index 0) of the received chain.
-func (a *AuthenticationAnalyzer) ReconcileXTLS(results *model.AuthenticationResults, chain *[]model.ReceivedHop) {
- if results == nil || results.XTls != nil {
- return
- }
- if chain == nil || len(*chain) == 0 {
- return
- }
-
- inbound := (*chain)[0]
- switch {
- case inbound.Tls != nil:
- // Full TLS parenthetical present (smtpd_tls_received_header = yes).
- var version, cipher, bits string
- if inbound.Tls.Version != nil {
- version = *inbound.Tls.Version
- }
- if inbound.Tls.Cipher != nil {
- cipher = *inbound.Tls.Cipher
- }
- if inbound.Tls.Bits != nil {
- bits = fmt.Sprintf("%d", *inbound.Tls.Bits)
- }
- results.XTls = &model.AuthResult{
- Result: model.AuthResultResultPass,
- Details: utils.PtrTo(formatTLSDetails(version, cipher, bits)),
- }
-
- case protocolIndicatesTLS(inbound.With):
- // No TLS parenthetical (smtpd_tls_received_header may be disabled), but the
- // transport keyword (ESMTPS, ESMTPSA, ...) tells us the session was encrypted.
- // We just don't have the cipher details.
- results.XTls = &model.AuthResult{
- Result: model.AuthResultResultPass,
- Details: utils.PtrTo(fmt.Sprintf("Encrypted connection (%s); cipher details unavailable", *inbound.With)),
- }
-
- case inbound.With != nil:
- // A plaintext transport keyword (SMTP, ESMTP, ESMTPA, ...) is positive
- // evidence the inbound connection was not encrypted.
- results.XTls = &model.AuthResult{
- Result: model.AuthResultResultNone,
- Details: utils.PtrTo(fmt.Sprintf("Inbound connection was not encrypted (%s)", *inbound.With)),
- }
-
- default:
- // Neither TLS details nor a transport keyword: we cannot tell whether the
- // connection was encrypted. Leave x-tls unset rather than wrongly penalize.
- }
-}
-
-// protocolIndicatesTLS reports whether an SMTP "with" transport keyword denotes a
-// TLS-encrypted session. Per RFC 3848 the keyword gains a trailing "S" when STARTTLS
-// (or implicit TLS) was negotiated: ESMTPS, ESMTPSA, SMTPS, LMTPS, LMTPSA, UTF8SMTPS...
-// The plaintext variants end in "P" (SMTP, ESMTP, LMTP) or "A" (ESMTPA, LMTPA).
-func protocolIndicatesTLS(with *string) bool {
- if with == nil {
- return false
- }
- p := strings.ToUpper(strings.TrimSpace(*with))
- return strings.HasSuffix(p, "S") || strings.HasSuffix(p, "SA")
-}
-
-// submatch returns the first capture group of pattern in s, or "".
-func submatch(s, pattern string) string {
- if matches := regexp.MustCompile(pattern).FindStringSubmatch(s); len(matches) > 1 {
- return matches[1]
- }
- return ""
-}
-
-// formatTLSDetails builds a human-readable summary of the TLS parameters.
-func formatTLSDetails(version, cipher, bits string) string {
- var parts []string
- if version != "" {
- parts = append(parts, version)
- }
- if cipher != "" {
- parts = append(parts, "cipher "+cipher)
- }
- if bits != "" {
- parts = append(parts, bits+" bits")
- }
- return strings.Join(parts, ", ")
-}
diff --git a/pkg/analyzer/authentication_x_tls_test.go b/pkg/analyzer/authentication_x_tls_test.go
deleted file mode 100644
index 52a655c..0000000
--- a/pkg/analyzer/authentication_x_tls_test.go
+++ /dev/null
@@ -1,165 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "strings"
- "testing"
-
- "git.happydns.org/happyDeliver/internal/model"
- "git.happydns.org/happyDeliver/internal/utils"
-)
-
-func TestParseXTLSResult(t *testing.T) {
- analyzer := NewAuthenticationAnalyzer("")
-
- result := analyzer.parseXTLSResult("x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256")
-
- if result.Result != model.AuthResultResultPass {
- t.Errorf("Result = %v, want pass", result.Result)
- }
- if result.Details == nil {
- t.Fatal("Details should not be nil")
- }
- for _, want := range []string{"TLSv1.3", "TLS_AES_256_GCM_SHA384", "256 bits"} {
- if !strings.Contains(*result.Details, want) {
- t.Errorf("Details %q should contain %q", *result.Details, want)
- }
- }
-}
-
-func TestCalculateXTLSScore(t *testing.T) {
- analyzer := NewAuthenticationAnalyzer("")
-
- tests := []struct {
- name string
- xtls *model.AuthResult
- score int
- }{
- {"nil", nil, 0},
- {"pass", &model.AuthResult{Result: model.AuthResultResultPass}, 0},
- {"none", &model.AuthResult{Result: model.AuthResultResultNone}, -100},
- {"fail", &model.AuthResult{Result: model.AuthResultResultFail}, -100},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- results := &model.AuthenticationResults{XTls: tt.xtls}
- if got := analyzer.calculateXTLSScore(results); got != tt.score {
- t.Errorf("calculateXTLSScore = %d, want %d", got, tt.score)
- }
- })
- }
-}
-
-func TestReconcileXTLS(t *testing.T) {
- analyzer := NewAuthenticationAnalyzer("")
-
- t.Run("keeps existing x-tls header result", func(t *testing.T) {
- existing := &model.AuthResult{Result: model.AuthResultResultFail}
- results := &model.AuthenticationResults{XTls: existing}
- chain := &[]model.ReceivedHop{{Tls: &model.TLSInfo{Version: utils.PtrTo("TLSv1.3")}}}
- analyzer.ReconcileXTLS(results, chain)
- if results.XTls != existing {
- t.Error("existing XTls should be preserved")
- }
- })
-
- t.Run("synthesizes pass from encrypted inbound hop", func(t *testing.T) {
- results := &model.AuthenticationResults{}
- chain := &[]model.ReceivedHop{{Tls: &model.TLSInfo{
- Version: utils.PtrTo("TLSv1.3"),
- Cipher: utils.PtrTo("TLS_AES_256_GCM_SHA384"),
- Bits: utils.PtrTo(256),
- }}}
- analyzer.ReconcileXTLS(results, chain)
- if results.XTls == nil || results.XTls.Result != model.AuthResultResultPass {
- t.Fatalf("expected synthesized pass, got %+v", results.XTls)
- }
- if results.XTls.Details == nil || !strings.Contains(*results.XTls.Details, "TLSv1.3") {
- t.Errorf("details should mention TLS version, got %v", results.XTls.Details)
- }
- })
-
- t.Run("synthesizes pass from ESMTPS protocol without TLS parenthetical", func(t *testing.T) {
- // smtpd_tls_received_header disabled: no TLS details, but ESMTPS proves encryption.
- results := &model.AuthenticationResults{}
- chain := &[]model.ReceivedHop{{With: utils.PtrTo("ESMTPS")}}
- analyzer.ReconcileXTLS(results, chain)
- if results.XTls == nil || results.XTls.Result != model.AuthResultResultPass {
- t.Fatalf("expected synthesized pass, got %+v", results.XTls)
- }
- })
-
- t.Run("synthesizes none from plaintext ESMTP protocol", func(t *testing.T) {
- results := &model.AuthenticationResults{}
- chain := &[]model.ReceivedHop{{With: utils.PtrTo("ESMTP")}}
- analyzer.ReconcileXTLS(results, chain)
- if results.XTls == nil || results.XTls.Result != model.AuthResultResultNone {
- t.Fatalf("expected synthesized none, got %+v", results.XTls)
- }
- })
-
- t.Run("leaves nil when neither TLS info nor protocol is known", func(t *testing.T) {
- results := &model.AuthenticationResults{}
- chain := &[]model.ReceivedHop{{}}
- analyzer.ReconcileXTLS(results, chain)
- if results.XTls != nil {
- t.Errorf("expected nil XTls when undetermined, got %+v", results.XTls)
- }
- })
-
- t.Run("leaves nil with empty chain", func(t *testing.T) {
- results := &model.AuthenticationResults{}
- analyzer.ReconcileXTLS(results, &[]model.ReceivedHop{})
- if results.XTls != nil {
- t.Errorf("expected nil XTls, got %+v", results.XTls)
- }
- })
-}
-
-func TestProtocolIndicatesTLS(t *testing.T) {
- tests := []struct {
- with string
- want bool
- }{
- {"ESMTPS", true},
- {"ESMTPSA", true},
- {"SMTPS", true},
- {"LMTPS", true},
- {"LMTPSA", true},
- {"SMTP", false},
- {"ESMTP", false},
- {"ESMTPA", false},
- {"LMTP", false},
- }
- for _, tt := range tests {
- t.Run(tt.with, func(t *testing.T) {
- if got := protocolIndicatesTLS(utils.PtrTo(tt.with)); got != tt.want {
- t.Errorf("protocolIndicatesTLS(%q) = %v, want %v", tt.with, got, tt.want)
- }
- })
- }
- if protocolIndicatesTLS(nil) {
- t.Error("protocolIndicatesTLS(nil) should be false")
- }
-}
diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go
index 3e29a7a..06f8ddf 100644
--- a/pkg/analyzer/content.go
+++ b/pkg/analyzer/content.go
@@ -501,11 +501,6 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool {
return false
}
- // Replace email addresses with just their domain part to avoid false positives
- // e.g. "john.doe@example.com" → "example.com" so local-part dots don't look like domains
- emailAddrRegex := regexp.MustCompile(`(?i)[a-z0-9._%+\-]+@([a-z0-9.\-]+\.[a-z]{2,})`)
- linkText = emailAddrRegex.ReplaceAllString(linkText, "$1")
-
// Common generic link texts that shouldn't trigger warnings
genericTexts := []string{
"click here", "read more", "learn more", "download", "subscribe",
diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go
index 9927d1b..6bc7c39 100644
--- a/pkg/analyzer/dns.go
+++ b/pkg/analyzer/dns.go
@@ -88,16 +88,6 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head
if len(forwardRecords) > 0 {
results.PtrForwardRecords = &forwardRecords
}
-
- // Record the announced HELO name and whether it matches the PTR record
- if firstHop.From != nil && *firstHop.From != "" {
- helo := *firstHop.From
- results.HeloHostname = &helo
- if len(ptrRecords) > 0 {
- match := checkHeloPtrMatch(helo, ptrRecords)
- results.HeloPtrMatch = &match
- }
- }
}
}
@@ -110,15 +100,6 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head
results.RpMxRecords = d.checkMXRecords(*results.RpDomain)
}
- // Verify the sender domains can actually receive replies/bounces (MX, with
- // A/AAAA fallback), mirroring the ReturnOK milter check.
- results.ReturnOk = &model.ReturnOK{
- From: d.checkReturnOKDomain(fromDomain, orgDomainOrEmpty(headersResults.DomainAlignment.FromOrgDomain)),
- }
- if results.RpDomain != nil && *results.RpDomain != "" {
- results.ReturnOk.ReturnPath = d.checkReturnOKDomain(*results.RpDomain, orgDomainOrEmpty(headersResults.DomainAlignment.ReturnPathOrgDomain))
- }
-
// Check SPF records (for Return-Path domain - this is the envelope sender)
// SPF validates the MAIL FROM command, which corresponds to Return-Path
results.SpfRecords = d.checkSPFRecords(spfDomain)
@@ -157,11 +138,6 @@ func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *model.DNSResults {
// Check SPF records
results.SpfRecords = d.checkSPFRecords(domain)
- // Verify the domain can receive replies/bounces (MX, with A/AAAA fallback)
- results.ReturnOk = &model.ReturnOK{
- From: d.checkReturnOKDomain(domain, ""),
- }
-
// Check DMARC record
results.DmarcRecord = d.checkDMARCRecord(domain)
@@ -193,9 +169,6 @@ func (d *DNSAnalyzer) CalculateDomainOnlyScore(results *model.DNSResults) (int,
// DMARC Record: 40 points
score += 40 * d.calculateDMARCScore(results) / 100
- // Penalty when a sender domain cannot receive replies/bounces at all
- score += calculateReturnOKPenalty(results)
-
// BIMI Record: only bonus
if results.BimiRecord != nil && results.BimiRecord.Valid {
if score >= 100 {
@@ -241,9 +214,6 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *model.DNSResults, senderIP stri
// DMARC Record: 20 points
score += 20 * d.calculateDMARCScore(results) / 100
- // Penalty when a sender domain cannot receive replies/bounces at all
- score += calculateReturnOKPenalty(results)
-
// BIMI Record
// BIMI is optional but indicates advanced email branding
if results.BimiRecord != nil && results.BimiRecord.Valid {
diff --git a/pkg/analyzer/dns_bimi.go b/pkg/analyzer/dns_bimi.go
index b037978..223bfdc 100644
--- a/pkg/analyzer/dns_bimi.go
+++ b/pkg/analyzer/dns_bimi.go
@@ -45,7 +45,7 @@ func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *model.BIMIRecord
Selector: selector,
Domain: domain,
Valid: false,
- Error: utils.PtrTo(fmt.Sprintf("Failed to lookup BIMI record: %s", formatDNSError(err))),
+ Error: utils.PtrTo(fmt.Sprintf("Failed to lookup BIMI record: %v", err)),
}
}
diff --git a/pkg/analyzer/dns_dkim.go b/pkg/analyzer/dns_dkim.go
index 5708d1c..115e347 100644
--- a/pkg/analyzer/dns_dkim.go
+++ b/pkg/analyzer/dns_dkim.go
@@ -122,7 +122,7 @@ func (d *DNSAnalyzer) checkDKIMRecord(h DKIMHeader) *model.DKIMRecord {
Domain: h.Domain,
SigningAlgorithm: signingAlgorithmPtr(h.Algorithm),
Valid: false,
- Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %s", formatDNSError(err))),
+ Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)),
}
}
diff --git a/pkg/analyzer/dns_dmarc.go b/pkg/analyzer/dns_dmarc.go
index 20058b2..b89500b 100644
--- a/pkg/analyzer/dns_dmarc.go
+++ b/pkg/analyzer/dns_dmarc.go
@@ -193,7 +193,7 @@ func (d *DNSAnalyzer) checkDMARCRecord(domain string) *model.DMARCRecord {
if err != nil {
return &model.DMARCRecord{
Valid: false,
- Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %s", formatDNSError(err))),
+ Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %v", err)),
}
}
if foundDomain == "" {
diff --git a/pkg/analyzer/dns_fcr.go b/pkg/analyzer/dns_fcr.go
index 2652b4c..07e5ab9 100644
--- a/pkg/analyzer/dns_fcr.go
+++ b/pkg/analyzer/dns_fcr.go
@@ -23,7 +23,6 @@ package analyzer
import (
"context"
- "strings"
"git.happydns.org/happyDeliver/internal/model"
)
@@ -63,21 +62,6 @@ func (d *DNSAnalyzer) checkPTRAndForward(ip string) ([]string, []string) {
return ptrNames, forwardIPs
}
-// checkHeloPtrMatch reports whether the announced HELO hostname matches one of
-// the sender's PTR records (case-insensitive, trailing dot ignored).
-func checkHeloPtrMatch(helo string, ptrRecords []string) bool {
- helo = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(helo)), ".")
- if helo == "" {
- return false
- }
- for _, ptr := range ptrRecords {
- if strings.TrimSuffix(strings.ToLower(ptr), ".") == helo {
- return true
- }
- }
- return false
-}
-
// Proper reverse DNS (PTR) and forward-confirmed reverse DNS (FCrDNS) is important for deliverability
func (d *DNSAnalyzer) calculatePTRScore(results *model.DNSResults, senderIP string) (score int) {
if results.PtrRecords != nil && len(*results.PtrRecords) > 0 {
@@ -89,11 +73,6 @@ func (d *DNSAnalyzer) calculatePTRScore(results *model.DNSResults, senderIP stri
score -= 15
}
- // Penalty when the announced HELO name doesn't match the PTR hostname
- if results.HeloPtrMatch != nil && !*results.HeloPtrMatch {
- score -= 15
- }
-
// Additional 50 points for forward-confirmed reverse DNS (FCrDNS)
// This means the PTR hostname resolves back to IPs that include the original sender IP
if results.PtrForwardRecords != nil && len(*results.PtrForwardRecords) > 0 && senderIP != "" {
diff --git a/pkg/analyzer/dns_fcr_test.go b/pkg/analyzer/dns_fcr_test.go
deleted file mode 100644
index 2b9429b..0000000
--- a/pkg/analyzer/dns_fcr_test.go
+++ /dev/null
@@ -1,104 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025-2026 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "testing"
-
- "git.happydns.org/happyDeliver/internal/model"
-)
-
-func TestCheckHeloPtrMatch(t *testing.T) {
- tests := []struct {
- name string
- helo string
- ptrRecords []string
- want bool
- }{
- {"exact match", "mail.example.com", []string{"mail.example.com"}, true},
- {"case insensitive", "Mail.Example.COM", []string{"mail.example.com"}, true},
- {"trailing dot ignored", "mail.example.com.", []string{"mail.example.com"}, true},
- {"mismatch", "relay.example.org", []string{"mail.example.com"}, false},
- {"match among several", "smtp.example.com", []string{"mail.example.com", "smtp.example.com"}, true},
- {"empty helo", "", []string{"mail.example.com"}, false},
- {"no ptr records", "mail.example.com", nil, false},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := checkHeloPtrMatch(tt.helo, tt.ptrRecords); got != tt.want {
- t.Errorf("checkHeloPtrMatch(%q, %v) = %v, want %v", tt.helo, tt.ptrRecords, got, tt.want)
- }
- })
- }
-}
-
-func TestCalculatePTRScoreHeloMismatch(t *testing.T) {
- d := NewDNSAnalyzer(0)
- senderIP := "80.67.179.207"
- ptr := []string{"mail.example.com"}
- forward := []string{senderIP}
-
- matchTrue := true
- matchFalse := false
-
- tests := []struct {
- name string
- results *model.DNSResults
- want int
- }{
- {
- name: "helo matches ptr - no penalty (PTR+FCrDNS)",
- results: &model.DNSResults{
- PtrRecords: &ptr,
- PtrForwardRecords: &forward,
- HeloPtrMatch: &matchTrue,
- },
- want: 100,
- },
- {
- name: "helo mismatch - 15 point penalty",
- results: &model.DNSResults{
- PtrRecords: &ptr,
- PtrForwardRecords: &forward,
- HeloPtrMatch: &matchFalse,
- },
- want: 85,
- },
- {
- name: "no helo info - no penalty",
- results: &model.DNSResults{
- PtrRecords: &ptr,
- PtrForwardRecords: &forward,
- },
- want: 100,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := d.calculatePTRScore(tt.results, senderIP); got != tt.want {
- t.Errorf("calculatePTRScore() = %d, want %d", got, tt.want)
- }
- })
- }
-}
diff --git a/pkg/analyzer/dns_mx.go b/pkg/analyzer/dns_mx.go
index 51c9eca..c48c9a4 100644
--- a/pkg/analyzer/dns_mx.go
+++ b/pkg/analyzer/dns_mx.go
@@ -39,7 +39,7 @@ func (d *DNSAnalyzer) checkMXRecords(domain string) *[]model.MXRecord {
return &[]model.MXRecord{
{
Valid: false,
- Error: utils.PtrTo(fmt.Sprintf("Failed to lookup MX records: %s", formatDNSError(err))),
+ Error: utils.PtrTo(fmt.Sprintf("Failed to lookup MX records: %v", err)),
},
}
}
diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go
index 266078e..f60484f 100644
--- a/pkg/analyzer/dns_resolver.go
+++ b/pkg/analyzer/dns_resolver.go
@@ -23,22 +23,9 @@ package analyzer
import (
"context"
- "errors"
"net"
)
-// formatDNSError renders a resolution error without exposing the upstream
-// resolver address that net.DNSError.Error() normally appends as " on ".
-func formatDNSError(err error) string {
- var dnsErr *net.DNSError
- if errors.As(err, &dnsErr) {
- sanitized := *dnsErr
- sanitized.Server = ""
- return sanitized.Error()
- }
- return err.Error()
-}
-
// DNSResolver defines the interface for DNS resolution operations.
// This interface abstracts DNS lookups to allow for custom implementations,
// such as mock resolvers for testing or caching resolvers for performance.
diff --git a/pkg/analyzer/dns_returnok.go b/pkg/analyzer/dns_returnok.go
deleted file mode 100644
index 29e12b3..0000000
--- a/pkg/analyzer/dns_returnok.go
+++ /dev/null
@@ -1,113 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025-2026 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "context"
-
- "git.happydns.org/happyDeliver/internal/model"
- "git.happydns.org/happyDeliver/internal/utils"
-)
-
-// ReturnOKDomain.Status values, matching the schema enum. Kept as a plain string
-// in the generated model (x-go-type) to avoid colliding with other "pass"/"fail"
-// enums in the global enum namespace.
-const (
- returnOKStatusPass = "pass"
- returnOKStatusWarn = "warn"
- returnOKStatusFail = "fail"
-)
-
-// domainCanReceive reports whether a domain can accept mail, looking up records
-// in the same order as Fastmail's ReturnOK milter: MX first, then A/AAAA.
-func (d *DNSAnalyzer) domainCanReceive(domain string) (hasMX, hasAddress bool) {
- ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
- defer cancel()
-
- if mxRecords, err := d.resolver.LookupMX(ctx, domain); err == nil && len(mxRecords) > 0 {
- return true, false
- }
-
- if addrs, err := d.resolver.LookupHost(ctx, domain); err == nil && len(addrs) > 0 {
- return false, true
- }
-
- return false, false
-}
-
-// checkReturnOKDomain verifies that a domain can receive replies/bounces.
-// It checks the domain itself, then falls back to its organizational domain
-// (when different) the same way the ReturnOK milter retries the org domain.
-func (d *DNSAnalyzer) checkReturnOKDomain(domain, orgDomain string) *model.ReturnOKDomain {
- if domain == "" {
- return nil
- }
-
- result := &model.ReturnOKDomain{Domain: domain}
-
- hasMX, hasAddress := d.domainCanReceive(domain)
-
- // Fall back to the organizational domain when the domain itself has nothing.
- if !hasMX && !hasAddress && orgDomain != "" && orgDomain != domain {
- if orgMX, orgAddr := d.domainCanReceive(orgDomain); orgMX || orgAddr {
- hasMX, hasAddress = orgMX, orgAddr
- result.OrgDomain = utils.PtrTo(orgDomain)
- }
- }
-
- result.HasMx = utils.PtrTo(hasMX)
- result.HasAddress = utils.PtrTo(hasAddress)
-
- switch {
- case hasMX:
- result.Status = returnOKStatusPass
- case hasAddress:
- result.Status = returnOKStatusWarn
- default:
- result.Status = returnOKStatusFail
- }
-
- return result
-}
-
-// calculateReturnOKPenalty returns a non-positive value: each sender domain that
-// can receive neither replies nor bounces (status=fail) costs points, since
-// those messages would be silently lost.
-func calculateReturnOKPenalty(results *model.DNSResults) (penalty int) {
- if results.ReturnOk == nil {
- return 0
- }
- for _, dom := range []*model.ReturnOKDomain{results.ReturnOk.From, results.ReturnOk.ReturnPath} {
- if dom != nil && dom.Status == returnOKStatusFail {
- penalty -= 10
- }
- }
- return
-}
-
-// orgDomainOrEmpty dereferences an optional organizational domain pointer.
-func orgDomainOrEmpty(orgDomain *string) string {
- if orgDomain == nil {
- return ""
- }
- return *orgDomain
-}
diff --git a/pkg/analyzer/dns_returnok_test.go b/pkg/analyzer/dns_returnok_test.go
deleted file mode 100644
index 55aaa5c..0000000
--- a/pkg/analyzer/dns_returnok_test.go
+++ /dev/null
@@ -1,170 +0,0 @@
-// This file is part of the happyDeliver (R) project.
-// Copyright (c) 2025-2026 happyDomain
-// Authors: Pierre-Olivier Mercier, et al.
-//
-// This program is offered under a commercial and under the AGPL license.
-// For commercial licensing, contact us at .
-//
-// For AGPL licensing:
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see .
-
-package analyzer
-
-import (
- "context"
- "net"
- "testing"
- "time"
-
- "git.happydns.org/happyDeliver/internal/model"
-)
-
-// returnOKMockResolver lets tests control MX and host (A/AAAA) lookups per domain.
-type returnOKMockResolver struct {
- mx map[string][]*net.MX
- hosts map[string][]string
-}
-
-func (m *returnOKMockResolver) LookupMX(_ context.Context, name string) ([]*net.MX, error) {
- if recs, ok := m.mx[name]; ok {
- return recs, nil
- }
- return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true}
-}
-
-func (m *returnOKMockResolver) LookupHost(_ context.Context, host string) ([]string, error) {
- if recs, ok := m.hosts[host]; ok {
- return recs, nil
- }
- return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true}
-}
-
-func (m *returnOKMockResolver) LookupTXT(_ context.Context, _ string) ([]string, error) {
- return nil, nil
-}
-func (m *returnOKMockResolver) LookupAddr(_ context.Context, _ string) ([]string, error) {
- return nil, nil
-}
-
-func TestCheckReturnOKDomain(t *testing.T) {
- mx := []*net.MX{{Host: "mail.example.com.", Pref: 10}}
-
- tests := []struct {
- name string
- domain string
- orgDomain string
- resolver *returnOKMockResolver
- wantStatus string
- wantHasMX bool
- wantHasAddr bool
- wantOrgDomain string // "" means OrgDomain should be nil
- }{
- {
- name: "domain with MX passes",
- domain: "example.com",
- resolver: &returnOKMockResolver{mx: map[string][]*net.MX{"example.com": mx}},
- wantStatus: returnOKStatusPass,
- wantHasMX: true,
- wantHasAddr: false,
- },
- {
- name: "no MX but A/AAAA warns",
- domain: "example.com",
- resolver: &returnOKMockResolver{hosts: map[string][]string{"example.com": {"192.0.2.1"}}},
- wantStatus: returnOKStatusWarn,
- wantHasMX: false,
- wantHasAddr: true,
- },
- {
- name: "fallback to org domain MX",
- domain: "sub.example.com",
- orgDomain: "example.com",
- resolver: &returnOKMockResolver{mx: map[string][]*net.MX{"example.com": mx}},
- wantStatus: returnOKStatusPass,
- wantHasMX: true,
- wantHasAddr: false,
- wantOrgDomain: "example.com",
- },
- {
- name: "nothing anywhere fails",
- domain: "example.com",
- orgDomain: "example.com",
- resolver: &returnOKMockResolver{},
- wantStatus: returnOKStatusFail,
- wantHasMX: false,
- wantHasAddr: false,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- d := NewDNSAnalyzerWithResolver(5*time.Second, tt.resolver)
- got := d.checkReturnOKDomain(tt.domain, tt.orgDomain)
- if got == nil {
- t.Fatalf("checkReturnOKDomain returned nil")
- }
- if got.Status != tt.wantStatus {
- t.Errorf("Status = %q, want %q", got.Status, tt.wantStatus)
- }
- if got.HasMx == nil || *got.HasMx != tt.wantHasMX {
- t.Errorf("HasMx = %v, want %v", got.HasMx, tt.wantHasMX)
- }
- if got.HasAddress == nil || *got.HasAddress != tt.wantHasAddr {
- t.Errorf("HasAddress = %v, want %v", got.HasAddress, tt.wantHasAddr)
- }
- if tt.wantOrgDomain == "" {
- if got.OrgDomain != nil {
- t.Errorf("OrgDomain = %v, want nil", *got.OrgDomain)
- }
- } else {
- if got.OrgDomain == nil || *got.OrgDomain != tt.wantOrgDomain {
- t.Errorf("OrgDomain = %v, want %q", got.OrgDomain, tt.wantOrgDomain)
- }
- }
- })
- }
-}
-
-func TestCheckReturnOKDomainEmpty(t *testing.T) {
- d := NewDNSAnalyzerWithResolver(5*time.Second, &returnOKMockResolver{})
- if got := d.checkReturnOKDomain("", ""); got != nil {
- t.Errorf("checkReturnOKDomain(\"\") = %v, want nil", got)
- }
-}
-
-func TestCalculateReturnOKPenalty(t *testing.T) {
- fail := &model.ReturnOKDomain{Domain: "a.example", Status: returnOKStatusFail}
- pass := &model.ReturnOKDomain{Domain: "b.example", Status: returnOKStatusPass}
- warn := &model.ReturnOKDomain{Domain: "c.example", Status: returnOKStatusWarn}
-
- tests := []struct {
- name string
- results *model.DNSResults
- want int
- }{
- {"nil return_ok", &model.DNSResults{}, 0},
- {"both pass", &model.DNSResults{ReturnOk: &model.ReturnOK{From: pass, ReturnPath: pass}}, 0},
- {"warn is not penalised", &model.DNSResults{ReturnOk: &model.ReturnOK{From: warn}}, 0},
- {"one fail", &model.DNSResults{ReturnOk: &model.ReturnOK{From: fail, ReturnPath: pass}}, -10},
- {"both fail", &model.DNSResults{ReturnOk: &model.ReturnOK{From: fail, ReturnPath: fail}}, -20},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := calculateReturnOKPenalty(tt.results); got != tt.want {
- t.Errorf("calculateReturnOKPenalty() = %d, want %d", got, tt.want)
- }
- })
- }
-}
diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go
index 5628986..ccb1674 100644
--- a/pkg/analyzer/dns_spf.go
+++ b/pkg/analyzer/dns_spf.go
@@ -67,7 +67,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool,
{
Domain: &domain,
Valid: false,
- Error: utils.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %s", formatDNSError(err))),
+ Error: utils.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %v", err)),
},
}
}
diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go
index 9e5853e..6d7b547 100644
--- a/pkg/analyzer/headers.go
+++ b/pkg/analyzer/headers.go
@@ -26,7 +26,6 @@ import (
"net"
"net/mail"
"regexp"
- "strconv"
"strings"
"time"
@@ -589,53 +588,9 @@ func (h *HeaderAnalyzer) findHeaderIssues(email *EmailMessage) []model.HeaderIss
})
}
- // Check for fake reply/forward: Subject has Re:/Fwd: prefix but no thread headers
- subject := email.GetHeaderValue("Subject")
- if h.hasReplyPrefix(subject) && !email.HasHeader("References") && !email.HasHeader("In-Reply-To") {
- issues = append(issues, model.HeaderIssue{
- Header: "Subject",
- Severity: model.HeaderIssueSeverityHigh,
- Message: "Subject indicates a reply or forward but no References or In-Reply-To header is present",
- Advice: utils.PtrTo("Remove the Re:/Fwd: prefix from the subject, or add References/In-Reply-To headers if this is a genuine reply"),
- })
- }
-
return issues
}
-// hasReplyPrefix reports whether a subject line starts with a reply or forward prefix.
-func (h *HeaderAnalyzer) hasReplyPrefix(subject string) bool {
- // Normalize: collapse leading whitespace and make comparison case-insensitive
- s := strings.ToLower(strings.TrimSpace(subject))
-
- prefixes := []string{
- "re:", // English / universal
- "fwd:", // English forward
- "fw:", // English forward (short)
- "aw:", // German Antwort
- "wg:", // German Weitergeleitet
- "sv:", // Scandinavian Svar
- "vs:", // Finnish Vastaus / Norwegian
- "ref:", // Some clients
- "rép:", // French Réponse
- "tr:", // French Transfert
- "odp:", // Polish Odpowiedź
- "ynt:", // Turkish Yanıt
- "res:", // Portuguese/Spanish Resposta/Respuesta
- "enc:", // Spanish Enviado/Reenviado
- "vl:", // Dutch Verwijzing
- "antw:", // Dutch Antwoord
- "rv:", // Norwegian/Swedish
- }
-
- for _, p := range prefixes {
- if strings.HasPrefix(s, p) {
- return true
- }
- }
- return false
-}
-
// parseReceivedChain extracts the chain of Received headers from an email
func (h *HeaderAnalyzer) parseReceivedChain(email *EmailMessage) []model.ReceivedHop {
if email == nil || email.Header == nil {
@@ -738,50 +693,5 @@ func (h *HeaderAnalyzer) parseReceivedHeader(receivedValue string) *model.Receiv
}
}
- // Extract TLS details from the Received header parentheticals
- // (e.g. "(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) ...)")
- hop.Tls = parseReceivedTLS(normalized)
-
return hop
}
-
-// parseReceivedTLS extracts TLS connection details from a normalized Received header value.
-// Returns nil when the hop was not encrypted (no TLS version/cipher found).
-func parseReceivedTLS(normalized string) *model.TLSInfo {
- tls := &model.TLSInfo{}
- found := false
-
- // TLS protocol version, e.g. "using TLSv1.3"
- if matches := regexp.MustCompile(`(?i)using\s+(TLSv[0-9.]+|SSLv[0-9.]+)`).FindStringSubmatch(normalized); len(matches) > 1 {
- tls.Version = &matches[1]
- found = true
- }
-
- // Cipher suite, e.g. "with cipher TLS_AES_256_GCM_SHA384"
- if matches := regexp.MustCompile(`(?i)with cipher\s+([A-Za-z0-9_-]+)`).FindStringSubmatch(normalized); len(matches) > 1 {
- tls.Cipher = &matches[1]
- found = true
- }
-
- // Cipher strength, e.g. "(256/256 bits)"
- if matches := regexp.MustCompile(`\((\d+)/\d+ bits\)`).FindStringSubmatch(normalized); len(matches) > 1 {
- if bits, err := strconv.Atoi(matches[1]); err == nil {
- tls.Bits = &bits
- }
- }
-
- if !found {
- return nil
- }
-
- // Certificate verification status. Postfix emits "(verified OK)" when the peer
- // certificate was trusted, "(not verified)" otherwise. "No client certificate
- // requested" leaves the field unset (trust is simply not applicable).
- if regexp.MustCompile(`(?i)verified OK`).MatchString(normalized) {
- tls.Verified = utils.PtrTo(true)
- } else if regexp.MustCompile(`(?i)not verified`).MatchString(normalized) {
- tls.Verified = utils.PtrTo(false)
- }
-
- return tls
-}
diff --git a/pkg/analyzer/headers_test.go b/pkg/analyzer/headers_test.go
index 8426c58..d7469d7 100644
--- a/pkg/analyzer/headers_test.go
+++ b/pkg/analyzer/headers_test.go
@@ -677,77 +677,6 @@ func TestParseReceivedHeader(t *testing.T) {
}
}
-func TestParseReceivedTLS(t *testing.T) {
- tests := []struct {
- name string
- receivedValue string
- expectNil bool
- expectVersion *string
- expectCipher *string
- expectBits *int
- expectVerified *bool
- }{
- {
- name: "TLS 1.3 no client certificate",
- receivedValue: "from mail.example.com (unknown [IPv6:2001:db8::1]) " +
- "(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) " +
- "key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) " +
- "(No client certificate requested) " +
- "by mx.example.org (Postfix) with ESMTPSA id 1EFD11611EA; Sun, 19 Oct 2025 09:40:33 +0000 (UTC)",
- expectVersion: strPtr("TLSv1.3"),
- expectCipher: strPtr("TLS_AES_256_GCM_SHA384"),
- expectBits: intPtr(256),
- expectVerified: nil,
- },
- {
- name: "TLS with verified client certificate",
- receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) " +
- "(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) " +
- "(Client CN \"example\", Issuer \"CA\" (verified OK)) " +
- "by mx.receiver.com (Postfix) with ESMTPS id ABC; Mon, 01 Jan 2024 12:00:00 +0000",
- expectVersion: strPtr("TLSv1.2"),
- expectCipher: strPtr("ECDHE-RSA-AES128-GCM-SHA256"),
- expectBits: intPtr(128),
- expectVerified: boolPtr(true),
- },
- {
- name: "Plaintext (no TLS)",
- receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) by mx.receiver.com (Postfix) with ESMTP id ABC; Mon, 01 Jan 2024 12:00:00 +0000",
- expectNil: true,
- },
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- normalized := strings.Join(strings.Fields(tt.receivedValue), " ")
- tls := parseReceivedTLS(normalized)
-
- if tt.expectNil {
- if tls != nil {
- t.Fatalf("expected nil TLS info, got %+v", tls)
- }
- return
- }
-
- if tls == nil {
- t.Fatal("parseReceivedTLS returned nil")
- }
- if !equalStrPtr(tls.Version, tt.expectVersion) {
- t.Errorf("Version = %v, want %v", ptrToStr(tls.Version), ptrToStr(tt.expectVersion))
- }
- if !equalStrPtr(tls.Cipher, tt.expectCipher) {
- t.Errorf("Cipher = %v, want %v", ptrToStr(tls.Cipher), ptrToStr(tt.expectCipher))
- }
- if (tls.Bits == nil) != (tt.expectBits == nil) || (tls.Bits != nil && *tls.Bits != *tt.expectBits) {
- t.Errorf("Bits = %v, want %v", tls.Bits, tt.expectBits)
- }
- if (tls.Verified == nil) != (tt.expectVerified == nil) || (tls.Verified != nil && *tls.Verified != *tt.expectVerified) {
- t.Errorf("Verified = %v, want %v", tls.Verified, tt.expectVerified)
- }
- })
- }
-}
-
func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) {
analyzer := NewHeaderAnalyzer()
@@ -974,155 +903,11 @@ func TestCheckHeader_DateValidation(t *testing.T) {
}
}
-func TestHasReplyPrefix(t *testing.T) {
- tests := []struct {
- subject string
- expected bool
- }{
- // Positive cases
- {"Re: Hello", true},
- {"RE: Hello", true},
- {"re: Hello", true},
- {"Fwd: Hello", true},
- {"FWD: Hello", true},
- {"fw: Hello", true},
- {"FW: Hello", true},
- {"Aw: Hallo", true},
- {"WG: Weitergeleitet", true},
- {"Sv: Hej", true},
- {"Vs: Vastaus", true},
- {"Ref: something", true},
- {"Rép: Bonjour", true},
- {"TR: Transféré", true},
- {"Odp: Odpowiedź", true},
- {"Ynt: Yanıt", true},
- {"Res: Resposta", true},
- {"Enc: Reenviado", true},
- {"Vl: Verwijzing", true},
- {"Antw: Antwoord", true},
- {"Rv: Svar", true},
- // Negative cases
- {"Hello", false},
- {"", false},
- {"react: something", false},
- {"reference: check this", false},
- {"Resources available", false},
- {"Friendly reminder", false},
- }
-
- analyzer := NewHeaderAnalyzer()
-
- for _, tt := range tests {
- t.Run(tt.subject, func(t *testing.T) {
- result := analyzer.hasReplyPrefix(tt.subject)
- if result != tt.expected {
- t.Errorf("hasReplyPrefix(%q) = %v, want %v", tt.subject, result, tt.expected)
- }
- })
- }
-}
-
-func TestFindHeaderIssues_FakeReply(t *testing.T) {
- tests := []struct {
- name string
- headers map[string]string
- expectIssueType string // non-empty means we expect an issue containing this substring
- }{
- {
- name: "Re: subject without thread headers",
- headers: map[string]string{
- "From": "sender@example.com",
- "Date": "Mon, 01 Jan 2024 12:00:00 +0000",
- "Message-ID": "",
- "Subject": "Re: Your invoice",
- },
- expectIssueType: "References or In-Reply-To",
- },
- {
- name: "Fwd: subject without thread headers",
- headers: map[string]string{
- "From": "sender@example.com",
- "Date": "Mon, 01 Jan 2024 12:00:00 +0000",
- "Message-ID": "",
- "Subject": "Fwd: Important update",
- },
- expectIssueType: "References or In-Reply-To",
- },
- {
- name: "Re: subject with References header - no issue",
- headers: map[string]string{
- "From": "sender@example.com",
- "Date": "Mon, 01 Jan 2024 12:00:00 +0000",
- "Message-ID": "",
- "Subject": "Re: Your invoice",
- "References": "",
- },
- expectIssueType: "",
- },
- {
- name: "Re: subject with In-Reply-To only - no issue",
- headers: map[string]string{
- "From": "sender@example.com",
- "Date": "Mon, 01 Jan 2024 12:00:00 +0000",
- "Message-ID": "",
- "Subject": "Re: Your invoice",
- "In-Reply-To": "",
- },
- expectIssueType: "",
- },
- {
- name: "Normal subject without thread headers - no issue",
- headers: map[string]string{
- "From": "sender@example.com",
- "Date": "Mon, 01 Jan 2024 12:00:00 +0000",
- "Message-ID": "",
- "Subject": "Your invoice",
- },
- expectIssueType: "",
- },
- }
-
- analyzer := NewHeaderAnalyzer()
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- email := &EmailMessage{
- Header: createHeaderWithFields(tt.headers),
- }
-
- issues := analyzer.findHeaderIssues(email)
-
- found := false
- for _, issue := range issues {
- if strings.Contains(issue.Message, tt.expectIssueType) {
- found = true
- break
- }
- }
-
- if tt.expectIssueType != "" && !found {
- t.Errorf("expected issue containing %q, but none found (issues: %v)", tt.expectIssueType, issues)
- }
- if tt.expectIssueType == "" {
- for _, issue := range issues {
- if strings.Contains(issue.Message, "References or In-Reply-To") {
- t.Errorf("unexpected fake-reply issue found: %s", issue.Message)
- }
- }
- }
- })
- }
-}
-
// Helper functions for testing
func strPtr(s string) *string {
return &s
}
-func boolPtr(b bool) *bool {
- return &b
-}
-
func ptrToStr(p *string) string {
if p == nil {
return ""
diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go
index e20e571..26cd59d 100644
--- a/pkg/analyzer/report.go
+++ b/pkg/analyzer/report.go
@@ -85,10 +85,6 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
// Run all analyzers
results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email)
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
- // Fall back to the received chain's inbound TLS when no x-tls header was present.
- if results.Authentication != nil && results.Headers != nil {
- r.authAnalyzer.ReconcileXTLS(results.Authentication, results.Headers.ReceivedChain)
- }
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Headers)
results.RBL = r.rblChecker.CheckEmail(email)
results.DNSWL = r.dnswlChecker.CheckEmail(email)
diff --git a/web/package-lock.json b/web/package-lock.json
index 27e6fc1..7aa8c15 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -867,9 +867,9 @@
}
},
"node_modules/@oxc-project/types": {
- "version": "0.130.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
- "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
+ "version": "0.132.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
+ "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -895,9 +895,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz",
- "integrity": "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
+ "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==",
"cpu": [
"arm64"
],
@@ -912,9 +912,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz",
- "integrity": "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz",
+ "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==",
"cpu": [
"arm64"
],
@@ -929,9 +929,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz",
- "integrity": "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz",
+ "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==",
"cpu": [
"x64"
],
@@ -946,9 +946,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz",
- "integrity": "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz",
+ "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==",
"cpu": [
"x64"
],
@@ -963,9 +963,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz",
- "integrity": "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz",
+ "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==",
"cpu": [
"arm"
],
@@ -980,9 +980,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz",
- "integrity": "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz",
+ "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==",
"cpu": [
"arm64"
],
@@ -1000,9 +1000,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz",
- "integrity": "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz",
+ "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==",
"cpu": [
"arm64"
],
@@ -1020,9 +1020,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz",
- "integrity": "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz",
+ "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==",
"cpu": [
"ppc64"
],
@@ -1040,9 +1040,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz",
- "integrity": "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz",
+ "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==",
"cpu": [
"s390x"
],
@@ -1060,9 +1060,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz",
- "integrity": "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz",
+ "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==",
"cpu": [
"x64"
],
@@ -1080,9 +1080,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz",
- "integrity": "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz",
+ "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==",
"cpu": [
"x64"
],
@@ -1100,9 +1100,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz",
- "integrity": "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz",
+ "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==",
"cpu": [
"arm64"
],
@@ -1117,9 +1117,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz",
- "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz",
+ "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==",
"cpu": [
"wasm32"
],
@@ -1136,9 +1136,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz",
- "integrity": "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz",
+ "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==",
"cpu": [
"arm64"
],
@@ -1153,9 +1153,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
- "integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz",
+ "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==",
"cpu": [
"x64"
],
@@ -1573,9 +1573,9 @@
"license": "MIT"
},
"node_modules/@sveltejs/acorn-typescript": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz",
- "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==",
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz",
+ "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1593,16 +1593,16 @@
}
},
"node_modules/@sveltejs/kit": {
- "version": "2.60.1",
- "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.60.1.tgz",
- "integrity": "sha512-mQjlkNo+rJvpln7V2IGY2j99BqhcFbS4UN0AQNKNYfhBAFZTuCDAdW3a1sgf330mvtNvsBXn3HpAhcmvdJTcIQ==",
+ "version": "2.61.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.61.1.tgz",
+ "integrity": "sha512-Ny8s1SR1TyQS2hD2Rvw0XKzU2Nw1eUF52dTb6T2bdcgz7wSC+Nyb5IwjWYlR4b2dvbbR5NJDiQwHg3rnNseghg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.0.0",
- "@sveltejs/acorn-typescript": "^1.0.5",
+ "@sveltejs/acorn-typescript": "^1.0.9",
"@types/cookie": "^0.6.0",
- "acorn": "^8.14.1",
+ "acorn": "^8.16.0",
"cookie": "^0.6.0",
"devalue": "^5.8.1",
"esm-env": "^1.2.2",
@@ -1729,17 +1729,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz",
- "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz",
+ "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/type-utils": "8.59.3",
- "@typescript-eslint/utils": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/type-utils": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1752,7 +1752,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.59.3",
+ "@typescript-eslint/parser": "^8.60.0",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1768,16 +1768,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz",
- "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz",
+ "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1793,14 +1793,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz",
- "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz",
+ "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.59.3",
- "@typescript-eslint/types": "^8.59.3",
+ "@typescript-eslint/tsconfig-utils": "^8.60.0",
+ "@typescript-eslint/types": "^8.60.0",
"debug": "^4.4.3"
},
"engines": {
@@ -1815,14 +1815,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz",
- "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz",
+ "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3"
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1833,9 +1833,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz",
- "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz",
+ "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1850,15 +1850,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz",
- "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz",
+ "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/utils": "8.59.3",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1875,9 +1875,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz",
- "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz",
+ "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1889,16 +1889,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz",
- "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz",
+ "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/project-service": "8.59.3",
- "@typescript-eslint/tsconfig-utils": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3",
+ "@typescript-eslint/project-service": "8.60.0",
+ "@typescript-eslint/tsconfig-utils": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/visitor-keys": "8.60.0",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1917,9 +1917,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.8.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
- "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz",
+ "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1930,16 +1930,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz",
- "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz",
+ "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.59.3",
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3"
+ "@typescript-eslint/scope-manager": "8.60.0",
+ "@typescript-eslint/types": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1954,13 +1954,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz",
- "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz",
+ "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/types": "8.60.0",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -3844,9 +3844,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.14",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
- "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
+ "version": "8.5.15",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
"dev": true,
"funding": [
{
@@ -3864,7 +3864,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -4053,13 +4053,13 @@
}
},
"node_modules/rolldown": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
- "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz",
+ "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.130.0",
+ "@oxc-project/types": "=0.132.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -4069,21 +4069,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.0.1",
- "@rolldown/binding-darwin-arm64": "1.0.1",
- "@rolldown/binding-darwin-x64": "1.0.1",
- "@rolldown/binding-freebsd-x64": "1.0.1",
- "@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
- "@rolldown/binding-linux-arm64-gnu": "1.0.1",
- "@rolldown/binding-linux-arm64-musl": "1.0.1",
- "@rolldown/binding-linux-ppc64-gnu": "1.0.1",
- "@rolldown/binding-linux-s390x-gnu": "1.0.1",
- "@rolldown/binding-linux-x64-gnu": "1.0.1",
- "@rolldown/binding-linux-x64-musl": "1.0.1",
- "@rolldown/binding-openharmony-arm64": "1.0.1",
- "@rolldown/binding-wasm32-wasi": "1.0.1",
- "@rolldown/binding-win32-arm64-msvc": "1.0.1",
- "@rolldown/binding-win32-x64-msvc": "1.0.1"
+ "@rolldown/binding-android-arm64": "1.0.2",
+ "@rolldown/binding-darwin-arm64": "1.0.2",
+ "@rolldown/binding-darwin-x64": "1.0.2",
+ "@rolldown/binding-freebsd-x64": "1.0.2",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.2",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.2",
+ "@rolldown/binding-linux-arm64-musl": "1.0.2",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.2",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-gnu": "1.0.2",
+ "@rolldown/binding-linux-x64-musl": "1.0.2",
+ "@rolldown/binding-openharmony-arm64": "1.0.2",
+ "@rolldown/binding-wasm32-wasi": "1.0.2",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.2",
+ "@rolldown/binding-win32-x64-msvc": "1.0.2"
}
},
"node_modules/rollup": {
@@ -4277,15 +4277,15 @@
}
},
"node_modules/svelte": {
- "version": "5.55.7",
- "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz",
- "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==",
+ "version": "5.55.9",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.9.tgz",
+ "integrity": "sha512-fTjjT8cHLDwigcu2j3pv7Jq04LklXevPB8uBgyHNiTXv+RMNvVnrjS4UEYrLMkhuq1vpCodHjiW+z/95SDs/fg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/remapping": "^2.3.4",
"@jridgewell/sourcemap-codec": "^1.5.0",
- "@sveltejs/acorn-typescript": "^1.0.5",
+ "@sveltejs/acorn-typescript": "^1.0.10",
"@types/estree": "^1.0.5",
"@types/trusted-types": "^2.0.7",
"acorn": "^8.12.1",
@@ -4294,7 +4294,7 @@
"clsx": "^2.1.1",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
- "esrap": "^2.2.4",
+ "esrap": "^2.2.9",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -4415,9 +4415,9 @@
"license": "MIT"
},
"node_modules/tinyexec": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz",
- "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==",
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.2.tgz",
+ "integrity": "sha512-M/Q0B2cp4K7kynaT/vnED1j8TlLY+Pp7C6Wl2bl/7u/F0mUVwdyOpwomQb8JpYLitHUssAJRmLZdMCGsrx7i+g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4530,16 +4530,16 @@
}
},
"node_modules/typescript-eslint": {
- "version": "8.59.3",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz",
- "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==",
+ "version": "8.60.0",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz",
+ "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/eslint-plugin": "8.59.3",
- "@typescript-eslint/parser": "8.59.3",
- "@typescript-eslint/typescript-estree": "8.59.3",
- "@typescript-eslint/utils": "8.59.3"
+ "@typescript-eslint/eslint-plugin": "8.60.0",
+ "@typescript-eslint/parser": "8.60.0",
+ "@typescript-eslint/typescript-estree": "8.60.0",
+ "@typescript-eslint/utils": "8.60.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4592,16 +4592,16 @@
"license": "MIT"
},
"node_modules/vite": {
- "version": "8.0.13",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
- "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
+ "version": "8.0.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz",
+ "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
- "postcss": "^8.5.14",
- "rolldown": "1.0.1",
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.2",
"tinyglobby": "^0.2.16"
},
"bin": {
diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte
index 749263d..46a4d2d 100644
--- a/web/src/lib/components/AuthenticationCard.svelte
+++ b/web/src/lib/components/AuthenticationCard.svelte
@@ -170,88 +170,6 @@
{/if}
-
- {#if authentication.x_ptr}
-
-
-
-
-
HELO / PTR
-
-
- {authentication.x_ptr.result}
-
- {#if authentication.x_ptr.helo}
-
- Announced HELO:
- {authentication.x_ptr.helo}
-
- {/if}
- {#if authentication.x_ptr.ptr}
-
- Reverse DNS (PTR):
- {authentication.x_ptr.ptr}
-
- {/if}
- {#if authentication.x_ptr.details}
-
{authentication.x_ptr.details}
- {/if}
-
-
-
- {/if}
-
-
- {#if authentication.x_tls}
-
-
-
-
-
Transport TLS
-
-
- {authentication.x_tls.result}
-
- {#if authentication.x_tls.details}
-
- {authentication.x_tls.details}
-
- {/if}
-
-
-
- {/if}
-
diff --git a/web/src/lib/components/BimiRecordDisplay.svelte b/web/src/lib/components/BimiRecordDisplay.svelte
index 8d21b1f..889e24f 100644
--- a/web/src/lib/components/BimiRecordDisplay.svelte
+++ b/web/src/lib/components/BimiRecordDisplay.svelte
@@ -72,26 +72,6 @@
{bimiRecord.error}
{/if}
- {#if !bimiRecord.valid}
-
-
-
- Explicitly decline BIMI participation
-
-
- If you do not intend to publish a brand logo, you can add a declination
- record to signal that this domain deliberately opts out of BIMI. This
- prevents mail clients from falling back to a parent-domain record:
-
-
{bimiRecord.selector}._bimi.{bimiRecord.domain}. IN TXT "v=BIMI1; l=; a="
-
- Declination record format as defined in § 4.3.1 of
- draft-brand-indicators-for-message-identification.
-
-
- {/if}
{/if}
diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte
index eedd0db..6dabe0b 100644
--- a/web/src/lib/components/DnsRecordsCard.svelte
+++ b/web/src/lib/components/DnsRecordsCard.svelte
@@ -6,11 +6,9 @@
import DkimRecordsDisplay from "./DkimRecordsDisplay.svelte";
import DmarcRecordDisplay from "./DmarcRecordDisplay.svelte";
import GradeDisplay from "./GradeDisplay.svelte";
- import HeloPtrMatchDisplay from "./HeloPtrMatchDisplay.svelte";
import MxRecordsDisplay from "./MxRecordsDisplay.svelte";
import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte";
import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte";
- import ReturnOkDisplay from "./ReturnOkDisplay.svelte";
import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte";
interface Props {
@@ -94,16 +92,6 @@
{senderIp}
/>
-
-
-
-
-
-
@@ -154,7 +142,8 @@
{#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain}
- Differs from Return-Path domain
+ Differs from Return-Path
+ domain
{/if}
diff --git a/web/src/lib/components/EmailPathCard.svelte b/web/src/lib/components/EmailPathCard.svelte
index 72cfd94..a4fda45 100644
--- a/web/src/lib/components/EmailPathCard.svelte
+++ b/web/src/lib/components/EmailPathCard.svelte
@@ -7,21 +7,6 @@
}
let { receivedChain }: Props = $props();
-
- // Mirror of the backend protocolIndicatesTLS (RFC 3848): the transport keyword
- // gains a trailing "S" when TLS was used (ESMTPS, ESMTPSA, SMTPS, LMTPS, LMTPSA...).
- function protocolIndicatesTLS(withProto: string | undefined | null): boolean {
- if (!withProto) return false;
- const p = withProto.trim().toUpperCase();
- return p.endsWith("S") || p.endsWith("SA");
- }
-
- // RFC 3848: a trailing "A" means the sender authenticated (SMTP AUTH):
- // ESMTPA, ESMTPSA, LMTPA, LMTPSA...
- function protocolIndicatesAuth(withProto: string | undefined | null): boolean {
- if (!withProto) return false;
- return withProto.trim().toUpperCase().endsWith("A");
- }
{#if receivedChain && receivedChain.length > 0}
@@ -75,63 +60,6 @@
{/if}
{/if}
-
- {#if hop.tls}
-
- TLS
-
- {#if hop.tls.version}
-
- Version:
- {hop.tls.version}
-
- {/if}
- {#if hop.tls.cipher}
-
- Cipher:
- {hop.tls.cipher}
-
- {/if}
- {#if hop.tls.bits}
-
- Strength:
- {hop.tls.bits} bits
-
- {/if}
- {#if hop.tls.verified !== undefined}
-
-
- {hop.tls.verified
- ? "Certificate trusted"
- : "Certificate not trusted"}
-
- {/if}
- {:else if protocolIndicatesTLS(hop.with)}
-
- TLS
-
- {:else if hop.with}
-
- No TLS
-
- {:else}
-
- TLS unknown
-
- {/if}
- {#if protocolIndicatesAuth(hop.with)}
-
- Authenticated
-
- {/if}
-
{/each}
diff --git a/web/src/lib/components/HeloPtrMatchDisplay.svelte b/web/src/lib/components/HeloPtrMatchDisplay.svelte
deleted file mode 100644
index 1d8cee7..0000000
--- a/web/src/lib/components/HeloPtrMatchDisplay.svelte
+++ /dev/null
@@ -1,87 +0,0 @@
-
-
-{#if heloHostname}
-
-
-
-
- The HELO/EHLO hostname is the name the sending server announces when it connects.
- Many mail servers check that this name matches the sender IP's reverse DNS (PTR)
- record. A mismatch is a common spam signal and can hurt deliverability.
-
-
- Announced HELO: {heloHostname}
-
- {#if ptrRecords && ptrRecords.length > 0}
-
-
PTR Hostname(s):
- {#each ptrRecords as ptr}
-
- {#if normalize(heloHostname) === normalize(ptr)}
- Match
- {:else}
- Different
- {/if}
- {ptr}
-
- {/each}
-
- {/if}
-
- {#if !isMatch}
-
-
-
-
- Warning: The announced HELO hostname
- {heloHostname}
- {#if ptrRecords && ptrRecords.length > 0}
- does not match the sender's PTR record{ptrRecords.length > 1 ? "s" : ""}
- ({#each ptrRecords as ptr, i}{ptr}{i <
- ptrRecords.length - 1
- ? ", "
- : ""}{/each}).
- {:else}
- could not be matched against a PTR record.
- {/if}
- Configuring the HELO name to match reverse DNS improves deliverability.
-
-
-
- {/if}
-
-{/if}
diff --git a/web/src/lib/components/ReturnOkDisplay.svelte b/web/src/lib/components/ReturnOkDisplay.svelte
deleted file mode 100644
index 11d4c00..0000000
--- a/web/src/lib/components/ReturnOkDisplay.svelte
+++ /dev/null
@@ -1,106 +0,0 @@
-
-
-{#if rows.length > 0}
-
-
-
-
- Replies (to the From address) and bounces (to the Return-Path) can only be delivered
- if the sender's domains accept mail. A domain should publish MX records; an A/AAAA
- record works as an implicit fallback but is not recommended. A domain with neither
- is unreachable and silently drops replies and bounces.
-
-
-
- {#each rows as { label, entry } (label)}
-
-
- {label} domain:
- {entry.domain}
-
- {badgeLabel(entry.status)}
-
- {#if entry.org_domain}
-
- via organizational domain {entry.org_domain}
-
- {/if}
-
-
- {/each}
-
- {#if hasFail || hasWarn}
-
-
- {#if hasFail}
-
-
- Error: At least one sender domain has no MX and no A/AAAA record.
- Replies or bounce messages to that domain will be lost. Publish an MX record pointing
- to a mail server that accepts mail.
-
- {:else if hasWarn}
-
-
- Warning: A sender domain has no MX record and relies on its A/AAAA
- record (implicit MX). Mail is still deliverable, but publishing an explicit MX
- record is recommended.
-
- {/if}
-
-
- {/if}
-
-{/if}