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 "" }