214 lines
6 KiB
Go
214 lines
6 KiB
Go
// 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 (
|
|
"time"
|
|
|
|
"git.happydns.org/happyDeliver/internal/api"
|
|
"git.happydns.org/happyDeliver/internal/utils"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// ReportGenerator generates comprehensive deliverability reports
|
|
type ReportGenerator struct {
|
|
authAnalyzer *AuthenticationAnalyzer
|
|
spamAnalyzer *SpamAssassinAnalyzer
|
|
dnsAnalyzer *DNSAnalyzer
|
|
rblChecker *RBLChecker
|
|
contentAnalyzer *ContentAnalyzer
|
|
headerAnalyzer *HeaderAnalyzer
|
|
}
|
|
|
|
// NewReportGenerator creates a new report generator
|
|
func NewReportGenerator(
|
|
dnsTimeout time.Duration,
|
|
httpTimeout time.Duration,
|
|
rbls []string,
|
|
) *ReportGenerator {
|
|
return &ReportGenerator{
|
|
authAnalyzer: NewAuthenticationAnalyzer(),
|
|
spamAnalyzer: NewSpamAssassinAnalyzer(),
|
|
dnsAnalyzer: NewDNSAnalyzer(dnsTimeout),
|
|
rblChecker: NewRBLChecker(dnsTimeout, rbls),
|
|
contentAnalyzer: NewContentAnalyzer(httpTimeout),
|
|
headerAnalyzer: NewHeaderAnalyzer(),
|
|
}
|
|
}
|
|
|
|
// AnalysisResults contains all intermediate analysis results
|
|
type AnalysisResults struct {
|
|
Email *EmailMessage
|
|
Authentication *api.AuthenticationResults
|
|
Content *ContentResults
|
|
DNS *api.DNSResults
|
|
Headers *api.HeaderAnalysis
|
|
RBL *RBLResults
|
|
SpamAssassin *SpamAssassinResult
|
|
}
|
|
|
|
// AnalyzeEmail performs complete email analysis
|
|
func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
|
|
results := &AnalysisResults{
|
|
Email: email,
|
|
}
|
|
|
|
// Run all analyzers
|
|
results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email)
|
|
results.Content = r.contentAnalyzer.AnalyzeContent(email)
|
|
results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication)
|
|
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email)
|
|
results.RBL = r.rblChecker.CheckEmail(email)
|
|
results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email)
|
|
|
|
return results
|
|
}
|
|
|
|
// GenerateReport creates a complete API report from analysis results
|
|
func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResults) *api.Report {
|
|
reportID := uuid.New()
|
|
now := time.Now()
|
|
|
|
report := &api.Report{
|
|
Id: utils.UUIDToBase32(reportID),
|
|
TestId: utils.UUIDToBase32(testID),
|
|
CreatedAt: now,
|
|
}
|
|
|
|
// Calculate scores directly from analyzers (no more checks array)
|
|
authScore := 0
|
|
if results.Authentication != nil {
|
|
authScore = r.authAnalyzer.CalculateAuthenticationScore(results.Authentication)
|
|
}
|
|
|
|
contentScore := 0
|
|
if results.Content != nil {
|
|
contentScore = r.contentAnalyzer.CalculateContentScore(results.Content)
|
|
}
|
|
|
|
headerScore := 0
|
|
if results.Headers != nil {
|
|
headerScore = r.headerAnalyzer.CalculateHeaderScore(results.Headers)
|
|
}
|
|
|
|
blacklistScore := 0
|
|
if results.RBL != nil {
|
|
blacklistScore = r.rblChecker.CalculateRBLScore(results.RBL)
|
|
}
|
|
|
|
spamScore := 0
|
|
if results.SpamAssassin != nil {
|
|
spamScore = r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin)
|
|
}
|
|
|
|
report.Summary = &api.ScoreSummary{
|
|
AuthenticationScore: authScore,
|
|
BlacklistScore: blacklistScore,
|
|
ContentScore: contentScore,
|
|
HeaderScore: headerScore,
|
|
SpamScore: spamScore,
|
|
}
|
|
|
|
// Add authentication results
|
|
report.Authentication = results.Authentication
|
|
|
|
// Add content analysis
|
|
if results.Content != nil {
|
|
contentAnalysis := r.contentAnalyzer.GenerateContentAnalysis(results.Content)
|
|
report.ContentAnalysis = contentAnalysis
|
|
}
|
|
|
|
// Add DNS records
|
|
if results.DNS != nil {
|
|
report.DnsResults = results.DNS
|
|
}
|
|
|
|
// Add headers results
|
|
report.HeaderAnalysis = results.Headers
|
|
|
|
// Add blacklist checks as a map of IP -> array of BlacklistCheck
|
|
if results.RBL != nil && len(results.RBL.Checks) > 0 {
|
|
report.Blacklists = &results.RBL.Checks
|
|
}
|
|
|
|
// Add SpamAssassin result
|
|
if results.SpamAssassin != nil {
|
|
report.Spamassassin = &api.SpamAssassinResult{
|
|
Score: float32(results.SpamAssassin.Score),
|
|
RequiredScore: float32(results.SpamAssassin.RequiredScore),
|
|
IsSpam: results.SpamAssassin.IsSpam,
|
|
}
|
|
|
|
if len(results.SpamAssassin.Tests) > 0 {
|
|
report.Spamassassin.Tests = &results.SpamAssassin.Tests
|
|
}
|
|
|
|
if results.SpamAssassin.RawReport != "" {
|
|
report.Spamassassin.Report = &results.SpamAssassin.RawReport
|
|
}
|
|
}
|
|
|
|
// Add raw headers
|
|
if results.Email != nil && results.Email.RawHeaders != "" {
|
|
report.RawHeaders = &results.Email.RawHeaders
|
|
}
|
|
|
|
// Calculate overall score as mean of all category scores
|
|
categoryScores := []int{
|
|
report.Summary.AuthenticationScore,
|
|
report.Summary.BlacklistScore,
|
|
report.Summary.ContentScore,
|
|
report.Summary.HeaderScore,
|
|
report.Summary.SpamScore,
|
|
}
|
|
|
|
var totalScore int
|
|
var categoryCount int
|
|
for _, score := range categoryScores {
|
|
totalScore += score
|
|
categoryCount++
|
|
}
|
|
|
|
if categoryCount > 0 {
|
|
report.Score = totalScore / categoryCount
|
|
} else {
|
|
report.Score = 0
|
|
}
|
|
|
|
report.Grade = ScoreToReportGrade(report.Score)
|
|
|
|
return report
|
|
}
|
|
|
|
// GenerateRawEmail returns the raw email message as a string
|
|
func (r *ReportGenerator) GenerateRawEmail(email *EmailMessage) string {
|
|
if email == nil {
|
|
return ""
|
|
}
|
|
|
|
raw := email.RawHeaders
|
|
if email.RawBody != "" {
|
|
raw += "\n" + email.RawBody
|
|
}
|
|
|
|
return raw
|
|
}
|