diff --git a/api/schemas.yaml b/api/schemas.yaml
index 53aa297..042a3b3 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -434,6 +434,29 @@ 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
@@ -537,6 +560,14 @@ 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
@@ -606,6 +637,29 @@ 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:
@@ -796,12 +850,56 @@ 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 5db3bbc..cd3bd03 100644
--- a/docker/authentication_milter/authentication_milter.json
+++ b/docker/authentication_milter/authentication_milter.json
@@ -52,6 +52,8 @@
"PTR" : {},
+ "TLS" : {},
+
"SenderID" : {
"hide_none" : 1
},
diff --git a/docker/postfix/main.cf b/docker/postfix/main.cf
index 5a73fb3..9f09396 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 = ipv4
+inet_protocols = all
# Recipient settings
mydestination = localhost.$mydomain, localhost
@@ -36,5 +36,8 @@ 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 9c2ac57..822d56e 100644
--- a/docker/postfix/master.cf
+++ b/docker/postfix/master.cf
@@ -3,6 +3,9 @@
# 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 0f23202..c553974 100644
--- a/go.mod
+++ b/go.mod
@@ -5,11 +5,11 @@ go 1.25.0
require (
github.com/JGLTechnologies/gin-rate-limit v1.5.8
github.com/emersion/go-smtp v0.24.0
- github.com/getkin/kin-openapi v0.138.0
+ github.com/getkin/kin-openapi v0.140.0
github.com/gin-gonic/gin v1.12.0
github.com/google/uuid v1.6.0
github.com/oapi-codegen/runtime v1.4.1
- golang.org/x/net v0.55.0
+ golang.org/x/net v0.56.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -50,7 +50,7 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
- github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 // indirect
+ github.com/oapi-codegen/oapi-codegen/v2 v2.7.1 // indirect
github.com/oasdiff/yaml v0.0.9 // indirect
github.com/oasdiff/yaml3 v0.0.12 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
@@ -69,7 +69,7 @@ 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.51.0 // indirect
+ golang.org/x/crypto v0.52.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
diff --git a/go.sum b/go.sum
index e90c11e..f467434 100644
--- a/go.sum
+++ b/go.sum
@@ -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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+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/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=
diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go
index bd8880d..666f9ee 100644
--- a/pkg/analyzer/authentication.go
+++ b/pkg/analyzer/authentication.go
@@ -140,6 +140,20 @@ 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)
+ }
+ }
}
}
@@ -176,6 +190,9 @@ 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
new file mode 100644
index 0000000..93ecd03
--- /dev/null
+++ b/pkg/analyzer/authentication_x_ptr.go
@@ -0,0 +1,61 @@
+// 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
new file mode 100644
index 0000000..7015951
--- /dev/null
+++ b/pkg/analyzer/authentication_x_ptr_test.go
@@ -0,0 +1,81 @@
+// 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
new file mode 100644
index 0000000..440f806
--- /dev/null
+++ b/pkg/analyzer/authentication_x_tls.go
@@ -0,0 +1,154 @@
+// 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
new file mode 100644
index 0000000..52a655c
--- /dev/null
+++ b/pkg/analyzer/authentication_x_tls_test.go
@@ -0,0 +1,165 @@
+// 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 06f8ddf..3e29a7a 100644
--- a/pkg/analyzer/content.go
+++ b/pkg/analyzer/content.go
@@ -501,6 +501,11 @@ 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 6bc7c39..9927d1b 100644
--- a/pkg/analyzer/dns.go
+++ b/pkg/analyzer/dns.go
@@ -88,6 +88,16 @@ 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
+ }
+ }
}
}
@@ -100,6 +110,15 @@ 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)
@@ -138,6 +157,11 @@ 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)
@@ -169,6 +193,9 @@ 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 {
@@ -214,6 +241,9 @@ 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_fcr.go b/pkg/analyzer/dns_fcr.go
index 07e5ab9..2652b4c 100644
--- a/pkg/analyzer/dns_fcr.go
+++ b/pkg/analyzer/dns_fcr.go
@@ -23,6 +23,7 @@ package analyzer
import (
"context"
+ "strings"
"git.happydns.org/happyDeliver/internal/model"
)
@@ -62,6 +63,21 @@ 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 {
@@ -73,6 +89,11 @@ 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
new file mode 100644
index 0000000..2b9429b
--- /dev/null
+++ b/pkg/analyzer/dns_fcr_test.go
@@ -0,0 +1,104 @@
+// 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_returnok.go b/pkg/analyzer/dns_returnok.go
new file mode 100644
index 0000000..29e12b3
--- /dev/null
+++ b/pkg/analyzer/dns_returnok.go
@@ -0,0 +1,113 @@
+// 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
new file mode 100644
index 0000000..55aaa5c
--- /dev/null
+++ b/pkg/analyzer/dns_returnok_test.go
@@ -0,0 +1,170 @@
+// 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/headers.go b/pkg/analyzer/headers.go
index 6d7b547..9e5853e 100644
--- a/pkg/analyzer/headers.go
+++ b/pkg/analyzer/headers.go
@@ -26,6 +26,7 @@ import (
"net"
"net/mail"
"regexp"
+ "strconv"
"strings"
"time"
@@ -588,9 +589,53 @@ 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 {
@@ -693,5 +738,50 @@ 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 d7469d7..8426c58 100644
--- a/pkg/analyzer/headers_test.go
+++ b/pkg/analyzer/headers_test.go
@@ -677,6 +677,77 @@ 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()
@@ -903,11 +974,155 @@ 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 26cd59d..e20e571 100644
--- a/pkg/analyzer/report.go
+++ b/pkg/analyzer/report.go
@@ -85,6 +85,10 @@ 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..7d3b2b0 100644
--- a/web/package-lock.json
+++ b/web/package-lock.json
@@ -25,7 +25,7 @@
"eslint-plugin-svelte": "^3.12.4",
"globals": "^17.0.0",
"prettier": "^3.6.2",
- "prettier-plugin-svelte": "^3.4.0",
+ "prettier-plugin-svelte": "^4.0.0",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"typescript": "^6.0.0",
@@ -646,9 +646,9 @@
}
},
"node_modules/@eslint/plugin-kit": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
- "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
+ "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -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.133.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
"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.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
"cpu": [
"x64"
],
@@ -1177,9 +1177,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz",
- "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz",
+ "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==",
"cpu": [
"arm"
],
@@ -1191,9 +1191,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz",
- "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz",
+ "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==",
"cpu": [
"arm64"
],
@@ -1205,9 +1205,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz",
- "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz",
+ "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==",
"cpu": [
"arm64"
],
@@ -1219,9 +1219,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz",
- "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz",
+ "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==",
"cpu": [
"x64"
],
@@ -1233,9 +1233,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz",
- "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz",
+ "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==",
"cpu": [
"arm64"
],
@@ -1247,9 +1247,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz",
- "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz",
+ "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==",
"cpu": [
"x64"
],
@@ -1261,9 +1261,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz",
- "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz",
+ "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==",
"cpu": [
"arm"
],
@@ -1278,9 +1278,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz",
- "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz",
+ "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==",
"cpu": [
"arm"
],
@@ -1295,9 +1295,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz",
- "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz",
+ "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==",
"cpu": [
"arm64"
],
@@ -1312,9 +1312,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz",
- "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz",
+ "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==",
"cpu": [
"arm64"
],
@@ -1329,9 +1329,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz",
- "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz",
+ "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==",
"cpu": [
"loong64"
],
@@ -1346,9 +1346,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz",
- "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz",
+ "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==",
"cpu": [
"loong64"
],
@@ -1363,9 +1363,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz",
- "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz",
+ "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==",
"cpu": [
"ppc64"
],
@@ -1380,9 +1380,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz",
- "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz",
+ "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==",
"cpu": [
"ppc64"
],
@@ -1397,9 +1397,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz",
- "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz",
+ "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==",
"cpu": [
"riscv64"
],
@@ -1414,9 +1414,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz",
- "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz",
+ "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==",
"cpu": [
"riscv64"
],
@@ -1431,9 +1431,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz",
- "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz",
+ "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==",
"cpu": [
"s390x"
],
@@ -1448,9 +1448,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==",
"cpu": [
"x64"
],
@@ -1465,9 +1465,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz",
- "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz",
+ "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==",
"cpu": [
"x64"
],
@@ -1482,9 +1482,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz",
- "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz",
+ "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==",
"cpu": [
"x64"
],
@@ -1496,9 +1496,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz",
- "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz",
+ "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==",
"cpu": [
"arm64"
],
@@ -1510,9 +1510,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz",
- "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz",
+ "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==",
"cpu": [
"arm64"
],
@@ -1524,9 +1524,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz",
- "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz",
+ "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==",
"cpu": [
"ia32"
],
@@ -1538,9 +1538,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz",
- "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz",
+ "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==",
"cpu": [
"x64"
],
@@ -1552,9 +1552,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz",
- "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz",
+ "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==",
"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.63.0",
+ "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.63.0.tgz",
+ "integrity": "sha512-1DrR7vQ9brXLrNE2sLtFXApwr7AUXPfpbIFYc+CQRf2+iURaZbXGU+7TG/RLr+9fdFkoRdyCAVUOHCChw11LFA==",
"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",
@@ -1634,6 +1634,16 @@
}
}
},
+ "node_modules/@sveltejs/load-config": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.1.1.tgz",
+ "integrity": "sha512-BXXm+VOH/9X4N7Dd1iZ2MqA1h7M+9i2noI8QYuLDY8QcN2WHYn7D/VK/+IJNfcAmRw7ACNJ538UT9GXIhnBTiA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18.0.0"
+ }
+ },
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz",
@@ -1712,13 +1722,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "24.12.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
- "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
+ "version": "24.13.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz",
+ "integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.16.0"
+ "undici-types": "~7.18.0"
}
},
"node_modules/@types/trusted-types": {
@@ -1729,17 +1739,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.1.tgz",
+ "integrity": "sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==",
"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.1",
+ "@typescript-eslint/type-utils": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
"ignore": "^7.0.5",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.5.0"
@@ -1752,7 +1762,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
- "@typescript-eslint/parser": "^8.59.3",
+ "@typescript-eslint/parser": "^8.60.1",
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
"typescript": ">=4.8.4 <6.1.0"
}
@@ -1768,16 +1778,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz",
+ "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==",
"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.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1793,14 +1803,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz",
+ "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.59.3",
- "@typescript-eslint/types": "^8.59.3",
+ "@typescript-eslint/tsconfig-utils": "^8.60.1",
+ "@typescript-eslint/types": "^8.60.1",
"debug": "^4.4.3"
},
"engines": {
@@ -1815,14 +1825,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.1.tgz",
+ "integrity": "sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
- "@typescript-eslint/visitor-keys": "8.59.3"
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1833,9 +1843,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz",
+ "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1850,15 +1860,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz",
+ "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==",
"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.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1",
"debug": "^4.4.3",
"ts-api-utils": "^2.5.0"
},
@@ -1875,9 +1885,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz",
+ "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1889,16 +1899,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz",
+ "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==",
"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.1",
+ "@typescript-eslint/tsconfig-utils": "8.60.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/visitor-keys": "8.60.1",
"debug": "^4.4.3",
"minimatch": "^10.2.2",
"semver": "^7.7.3",
@@ -1917,9 +1927,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.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz",
+ "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==",
"dev": true,
"license": "ISC",
"bin": {
@@ -1930,16 +1940,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz",
+ "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==",
"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.1",
+ "@typescript-eslint/types": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1954,13 +1964,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.1",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz",
+ "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.59.3",
+ "@typescript-eslint/types": "8.60.1",
"eslint-visitor-keys": "^5.0.0"
},
"engines": {
@@ -1972,15 +1982,15 @@
}
},
"node_modules/@vitest/expect": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
- "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz",
+ "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
"chai": "^5.2.0",
"tinyrainbow": "^2.0.0"
},
@@ -1989,9 +1999,9 @@
}
},
"node_modules/@vitest/pretty-format": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
- "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz",
+ "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2002,13 +2012,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
- "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz",
+ "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "3.2.4",
+ "@vitest/utils": "3.2.6",
"pathe": "^2.0.3",
"strip-literal": "^3.0.0"
},
@@ -2017,13 +2027,13 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
- "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz",
+ "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
+ "@vitest/pretty-format": "3.2.6",
"magic-string": "^0.30.17",
"pathe": "^2.0.3"
},
@@ -2032,9 +2042,9 @@
}
},
"node_modules/@vitest/spy": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
- "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz",
+ "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2045,13 +2055,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
- "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz",
+ "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "3.2.4",
+ "@vitest/pretty-format": "3.2.6",
"loupe": "^3.1.4",
"tinyrainbow": "^2.0.0"
},
@@ -2592,9 +2602,9 @@
}
},
"node_modules/eslint": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz",
- "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==",
+ "version": "10.4.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz",
+ "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2603,7 +2613,7 @@
"@eslint/config-array": "^0.23.5",
"@eslint/config-helpers": "^0.6.0",
"@eslint/core": "^1.2.1",
- "@eslint/plugin-kit": "^0.7.1",
+ "@eslint/plugin-kit": "^0.7.2",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
@@ -2664,9 +2674,9 @@
}
},
"node_modules/eslint-plugin-svelte": {
- "version": "3.17.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.17.1.tgz",
- "integrity": "sha512-NyiXHtS3Ni7e532RBwS9OXlMKDIrENg3gY+/+ODjZzQx2xhU3NlJ+nIl1a93iUUQeiJL3lS8KLmY+W8hklzweQ==",
+ "version": "3.19.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.19.0.tgz",
+ "integrity": "sha512-t3rNaZeXz4d2gG4uJyMEYfJCFKf22+SWbSizIIXIWKu4wM+XPLiMWuSSr/C5821JmFeN9ogK+eExbG+z+twyxw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2679,7 +2689,7 @@
"postcss-load-config": "^3.1.4",
"postcss-safe-parser": "^7.0.0",
"semver": "^7.6.3",
- "svelte-eslint-parser": "^1.4.0"
+ "svelte-eslint-parser": "^1.7.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2781,9 +2791,9 @@
}
},
"node_modules/esrap": {
- "version": "2.2.9",
- "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz",
- "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==",
+ "version": "2.2.11",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz",
+ "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3158,10 +3168,20 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
- "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
+ "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
"dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/nodeca"
+ }
+ ],
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -3681,15 +3701,18 @@
"license": "MIT"
},
"node_modules/obug": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
- "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz",
+ "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==",
"dev": true,
"funding": [
"https://github.com/sponsors/sxzz",
"https://opencollective.com/debug"
],
- "license": "MIT"
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ }
},
"node_modules/ohash": {
"version": "2.0.11",
@@ -3844,9 +3867,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 +3887,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -4007,14 +4030,17 @@
}
},
"node_modules/prettier-plugin-svelte": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz",
- "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==",
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-4.1.0.tgz",
+ "integrity": "sha512-YZkhA2Q9oOerFFG9tq+2f98WYT7Z2JgrybJrAyrB78jpsH9i/DdgplXemehuFPgsldetFNCcR/yCcYlDjPy94Q==",
"dev": true,
"license": "MIT",
+ "engines": {
+ "node": ">=20"
+ },
"peerDependencies": {
"prettier": "^3.0.0",
- "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0"
+ "svelte": "^5.0.0"
}
},
"node_modules/punycode": {
@@ -4053,13 +4079,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.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.130.0",
+ "@oxc-project/types": "=0.133.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -4069,31 +4095,31 @@
"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.3",
+ "@rolldown/binding-darwin-arm64": "1.0.3",
+ "@rolldown/binding-darwin-x64": "1.0.3",
+ "@rolldown/binding-freebsd-x64": "1.0.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
}
},
"node_modules/rollup": {
- "version": "4.60.4",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz",
- "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==",
+ "version": "4.61.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz",
+ "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@types/estree": "1.0.8"
+ "@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -4103,41 +4129,34 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.60.4",
- "@rollup/rollup-android-arm64": "4.60.4",
- "@rollup/rollup-darwin-arm64": "4.60.4",
- "@rollup/rollup-darwin-x64": "4.60.4",
- "@rollup/rollup-freebsd-arm64": "4.60.4",
- "@rollup/rollup-freebsd-x64": "4.60.4",
- "@rollup/rollup-linux-arm-gnueabihf": "4.60.4",
- "@rollup/rollup-linux-arm-musleabihf": "4.60.4",
- "@rollup/rollup-linux-arm64-gnu": "4.60.4",
- "@rollup/rollup-linux-arm64-musl": "4.60.4",
- "@rollup/rollup-linux-loong64-gnu": "4.60.4",
- "@rollup/rollup-linux-loong64-musl": "4.60.4",
- "@rollup/rollup-linux-ppc64-gnu": "4.60.4",
- "@rollup/rollup-linux-ppc64-musl": "4.60.4",
- "@rollup/rollup-linux-riscv64-gnu": "4.60.4",
- "@rollup/rollup-linux-riscv64-musl": "4.60.4",
- "@rollup/rollup-linux-s390x-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-gnu": "4.60.4",
- "@rollup/rollup-linux-x64-musl": "4.60.4",
- "@rollup/rollup-openbsd-x64": "4.60.4",
- "@rollup/rollup-openharmony-arm64": "4.60.4",
- "@rollup/rollup-win32-arm64-msvc": "4.60.4",
- "@rollup/rollup-win32-ia32-msvc": "4.60.4",
- "@rollup/rollup-win32-x64-gnu": "4.60.4",
- "@rollup/rollup-win32-x64-msvc": "4.60.4",
+ "@rollup/rollup-android-arm-eabi": "4.61.1",
+ "@rollup/rollup-android-arm64": "4.61.1",
+ "@rollup/rollup-darwin-arm64": "4.61.1",
+ "@rollup/rollup-darwin-x64": "4.61.1",
+ "@rollup/rollup-freebsd-arm64": "4.61.1",
+ "@rollup/rollup-freebsd-x64": "4.61.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.61.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.61.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.61.1",
+ "@rollup/rollup-linux-arm64-musl": "4.61.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.61.1",
+ "@rollup/rollup-linux-loong64-musl": "4.61.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.61.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.61.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.61.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.61.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-gnu": "4.61.1",
+ "@rollup/rollup-linux-x64-musl": "4.61.1",
+ "@rollup/rollup-openbsd-x64": "4.61.1",
+ "@rollup/rollup-openharmony-arm64": "4.61.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.61.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.61.1",
+ "@rollup/rollup-win32-x64-gnu": "4.61.1",
+ "@rollup/rollup-win32-x64-msvc": "4.61.1",
"fsevents": "~2.3.2"
}
},
- "node_modules/rollup/node_modules/@types/estree": {
- "version": "1.0.8",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
- "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/run-applescript": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
@@ -4277,15 +4296,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.56.3",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz",
+ "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==",
"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 +4313,7 @@
"clsx": "^2.1.1",
"devalue": "^5.8.1",
"esm-env": "^1.2.1",
- "esrap": "^2.2.4",
+ "esrap": "^2.2.11",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@@ -4305,13 +4324,14 @@
}
},
"node_modules/svelte-check": {
- "version": "4.4.8",
- "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz",
- "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==",
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz",
+ "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
+ "@sveltejs/load-config": "0.1.1",
"chokidar": "^4.0.1",
"fdir": "^6.2.0",
"picocolors": "^1.0.0",
@@ -4329,9 +4349,9 @@
}
},
"node_modules/svelte-eslint-parser": {
- "version": "1.6.1",
- "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.1.tgz",
- "integrity": "sha512-hhvSH6kRj46UzrBVO5TaotD+Iuvruj5ccKBcO4wAhVcPTLmIc/c32D8UllBTYO0on4LzYuM0rNzf1lM/gBlkSQ==",
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.8.0.tgz",
+ "integrity": "sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4345,7 +4365,7 @@
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0",
- "pnpm": "10.33.0"
+ "pnpm": "10.34.1"
},
"funding": {
"url": "https://github.com/sponsors/ota-meshi"
@@ -4415,9 +4435,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.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
+ "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -4425,9 +4445,9 @@
}
},
"node_modules/tinyglobby": {
- "version": "0.2.16",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
- "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4530,16 +4550,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.1",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz",
+ "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==",
"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.1",
+ "@typescript-eslint/parser": "8.60.1",
+ "@typescript-eslint/typescript-estree": "8.60.1",
+ "@typescript-eslint/utils": "8.60.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -4568,9 +4588,9 @@
}
},
"node_modules/undici-types": {
- "version": "7.16.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "version": "7.18.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
+ "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
"dev": true,
"license": "MIT"
},
@@ -4592,17 +4612,17 @@
"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.16",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
- "postcss": "^8.5.14",
- "rolldown": "1.0.1",
- "tinyglobby": "^0.2.16"
+ "postcss": "^8.5.15",
+ "rolldown": "1.0.3",
+ "tinyglobby": "^0.2.17"
},
"bin": {
"vite": "bin/vite.js"
@@ -4693,9 +4713,9 @@
}
},
"node_modules/vite-node/node_modules/vite": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
- "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz",
+ "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4788,20 +4808,20 @@
}
},
"node_modules/vitest": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
- "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz",
+ "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/chai": "^5.2.2",
- "@vitest/expect": "3.2.4",
- "@vitest/mocker": "3.2.4",
- "@vitest/pretty-format": "^3.2.4",
- "@vitest/runner": "3.2.4",
- "@vitest/snapshot": "3.2.4",
- "@vitest/spy": "3.2.4",
- "@vitest/utils": "3.2.4",
+ "@vitest/expect": "3.2.6",
+ "@vitest/mocker": "3.2.6",
+ "@vitest/pretty-format": "^3.2.6",
+ "@vitest/runner": "3.2.6",
+ "@vitest/snapshot": "3.2.6",
+ "@vitest/spy": "3.2.6",
+ "@vitest/utils": "3.2.6",
"chai": "^5.2.0",
"debug": "^4.4.1",
"expect-type": "^1.2.1",
@@ -4831,8 +4851,8 @@
"@edge-runtime/vm": "*",
"@types/debug": "^4.1.12",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
- "@vitest/browser": "3.2.4",
- "@vitest/ui": "3.2.4",
+ "@vitest/browser": "3.2.6",
+ "@vitest/ui": "3.2.6",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4861,13 +4881,13 @@
}
},
"node_modules/vitest/node_modules/@vitest/mocker": {
- "version": "3.2.4",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
- "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz",
+ "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "3.2.4",
+ "@vitest/spy": "3.2.6",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.17"
},
@@ -4895,9 +4915,9 @@
"license": "MIT"
},
"node_modules/vitest/node_modules/vite": {
- "version": "7.3.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz",
- "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==",
+ "version": "7.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz",
+ "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5035,22 +5055,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/yaml": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
- "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
- "extraneous": true,
- "license": "ISC",
- "bin": {
- "yaml": "bin.mjs"
- },
- "engines": {
- "node": ">= 14.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/eemeli"
- }
- },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
diff --git a/web/package.json b/web/package.json
index 90b545e..2eeaa52 100644
--- a/web/package.json
+++ b/web/package.json
@@ -28,7 +28,7 @@
"eslint-plugin-svelte": "^3.12.4",
"globals": "^17.0.0",
"prettier": "^3.6.2",
- "prettier-plugin-svelte": "^3.4.0",
+ "prettier-plugin-svelte": "^4.0.0",
"svelte": "^5.39.5",
"svelte-check": "^4.3.2",
"typescript": "^6.0.0",
diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte
index 46a4d2d..749263d 100644
--- a/web/src/lib/components/AuthenticationCard.svelte
+++ b/web/src/lib/components/AuthenticationCard.svelte
@@ -170,6 +170,88 @@
{/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 889e24f..8d21b1f 100644
--- a/web/src/lib/components/BimiRecordDisplay.svelte
+++ b/web/src/lib/components/BimiRecordDisplay.svelte
@@ -72,6 +72,26 @@
{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 6dabe0b..eedd0db 100644
--- a/web/src/lib/components/DnsRecordsCard.svelte
+++ b/web/src/lib/components/DnsRecordsCard.svelte
@@ -6,9 +6,11 @@
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 {
@@ -92,6 +94,16 @@
{senderIp}
/>
+
+
+
+
+
+
@@ -142,8 +154,7 @@
{#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 a4fda45..72cfd94 100644
--- a/web/src/lib/components/EmailPathCard.svelte
+++ b/web/src/lib/components/EmailPathCard.svelte
@@ -7,6 +7,21 @@
}
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}
@@ -60,6 +75,63 @@
{/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
new file mode 100644
index 0000000..1d8cee7
--- /dev/null
+++ b/web/src/lib/components/HeloPtrMatchDisplay.svelte
@@ -0,0 +1,87 @@
+
+
+{#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
new file mode 100644
index 0000000..11d4c00
--- /dev/null
+++ b/web/src/lib/components/ReturnOkDisplay.svelte
@@ -0,0 +1,106 @@
+
+
+{#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}