8 "github.com/mjl-/mox/moxvar"
9 "github.com/mjl-/mox/smtp"
12var errBadMessageID = errors.New("not a message-id")
14// MessageIDCanonical parses the Message-ID, returning a canonical value that is
15// lower-cased, without <>, and no unneeded quoting. For matching in threading,
16// with References/In-Reply-To. If the message-id is invalid (e.g. no <>), an error
17// is returned. If the message-id could not be parsed as address (localpart "@"
18// domain), the raw value and the bool return parameter true is returned. It is
19// quite common that message-id's don't adhere to the localpart @ domain
21func MessageIDCanonical(s string) (string, bool, error) {
24 s = strings.TrimSpace(s)
25 if !strings.HasPrefix(s, "<") {
26 return "", false, fmt.Errorf("%w: missing <", errBadMessageID)
29 // Seen in practice: Message-ID: <valid@valid.example> (added by postmaster@some.example)
30 // Doesn't seem valid, but we allow it.
31 s, rem, have := strings.Cut(s, ">")
32 if !have || (rem != "" && (moxvar.Pedantic || !strings.HasPrefix(rem, " "))) {
33 return "", false, fmt.Errorf("%w: missing >", errBadMessageID)
35 // We canonicalize the Message-ID: lower-case, no unneeded quoting.
36 s = strings.ToLower(s)
38 return "", false, fmt.Errorf("%w: empty message-id", errBadMessageID)
40 addr, err := smtp.ParseAddress(s)
42 // Common reasons for not being an address:
43 // 1. underscore in hostname.
44 // 2. ip literal instead of domain.
45 // 3. two @'s, perhaps intended as time-separator
46 // 4. no @'s, so no domain/host
49 // We preserve the unicode-ness of domain.
50 t := strings.Split(s, "@")
51 s = addr.Localpart.String() + "@" + t[len(t)-1]