Add grades
This commit is contained in:
parent
33d394a27b
commit
a64b866cfa
25 changed files with 362 additions and 207 deletions
|
|
@ -463,9 +463,9 @@ func textprotoCanonical(s string) string {
|
|||
|
||||
// CalculateAuthenticationScore calculates the authentication score from auth results
|
||||
// Returns a score from 0-100 where higher is better
|
||||
func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *api.AuthenticationResults) int {
|
||||
func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *api.AuthenticationResults) (int, string) {
|
||||
if results == nil {
|
||||
return 0
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
|
@ -530,5 +530,5 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *api.Authe
|
|||
score = 100
|
||||
}
|
||||
|
||||
return score
|
||||
return score, ScoreToGrade(score)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -323,7 +323,7 @@ func TestGetAuthenticationScore(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
score := scorer.CalculateAuthenticationScore(tt.results)
|
||||
score, _ := scorer.CalculateAuthenticationScore(tt.results)
|
||||
|
||||
if score != tt.expectedScore {
|
||||
t.Errorf("Score = %v, want %v", score, tt.expectedScore)
|
||||
|
|
@ -370,16 +370,16 @@ func TestParseARCResult(t *testing.T) {
|
|||
|
||||
func TestParseAuthenticationResultsHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
header string
|
||||
expectedSPFResult *api.AuthResultResult
|
||||
expectedSPFDomain *string
|
||||
expectedDKIMCount int
|
||||
expectedDKIMResult *api.AuthResultResult
|
||||
expectedDMARCResult *api.AuthResultResult
|
||||
expectedDMARCDomain *string
|
||||
expectedBIMIResult *api.AuthResultResult
|
||||
expectedARCResult *api.ARCResultResult
|
||||
name string
|
||||
header string
|
||||
expectedSPFResult *api.AuthResultResult
|
||||
expectedSPFDomain *string
|
||||
expectedDKIMCount int
|
||||
expectedDKIMResult *api.AuthResultResult
|
||||
expectedDMARCResult *api.AuthResultResult
|
||||
expectedDMARCDomain *string
|
||||
expectedBIMIResult *api.AuthResultResult
|
||||
expectedARCResult *api.ARCResultResult
|
||||
}{
|
||||
{
|
||||
name: "Complete authentication results",
|
||||
|
|
@ -441,12 +441,12 @@ func TestParseAuthenticationResultsHeader(t *testing.T) {
|
|||
expectedDMARCDomain: api.PtrTo("example.com"),
|
||||
},
|
||||
{
|
||||
name: "BIMI pass",
|
||||
header: "mail.example.com; spf=pass smtp.mailfrom=sender@example.com; bimi=pass header.d=example.com header.selector=default",
|
||||
expectedSPFResult: api.PtrTo(api.AuthResultResultPass),
|
||||
expectedSPFDomain: api.PtrTo("example.com"),
|
||||
expectedDKIMCount: 0,
|
||||
expectedBIMIResult: api.PtrTo(api.AuthResultResultPass),
|
||||
name: "BIMI pass",
|
||||
header: "mail.example.com; spf=pass smtp.mailfrom=sender@example.com; bimi=pass header.d=example.com header.selector=default",
|
||||
expectedSPFResult: api.PtrTo(api.AuthResultResultPass),
|
||||
expectedSPFDomain: api.PtrTo("example.com"),
|
||||
expectedDKIMCount: 0,
|
||||
expectedBIMIResult: api.PtrTo(api.AuthResultResultPass),
|
||||
},
|
||||
{
|
||||
name: "ARC pass",
|
||||
|
|
@ -468,24 +468,24 @@ func TestParseAuthenticationResultsHeader(t *testing.T) {
|
|||
expectedARCResult: api.PtrTo(api.ARCResultResultPass),
|
||||
},
|
||||
{
|
||||
name: "Empty header (authserv-id only)",
|
||||
header: "mx.google.com",
|
||||
name: "Empty header (authserv-id only)",
|
||||
header: "mx.google.com",
|
||||
expectedSPFResult: nil,
|
||||
expectedDKIMCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Empty parts with semicolons",
|
||||
header: "mx.google.com; ; ; spf=pass smtp.mailfrom=sender@example.com; ;",
|
||||
expectedSPFResult: api.PtrTo(api.AuthResultResultPass),
|
||||
expectedSPFDomain: api.PtrTo("example.com"),
|
||||
expectedDKIMCount: 0,
|
||||
},
|
||||
{
|
||||
name: "DKIM with short form parameters",
|
||||
header: "mail.example.com; dkim=pass d=example.com s=selector1",
|
||||
expectedSPFResult: nil,
|
||||
expectedDKIMCount: 0,
|
||||
},
|
||||
{
|
||||
name: "Empty parts with semicolons",
|
||||
header: "mx.google.com; ; ; spf=pass smtp.mailfrom=sender@example.com; ;",
|
||||
expectedSPFResult: api.PtrTo(api.AuthResultResultPass),
|
||||
expectedSPFDomain: api.PtrTo("example.com"),
|
||||
expectedDKIMCount: 0,
|
||||
},
|
||||
{
|
||||
name: "DKIM with short form parameters",
|
||||
header: "mail.example.com; dkim=pass d=example.com s=selector1",
|
||||
expectedSPFResult: nil,
|
||||
expectedDKIMCount: 1,
|
||||
expectedDKIMResult: api.PtrTo(api.AuthResultResultPass),
|
||||
expectedDKIMCount: 1,
|
||||
expectedDKIMResult: api.PtrTo(api.AuthResultResultPass),
|
||||
},
|
||||
{
|
||||
name: "SPF neutral",
|
||||
|
|
|
|||
|
|
@ -726,9 +726,9 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api.
|
|||
}
|
||||
|
||||
// CalculateContentScore calculates the content score (0-20 points)
|
||||
func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) int {
|
||||
func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) (int, string) {
|
||||
if results == nil {
|
||||
return 0
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
var score int = 10
|
||||
|
|
@ -819,5 +819,5 @@ func (c *ContentAnalyzer) CalculateContentScore(results *ContentResults) int {
|
|||
score = 100
|
||||
}
|
||||
|
||||
return score
|
||||
return score, ScoreToGrade(score)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -444,9 +444,9 @@ func (d *DNSAnalyzer) validateBIMI(record string) bool {
|
|||
|
||||
// CalculateDNSScore calculates the DNS score from records results
|
||||
// Returns a score from 0-100 where higher is better
|
||||
func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) int {
|
||||
func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) (int, string) {
|
||||
if results == nil {
|
||||
return 0
|
||||
return 0, ""
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
|
@ -525,7 +525,7 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) int {
|
|||
// BIMI is optional but indicates advanced email branding
|
||||
if results.BimiRecord != nil && results.BimiRecord.Valid {
|
||||
if score >= 100 {
|
||||
return 100
|
||||
return 100, "A+"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -539,5 +539,5 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) int {
|
|||
score = 0
|
||||
}
|
||||
|
||||
return score
|
||||
return score, ScoreToGrade(score)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,12 +37,13 @@ func NewHeaderAnalyzer() *HeaderAnalyzer {
|
|||
}
|
||||
|
||||
// CalculateHeaderScore evaluates email structural quality from header analysis
|
||||
func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) int {
|
||||
func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int, rune) {
|
||||
if analysis == nil || analysis.Headers == nil {
|
||||
return 0
|
||||
return 0, ' '
|
||||
}
|
||||
|
||||
score := 0
|
||||
maxGrade := 6
|
||||
headers := *analysis.Headers
|
||||
|
||||
// Check required headers (RFC 5322) - 40 points
|
||||
|
|
@ -60,6 +61,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) int
|
|||
score += 40
|
||||
} else {
|
||||
score += int(40 * (float32(presentRequired) / float32(requiredCount)))
|
||||
maxGrade = 1
|
||||
}
|
||||
|
||||
// Check recommended headers (30 points)
|
||||
|
|
@ -80,9 +82,15 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) int
|
|||
}
|
||||
score += presentRecommended * 30 / recommendedCount
|
||||
|
||||
if presentRecommended < recommendedCount {
|
||||
maxGrade -= 1
|
||||
}
|
||||
|
||||
// Check for proper MIME structure (20 points)
|
||||
if analysis.HasMimeStructure != nil && *analysis.HasMimeStructure {
|
||||
score += 20
|
||||
} else {
|
||||
maxGrade -= 1
|
||||
}
|
||||
|
||||
// Check Message-ID format (10 points)
|
||||
|
|
@ -90,15 +98,20 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) int
|
|||
// If Valid is set and true, award points
|
||||
if check.Valid != nil && *check.Valid {
|
||||
score += 10
|
||||
} else {
|
||||
maxGrade -= 1
|
||||
}
|
||||
} else {
|
||||
maxGrade -= 1
|
||||
}
|
||||
|
||||
// Ensure score doesn't exceed 100
|
||||
if score > 100 {
|
||||
score = 100
|
||||
}
|
||||
grade := 'A' + max(6-maxGrade, 0)
|
||||
|
||||
return score
|
||||
return score, rune(grade)
|
||||
}
|
||||
|
||||
// isValidMessageID checks if a Message-ID has proper format
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ func TestCalculateHeaderScore(t *testing.T) {
|
|||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Generate header analysis first
|
||||
analysis := analyzer.GenerateHeaderAnalysis(tt.email)
|
||||
score := analyzer.CalculateHeaderScore(analysis)
|
||||
score, _ := analyzer.CalculateHeaderScore(analysis)
|
||||
if score < tt.minScore || score > tt.maxScore {
|
||||
t.Errorf("CalculateHeaderScore() = %v, want between %v and %v", score, tt.minScore, tt.maxScore)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,13 +240,14 @@ func (r *RBLChecker) reverseIP(ipStr string) string {
|
|||
}
|
||||
|
||||
// CalculateRBLScore calculates the blacklist contribution to deliverability
|
||||
func (r *RBLChecker) CalculateRBLScore(results *RBLResults) int {
|
||||
func (r *RBLChecker) CalculateRBLScore(results *RBLResults) (int, string) {
|
||||
if results == nil || len(results.IPsChecked) == 0 {
|
||||
// No IPs to check, give benefit of doubt
|
||||
return 100
|
||||
return 100, ""
|
||||
}
|
||||
|
||||
return 100 - results.ListedCount*100/len(r.RBLs)
|
||||
percentage := 100 - results.ListedCount*100/len(r.RBLs)
|
||||
return percentage, ScoreToGrade(percentage)
|
||||
}
|
||||
|
||||
// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one RBL
|
||||
|
|
|
|||
|
|
@ -271,14 +271,14 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
{
|
||||
name: "Nil results",
|
||||
results: nil,
|
||||
expectedScore: 200,
|
||||
expectedScore: 100,
|
||||
},
|
||||
{
|
||||
name: "No IPs checked",
|
||||
results: &RBLResults{
|
||||
IPsChecked: []string{},
|
||||
},
|
||||
expectedScore: 200,
|
||||
expectedScore: 100,
|
||||
},
|
||||
{
|
||||
name: "Not listed on any RBL",
|
||||
|
|
@ -286,7 +286,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
IPsChecked: []string{"198.51.100.1"},
|
||||
ListedCount: 0,
|
||||
},
|
||||
expectedScore: 200,
|
||||
expectedScore: 100,
|
||||
},
|
||||
{
|
||||
name: "Listed on 1 RBL",
|
||||
|
|
@ -294,7 +294,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
IPsChecked: []string{"198.51.100.1"},
|
||||
ListedCount: 1,
|
||||
},
|
||||
expectedScore: 100,
|
||||
expectedScore: 84, // 100 - 1*100/6 = 84 (integer division: 100/6=16)
|
||||
},
|
||||
{
|
||||
name: "Listed on 2 RBLs",
|
||||
|
|
@ -302,7 +302,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
IPsChecked: []string{"198.51.100.1"},
|
||||
ListedCount: 2,
|
||||
},
|
||||
expectedScore: 50,
|
||||
expectedScore: 67, // 100 - 2*100/6 = 67 (integer division: 200/6=33)
|
||||
},
|
||||
{
|
||||
name: "Listed on 3 RBLs",
|
||||
|
|
@ -310,7 +310,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
IPsChecked: []string{"198.51.100.1"},
|
||||
ListedCount: 3,
|
||||
},
|
||||
expectedScore: 50,
|
||||
expectedScore: 50, // 100 - 3*100/6 = 50 (integer division: 300/6=50)
|
||||
},
|
||||
{
|
||||
name: "Listed on 4+ RBLs",
|
||||
|
|
@ -318,7 +318,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
IPsChecked: []string{"198.51.100.1"},
|
||||
ListedCount: 4,
|
||||
},
|
||||
expectedScore: 0,
|
||||
expectedScore: 34, // 100 - 4*100/6 = 34 (integer division: 400/6=66)
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -326,7 +326,7 @@ func TestGetBlacklistScore(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
score := checker.CalculateRBLScore(tt.results)
|
||||
score, _ := checker.CalculateRBLScore(tt.results)
|
||||
if score != tt.expectedScore {
|
||||
t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,42 +96,54 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
|||
|
||||
// Calculate scores directly from analyzers (no more checks array)
|
||||
dnsScore := 0
|
||||
var dnsGrade string
|
||||
if results.DNS != nil {
|
||||
dnsScore = r.dnsAnalyzer.CalculateDNSScore(results.DNS)
|
||||
dnsScore, dnsGrade = r.dnsAnalyzer.CalculateDNSScore(results.DNS)
|
||||
}
|
||||
|
||||
authScore := 0
|
||||
var authGrade string
|
||||
if results.Authentication != nil {
|
||||
authScore = r.authAnalyzer.CalculateAuthenticationScore(results.Authentication)
|
||||
authScore, authGrade = r.authAnalyzer.CalculateAuthenticationScore(results.Authentication)
|
||||
}
|
||||
|
||||
contentScore := 0
|
||||
var contentGrade string
|
||||
if results.Content != nil {
|
||||
contentScore = r.contentAnalyzer.CalculateContentScore(results.Content)
|
||||
contentScore, contentGrade = r.contentAnalyzer.CalculateContentScore(results.Content)
|
||||
}
|
||||
|
||||
headerScore := 0
|
||||
var headerGrade rune
|
||||
if results.Headers != nil {
|
||||
headerScore = r.headerAnalyzer.CalculateHeaderScore(results.Headers)
|
||||
headerScore, headerGrade = r.headerAnalyzer.CalculateHeaderScore(results.Headers)
|
||||
}
|
||||
|
||||
blacklistScore := 0
|
||||
var blacklistGrade string
|
||||
if results.RBL != nil {
|
||||
blacklistScore = r.rblChecker.CalculateRBLScore(results.RBL)
|
||||
blacklistScore, blacklistGrade = r.rblChecker.CalculateRBLScore(results.RBL)
|
||||
}
|
||||
|
||||
spamScore := 0
|
||||
var spamGrade string
|
||||
if results.SpamAssassin != nil {
|
||||
spamScore = r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin)
|
||||
spamScore, spamGrade = r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin)
|
||||
}
|
||||
|
||||
report.Summary = &api.ScoreSummary{
|
||||
DnsScore: dnsScore,
|
||||
DnsGrade: api.ScoreSummaryDnsGrade(dnsGrade),
|
||||
AuthenticationScore: authScore,
|
||||
AuthenticationGrade: api.ScoreSummaryAuthenticationGrade(authGrade),
|
||||
BlacklistScore: blacklistScore,
|
||||
BlacklistGrade: api.ScoreSummaryBlacklistGrade(blacklistGrade),
|
||||
ContentScore: contentScore,
|
||||
ContentGrade: api.ScoreSummaryContentGrade(contentGrade),
|
||||
HeaderScore: headerScore,
|
||||
HeaderGrade: api.ScoreSummaryHeaderGrade(headerGrade),
|
||||
SpamScore: spamScore,
|
||||
SpamGrade: api.ScoreSummarySpamGrade(spamGrade),
|
||||
}
|
||||
|
||||
// Add authentication results
|
||||
|
|
@ -187,6 +199,41 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
|||
}
|
||||
|
||||
report.Grade = ScoreToReportGrade(report.Score)
|
||||
categoryGrades := []string{
|
||||
string(report.Summary.DnsGrade),
|
||||
string(report.Summary.AuthenticationGrade),
|
||||
string(report.Summary.BlacklistGrade),
|
||||
string(report.Summary.ContentGrade),
|
||||
string(report.Summary.HeaderGrade),
|
||||
string(report.Summary.SpamGrade),
|
||||
}
|
||||
if report.Score >= 100 {
|
||||
hasLessThanA := false
|
||||
|
||||
for _, grade := range categoryGrades {
|
||||
if len(grade) < 1 || grade[0] != 'A' {
|
||||
hasLessThanA = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasLessThanA {
|
||||
report.Grade = "A+"
|
||||
}
|
||||
} else {
|
||||
var minusGrade byte = 0
|
||||
for _, grade := range categoryGrades {
|
||||
if len(grade) == 0 {
|
||||
minusGrade = 255
|
||||
break
|
||||
} else if grade[0]-'A' > minusGrade {
|
||||
minusGrade = grade[0] - 'A'
|
||||
}
|
||||
}
|
||||
|
||||
if minusGrade < 255 {
|
||||
report.Grade = api.ReportGrade(string([]byte{'A' + minusGrade}))
|
||||
}
|
||||
}
|
||||
|
||||
return report
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,23 +106,26 @@ func TestGenerateReport(t *testing.T) {
|
|||
t.Error("Summary should not be nil")
|
||||
}
|
||||
|
||||
// Verify score summary
|
||||
// Verify score summary (all scores are 0-100 percentages)
|
||||
if report.Summary != nil {
|
||||
if report.Summary.AuthenticationScore < 0 || report.Summary.AuthenticationScore > 3 {
|
||||
if report.Summary.AuthenticationScore < 0 || report.Summary.AuthenticationScore > 100 {
|
||||
t.Errorf("AuthenticationScore %v is out of bounds", report.Summary.AuthenticationScore)
|
||||
}
|
||||
if report.Summary.SpamScore < 0 || report.Summary.SpamScore > 2 {
|
||||
if report.Summary.SpamScore < 0 || report.Summary.SpamScore > 100 {
|
||||
t.Errorf("SpamScore %v is out of bounds", report.Summary.SpamScore)
|
||||
}
|
||||
if report.Summary.BlacklistScore < 0 || report.Summary.BlacklistScore > 20 {
|
||||
if report.Summary.BlacklistScore < 0 || report.Summary.BlacklistScore > 100 {
|
||||
t.Errorf("BlacklistScore %v is out of bounds", report.Summary.BlacklistScore)
|
||||
}
|
||||
if report.Summary.ContentScore < 0 || report.Summary.ContentScore > 20 {
|
||||
if report.Summary.ContentScore < 0 || report.Summary.ContentScore > 100 {
|
||||
t.Errorf("ContentScore %v is out of bounds", report.Summary.ContentScore)
|
||||
}
|
||||
if report.Summary.HeaderScore < 0 || report.Summary.HeaderScore > 10 {
|
||||
if report.Summary.HeaderScore < 0 || report.Summary.HeaderScore > 100 {
|
||||
t.Errorf("HeaderScore %v is out of bounds", report.Summary.HeaderScore)
|
||||
}
|
||||
if report.Summary.DnsScore < 0 || report.Summary.DnsScore > 100 {
|
||||
t.Errorf("DnsScore %v is out of bounds", report.Summary.DnsScore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,9 +28,9 @@ import (
|
|||
// ScoreToGrade converts a percentage score (0-100) to a letter grade
|
||||
func ScoreToGrade(score int) string {
|
||||
switch {
|
||||
case score >= 97:
|
||||
case score > 100:
|
||||
return "A+"
|
||||
case score >= 93:
|
||||
case score > 95:
|
||||
return "A"
|
||||
case score >= 85:
|
||||
return "B"
|
||||
|
|
|
|||
|
|
@ -193,9 +193,9 @@ func (a *SpamAssassinAnalyzer) parseSpamReport(report string, result *api.SpamAs
|
|||
}
|
||||
|
||||
// CalculateSpamAssassinScore calculates the SpamAssassin contribution to deliverability
|
||||
func (a *SpamAssassinAnalyzer) CalculateSpamAssassinScore(result *api.SpamAssassinResult) int {
|
||||
func (a *SpamAssassinAnalyzer) CalculateSpamAssassinScore(result *api.SpamAssassinResult) (int, string) {
|
||||
if result == nil {
|
||||
return 100 // No spam scan results, assume good
|
||||
return 100, "" // No spam scan results, assume good
|
||||
}
|
||||
|
||||
// SpamAssassin score typically ranges from -10 to +20
|
||||
|
|
@ -206,12 +206,15 @@ func (a *SpamAssassinAnalyzer) CalculateSpamAssassinScore(result *api.SpamAssass
|
|||
score := result.Score
|
||||
|
||||
// Convert SpamAssassin score to 0-100 scale (inverted - lower SA score is better)
|
||||
if score <= 0 {
|
||||
return 100 // Perfect score for ham
|
||||
if score < 0 {
|
||||
return 100, "A+" // Perfect score for ham
|
||||
} else if score == 0 {
|
||||
return 100, "A" // Perfect score for ham
|
||||
} else if score >= result.RequiredScore {
|
||||
return 0 // Failed spam test
|
||||
return 0, "F" // Failed spam test
|
||||
} else {
|
||||
// Linear scale between 0 and required threshold
|
||||
return 100 - int(math.Round(float64(score*100/result.RequiredScore)))
|
||||
percentage := 100 - int(math.Round(float64(score*100/result.RequiredScore)))
|
||||
return percentage, ScoreToGrade(percentage - 15)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue