1package message
2
3import (
4 "strings"
5
6 "github.com/mjl-/mox/smtp"
7)
8
9// ReferencedIDs returns the Message-IDs referenced from the References header(s),
10// with a fallback to the In-Reply-To header(s). The ids are canonicalized for
11// thread-matching, like with MessageIDCanonical. Empty message-id's are skipped.
12func ReferencedIDs(references []string, inReplyTo []string) ([]string, error) {
13 var refids []string // In thread-canonical form.
14
15 // parse and add 0 or 1 reference, returning the remaining refs string for a next attempt.
16 parse1 := func(refs string, one bool) string {
17 refs = strings.TrimLeft(refs, " \t\r\n")
18 if !strings.HasPrefix(refs, "<") {
19 // To make progress, we skip to next space or >.
20 i := strings.IndexAny(refs, " >")
21 if i < 0 {
22 return ""
23 }
24 return refs[i+1:]
25 }
26 refs = refs[1:]
27 // Look for the ending > or next <. If < is before >, this entry is truncated.
28 i := strings.IndexAny(refs, "<>")
29 if i < 0 {
30 return ""
31 }
32 if refs[i] == '<' {
33 // Truncated entry, we ignore it.
34 return refs[i:]
35 }
36 ref := strings.ToLower(refs[:i])
37 // Some MUAs wrap References line in the middle of message-id's, and others
38 // recombine them. Take out bare WSP in message-id's.
39 ref = strings.ReplaceAll(ref, " ", "")
40 ref = strings.ReplaceAll(ref, "\t", "")
41 refs = refs[i+1:]
42 // Canonicalize the quotedness of the message-id.
43 addr, err := smtp.ParseAddress(ref)
44 if err == nil {
45 // Leave the hostname form intact.
46 t := strings.Split(ref, "@")
47 ref = addr.Localpart.String() + "@" + t[len(t)-1]
48 }
49 // log.Errorx("assigning threads: bad reference in references header, using raw value", err, mlog.Field("msgid", mid), mlog.Field("reference", ref))
50 if ref != "" {
51 refids = append(refids, ref)
52 }
53 return refs
54 }
55
56 // References is the modern way (for a long time already) to reference ancestors.
57 // The direct parent is typically at the end of the list.
58 for _, refs := range references {
59 for refs != "" {
60 refs = parse1(refs, false)
61 }
62 }
63 // We only look at the In-Reply-To header if we didn't find any References.
64 if len(refids) == 0 {
65 for _, s := range inReplyTo {
66 parse1(s, true)
67 if len(refids) > 0 {
68 break
69 }
70 }
71 }
72
73 return refids, nil
74}
75