summaryrefslogtreecommitdiff
path: root/cmd/generate/mailstuff/thread.go
blob: 2cdf9a45e436ff912d990626699ca82bf3836689 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
}