package mailstuff import ( "fmt" "net/mail" "regexp" "strings" ) type Set[T comparable] map[T]struct{} func (s Set[T]) Insert(val T) { s[val] = struct{}{} } func mapHas[K comparable, V any](m map[K]V, k K) bool { _, ok := m[k] return ok } func (s Set[T]) Has(val T) bool { return mapHas(s, val) } func (s Set[T]) PickOne() T { for v := range s { return v } var zero T return zero } type MessageID string type ThreadedMessage struct { *mail.Message Parent *ThreadedMessage Children Set[*ThreadedMessage] } var reReplyID = regexp.MustCompile("<[^> \t\r\n]+>") func rfc2822parse(msg *mail.Message) *jwzMessage { // TODO: This is bad, and needs a real implementation. ret := &jwzMessage{ Subject: msg.Header.Get("Subject"), ID: jwzID(msg.Header.Get("Message-ID")), } refIDs := strings.Fields(msg.Header.Get("References")) strings.Fields(msg.Header.Get("References")) if replyID := reReplyID.FindString(msg.Header.Get("In-Reply-To")); replyID != "" { refIDs = append(refIDs, replyID) } ret.References = make([]jwzID, len(refIDs)) for i := range refIDs { ret.References[i] = jwzID(refIDs[i]) } return ret } func ThreadMessages(msgs []*mail.Message) (Set[*ThreadedMessage], map[MessageID]*ThreadedMessage) { jwzMsgs := make(map[jwzID]*jwzMessage, len(msgs)) retMsgs := make(map[jwzID]*ThreadedMessage, len(msgs)) bogusCnt := 0 for _, msg := range msgs { jwzMsg := rfc2822parse(msg) // RFC 5256: // // If a message does not contain a Message-ID header // line, or the Message-ID header line does not // contain a valid Message ID, then assign a unique // Message ID to this message. // // If two or more messages have the same Message ID, // then only use that Message ID in the first (lowest // sequence number) message, and assign a unique // Message ID to each of the subsequent messages with // a duplicate of that Message ID. for jwzMsg.ID == "" || mapHas(jwzMsgs, jwzMsg.ID) { jwzMsg.ID = jwzID(fmt.Sprintf("bogus.%d", bogusCnt)) bogusCnt++ } jwzMsgs[jwzMsg.ID] = jwzMsg retMsgs[jwzMsg.ID] = &ThreadedMessage{ Message: msg, } } jwzThreads := jwzThreadMessages(jwzMsgs) var convertMessage func(*jwzContainer) *ThreadedMessage convertMessage = func(in *jwzContainer) *ThreadedMessage { var out *ThreadedMessage if in.Message == nil { out = new(ThreadedMessage) } else { out = retMsgs[in.Message.ID] } out.Children = make(Set[*ThreadedMessage], len(in.Children)) for inChild := range in.Children { outChild := convertMessage(inChild) out.Children.Insert(outChild) outChild.Parent = out } return out } retThreads := make(Set[*ThreadedMessage], len(jwzThreads)) for inThread := range jwzThreads { retThreads.Insert(convertMessage(inThread)) } return retThreads, retMsgs }