Add one-click unsubscribe detection and warning
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Detect the List-Unsubscribe-Post: List-Unsubscribe=One-Click header (RFC 8058) and expose it as the 'one-click' unsubscribe method in the content analysis. When unsubscribe methods are present but one-click is absent, the summary card now shows a warning nudging senders to adopt it.
This commit is contained in:
parent
96e83ff70d
commit
8fda7746a1
2 changed files with 33 additions and 5 deletions
|
|
@ -38,9 +38,10 @@ import (
|
||||||
|
|
||||||
// ContentAnalyzer analyzes email content (HTML, links, images)
|
// ContentAnalyzer analyzes email content (HTML, links, images)
|
||||||
type ContentAnalyzer struct {
|
type ContentAnalyzer struct {
|
||||||
Timeout time.Duration
|
Timeout time.Duration
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
listUnsubscribeURLs []string // URLs from List-Unsubscribe header
|
listUnsubscribeURLs []string // URLs from List-Unsubscribe header
|
||||||
|
hasOneClickUnsubscribe bool // True if List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContentAnalyzer creates a new content analyzer with configurable timeout
|
// NewContentAnalyzer creates a new content analyzer with configurable timeout
|
||||||
|
|
@ -115,6 +116,10 @@ func (c *ContentAnalyzer) AnalyzeContent(email *EmailMessage) *ContentResults {
|
||||||
// Parse List-Unsubscribe header URLs for use in link detection
|
// Parse List-Unsubscribe header URLs for use in link detection
|
||||||
c.listUnsubscribeURLs = email.GetListUnsubscribeURLs()
|
c.listUnsubscribeURLs = email.GetListUnsubscribeURLs()
|
||||||
|
|
||||||
|
// Check for one-click unsubscribe support
|
||||||
|
listUnsubscribePost := email.Header.Get("List-Unsubscribe-Post")
|
||||||
|
c.hasOneClickUnsubscribe = strings.EqualFold(strings.TrimSpace(listUnsubscribePost), "List-Unsubscribe=One-Click")
|
||||||
|
|
||||||
// Get HTML and text parts
|
// Get HTML and text parts
|
||||||
htmlParts := email.GetHTMLParts()
|
htmlParts := email.GetHTMLParts()
|
||||||
textParts := email.GetTextParts()
|
textParts := email.GetTextParts()
|
||||||
|
|
@ -732,6 +737,7 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api.
|
||||||
HasHtml: api.PtrTo(results.HTMLContent != ""),
|
HasHtml: api.PtrTo(results.HTMLContent != ""),
|
||||||
HasPlaintext: api.PtrTo(results.TextContent != ""),
|
HasPlaintext: api.PtrTo(results.TextContent != ""),
|
||||||
HasUnsubscribeLink: api.PtrTo(results.HasUnsubscribe),
|
HasUnsubscribeLink: api.PtrTo(results.HasUnsubscribe),
|
||||||
|
UnsubscribeMethods: &[]api.ContentAnalysisUnsubscribeMethods{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate text-to-image ratio (inverse of image-to-text)
|
// Calculate text-to-image ratio (inverse of image-to-text)
|
||||||
|
|
@ -878,8 +884,19 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api.
|
||||||
|
|
||||||
// Unsubscribe methods
|
// Unsubscribe methods
|
||||||
if results.HasUnsubscribe {
|
if results.HasUnsubscribe {
|
||||||
methods := []api.ContentAnalysisUnsubscribeMethods{api.Link}
|
*analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.Link)
|
||||||
analysis.UnsubscribeMethods = &methods
|
}
|
||||||
|
|
||||||
|
for _, url := range c.listUnsubscribeURLs {
|
||||||
|
if strings.HasPrefix(url, "mailto:") {
|
||||||
|
*analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.Mailto)
|
||||||
|
} else if strings.HasPrefix(url, "http:") || strings.HasPrefix(url, "https:") {
|
||||||
|
*analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.ListUnsubscribeHeader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if slices.Contains(*analysis.UnsubscribeMethods, api.ListUnsubscribeHeader) && c.hasOneClickUnsubscribe {
|
||||||
|
*analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.OneClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
return analysis
|
return analysis
|
||||||
|
|
|
||||||
|
|
@ -422,6 +422,17 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One-click unsubscribe check
|
||||||
|
const unsubscribeMethods = report.content_analysis?.unsubscribe_methods;
|
||||||
|
if (unsubscribeMethods && unsubscribeMethods.length > 0 && !unsubscribeMethods.includes("one-click")) {
|
||||||
|
segments.push({ text: ". This email could benefit from " });
|
||||||
|
segments.push({
|
||||||
|
text: "one-click unsubscribe",
|
||||||
|
highlight: { color: "warning", bold: true },
|
||||||
|
link: "#content-details",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Content/spam assessment
|
// Content/spam assessment
|
||||||
const spamAssassin = report.spamassassin;
|
const spamAssassin = report.spamassassin;
|
||||||
const contentScore = report.summary?.content_score || 0;
|
const contentScore = report.summary?.content_score || 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue