Compare commits
1 commit
e2ec55e933
...
1a96e8665f
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a96e8665f |
4 changed files with 0 additions and 209 deletions
|
|
@ -501,11 +501,6 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool {
|
||||||
return false
|
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
|
// Common generic link texts that shouldn't trigger warnings
|
||||||
genericTexts := []string{
|
genericTexts := []string{
|
||||||
"click here", "read more", "learn more", "download", "subscribe",
|
"click here", "read more", "learn more", "download", "subscribe",
|
||||||
|
|
|
||||||
|
|
@ -589,53 +589,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
|
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
|
// parseReceivedChain extracts the chain of Received headers from an email
|
||||||
func (h *HeaderAnalyzer) parseReceivedChain(email *EmailMessage) []model.ReceivedHop {
|
func (h *HeaderAnalyzer) parseReceivedChain(email *EmailMessage) []model.ReceivedHop {
|
||||||
if email == nil || email.Header == nil {
|
if email == nil || email.Header == nil {
|
||||||
|
|
|
||||||
|
|
@ -974,146 +974,6 @@ 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": "<abc@example.com>",
|
|
||||||
"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": "<abc@example.com>",
|
|
||||||
"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": "<abc@example.com>",
|
|
||||||
"Subject": "Re: Your invoice",
|
|
||||||
"References": "<original@example.com>",
|
|
||||||
},
|
|
||||||
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": "<abc@example.com>",
|
|
||||||
"Subject": "Re: Your invoice",
|
|
||||||
"In-Reply-To": "<original@example.com>",
|
|
||||||
},
|
|
||||||
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": "<abc@example.com>",
|
|
||||||
"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
|
// Helper functions for testing
|
||||||
func strPtr(s string) *string {
|
func strPtr(s string) *string {
|
||||||
return &s
|
return &s
|
||||||
|
|
|
||||||
|
|
@ -72,26 +72,6 @@
|
||||||
{bimiRecord.error}
|
{bimiRecord.error}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if !bimiRecord.valid}
|
|
||||||
<div class="alert alert-info mt-3 mb-0">
|
|
||||||
<h6 class="alert-heading">
|
|
||||||
<i class="bi bi-lightbulb me-1"></i>
|
|
||||||
Explicitly decline BIMI participation
|
|
||||||
</h6>
|
|
||||||
<p class="mb-2 small">
|
|
||||||
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:
|
|
||||||
</p>
|
|
||||||
<code class="d-block bg-white rounded p-2 text-break border"
|
|
||||||
>{bimiRecord.selector}._bimi.{bimiRecord.domain}. IN TXT "v=BIMI1; l=; a="</code
|
|
||||||
>
|
|
||||||
<p class="mt-1 mb-0 small text-muted">
|
|
||||||
Declination record format as defined in § 4.3.1 of
|
|
||||||
<em>draft-brand-indicators-for-message-identification</em>.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue