tls: surface transport TLS status in email path and authentication
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Parse TLS details (version, cipher, bits, cert verification) from the Postfix Received header parenthetical and expose them per hop, rendered as a per-hop badge in the Email Path card. Add an x-tls Authentication-Results result: parse it when present, and otherwise synthesize it from the inbound hop's TLS info. A negative result (unencrypted inbound connection) applies a -10 authentication score penalty and is shown in the Authentication card. Enable the TLS handler in authentication_milter. Closes: #40
This commit is contained in:
parent
8e7e56851b
commit
d53c1b1e00
11 changed files with 593 additions and 0 deletions
|
|
@ -147,6 +147,13 @@ func (a *AuthenticationAnalyzer) parseAuthenticationResultsHeader(header string,
|
|||
results.XPtr = a.parseXPtrResult(part)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse x-tls
|
||||
if strings.HasPrefix(part, "x-tls=") {
|
||||
if results.XTls == nil {
|
||||
results.XTls = a.parseXTLSResult(part)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,6 +190,9 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *model.Aut
|
|||
// Penalty-only: X-Aligned-From (up to -5 points on failure)
|
||||
score += 5 * a.calculateXAlignedFromScore(results) / 100
|
||||
|
||||
// Penalty-only: X-TLS / transport encryption (-10 points when not encrypted)
|
||||
score += 10 * a.calculateXTLSScore(results) / 100
|
||||
|
||||
// Ensure score doesn't exceed 100
|
||||
if score > 100 {
|
||||
score = 100
|
||||
|
|
|
|||
154
pkg/analyzer/authentication_x_tls.go
Normal file
154
pkg/analyzer/authentication_x_tls.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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, ", ")
|
||||
}
|
||||
165
pkg/analyzer/authentication_x_tls_test.go
Normal file
165
pkg/analyzer/authentication_x_tls_test.go
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
// This file is part of the happyDeliver (R) project.
|
||||
// Copyright (c) 2025 happyDomain
|
||||
// Authors: Pierre-Olivier Mercier, et al.
|
||||
//
|
||||
// This program is offered under a commercial and under the AGPL license.
|
||||
// For commercial licensing, contact us at <contact@happydomain.org>.
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"net"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
|
@ -693,5 +694,50 @@ func (h *HeaderAnalyzer) parseReceivedHeader(receivedValue string) *model.Receiv
|
|||
}
|
||||
}
|
||||
|
||||
// Extract TLS details from the Received header parentheticals
|
||||
// (e.g. "(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) ...)")
|
||||
hop.Tls = parseReceivedTLS(normalized)
|
||||
|
||||
return hop
|
||||
}
|
||||
|
||||
// parseReceivedTLS extracts TLS connection details from a normalized Received header value.
|
||||
// Returns nil when the hop was not encrypted (no TLS version/cipher found).
|
||||
func parseReceivedTLS(normalized string) *model.TLSInfo {
|
||||
tls := &model.TLSInfo{}
|
||||
found := false
|
||||
|
||||
// TLS protocol version, e.g. "using TLSv1.3"
|
||||
if matches := regexp.MustCompile(`(?i)using\s+(TLSv[0-9.]+|SSLv[0-9.]+)`).FindStringSubmatch(normalized); len(matches) > 1 {
|
||||
tls.Version = &matches[1]
|
||||
found = true
|
||||
}
|
||||
|
||||
// Cipher suite, e.g. "with cipher TLS_AES_256_GCM_SHA384"
|
||||
if matches := regexp.MustCompile(`(?i)with cipher\s+([A-Za-z0-9_-]+)`).FindStringSubmatch(normalized); len(matches) > 1 {
|
||||
tls.Cipher = &matches[1]
|
||||
found = true
|
||||
}
|
||||
|
||||
// Cipher strength, e.g. "(256/256 bits)"
|
||||
if matches := regexp.MustCompile(`\((\d+)/\d+ bits\)`).FindStringSubmatch(normalized); len(matches) > 1 {
|
||||
if bits, err := strconv.Atoi(matches[1]); err == nil {
|
||||
tls.Bits = &bits
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Certificate verification status. Postfix emits "(verified OK)" when the peer
|
||||
// certificate was trusted, "(not verified)" otherwise. "No client certificate
|
||||
// requested" leaves the field unset (trust is simply not applicable).
|
||||
if regexp.MustCompile(`(?i)verified OK`).MatchString(normalized) {
|
||||
tls.Verified = utils.PtrTo(true)
|
||||
} else if regexp.MustCompile(`(?i)not verified`).MatchString(normalized) {
|
||||
tls.Verified = utils.PtrTo(false)
|
||||
}
|
||||
|
||||
return tls
|
||||
}
|
||||
|
|
|
|||
|
|
@ -677,6 +677,77 @@ func TestParseReceivedHeader(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestParseReceivedTLS(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
receivedValue string
|
||||
expectNil bool
|
||||
expectVersion *string
|
||||
expectCipher *string
|
||||
expectBits *int
|
||||
expectVerified *bool
|
||||
}{
|
||||
{
|
||||
name: "TLS 1.3 no client certificate",
|
||||
receivedValue: "from mail.example.com (unknown [IPv6:2001:db8::1]) " +
|
||||
"(using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) " +
|
||||
"key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) " +
|
||||
"(No client certificate requested) " +
|
||||
"by mx.example.org (Postfix) with ESMTPSA id 1EFD11611EA; Sun, 19 Oct 2025 09:40:33 +0000 (UTC)",
|
||||
expectVersion: strPtr("TLSv1.3"),
|
||||
expectCipher: strPtr("TLS_AES_256_GCM_SHA384"),
|
||||
expectBits: intPtr(256),
|
||||
expectVerified: nil,
|
||||
},
|
||||
{
|
||||
name: "TLS with verified client certificate",
|
||||
receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) " +
|
||||
"(using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) " +
|
||||
"(Client CN \"example\", Issuer \"CA\" (verified OK)) " +
|
||||
"by mx.receiver.com (Postfix) with ESMTPS id ABC; Mon, 01 Jan 2024 12:00:00 +0000",
|
||||
expectVersion: strPtr("TLSv1.2"),
|
||||
expectCipher: strPtr("ECDHE-RSA-AES128-GCM-SHA256"),
|
||||
expectBits: intPtr(128),
|
||||
expectVerified: boolPtr(true),
|
||||
},
|
||||
{
|
||||
name: "Plaintext (no TLS)",
|
||||
receivedValue: "from mail.example.com (mail.example.com [192.0.2.1]) by mx.receiver.com (Postfix) with ESMTP id ABC; Mon, 01 Jan 2024 12:00:00 +0000",
|
||||
expectNil: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
normalized := strings.Join(strings.Fields(tt.receivedValue), " ")
|
||||
tls := parseReceivedTLS(normalized)
|
||||
|
||||
if tt.expectNil {
|
||||
if tls != nil {
|
||||
t.Fatalf("expected nil TLS info, got %+v", tls)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if tls == nil {
|
||||
t.Fatal("parseReceivedTLS returned nil")
|
||||
}
|
||||
if !equalStrPtr(tls.Version, tt.expectVersion) {
|
||||
t.Errorf("Version = %v, want %v", ptrToStr(tls.Version), ptrToStr(tt.expectVersion))
|
||||
}
|
||||
if !equalStrPtr(tls.Cipher, tt.expectCipher) {
|
||||
t.Errorf("Cipher = %v, want %v", ptrToStr(tls.Cipher), ptrToStr(tt.expectCipher))
|
||||
}
|
||||
if (tls.Bits == nil) != (tt.expectBits == nil) || (tls.Bits != nil && *tls.Bits != *tt.expectBits) {
|
||||
t.Errorf("Bits = %v, want %v", tls.Bits, tt.expectBits)
|
||||
}
|
||||
if (tls.Verified == nil) != (tt.expectVerified == nil) || (tls.Verified != nil && *tls.Verified != *tt.expectVerified) {
|
||||
t.Errorf("Verified = %v, want %v", tls.Verified, tt.expectVerified)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) {
|
||||
analyzer := NewHeaderAnalyzer()
|
||||
|
||||
|
|
@ -908,6 +979,10 @@ func strPtr(s string) *string {
|
|||
return &s
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func ptrToStr(p *string) string {
|
||||
if p == nil {
|
||||
return "<nil>"
|
||||
|
|
|
|||
|
|
@ -85,6 +85,10 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
|
|||
// Run all analyzers
|
||||
results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email)
|
||||
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
|
||||
// Fall back to the received chain's inbound TLS when no x-tls header was present.
|
||||
if results.Authentication != nil && results.Headers != nil {
|
||||
r.authAnalyzer.ReconcileXTLS(results.Authentication, results.Headers.ReceivedChain)
|
||||
}
|
||||
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Headers)
|
||||
results.RBL = r.rblChecker.CheckEmail(email)
|
||||
results.DNSWL = r.dnswlChecker.CheckEmail(email)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue