diff --git a/api/schemas.yaml b/api/schemas.yaml index 042a3b3..53aa297 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -434,29 +434,6 @@ components: type: string description: Reverse DNS (PTR record) for the IP address example: "mail.example.com" - tls: - $ref: '#/components/schemas/TLSInfo' - description: TLS details of the connection for this hop, if encrypted - - TLSInfo: - type: object - properties: - version: - type: string - description: TLS protocol version - example: "TLSv1.3" - cipher: - type: string - description: Cipher suite used - example: "TLS_AES_256_GCM_SHA384" - bits: - type: integer - description: Cipher strength in bits - example: 256 - verified: - type: boolean - description: Whether the peer certificate was verified/trusted - example: true DKIMDomainInfo: type: object @@ -560,14 +537,6 @@ components: x_aligned_from: $ref: '#/components/schemas/AuthResult' description: X-Aligned-From authentication result (checks address alignment) - x_ptr: - $ref: '#/components/schemas/XPtrResult' - description: X-Ptr result (HELO hostname vs reverse DNS consistency check) - x_tls: - $ref: '#/components/schemas/AuthResult' - description: >- - Transport TLS encryption of the inbound connection (x-tls). - Synthesized from the inbound Received hop when no x-tls header is present. AuthResult: type: object @@ -637,29 +606,6 @@ components: description: Additional details about the IP reverse lookup example: "smtp.remote-ip=195.110.101.58 (authsmtp74.register.it)" - XPtrResult: - type: object - required: - - result - properties: - result: - type: string - enum: [pass, fail, none, temperror, permerror] - description: HELO/PTR consistency check result - example: "fail" - helo: - type: string - description: HELO/EHLO hostname announced by the sending server (smtp.helo) - example: "relay.example.org" - ptr: - type: string - description: Reverse DNS (PTR) hostname of the sender IP (policy.ptr) - example: "mail.example.com" - details: - type: string - description: Additional details about the x-ptr check - example: "smtp.helo=relay.example.org policy.ptr=mail.example.com" - SpamAssassinResult: type: object required: @@ -850,56 +796,12 @@ components: type: string description: A or AAAA records resolved from the PTR hostnames (forward confirmation) example: ["192.0.2.1", "2001:db8::1"] - helo_hostname: - type: string - description: HELO/EHLO hostname announced by the sending server (from the first Received hop) - example: "mail.example.com" - helo_ptr_match: - type: boolean - description: Whether the announced HELO hostname matches one of the sender's PTR records (case-insensitive) - return_ok: - $ref: '#/components/schemas/ReturnOK' errors: type: array items: type: string description: DNS lookup errors - ReturnOK: - type: object - description: Whether the sender domains can receive replies and bounces (MX, with A/AAAA fallback) - properties: - from: - $ref: '#/components/schemas/ReturnOKDomain' - return_path: - $ref: '#/components/schemas/ReturnOKDomain' - - ReturnOKDomain: - type: object - required: - - domain - - status - properties: - domain: - type: string - description: Domain that was evaluated - example: "example.com" - status: - type: string - enum: [pass, warn, fail] - x-go-type: string - description: pass = MX present, warn = only A/AAAA records (implicit MX), fail = no records - has_mx: - type: boolean - description: Whether the domain has at least one MX record - has_address: - type: boolean - description: Whether the domain has an A or AAAA record (implicit MX fallback) - org_domain: - type: string - description: Organizational domain used as fallback when the domain itself had no records - example: "example.com" - MXRecord: type: object required: diff --git a/docker/authentication_milter/authentication_milter.json b/docker/authentication_milter/authentication_milter.json index cd3bd03..5db3bbc 100644 --- a/docker/authentication_milter/authentication_milter.json +++ b/docker/authentication_milter/authentication_milter.json @@ -52,8 +52,6 @@ "PTR" : {}, - "TLS" : {}, - "SenderID" : { "hide_none" : 1 }, diff --git a/docker/postfix/main.cf b/docker/postfix/main.cf index 9f09396..5a73fb3 100644 --- a/docker/postfix/main.cf +++ b/docker/postfix/main.cf @@ -7,7 +7,7 @@ myhostname = __HOSTNAME__ mydomain = __DOMAIN__ myorigin = $mydomain inet_interfaces = all -inet_protocols = all +inet_protocols = ipv4 # Recipient settings mydestination = localhost.$mydomain, localhost @@ -36,8 +36,5 @@ smtpd_recipient_restrictions = permit_mynetworks, reject_unauth_destination -# TLS - record the negotiated cipher/protocol in the Received: header -smtpd_tls_received_header = yes - # Logging debug_peer_level = 2 diff --git a/docker/postfix/master.cf b/docker/postfix/master.cf index 822d56e..9c2ac57 100644 --- a/docker/postfix/master.cf +++ b/docker/postfix/master.cf @@ -3,9 +3,6 @@ # SMTP service smtp inet n - n - - smtpd -# TLS session cache and PRNG manager (required for STARTTLS) -tlsmgr unix - - n 1000? 1 tlsmgr - # Pickup service pickup unix n - n 60 1 pickup diff --git a/go.mod b/go.mod index 86246b6..a975215 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.140.0 + github.com/getkin/kin-openapi v0.138.0 github.com/gin-gonic/gin v1.12.0 github.com/google/uuid v1.6.0 github.com/oapi-codegen/runtime v1.4.0 - golang.org/x/net v0.56.0 + golang.org/x/net v0.54.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -36,7 +36,7 @@ require ( github.com/goccy/go-yaml v1.19.2 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.9.2 // indirect + github.com/jackc/pgx/v5 v5.8.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -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.1 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.7.0 // indirect github.com/oasdiff/yaml v0.0.9 // indirect github.com/oasdiff/yaml3 v0.0.12 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect @@ -69,10 +69,10 @@ require ( go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.52.0 // indirect + golang.org/x/crypto v0.51.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect + golang.org/x/sys v0.44.0 // indirect golang.org/x/text v0.37.0 // indirect golang.org/x/tools v0.44.0 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/go.sum b/go.sum index f467434..f4c8d28 100644 --- a/go.sum +++ b/go.sum @@ -93,8 +93,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= -github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= +github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -215,8 +215,8 @@ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= @@ -227,8 +227,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -249,8 +249,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= +golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index 666f9ee..bd8880d 100644 --- a/pkg/analyzer/authentication.go +++ b/pkg/analyzer/authentication.go @@ -140,20 +140,6 @@ func (a *AuthenticationAnalyzer) parseAuthenticationResultsHeader(header string, results.XAlignedFrom = a.parseXAlignedFromResult(part) } } - - // Parse x-ptr - if strings.HasPrefix(part, "x-ptr=") { - if results.XPtr == nil { - results.XPtr = a.parseXPtrResult(part) - } - } - - // Parse x-tls - if strings.HasPrefix(part, "x-tls=") { - if results.XTls == nil { - results.XTls = a.parseXTLSResult(part) - } - } } } @@ -190,9 +176,6 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *model.Aut // Penalty-only: X-Aligned-From (up to -5 points on failure) score += 5 * a.calculateXAlignedFromScore(results) / 100 - // Penalty-only: X-TLS / transport encryption (-10 points when not encrypted) - score += 10 * a.calculateXTLSScore(results) / 100 - // Ensure score doesn't exceed 100 if score > 100 { score = 100 diff --git a/pkg/analyzer/authentication_x_ptr.go b/pkg/analyzer/authentication_x_ptr.go deleted file mode 100644 index 93ecd03..0000000 --- a/pkg/analyzer/authentication_x_ptr.go +++ /dev/null @@ -1,61 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025-2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "regexp" - "strings" - - "git.happydns.org/happyDeliver/internal/model" - "git.happydns.org/happyDeliver/internal/utils" -) - -// parseXPtrResult parses the x-ptr result from Authentication-Results. -// Example: x-ptr=fail smtp.helo=relay.example.org policy.ptr=mail.example.com -func (a *AuthenticationAnalyzer) parseXPtrResult(part string) *model.XPtrResult { - result := &model.XPtrResult{} - - // Extract result (pass, fail, none, temperror, permerror) - re := regexp.MustCompile(`x-ptr=(\w+)`) - if matches := re.FindStringSubmatch(part); len(matches) > 1 { - resultStr := strings.ToLower(matches[1]) - result.Result = model.XPtrResultResult(resultStr) - } - - // Extract announced HELO hostname (smtp.helo) - heloRe := regexp.MustCompile(`smtp\.helo=([^\s;()]+)`) - if matches := heloRe.FindStringSubmatch(part); len(matches) > 1 { - helo := matches[1] - result.Helo = &helo - } - - // Extract reverse DNS hostname (policy.ptr) - ptrRe := regexp.MustCompile(`policy\.ptr=([^\s;()]+)`) - if matches := ptrRe.FindStringSubmatch(part); len(matches) > 1 { - ptr := matches[1] - result.Ptr = &ptr - } - - result.Details = utils.PtrTo(strings.TrimPrefix(part, "x-ptr=")) - - return result -} diff --git a/pkg/analyzer/authentication_x_ptr_test.go b/pkg/analyzer/authentication_x_ptr_test.go deleted file mode 100644 index 7015951..0000000 --- a/pkg/analyzer/authentication_x_ptr_test.go +++ /dev/null @@ -1,81 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025-2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "testing" - - "git.happydns.org/happyDeliver/internal/model" - "git.happydns.org/happyDeliver/internal/utils" -) - -func TestParseXPtrResult(t *testing.T) { - a := NewAuthenticationAnalyzer("receiver.com") - - tests := []struct { - name string - part string - expectedResult model.XPtrResultResult - expectedHelo *string - expectedPtr *string - }{ - { - name: "x-ptr fail with helo and ptr", - part: "x-ptr=fail smtp.helo=relay.example.org policy.ptr=mail.example.com", - expectedResult: model.XPtrResultResultFail, - expectedHelo: utils.PtrTo("relay.example.org"), - expectedPtr: utils.PtrTo("mail.example.com"), - }, - { - name: "x-ptr pass", - part: "x-ptr=pass smtp.helo=mail.example.com policy.ptr=mail.example.com", - expectedResult: model.XPtrResultResultPass, - expectedHelo: utils.PtrTo("mail.example.com"), - expectedPtr: utils.PtrTo("mail.example.com"), - }, - { - name: "x-ptr none without ptr", - part: "x-ptr=none smtp.helo=relay.example.org", - expectedResult: model.XPtrResultResultNone, - expectedHelo: utils.PtrTo("relay.example.org"), - expectedPtr: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := a.parseXPtrResult(tt.part) - if result == nil { - t.Fatal("expected non-nil result") - } - if result.Result != tt.expectedResult { - t.Errorf("Result = %q, want %q", result.Result, tt.expectedResult) - } - if !equalStrPtr(result.Helo, tt.expectedHelo) { - t.Errorf("Helo = %v, want %v", result.Helo, tt.expectedHelo) - } - if !equalStrPtr(result.Ptr, tt.expectedPtr) { - t.Errorf("Ptr = %v, want %v", result.Ptr, tt.expectedPtr) - } - }) - } -} diff --git a/pkg/analyzer/authentication_x_tls.go b/pkg/analyzer/authentication_x_tls.go deleted file mode 100644 index 440f806..0000000 --- a/pkg/analyzer/authentication_x_tls.go +++ /dev/null @@ -1,154 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "fmt" - "regexp" - "strings" - - "git.happydns.org/happyDeliver/internal/model" - "git.happydns.org/happyDeliver/internal/utils" -) - -// parseXTLSResult parses the x-tls result from Authentication-Results. -// Example: x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256 -func (a *AuthenticationAnalyzer) parseXTLSResult(part string) *model.AuthResult { - result := &model.AuthResult{} - - // Extract result (pass, fail, none, ...) - re := regexp.MustCompile(`x-tls=(\w+)`) - if matches := re.FindStringSubmatch(part); len(matches) > 1 { - result.Result = model.AuthResultResult(strings.ToLower(matches[1])) - } - - result.Details = utils.PtrTo(formatTLSDetails( - submatch(part, `smtp\.version=([^\s;()]+)`), - submatch(part, `smtp\.cipher=([^\s;()]+)`), - submatch(part, `smtp\.bits=(\d+)`), - )) - - return result -} - -// calculateXTLSScore returns a penalty for a negative transport-TLS result. -// pass (or absent) does not alter the score; any other result is penalized. -func (a *AuthenticationAnalyzer) calculateXTLSScore(results *model.AuthenticationResults) (score int) { - if results.XTls != nil { - switch results.XTls.Result { - case model.AuthResultResultPass: - // pass: don't alter the score - default: - return -100 - } - } - - return 0 -} - -// ReconcileXTLS fills in the x-tls result from the inbound connection's parsed TLS -// information when no x-tls Authentication-Results header was present. The inbound -// connection is the most recent hop (index 0) of the received chain. -func (a *AuthenticationAnalyzer) ReconcileXTLS(results *model.AuthenticationResults, chain *[]model.ReceivedHop) { - if results == nil || results.XTls != nil { - return - } - if chain == nil || len(*chain) == 0 { - return - } - - inbound := (*chain)[0] - switch { - case inbound.Tls != nil: - // Full TLS parenthetical present (smtpd_tls_received_header = yes). - var version, cipher, bits string - if inbound.Tls.Version != nil { - version = *inbound.Tls.Version - } - if inbound.Tls.Cipher != nil { - cipher = *inbound.Tls.Cipher - } - if inbound.Tls.Bits != nil { - bits = fmt.Sprintf("%d", *inbound.Tls.Bits) - } - results.XTls = &model.AuthResult{ - Result: model.AuthResultResultPass, - Details: utils.PtrTo(formatTLSDetails(version, cipher, bits)), - } - - case protocolIndicatesTLS(inbound.With): - // No TLS parenthetical (smtpd_tls_received_header may be disabled), but the - // transport keyword (ESMTPS, ESMTPSA, ...) tells us the session was encrypted. - // We just don't have the cipher details. - results.XTls = &model.AuthResult{ - Result: model.AuthResultResultPass, - Details: utils.PtrTo(fmt.Sprintf("Encrypted connection (%s); cipher details unavailable", *inbound.With)), - } - - case inbound.With != nil: - // A plaintext transport keyword (SMTP, ESMTP, ESMTPA, ...) is positive - // evidence the inbound connection was not encrypted. - results.XTls = &model.AuthResult{ - Result: model.AuthResultResultNone, - Details: utils.PtrTo(fmt.Sprintf("Inbound connection was not encrypted (%s)", *inbound.With)), - } - - default: - // Neither TLS details nor a transport keyword: we cannot tell whether the - // connection was encrypted. Leave x-tls unset rather than wrongly penalize. - } -} - -// protocolIndicatesTLS reports whether an SMTP "with" transport keyword denotes a -// TLS-encrypted session. Per RFC 3848 the keyword gains a trailing "S" when STARTTLS -// (or implicit TLS) was negotiated: ESMTPS, ESMTPSA, SMTPS, LMTPS, LMTPSA, UTF8SMTPS... -// The plaintext variants end in "P" (SMTP, ESMTP, LMTP) or "A" (ESMTPA, LMTPA). -func protocolIndicatesTLS(with *string) bool { - if with == nil { - return false - } - p := strings.ToUpper(strings.TrimSpace(*with)) - return strings.HasSuffix(p, "S") || strings.HasSuffix(p, "SA") -} - -// submatch returns the first capture group of pattern in s, or "". -func submatch(s, pattern string) string { - if matches := regexp.MustCompile(pattern).FindStringSubmatch(s); len(matches) > 1 { - return matches[1] - } - return "" -} - -// formatTLSDetails builds a human-readable summary of the TLS parameters. -func formatTLSDetails(version, cipher, bits string) string { - var parts []string - if version != "" { - parts = append(parts, version) - } - if cipher != "" { - parts = append(parts, "cipher "+cipher) - } - if bits != "" { - parts = append(parts, bits+" bits") - } - return strings.Join(parts, ", ") -} diff --git a/pkg/analyzer/authentication_x_tls_test.go b/pkg/analyzer/authentication_x_tls_test.go deleted file mode 100644 index 52a655c..0000000 --- a/pkg/analyzer/authentication_x_tls_test.go +++ /dev/null @@ -1,165 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "strings" - "testing" - - "git.happydns.org/happyDeliver/internal/model" - "git.happydns.org/happyDeliver/internal/utils" -) - -func TestParseXTLSResult(t *testing.T) { - analyzer := NewAuthenticationAnalyzer("") - - result := analyzer.parseXTLSResult("x-tls=pass smtp.version=TLSv1.3 smtp.cipher=TLS_AES_256_GCM_SHA384 smtp.bits=256") - - if result.Result != model.AuthResultResultPass { - t.Errorf("Result = %v, want pass", result.Result) - } - if result.Details == nil { - t.Fatal("Details should not be nil") - } - for _, want := range []string{"TLSv1.3", "TLS_AES_256_GCM_SHA384", "256 bits"} { - if !strings.Contains(*result.Details, want) { - t.Errorf("Details %q should contain %q", *result.Details, want) - } - } -} - -func TestCalculateXTLSScore(t *testing.T) { - analyzer := NewAuthenticationAnalyzer("") - - tests := []struct { - name string - xtls *model.AuthResult - score int - }{ - {"nil", nil, 0}, - {"pass", &model.AuthResult{Result: model.AuthResultResultPass}, 0}, - {"none", &model.AuthResult{Result: model.AuthResultResultNone}, -100}, - {"fail", &model.AuthResult{Result: model.AuthResultResultFail}, -100}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - results := &model.AuthenticationResults{XTls: tt.xtls} - if got := analyzer.calculateXTLSScore(results); got != tt.score { - t.Errorf("calculateXTLSScore = %d, want %d", got, tt.score) - } - }) - } -} - -func TestReconcileXTLS(t *testing.T) { - analyzer := NewAuthenticationAnalyzer("") - - t.Run("keeps existing x-tls header result", func(t *testing.T) { - existing := &model.AuthResult{Result: model.AuthResultResultFail} - results := &model.AuthenticationResults{XTls: existing} - chain := &[]model.ReceivedHop{{Tls: &model.TLSInfo{Version: utils.PtrTo("TLSv1.3")}}} - analyzer.ReconcileXTLS(results, chain) - if results.XTls != existing { - t.Error("existing XTls should be preserved") - } - }) - - t.Run("synthesizes pass from encrypted inbound hop", func(t *testing.T) { - results := &model.AuthenticationResults{} - chain := &[]model.ReceivedHop{{Tls: &model.TLSInfo{ - Version: utils.PtrTo("TLSv1.3"), - Cipher: utils.PtrTo("TLS_AES_256_GCM_SHA384"), - Bits: utils.PtrTo(256), - }}} - analyzer.ReconcileXTLS(results, chain) - if results.XTls == nil || results.XTls.Result != model.AuthResultResultPass { - t.Fatalf("expected synthesized pass, got %+v", results.XTls) - } - if results.XTls.Details == nil || !strings.Contains(*results.XTls.Details, "TLSv1.3") { - t.Errorf("details should mention TLS version, got %v", results.XTls.Details) - } - }) - - t.Run("synthesizes pass from ESMTPS protocol without TLS parenthetical", func(t *testing.T) { - // smtpd_tls_received_header disabled: no TLS details, but ESMTPS proves encryption. - results := &model.AuthenticationResults{} - chain := &[]model.ReceivedHop{{With: utils.PtrTo("ESMTPS")}} - analyzer.ReconcileXTLS(results, chain) - if results.XTls == nil || results.XTls.Result != model.AuthResultResultPass { - t.Fatalf("expected synthesized pass, got %+v", results.XTls) - } - }) - - t.Run("synthesizes none from plaintext ESMTP protocol", func(t *testing.T) { - results := &model.AuthenticationResults{} - chain := &[]model.ReceivedHop{{With: utils.PtrTo("ESMTP")}} - analyzer.ReconcileXTLS(results, chain) - if results.XTls == nil || results.XTls.Result != model.AuthResultResultNone { - t.Fatalf("expected synthesized none, got %+v", results.XTls) - } - }) - - t.Run("leaves nil when neither TLS info nor protocol is known", func(t *testing.T) { - results := &model.AuthenticationResults{} - chain := &[]model.ReceivedHop{{}} - analyzer.ReconcileXTLS(results, chain) - if results.XTls != nil { - t.Errorf("expected nil XTls when undetermined, got %+v", results.XTls) - } - }) - - t.Run("leaves nil with empty chain", func(t *testing.T) { - results := &model.AuthenticationResults{} - analyzer.ReconcileXTLS(results, &[]model.ReceivedHop{}) - if results.XTls != nil { - t.Errorf("expected nil XTls, got %+v", results.XTls) - } - }) -} - -func TestProtocolIndicatesTLS(t *testing.T) { - tests := []struct { - with string - want bool - }{ - {"ESMTPS", true}, - {"ESMTPSA", true}, - {"SMTPS", true}, - {"LMTPS", true}, - {"LMTPSA", true}, - {"SMTP", false}, - {"ESMTP", false}, - {"ESMTPA", false}, - {"LMTP", false}, - } - for _, tt := range tests { - t.Run(tt.with, func(t *testing.T) { - if got := protocolIndicatesTLS(utils.PtrTo(tt.with)); got != tt.want { - t.Errorf("protocolIndicatesTLS(%q) = %v, want %v", tt.with, got, tt.want) - } - }) - } - if protocolIndicatesTLS(nil) { - t.Error("protocolIndicatesTLS(nil) should be false") - } -} diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index 3e29a7a..06f8ddf 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -501,11 +501,6 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { return false } - // Replace email addresses with just their domain part to avoid false positives - // e.g. "john.doe@example.com" → "example.com" so local-part dots don't look like domains - emailAddrRegex := regexp.MustCompile(`(?i)[a-z0-9._%+\-]+@([a-z0-9.\-]+\.[a-z]{2,})`) - linkText = emailAddrRegex.ReplaceAllString(linkText, "$1") - // Common generic link texts that shouldn't trigger warnings genericTexts := []string{ "click here", "read more", "learn more", "download", "subscribe", diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 9927d1b..6bc7c39 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -88,16 +88,6 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head if len(forwardRecords) > 0 { results.PtrForwardRecords = &forwardRecords } - - // Record the announced HELO name and whether it matches the PTR record - if firstHop.From != nil && *firstHop.From != "" { - helo := *firstHop.From - results.HeloHostname = &helo - if len(ptrRecords) > 0 { - match := checkHeloPtrMatch(helo, ptrRecords) - results.HeloPtrMatch = &match - } - } } } @@ -110,15 +100,6 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *model.Head results.RpMxRecords = d.checkMXRecords(*results.RpDomain) } - // Verify the sender domains can actually receive replies/bounces (MX, with - // A/AAAA fallback), mirroring the ReturnOK milter check. - results.ReturnOk = &model.ReturnOK{ - From: d.checkReturnOKDomain(fromDomain, orgDomainOrEmpty(headersResults.DomainAlignment.FromOrgDomain)), - } - if results.RpDomain != nil && *results.RpDomain != "" { - results.ReturnOk.ReturnPath = d.checkReturnOKDomain(*results.RpDomain, orgDomainOrEmpty(headersResults.DomainAlignment.ReturnPathOrgDomain)) - } - // Check SPF records (for Return-Path domain - this is the envelope sender) // SPF validates the MAIL FROM command, which corresponds to Return-Path results.SpfRecords = d.checkSPFRecords(spfDomain) @@ -157,11 +138,6 @@ func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *model.DNSResults { // Check SPF records results.SpfRecords = d.checkSPFRecords(domain) - // Verify the domain can receive replies/bounces (MX, with A/AAAA fallback) - results.ReturnOk = &model.ReturnOK{ - From: d.checkReturnOKDomain(domain, ""), - } - // Check DMARC record results.DmarcRecord = d.checkDMARCRecord(domain) @@ -193,9 +169,6 @@ func (d *DNSAnalyzer) CalculateDomainOnlyScore(results *model.DNSResults) (int, // DMARC Record: 40 points score += 40 * d.calculateDMARCScore(results) / 100 - // Penalty when a sender domain cannot receive replies/bounces at all - score += calculateReturnOKPenalty(results) - // BIMI Record: only bonus if results.BimiRecord != nil && results.BimiRecord.Valid { if score >= 100 { @@ -241,9 +214,6 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *model.DNSResults, senderIP stri // DMARC Record: 20 points score += 20 * d.calculateDMARCScore(results) / 100 - // Penalty when a sender domain cannot receive replies/bounces at all - score += calculateReturnOKPenalty(results) - // BIMI Record // BIMI is optional but indicates advanced email branding if results.BimiRecord != nil && results.BimiRecord.Valid { diff --git a/pkg/analyzer/dns_bimi.go b/pkg/analyzer/dns_bimi.go index b037978..223bfdc 100644 --- a/pkg/analyzer/dns_bimi.go +++ b/pkg/analyzer/dns_bimi.go @@ -45,7 +45,7 @@ func (d *DNSAnalyzer) checkBIMIRecord(domain, selector string) *model.BIMIRecord Selector: selector, Domain: domain, Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup BIMI record: %s", formatDNSError(err))), + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup BIMI record: %v", err)), } } diff --git a/pkg/analyzer/dns_dkim.go b/pkg/analyzer/dns_dkim.go index 5708d1c..115e347 100644 --- a/pkg/analyzer/dns_dkim.go +++ b/pkg/analyzer/dns_dkim.go @@ -122,7 +122,7 @@ func (d *DNSAnalyzer) checkDKIMRecord(h DKIMHeader) *model.DKIMRecord { Domain: h.Domain, SigningAlgorithm: signingAlgorithmPtr(h.Algorithm), Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %s", formatDNSError(err))), + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DKIM record: %v", err)), } } diff --git a/pkg/analyzer/dns_dmarc.go b/pkg/analyzer/dns_dmarc.go index 20058b2..b89500b 100644 --- a/pkg/analyzer/dns_dmarc.go +++ b/pkg/analyzer/dns_dmarc.go @@ -193,7 +193,7 @@ func (d *DNSAnalyzer) checkDMARCRecord(domain string) *model.DMARCRecord { if err != nil { return &model.DMARCRecord{ Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %s", formatDNSError(err))), + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup DMARC record: %v", err)), } } if foundDomain == "" { diff --git a/pkg/analyzer/dns_fcr.go b/pkg/analyzer/dns_fcr.go index 2652b4c..07e5ab9 100644 --- a/pkg/analyzer/dns_fcr.go +++ b/pkg/analyzer/dns_fcr.go @@ -23,7 +23,6 @@ package analyzer import ( "context" - "strings" "git.happydns.org/happyDeliver/internal/model" ) @@ -63,21 +62,6 @@ func (d *DNSAnalyzer) checkPTRAndForward(ip string) ([]string, []string) { return ptrNames, forwardIPs } -// checkHeloPtrMatch reports whether the announced HELO hostname matches one of -// the sender's PTR records (case-insensitive, trailing dot ignored). -func checkHeloPtrMatch(helo string, ptrRecords []string) bool { - helo = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(helo)), ".") - if helo == "" { - return false - } - for _, ptr := range ptrRecords { - if strings.TrimSuffix(strings.ToLower(ptr), ".") == helo { - return true - } - } - return false -} - // Proper reverse DNS (PTR) and forward-confirmed reverse DNS (FCrDNS) is important for deliverability func (d *DNSAnalyzer) calculatePTRScore(results *model.DNSResults, senderIP string) (score int) { if results.PtrRecords != nil && len(*results.PtrRecords) > 0 { @@ -89,11 +73,6 @@ func (d *DNSAnalyzer) calculatePTRScore(results *model.DNSResults, senderIP stri score -= 15 } - // Penalty when the announced HELO name doesn't match the PTR hostname - if results.HeloPtrMatch != nil && !*results.HeloPtrMatch { - score -= 15 - } - // Additional 50 points for forward-confirmed reverse DNS (FCrDNS) // This means the PTR hostname resolves back to IPs that include the original sender IP if results.PtrForwardRecords != nil && len(*results.PtrForwardRecords) > 0 && senderIP != "" { diff --git a/pkg/analyzer/dns_fcr_test.go b/pkg/analyzer/dns_fcr_test.go deleted file mode 100644 index 2b9429b..0000000 --- a/pkg/analyzer/dns_fcr_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025-2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "testing" - - "git.happydns.org/happyDeliver/internal/model" -) - -func TestCheckHeloPtrMatch(t *testing.T) { - tests := []struct { - name string - helo string - ptrRecords []string - want bool - }{ - {"exact match", "mail.example.com", []string{"mail.example.com"}, true}, - {"case insensitive", "Mail.Example.COM", []string{"mail.example.com"}, true}, - {"trailing dot ignored", "mail.example.com.", []string{"mail.example.com"}, true}, - {"mismatch", "relay.example.org", []string{"mail.example.com"}, false}, - {"match among several", "smtp.example.com", []string{"mail.example.com", "smtp.example.com"}, true}, - {"empty helo", "", []string{"mail.example.com"}, false}, - {"no ptr records", "mail.example.com", nil, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := checkHeloPtrMatch(tt.helo, tt.ptrRecords); got != tt.want { - t.Errorf("checkHeloPtrMatch(%q, %v) = %v, want %v", tt.helo, tt.ptrRecords, got, tt.want) - } - }) - } -} - -func TestCalculatePTRScoreHeloMismatch(t *testing.T) { - d := NewDNSAnalyzer(0) - senderIP := "80.67.179.207" - ptr := []string{"mail.example.com"} - forward := []string{senderIP} - - matchTrue := true - matchFalse := false - - tests := []struct { - name string - results *model.DNSResults - want int - }{ - { - name: "helo matches ptr - no penalty (PTR+FCrDNS)", - results: &model.DNSResults{ - PtrRecords: &ptr, - PtrForwardRecords: &forward, - HeloPtrMatch: &matchTrue, - }, - want: 100, - }, - { - name: "helo mismatch - 15 point penalty", - results: &model.DNSResults{ - PtrRecords: &ptr, - PtrForwardRecords: &forward, - HeloPtrMatch: &matchFalse, - }, - want: 85, - }, - { - name: "no helo info - no penalty", - results: &model.DNSResults{ - PtrRecords: &ptr, - PtrForwardRecords: &forward, - }, - want: 100, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := d.calculatePTRScore(tt.results, senderIP); got != tt.want { - t.Errorf("calculatePTRScore() = %d, want %d", got, tt.want) - } - }) - } -} diff --git a/pkg/analyzer/dns_mx.go b/pkg/analyzer/dns_mx.go index 51c9eca..c48c9a4 100644 --- a/pkg/analyzer/dns_mx.go +++ b/pkg/analyzer/dns_mx.go @@ -39,7 +39,7 @@ func (d *DNSAnalyzer) checkMXRecords(domain string) *[]model.MXRecord { return &[]model.MXRecord{ { Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup MX records: %s", formatDNSError(err))), + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup MX records: %v", err)), }, } } diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go index 266078e..f60484f 100644 --- a/pkg/analyzer/dns_resolver.go +++ b/pkg/analyzer/dns_resolver.go @@ -23,22 +23,9 @@ package analyzer import ( "context" - "errors" "net" ) -// formatDNSError renders a resolution error without exposing the upstream -// resolver address that net.DNSError.Error() normally appends as " on ". -func formatDNSError(err error) string { - var dnsErr *net.DNSError - if errors.As(err, &dnsErr) { - sanitized := *dnsErr - sanitized.Server = "" - return sanitized.Error() - } - return err.Error() -} - // DNSResolver defines the interface for DNS resolution operations. // This interface abstracts DNS lookups to allow for custom implementations, // such as mock resolvers for testing or caching resolvers for performance. diff --git a/pkg/analyzer/dns_returnok.go b/pkg/analyzer/dns_returnok.go deleted file mode 100644 index 29e12b3..0000000 --- a/pkg/analyzer/dns_returnok.go +++ /dev/null @@ -1,113 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025-2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "context" - - "git.happydns.org/happyDeliver/internal/model" - "git.happydns.org/happyDeliver/internal/utils" -) - -// ReturnOKDomain.Status values, matching the schema enum. Kept as a plain string -// in the generated model (x-go-type) to avoid colliding with other "pass"/"fail" -// enums in the global enum namespace. -const ( - returnOKStatusPass = "pass" - returnOKStatusWarn = "warn" - returnOKStatusFail = "fail" -) - -// domainCanReceive reports whether a domain can accept mail, looking up records -// in the same order as Fastmail's ReturnOK milter: MX first, then A/AAAA. -func (d *DNSAnalyzer) domainCanReceive(domain string) (hasMX, hasAddress bool) { - ctx, cancel := context.WithTimeout(context.Background(), d.Timeout) - defer cancel() - - if mxRecords, err := d.resolver.LookupMX(ctx, domain); err == nil && len(mxRecords) > 0 { - return true, false - } - - if addrs, err := d.resolver.LookupHost(ctx, domain); err == nil && len(addrs) > 0 { - return false, true - } - - return false, false -} - -// checkReturnOKDomain verifies that a domain can receive replies/bounces. -// It checks the domain itself, then falls back to its organizational domain -// (when different) the same way the ReturnOK milter retries the org domain. -func (d *DNSAnalyzer) checkReturnOKDomain(domain, orgDomain string) *model.ReturnOKDomain { - if domain == "" { - return nil - } - - result := &model.ReturnOKDomain{Domain: domain} - - hasMX, hasAddress := d.domainCanReceive(domain) - - // Fall back to the organizational domain when the domain itself has nothing. - if !hasMX && !hasAddress && orgDomain != "" && orgDomain != domain { - if orgMX, orgAddr := d.domainCanReceive(orgDomain); orgMX || orgAddr { - hasMX, hasAddress = orgMX, orgAddr - result.OrgDomain = utils.PtrTo(orgDomain) - } - } - - result.HasMx = utils.PtrTo(hasMX) - result.HasAddress = utils.PtrTo(hasAddress) - - switch { - case hasMX: - result.Status = returnOKStatusPass - case hasAddress: - result.Status = returnOKStatusWarn - default: - result.Status = returnOKStatusFail - } - - return result -} - -// calculateReturnOKPenalty returns a non-positive value: each sender domain that -// can receive neither replies nor bounces (status=fail) costs points, since -// those messages would be silently lost. -func calculateReturnOKPenalty(results *model.DNSResults) (penalty int) { - if results.ReturnOk == nil { - return 0 - } - for _, dom := range []*model.ReturnOKDomain{results.ReturnOk.From, results.ReturnOk.ReturnPath} { - if dom != nil && dom.Status == returnOKStatusFail { - penalty -= 10 - } - } - return -} - -// orgDomainOrEmpty dereferences an optional organizational domain pointer. -func orgDomainOrEmpty(orgDomain *string) string { - if orgDomain == nil { - return "" - } - return *orgDomain -} diff --git a/pkg/analyzer/dns_returnok_test.go b/pkg/analyzer/dns_returnok_test.go deleted file mode 100644 index 55aaa5c..0000000 --- a/pkg/analyzer/dns_returnok_test.go +++ /dev/null @@ -1,170 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025-2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "context" - "net" - "testing" - "time" - - "git.happydns.org/happyDeliver/internal/model" -) - -// returnOKMockResolver lets tests control MX and host (A/AAAA) lookups per domain. -type returnOKMockResolver struct { - mx map[string][]*net.MX - hosts map[string][]string -} - -func (m *returnOKMockResolver) LookupMX(_ context.Context, name string) ([]*net.MX, error) { - if recs, ok := m.mx[name]; ok { - return recs, nil - } - return nil, &net.DNSError{Err: "no such host", Name: name, IsNotFound: true} -} - -func (m *returnOKMockResolver) LookupHost(_ context.Context, host string) ([]string, error) { - if recs, ok := m.hosts[host]; ok { - return recs, nil - } - return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true} -} - -func (m *returnOKMockResolver) LookupTXT(_ context.Context, _ string) ([]string, error) { - return nil, nil -} -func (m *returnOKMockResolver) LookupAddr(_ context.Context, _ string) ([]string, error) { - return nil, nil -} - -func TestCheckReturnOKDomain(t *testing.T) { - mx := []*net.MX{{Host: "mail.example.com.", Pref: 10}} - - tests := []struct { - name string - domain string - orgDomain string - resolver *returnOKMockResolver - wantStatus string - wantHasMX bool - wantHasAddr bool - wantOrgDomain string // "" means OrgDomain should be nil - }{ - { - name: "domain with MX passes", - domain: "example.com", - resolver: &returnOKMockResolver{mx: map[string][]*net.MX{"example.com": mx}}, - wantStatus: returnOKStatusPass, - wantHasMX: true, - wantHasAddr: false, - }, - { - name: "no MX but A/AAAA warns", - domain: "example.com", - resolver: &returnOKMockResolver{hosts: map[string][]string{"example.com": {"192.0.2.1"}}}, - wantStatus: returnOKStatusWarn, - wantHasMX: false, - wantHasAddr: true, - }, - { - name: "fallback to org domain MX", - domain: "sub.example.com", - orgDomain: "example.com", - resolver: &returnOKMockResolver{mx: map[string][]*net.MX{"example.com": mx}}, - wantStatus: returnOKStatusPass, - wantHasMX: true, - wantHasAddr: false, - wantOrgDomain: "example.com", - }, - { - name: "nothing anywhere fails", - domain: "example.com", - orgDomain: "example.com", - resolver: &returnOKMockResolver{}, - wantStatus: returnOKStatusFail, - wantHasMX: false, - wantHasAddr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - d := NewDNSAnalyzerWithResolver(5*time.Second, tt.resolver) - got := d.checkReturnOKDomain(tt.domain, tt.orgDomain) - if got == nil { - t.Fatalf("checkReturnOKDomain returned nil") - } - if got.Status != tt.wantStatus { - t.Errorf("Status = %q, want %q", got.Status, tt.wantStatus) - } - if got.HasMx == nil || *got.HasMx != tt.wantHasMX { - t.Errorf("HasMx = %v, want %v", got.HasMx, tt.wantHasMX) - } - if got.HasAddress == nil || *got.HasAddress != tt.wantHasAddr { - t.Errorf("HasAddress = %v, want %v", got.HasAddress, tt.wantHasAddr) - } - if tt.wantOrgDomain == "" { - if got.OrgDomain != nil { - t.Errorf("OrgDomain = %v, want nil", *got.OrgDomain) - } - } else { - if got.OrgDomain == nil || *got.OrgDomain != tt.wantOrgDomain { - t.Errorf("OrgDomain = %v, want %q", got.OrgDomain, tt.wantOrgDomain) - } - } - }) - } -} - -func TestCheckReturnOKDomainEmpty(t *testing.T) { - d := NewDNSAnalyzerWithResolver(5*time.Second, &returnOKMockResolver{}) - if got := d.checkReturnOKDomain("", ""); got != nil { - t.Errorf("checkReturnOKDomain(\"\") = %v, want nil", got) - } -} - -func TestCalculateReturnOKPenalty(t *testing.T) { - fail := &model.ReturnOKDomain{Domain: "a.example", Status: returnOKStatusFail} - pass := &model.ReturnOKDomain{Domain: "b.example", Status: returnOKStatusPass} - warn := &model.ReturnOKDomain{Domain: "c.example", Status: returnOKStatusWarn} - - tests := []struct { - name string - results *model.DNSResults - want int - }{ - {"nil return_ok", &model.DNSResults{}, 0}, - {"both pass", &model.DNSResults{ReturnOk: &model.ReturnOK{From: pass, ReturnPath: pass}}, 0}, - {"warn is not penalised", &model.DNSResults{ReturnOk: &model.ReturnOK{From: warn}}, 0}, - {"one fail", &model.DNSResults{ReturnOk: &model.ReturnOK{From: fail, ReturnPath: pass}}, -10}, - {"both fail", &model.DNSResults{ReturnOk: &model.ReturnOK{From: fail, ReturnPath: fail}}, -20}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := calculateReturnOKPenalty(tt.results); got != tt.want { - t.Errorf("calculateReturnOKPenalty() = %d, want %d", got, tt.want) - } - }) - } -} diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go index 5628986..ccb1674 100644 --- a/pkg/analyzer/dns_spf.go +++ b/pkg/analyzer/dns_spf.go @@ -67,7 +67,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, { Domain: &domain, Valid: false, - Error: utils.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %s", formatDNSError(err))), + Error: utils.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %v", err)), }, } } diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index 9e5853e..6d7b547 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -26,7 +26,6 @@ import ( "net" "net/mail" "regexp" - "strconv" "strings" "time" @@ -589,53 +588,9 @@ func (h *HeaderAnalyzer) findHeaderIssues(email *EmailMessage) []model.HeaderIss }) } - // Check for fake reply/forward: Subject has Re:/Fwd: prefix but no thread headers - subject := email.GetHeaderValue("Subject") - if h.hasReplyPrefix(subject) && !email.HasHeader("References") && !email.HasHeader("In-Reply-To") { - issues = append(issues, model.HeaderIssue{ - Header: "Subject", - Severity: model.HeaderIssueSeverityHigh, - Message: "Subject indicates a reply or forward but no References or In-Reply-To header is present", - Advice: utils.PtrTo("Remove the Re:/Fwd: prefix from the subject, or add References/In-Reply-To headers if this is a genuine reply"), - }) - } - return issues } -// hasReplyPrefix reports whether a subject line starts with a reply or forward prefix. -func (h *HeaderAnalyzer) hasReplyPrefix(subject string) bool { - // Normalize: collapse leading whitespace and make comparison case-insensitive - s := strings.ToLower(strings.TrimSpace(subject)) - - prefixes := []string{ - "re:", // English / universal - "fwd:", // English forward - "fw:", // English forward (short) - "aw:", // German Antwort - "wg:", // German Weitergeleitet - "sv:", // Scandinavian Svar - "vs:", // Finnish Vastaus / Norwegian - "ref:", // Some clients - "rép:", // French Réponse - "tr:", // French Transfert - "odp:", // Polish Odpowiedź - "ynt:", // Turkish Yanıt - "res:", // Portuguese/Spanish Resposta/Respuesta - "enc:", // Spanish Enviado/Reenviado - "vl:", // Dutch Verwijzing - "antw:", // Dutch Antwoord - "rv:", // Norwegian/Swedish - } - - for _, p := range prefixes { - if strings.HasPrefix(s, p) { - return true - } - } - return false -} - // parseReceivedChain extracts the chain of Received headers from an email func (h *HeaderAnalyzer) parseReceivedChain(email *EmailMessage) []model.ReceivedHop { if email == nil || email.Header == nil { @@ -738,50 +693,5 @@ func (h *HeaderAnalyzer) parseReceivedHeader(receivedValue string) *model.Receiv } } - // Extract TLS details from the Received header parentheticals - // (e.g. "(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) ...)") - hop.Tls = parseReceivedTLS(normalized) - return hop } - -// parseReceivedTLS extracts TLS connection details from a normalized Received header value. -// Returns nil when the hop was not encrypted (no TLS version/cipher found). -func parseReceivedTLS(normalized string) *model.TLSInfo { - tls := &model.TLSInfo{} - found := false - - // TLS protocol version, e.g. "using TLSv1.3" - if matches := regexp.MustCompile(`(?i)using\s+(TLSv[0-9.]+|SSLv[0-9.]+)`).FindStringSubmatch(normalized); len(matches) > 1 { - tls.Version = &matches[1] - found = true - } - - // Cipher suite, e.g. "with cipher TLS_AES_256_GCM_SHA384" - if matches := regexp.MustCompile(`(?i)with cipher\s+([A-Za-z0-9_-]+)`).FindStringSubmatch(normalized); len(matches) > 1 { - tls.Cipher = &matches[1] - found = true - } - - // Cipher strength, e.g. "(256/256 bits)" - if matches := regexp.MustCompile(`\((\d+)/\d+ bits\)`).FindStringSubmatch(normalized); len(matches) > 1 { - if bits, err := strconv.Atoi(matches[1]); err == nil { - tls.Bits = &bits - } - } - - if !found { - return nil - } - - // Certificate verification status. Postfix emits "(verified OK)" when the peer - // certificate was trusted, "(not verified)" otherwise. "No client certificate - // requested" leaves the field unset (trust is simply not applicable). - if regexp.MustCompile(`(?i)verified OK`).MatchString(normalized) { - tls.Verified = utils.PtrTo(true) - } else if regexp.MustCompile(`(?i)not verified`).MatchString(normalized) { - tls.Verified = utils.PtrTo(false) - } - - return tls -} diff --git a/pkg/analyzer/headers_test.go b/pkg/analyzer/headers_test.go index 8426c58..d7469d7 100644 --- a/pkg/analyzer/headers_test.go +++ b/pkg/analyzer/headers_test.go @@ -677,77 +677,6 @@ func TestParseReceivedHeader(t *testing.T) { } } -func TestParseReceivedTLS(t *testing.T) { - tests := []struct { - name string - receivedValue string - expectNil bool - expectVersion *string - expectCipher *string - expectBits *int - expectVerified *bool - }{ - { - name: "TLS 1.3 no client certificate", - receivedValue: "from mail.example.com (unknown [IPv6:2001:db8::1]) " + - "(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) " + - "key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) " + - "(No client certificate requested) " + - "by mx.example.org (Postfix) with ESMTPSA id 1EFD11611EA; Sun, 19 Oct 2025 09:40:33 +0000 (UTC)", - expectVersion: strPtr("TLSv1.3"), - expectCipher: strPtr("TLS_AES_256_GCM_SHA384"), - expectBits: intPtr(256), - expectVerified: nil, - }, - { - name: "TLS with verified client certificate", - receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) " + - "(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) " + - "(Client CN \"example\", Issuer \"CA\" (verified OK)) " + - "by mx.receiver.com (Postfix) with ESMTPS id ABC; Mon, 01 Jan 2024 12:00:00 +0000", - expectVersion: strPtr("TLSv1.2"), - expectCipher: strPtr("ECDHE-RSA-AES128-GCM-SHA256"), - expectBits: intPtr(128), - expectVerified: boolPtr(true), - }, - { - name: "Plaintext (no TLS)", - receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) by mx.receiver.com (Postfix) with ESMTP id ABC; Mon, 01 Jan 2024 12:00:00 +0000", - expectNil: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - normalized := strings.Join(strings.Fields(tt.receivedValue), " ") - tls := parseReceivedTLS(normalized) - - if tt.expectNil { - if tls != nil { - t.Fatalf("expected nil TLS info, got %+v", tls) - } - return - } - - if tls == nil { - t.Fatal("parseReceivedTLS returned nil") - } - if !equalStrPtr(tls.Version, tt.expectVersion) { - t.Errorf("Version = %v, want %v", ptrToStr(tls.Version), ptrToStr(tt.expectVersion)) - } - if !equalStrPtr(tls.Cipher, tt.expectCipher) { - t.Errorf("Cipher = %v, want %v", ptrToStr(tls.Cipher), ptrToStr(tt.expectCipher)) - } - if (tls.Bits == nil) != (tt.expectBits == nil) || (tls.Bits != nil && *tls.Bits != *tt.expectBits) { - t.Errorf("Bits = %v, want %v", tls.Bits, tt.expectBits) - } - if (tls.Verified == nil) != (tt.expectVerified == nil) || (tls.Verified != nil && *tls.Verified != *tt.expectVerified) { - t.Errorf("Verified = %v, want %v", tls.Verified, tt.expectVerified) - } - }) - } -} - func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) { analyzer := NewHeaderAnalyzer() @@ -974,155 +903,11 @@ func TestCheckHeader_DateValidation(t *testing.T) { } } -func TestHasReplyPrefix(t *testing.T) { - tests := []struct { - subject string - expected bool - }{ - // Positive cases - {"Re: Hello", true}, - {"RE: Hello", true}, - {"re: Hello", true}, - {"Fwd: Hello", true}, - {"FWD: Hello", true}, - {"fw: Hello", true}, - {"FW: Hello", true}, - {"Aw: Hallo", true}, - {"WG: Weitergeleitet", true}, - {"Sv: Hej", true}, - {"Vs: Vastaus", true}, - {"Ref: something", true}, - {"Rép: Bonjour", true}, - {"TR: Transféré", true}, - {"Odp: Odpowiedź", true}, - {"Ynt: Yanıt", true}, - {"Res: Resposta", true}, - {"Enc: Reenviado", true}, - {"Vl: Verwijzing", true}, - {"Antw: Antwoord", true}, - {"Rv: Svar", true}, - // Negative cases - {"Hello", false}, - {"", false}, - {"react: something", false}, - {"reference: check this", false}, - {"Resources available", false}, - {"Friendly reminder", false}, - } - - analyzer := NewHeaderAnalyzer() - - for _, tt := range tests { - t.Run(tt.subject, func(t *testing.T) { - result := analyzer.hasReplyPrefix(tt.subject) - if result != tt.expected { - t.Errorf("hasReplyPrefix(%q) = %v, want %v", tt.subject, result, tt.expected) - } - }) - } -} - -func TestFindHeaderIssues_FakeReply(t *testing.T) { - tests := []struct { - name string - headers map[string]string - expectIssueType string // non-empty means we expect an issue containing this substring - }{ - { - name: "Re: subject without thread headers", - headers: map[string]string{ - "From": "sender@example.com", - "Date": "Mon, 01 Jan 2024 12:00:00 +0000", - "Message-ID": "", - "Subject": "Re: Your invoice", - }, - expectIssueType: "References or In-Reply-To", - }, - { - name: "Fwd: subject without thread headers", - headers: map[string]string{ - "From": "sender@example.com", - "Date": "Mon, 01 Jan 2024 12:00:00 +0000", - "Message-ID": "", - "Subject": "Fwd: Important update", - }, - expectIssueType: "References or In-Reply-To", - }, - { - name: "Re: subject with References header - no issue", - headers: map[string]string{ - "From": "sender@example.com", - "Date": "Mon, 01 Jan 2024 12:00:00 +0000", - "Message-ID": "", - "Subject": "Re: Your invoice", - "References": "", - }, - expectIssueType: "", - }, - { - name: "Re: subject with In-Reply-To only - no issue", - headers: map[string]string{ - "From": "sender@example.com", - "Date": "Mon, 01 Jan 2024 12:00:00 +0000", - "Message-ID": "", - "Subject": "Re: Your invoice", - "In-Reply-To": "", - }, - expectIssueType: "", - }, - { - name: "Normal subject without thread headers - no issue", - headers: map[string]string{ - "From": "sender@example.com", - "Date": "Mon, 01 Jan 2024 12:00:00 +0000", - "Message-ID": "", - "Subject": "Your invoice", - }, - expectIssueType: "", - }, - } - - analyzer := NewHeaderAnalyzer() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - email := &EmailMessage{ - Header: createHeaderWithFields(tt.headers), - } - - issues := analyzer.findHeaderIssues(email) - - found := false - for _, issue := range issues { - if strings.Contains(issue.Message, tt.expectIssueType) { - found = true - break - } - } - - if tt.expectIssueType != "" && !found { - t.Errorf("expected issue containing %q, but none found (issues: %v)", tt.expectIssueType, issues) - } - if tt.expectIssueType == "" { - for _, issue := range issues { - if strings.Contains(issue.Message, "References or In-Reply-To") { - t.Errorf("unexpected fake-reply issue found: %s", issue.Message) - } - } - } - }) - } -} - // Helper functions for testing func strPtr(s string) *string { return &s } -func boolPtr(b bool) *bool { - return &b -} - func ptrToStr(p *string) string { if p == nil { return "" diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go index e20e571..26cd59d 100644 --- a/pkg/analyzer/report.go +++ b/pkg/analyzer/report.go @@ -85,10 +85,6 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults { // Run all analyzers results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email) results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication) - // Fall back to the received chain's inbound TLS when no x-tls header was present. - if results.Authentication != nil && results.Headers != nil { - r.authAnalyzer.ReconcileXTLS(results.Authentication, results.Headers.ReceivedChain) - } results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Headers) results.RBL = r.rblChecker.CheckEmail(email) results.DNSWL = r.dnswlChecker.CheckEmail(email) diff --git a/web/package-lock.json b/web/package-lock.json index 707d2d4..91b7bbf 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": "^4.0.0", + "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", "svelte-check": "^4.3.2", "typescript": "^6.0.0", @@ -204,9 +204,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "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==", + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -425,9 +425,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.133.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", - "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "version": "0.130.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", + "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", "dev": true, "license": "MIT", "funding": { @@ -453,9 +453,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -470,9 +470,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -487,9 +487,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", - "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "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==", "cpu": [ "x64" ], @@ -504,9 +504,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "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==", + "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==", "cpu": [ "x64" ], @@ -521,9 +521,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "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==", + "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==", "cpu": [ "arm" ], @@ -538,9 +538,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -558,9 +558,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -578,9 +578,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "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==", + "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==", "cpu": [ "ppc64" ], @@ -598,9 +598,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "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==", + "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==", "cpu": [ "s390x" ], @@ -618,9 +618,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "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==", + "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==", "cpu": [ "x64" ], @@ -638,9 +638,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "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==", + "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==", "cpu": [ "x64" ], @@ -658,9 +658,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -675,9 +675,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "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==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", + "integrity": "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ==", "cpu": [ "wasm32" ], @@ -694,9 +694,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "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==", + "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==", "cpu": [ "arm64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "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==", + "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==", "cpu": [ "x64" ], @@ -742,9 +742,9 @@ "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.10.tgz", - "integrity": "sha512-4WfKk68eTih+MiJD4fSbxN7E8kVBmTMPWHUPYjvl2N0rMs53YLTT8/YjKU5Dtnz5LqDjl7LEw4U7lXR2W3J5WA==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -762,16 +762,16 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.63.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.63.0.tgz", - "integrity": "sha512-1DrR7vQ9brXLrNE2sLtFXApwr7AUXPfpbIFYc+CQRf2+iURaZbXGU+7TG/RLr+9fdFkoRdyCAVUOHCChw11LFA==", + "version": "2.60.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.60.1.tgz", + "integrity": "sha512-mQjlkNo+rJvpln7V2IGY2j99BqhcFbS4UN0AQNKNYfhBAFZTuCDAdW3a1sgf330mvtNvsBXn3HpAhcmvdJTcIQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", - "@sveltejs/acorn-typescript": "^1.0.9", + "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", - "acorn": "^8.16.0", + "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", @@ -803,16 +803,6 @@ } } }, - "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", @@ -891,13 +881,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.13.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.1.tgz", - "integrity": "sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/trusted-types": { @@ -908,17 +898,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "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==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@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", + "@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", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -931,7 +921,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.60.1", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -947,16 +937,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.1.tgz", - "integrity": "sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@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", "debug": "^4.4.3" }, "engines": { @@ -972,14 +962,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.1.tgz", - "integrity": "sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.60.1", - "@typescript-eslint/types": "^8.60.1", + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "engines": { @@ -994,14 +984,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "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==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/visitor-keys": "8.60.1" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1012,9 +1002,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.1.tgz", - "integrity": "sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", "engines": { @@ -1029,15 +1019,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.1.tgz", - "integrity": "sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1", - "@typescript-eslint/utils": "8.60.1", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1054,9 +1044,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.1.tgz", - "integrity": "sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", "engines": { @@ -1068,16 +1058,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.1.tgz", - "integrity": "sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@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", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1096,9 +1086,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", - "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -1109,16 +1099,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.1.tgz", - "integrity": "sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.60.1", - "@typescript-eslint/types": "8.60.1", - "@typescript-eslint/typescript-estree": "8.60.1" + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1133,13 +1123,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.1.tgz", - "integrity": "sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.60.1", + "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -1717,9 +1707,9 @@ } }, "node_modules/eslint": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.1.tgz", - "integrity": "sha512-AyIKhnOBuOAdueD7RB3xB+YeAWScb9jHsJBgH2Hcde8InP5JYhqrRR6iTMHyTEwgENK54Cp44e4v8BwNhsuHuw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1728,7 +1718,7 @@ "@eslint/config-array": "^0.23.5", "@eslint/config-helpers": "^0.6.0", "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", + "@eslint/plugin-kit": "^0.7.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1789,9 +1779,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { @@ -1804,7 +1794,7 @@ "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", "semver": "^7.6.3", - "svelte-eslint-parser": "^1.7.0" + "svelte-eslint-parser": "^1.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1906,9 +1896,9 @@ } }, "node_modules/esrap": { - "version": "2.2.11", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.11.tgz", - "integrity": "sha512-gPdx+I+BjYEinNMQaBXFjbaJVyoPMU4ZODg5mE+M4DqVG9VusAVHHjcBX+zqyITlI0DIARwDMMzZwAWj36dRoQ==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", + "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", "dev": true, "license": "MIT", "dependencies": { @@ -2276,20 +2266,10 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "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" @@ -2802,18 +2782,15 @@ "license": "MIT" }, "node_modules/obug": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", - "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT", - "engines": { - "node": ">=12.20.0" - } + "license": "MIT" }, "node_modules/ohash": { "version": "2.0.11", @@ -2958,9 +2935,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", "dev": true, "funding": [ { @@ -2978,7 +2955,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -3121,17 +3098,14 @@ } }, "node_modules/prettier-plugin-svelte": { - "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==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.2.tgz", + "integrity": "sha512-ItFouLvzSFE3ulNl4DKoWM3BGcbDCNVpIyy/Y3F2gC3aNiGLxtFUdffVqO5Z5hhYG+DFT5KULWaxmeFFpdbvaQ==", "dev": true, "license": "MIT", - "engines": { - "node": ">=20" - }, "peerDependencies": { "prettier": "^3.0.0", - "svelte": "^5.0.0" + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "node_modules/punycode": { @@ -3170,13 +3144,13 @@ } }, "node_modules/rolldown": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", - "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", + "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.133.0", + "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -3186,21 +3160,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@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" + "@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" } }, "node_modules/run-applescript": { @@ -3329,15 +3303,15 @@ "license": "MIT" }, "node_modules/svelte": { - "version": "5.56.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.56.3.tgz", - "integrity": "sha512-w7JvrM5IFl5cmfbY0TLik9o7mjRUJmRMhOR51tBPu708Gr/MjbGs7VnJnr/B0CaXeI4vtnOh7RKxDr0cwhMdDA==", + "version": "5.55.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.7.tgz", + "integrity": "sha512-ymI5ykLPwIHW839E053FQbI1G+jnRFJEw3Kv5Y4njixVWywQBx+NUFpkkKyk5LIb36Fg9DVXSYpqiGekLD0hyw==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", - "@sveltejs/acorn-typescript": "^1.0.10", + "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", @@ -3346,7 +3320,7 @@ "clsx": "^2.1.1", "devalue": "^5.8.1", "esm-env": "^1.2.1", - "esrap": "^2.2.11", + "esrap": "^2.2.4", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -3357,14 +3331,13 @@ } }, "node_modules/svelte-check": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.6.0.tgz", - "integrity": "sha512-KhVnDFDSid57mmZtHz8gfW8AAGylOZ0vPnOIzVmAL+urzwK8sBYXRss953gD8T0OdgAQ11mdWhE6uadmtOz8TQ==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.8.tgz", + "integrity": "sha512-67adfgBox5eNSNIvIIwgFizKGdcRrGpiMoNO2obHcYuLz7iTa8Xgm/NGU3ntMFnNm8K1grFOIG6HhMLX/vcN8w==", "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", @@ -3382,9 +3355,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.8.0.tgz", - "integrity": "sha512-mikR1qwIVy3t5WthUoAXkMwxkXvabZP9FJgdx35Ei7EbGWmctva1Pih16Koeor/bdNNq8NXHlwKGS6NkYTawLg==", + "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==", "dev": true, "license": "MIT", "dependencies": { @@ -3398,7 +3371,7 @@ }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.34.1" + "pnpm": "10.33.0" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -3468,9 +3441,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -3478,9 +3451,9 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "license": "MIT", "dependencies": { @@ -3563,16 +3536,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.60.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.1.tgz", - "integrity": "sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@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" + "@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" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3601,9 +3574,9 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "dev": true, "license": "MIT" }, @@ -3625,17 +3598,17 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.16", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", - "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "version": "8.0.13", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", + "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", - "postcss": "^8.5.15", - "rolldown": "1.0.3", - "tinyglobby": "^0.2.17" + "postcss": "^8.5.14", + "rolldown": "1.0.1", + "tinyglobby": "^0.2.16" }, "bin": { "vite": "bin/vite.js" diff --git a/web/package.json b/web/package.json index b424e20..66b2c8c 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": "^4.0.0", + "prettier-plugin-svelte": "^3.4.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 749263d..46a4d2d 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -170,88 +170,6 @@ {/if} - - {#if authentication.x_ptr} -
-
- -
- HELO / PTR - - - {authentication.x_ptr.result} - - {#if authentication.x_ptr.helo} -
- Announced HELO: - {authentication.x_ptr.helo} -
- {/if} - {#if authentication.x_ptr.ptr} -
- Reverse DNS (PTR): - {authentication.x_ptr.ptr} -
- {/if} - {#if authentication.x_ptr.details} -
{authentication.x_ptr.details}
- {/if} -
-
-
- {/if} - - - {#if authentication.x_tls} -
-
- -
- Transport TLS - - - {authentication.x_tls.result} - - {#if authentication.x_tls.details} -
- {authentication.x_tls.details} -
- {/if} -
-
-
- {/if} -
diff --git a/web/src/lib/components/BimiRecordDisplay.svelte b/web/src/lib/components/BimiRecordDisplay.svelte index 8d21b1f..889e24f 100644 --- a/web/src/lib/components/BimiRecordDisplay.svelte +++ b/web/src/lib/components/BimiRecordDisplay.svelte @@ -72,26 +72,6 @@ {bimiRecord.error}
{/if} - {#if !bimiRecord.valid} -
-
- - Explicitly decline BIMI participation -
-

- If you do not intend to publish a brand logo, you can add a declination - record to signal that this domain deliberately opts out of BIMI. This - prevents mail clients from falling back to a parent-domain record: -

- {bimiRecord.selector}._bimi.{bimiRecord.domain}. IN TXT "v=BIMI1; l=; a=" -

- Declination record format as defined in § 4.3.1 of - draft-brand-indicators-for-message-identification. -

-
- {/if}
{/if} diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index eedd0db..6dabe0b 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -6,11 +6,9 @@ import DkimRecordsDisplay from "./DkimRecordsDisplay.svelte"; import DmarcRecordDisplay from "./DmarcRecordDisplay.svelte"; import GradeDisplay from "./GradeDisplay.svelte"; - import HeloPtrMatchDisplay from "./HeloPtrMatchDisplay.svelte"; import MxRecordsDisplay from "./MxRecordsDisplay.svelte"; import PtrForwardRecordsDisplay from "./PtrForwardRecordsDisplay.svelte"; import PtrRecordsDisplay from "./PtrRecordsDisplay.svelte"; - import ReturnOkDisplay from "./ReturnOkDisplay.svelte"; import SpfRecordsDisplay from "./SpfRecordsDisplay.svelte"; interface Props { @@ -94,16 +92,6 @@ {senderIp} /> - - - - - -
@@ -154,7 +142,8 @@ {#if dnsResults.rp_domain && dnsResults.rp_domain !== dnsResults.from_domain} - Differs from Return-Path domain + Differs from Return-Path + domain {/if} diff --git a/web/src/lib/components/EmailPathCard.svelte b/web/src/lib/components/EmailPathCard.svelte index 72cfd94..a4fda45 100644 --- a/web/src/lib/components/EmailPathCard.svelte +++ b/web/src/lib/components/EmailPathCard.svelte @@ -7,21 +7,6 @@ } let { receivedChain }: Props = $props(); - - // Mirror of the backend protocolIndicatesTLS (RFC 3848): the transport keyword - // gains a trailing "S" when TLS was used (ESMTPS, ESMTPSA, SMTPS, LMTPS, LMTPSA...). - function protocolIndicatesTLS(withProto: string | undefined | null): boolean { - if (!withProto) return false; - const p = withProto.trim().toUpperCase(); - return p.endsWith("S") || p.endsWith("SA"); - } - - // RFC 3848: a trailing "A" means the sender authenticated (SMTP AUTH): - // ESMTPA, ESMTPSA, LMTPA, LMTPSA... - function protocolIndicatesAuth(withProto: string | undefined | null): boolean { - if (!withProto) return false; - return withProto.trim().toUpperCase().endsWith("A"); - } {#if receivedChain && receivedChain.length > 0} @@ -75,63 +60,6 @@ {/if}

{/if} -

- {#if hop.tls} - - TLS - - {#if hop.tls.version} - - Version: - {hop.tls.version} - - {/if} - {#if hop.tls.cipher} - - Cipher: - {hop.tls.cipher} - - {/if} - {#if hop.tls.bits} - - Strength: - {hop.tls.bits} bits - - {/if} - {#if hop.tls.verified !== undefined} - - - {hop.tls.verified - ? "Certificate trusted" - : "Certificate not trusted"} - - {/if} - {:else if protocolIndicatesTLS(hop.with)} - - TLS - - {:else if hop.with} - - No TLS - - {:else} - - TLS unknown - - {/if} - {#if protocolIndicatesAuth(hop.with)} - - Authenticated - - {/if} -

{/each} diff --git a/web/src/lib/components/HeloPtrMatchDisplay.svelte b/web/src/lib/components/HeloPtrMatchDisplay.svelte deleted file mode 100644 index 1d8cee7..0000000 --- a/web/src/lib/components/HeloPtrMatchDisplay.svelte +++ /dev/null @@ -1,87 +0,0 @@ - - -{#if heloHostname} -
-
-
- - HELO / PTR Consistency -
- HELO -
-
-

- The HELO/EHLO hostname is the name the sending server announces when it connects. - Many mail servers check that this name matches the sender IP's reverse DNS (PTR) - record. A mismatch is a common spam signal and can hurt deliverability. -

-
- Announced HELO: {heloHostname} -
- {#if ptrRecords && ptrRecords.length > 0} -
- PTR Hostname(s): - {#each ptrRecords as ptr} -
- {#if normalize(heloHostname) === normalize(ptr)} - Match - {:else} - Different - {/if} - {ptr} -
- {/each} -
- {/if} -
- {#if !isMatch} -
-
-
- - Warning: The announced HELO hostname - {heloHostname} - {#if ptrRecords && ptrRecords.length > 0} - does not match the sender's PTR record{ptrRecords.length > 1 ? "s" : ""} - ({#each ptrRecords as ptr, i}{ptr}{i < - ptrRecords.length - 1 - ? ", " - : ""}{/each}). - {:else} - could not be matched against a PTR record. - {/if} - Configuring the HELO name to match reverse DNS improves deliverability. -
-
-
- {/if} -
-{/if} diff --git a/web/src/lib/components/ReturnOkDisplay.svelte b/web/src/lib/components/ReturnOkDisplay.svelte deleted file mode 100644 index 11d4c00..0000000 --- a/web/src/lib/components/ReturnOkDisplay.svelte +++ /dev/null @@ -1,106 +0,0 @@ - - -{#if rows.length > 0} -
-
-
- - Return Address Reachability -
- RETURN-OK -
-
-

- 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}