aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/email/lore/parse.go
blob: d0e4d4fe242f7ae8e0e5eed57bf56534a85d2703 (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
115
116
117
118
119
120
// Copyright 2023 syzkaller project authors. All rights reserved.
// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.

package lore

import (
	"regexp"
	"sort"
	"strings"

	"github.com/google/syzkaller/dashboard/dashapi"
	"github.com/google/syzkaller/pkg/email"
)

type Thread struct {
	Subject   string
	MessageID string
	Type      dashapi.DiscussionType
	BugIDs    []string
	Messages  []*email.Email
}

// Threads extracts individual threads from a list of emails.
func Threads(emails []*email.Email) []*Thread {
	ctx := &parseCtx{
		messages: map[string]*email.Email{},
		next:     map[*email.Email][]*email.Email{},
	}
	for _, email := range emails {
		ctx.record(email)
	}
	ctx.process()
	return ctx.threads
}

// DiscussionType extracts the specific discussion type from an email.
func DiscussionType(msg *email.Email) dashapi.DiscussionType {
	discType := dashapi.DiscussionMention
	if msg.OwnEmail {
		discType = dashapi.DiscussionReport
	}
	// This is very crude, but should work for now.
	if patchSubjectRe.MatchString(strings.ToLower(msg.Subject)) {
		discType = dashapi.DiscussionPatch
	} else if strings.Contains(msg.Subject, "Monthly") {
		discType = dashapi.DiscussionReminder
	}
	return discType
}

var patchSubjectRe = regexp.MustCompile(`\[(?:(?:rfc|resend)\s+)*patch`)

type parseCtx struct {
	threads  []*Thread
	messages map[string]*email.Email
	next     map[*email.Email][]*email.Email
}

func (c *parseCtx) record(msg *email.Email) {
	c.messages[msg.MessageID] = msg
}

func (c *parseCtx) process() {
	// List messages for which we dont't have ancestors.
	nodes := []*email.Email{}
	for _, msg := range c.messages {
		if msg.InReplyTo == "" || c.messages[msg.InReplyTo] == nil {
			nodes = append(nodes, msg)
		} else {
			parent := c.messages[msg.InReplyTo]
			c.next[parent] = append(c.next[parent], msg)
		}
	}
	// Iterate starting from these tree nodes.
	for _, node := range nodes {
		c.visit(node, nil)
	}
	// Collect BugIDs.
	for _, thread := range c.threads {
		unique := map[string]struct{}{}
		for _, msg := range thread.Messages {
			for _, id := range msg.BugIDs {
				unique[id] = struct{}{}
			}
		}
		var ids []string
		for id := range unique {
			ids = append(ids, id)
		}
		sort.Strings(ids)
		thread.BugIDs = ids
	}
}

func (c *parseCtx) visit(msg *email.Email, thread *Thread) {
	var oldInfo *email.OldThreadInfo
	if thread != nil {
		oldInfo = &email.OldThreadInfo{
			ThreadType: thread.Type,
		}
	}
	msgType := DiscussionType(msg)
	switch email.NewMessageAction(msg, msgType, oldInfo) {
	case email.ActionIgnore:
		thread = nil
	case email.ActionAppend:
		thread.Messages = append(thread.Messages, msg)
	case email.ActionNewThread:
		thread = &Thread{
			MessageID: msg.MessageID,
			Subject:   msg.Subject,
			Type:      msgType,
			Messages:  []*email.Email{msg},
		}
		c.threads = append(c.threads, thread)
	}
	for _, nextMsg := range c.next[msg] {
		c.visit(nextMsg, thread)
	}
}