Add one-click unsubscribe detection and warning
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:
nemunaire 2026-02-22 23:57:39 +07:00
commit 8fda7746a1
2 changed files with 33 additions and 5 deletions

View file

@ -38,9 +38,10 @@ import (
// ContentAnalyzer analyzes email content (HTML, links, images)
type ContentAnalyzer struct {
Timeout time.Duration
httpClient *http.Client
listUnsubscribeURLs []string // URLs from List-Unsubscribe header
Timeout time.Duration
httpClient *http.Client
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
@ -115,6 +116,10 @@ func (c *ContentAnalyzer) AnalyzeContent(email *EmailMessage) *ContentResults {
// Parse List-Unsubscribe header URLs for use in link detection
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
htmlParts := email.GetHTMLParts()
textParts := email.GetTextParts()
@ -732,6 +737,7 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api.
HasHtml: api.PtrTo(results.HTMLContent != ""),
HasPlaintext: api.PtrTo(results.TextContent != ""),
HasUnsubscribeLink: api.PtrTo(results.HasUnsubscribe),
UnsubscribeMethods: &[]api.ContentAnalysisUnsubscribeMethods{},
}
// Calculate text-to-image ratio (inverse of image-to-text)
@ -878,8 +884,19 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api.
// Unsubscribe methods
if results.HasUnsubscribe {
methods := []api.ContentAnalysisUnsubscribeMethods{api.Link}
analysis.UnsubscribeMethods = &methods
*analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.Link)
}
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

View file

@ -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
const spamAssassin = report.spamassassin;
const contentScore = report.summary?.content_score || 0;