ultisuite-backend/internal/mail/listunsubscribe/parse.go
R3D347HR4Y cd0a80f5e8 huhu
2026-05-25 13:52:27 +02:00

114 lines
2.2 KiB
Go

package listunsubscribe
import (
"net/mail"
"net/url"
"strings"
)
// Mailto holds a one-click mailto unsubscribe target.
type Mailto struct {
Address string
Subject string
Body string
}
// Parsed from List-Unsubscribe (RFC 2369).
type Parsed struct {
Mailto *Mailto
HTTP string
}
func splitHeaderParts(raw string) []string {
var parts []string
var cur strings.Builder
depth := 0
for _, r := range raw {
switch r {
case '<':
depth++
cur.WriteRune(r)
case '>':
if depth > 0 {
depth--
}
cur.WriteRune(r)
case ',':
if depth == 0 {
if p := strings.TrimSpace(cur.String()); p != "" {
parts = append(parts, p)
}
cur.Reset()
continue
}
cur.WriteRune(r)
default:
cur.WriteRune(r)
}
}
if p := strings.TrimSpace(cur.String()); p != "" {
parts = append(parts, p)
}
return parts
}
func unwrapAngle(s string) string {
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "<") && strings.HasSuffix(s, ">") {
return strings.TrimSpace(s[1 : len(s)-1])
}
return s
}
// ParseMailtoURL parses mailto:user@host?subject=...&body=...
func ParseMailtoURL(raw string) (*Mailto, bool) {
raw = unwrapAngle(strings.TrimSpace(raw))
if raw == "" {
return nil, false
}
if !strings.HasPrefix(strings.ToLower(raw), "mailto:") {
return nil, false
}
u, err := url.Parse(raw)
if err != nil {
return nil, false
}
addr := strings.TrimSpace(u.Opaque)
if addr == "" {
addr = strings.TrimSpace(strings.TrimPrefix(u.Path, "/"))
}
if addr == "" {
return nil, false
}
if _, err := mail.ParseAddress(addr); err != nil {
// bare addr@host
if !strings.Contains(addr, "@") {
return nil, false
}
}
return &Mailto{
Address: addr,
Subject: u.Query().Get("subject"),
Body: u.Query().Get("body"),
}, true
}
// Parse extracts mailto and https targets from a List-Unsubscribe header value.
func Parse(listUnsubscribe string) Parsed {
out := Parsed{}
for _, part := range splitHeaderParts(listUnsubscribe) {
part = unwrapAngle(part)
if m, ok := ParseMailtoURL(part); ok && out.Mailto == nil {
out.Mailto = m
continue
}
lower := strings.ToLower(part)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
if out.HTTP == "" {
out.HTTP = part
}
}
}
return out
}