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)
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue