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

151 lines
4.0 KiB
Go

package imap
import (
"bytes"
"encoding/json"
"net/mail"
"regexp"
"strings"
"github.com/emersion/go-imap/v2"
)
// MessageAuthInfo is persisted in messages.auth_info (JSON).
type MessageAuthInfo struct {
MailedBy string `json:"mailed_by,omitempty"`
SignedBy string `json:"signed_by,omitempty"`
DKIMPass *bool `json:"dkim_pass,omitempty"`
TLS bool `json:"tls,omitempty"`
ListUnsubscribe string `json:"list_unsubscribe,omitempty"`
}
var (
dkimDomainRe = regexp.MustCompile(`(?i)header\.d=([^\s;]+)`)
dkimSigDRe = regexp.MustCompile(`(?i)\bd=([^;\s]+)`)
returnPathRe = regexp.MustCompile(`(?i)<([^>]+)>`)
receivedFromRe = regexp.MustCompile(`(?i)from\s+([^\s;(\[]+)`)
)
func parseMessageMeta(raw []byte, envelope *imap.Envelope) (replyToJSON, authJSON []byte) {
auth := MessageAuthInfo{}
replyTo := replyAddresses(envelope, raw)
if len(raw) > 0 {
msg, err := mail.ReadMessage(bytes.NewReader(raw))
if err == nil {
if len(replyTo) == 0 {
replyTo = parseAddressListHeader(msg.Header.Get("Reply-To"))
}
auth.ListUnsubscribe = strings.TrimSpace(msg.Header.Get("List-Unsubscribe"))
mergeAuthFromHeaders(&auth, msg)
}
}
if auth.MailedBy == "" && len(envelope.From) > 0 {
auth.MailedBy = domainFromAddr(envelope.From[0].Addr())
}
if auth.SignedBy == "" && auth.MailedBy != "" {
auth.SignedBy = auth.MailedBy
}
authJSON, _ = json.Marshal(auth)
replyToJSON, _ = json.Marshal(replyTo)
return replyToJSON, authJSON
}
func replyAddresses(envelope *imap.Envelope, raw []byte) []EmailAddress {
if len(envelope.ReplyTo) > 0 {
return imapAddressesToEmail(envelope.ReplyTo)
}
if len(raw) == 0 {
return nil
}
msg, err := mail.ReadMessage(bytes.NewReader(raw))
if err != nil {
return nil
}
return parseAddressListHeader(msg.Header.Get("Reply-To"))
}
func imapAddressesToEmail(addrs []imap.Address) []EmailAddress {
out := make([]EmailAddress, 0, len(addrs))
for _, a := range addrs {
out = append(out, EmailAddress{Name: a.Name, Address: a.Addr()})
}
return out
}
func parseAddressListHeader(header string) []EmailAddress {
header = strings.TrimSpace(header)
if header == "" {
return nil
}
parsed, err := mail.ParseAddressList(header)
if err != nil {
return nil
}
out := make([]EmailAddress, 0, len(parsed))
for _, a := range parsed {
out = append(out, EmailAddress{Name: a.Name, Address: a.Address})
}
return out
}
func headerValues(h mail.Header, key string) []string {
return h[key]
}
func mergeAuthFromHeaders(auth *MessageAuthInfo, msg *mail.Message) {
for _, line := range headerValues(msg.Header, "Authentication-Results") {
lower := strings.ToLower(line)
if strings.Contains(lower, "dkim=pass") {
pass := true
auth.DKIMPass = &pass
if m := dkimDomainRe.FindStringSubmatch(line); len(m) > 1 && auth.SignedBy == "" {
auth.SignedBy = strings.Trim(m[1], `"'`)
}
} else if strings.Contains(lower, "dkim=fail") {
fail := false
auth.DKIMPass = &fail
}
if strings.Contains(lower, "tls=1") || strings.Contains(lower, "version=tls") {
auth.TLS = true
}
}
if auth.SignedBy == "" {
if sig := msg.Header.Get("DKIM-Signature"); sig != "" {
if m := dkimSigDRe.FindStringSubmatch(sig); len(m) > 1 {
auth.SignedBy = strings.Trim(m[1], `"'`)
}
}
}
if rp := msg.Header.Get("Return-Path"); rp != "" {
if m := returnPathRe.FindStringSubmatch(rp); len(m) > 1 {
auth.MailedBy = domainFromAddr(m[1])
}
}
for _, recv := range headerValues(msg.Header, "Received") {
lower := strings.ToLower(recv)
if strings.Contains(lower, "esmtps") || strings.Contains(lower, "tls") {
auth.TLS = true
}
if auth.MailedBy == "" {
if m := receivedFromRe.FindStringSubmatch(recv); len(m) > 1 {
auth.MailedBy = domainFromAddr(m[1])
}
}
}
}
func domainFromAddr(addr string) string {
addr = strings.Trim(addr, "<>")
if i := strings.LastIndex(addr, "@"); i >= 0 && i < len(addr)-1 {
return strings.ToLower(addr[i+1:])
}
host := strings.TrimSpace(addr)
if strings.Contains(host, ".") {
return strings.ToLower(host)
}
return ""
}