151 lines
4.0 KiB
Go
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 ""
|
|
}
|