github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/email/lore/parse.go (about)

     1  // Copyright 2023 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  package lore
     5  
     6  import (
     7  	"regexp"
     8  	"sort"
     9  	"strings"
    10  
    11  	"github.com/google/syzkaller/dashboard/dashapi"
    12  	"github.com/google/syzkaller/pkg/email"
    13  )
    14  
    15  type Thread struct {
    16  	Subject   string
    17  	MessageID string
    18  	Type      dashapi.DiscussionType
    19  	BugIDs    []string
    20  	Messages  []*email.Email
    21  }
    22  
    23  // Threads extracts individual threads from a list of emails.
    24  func Threads(emails []*email.Email) []*Thread {
    25  	ctx := &parseCtx{
    26  		messages: map[string]*email.Email{},
    27  		next:     map[*email.Email][]*email.Email{},
    28  	}
    29  	for _, email := range emails {
    30  		ctx.record(email)
    31  	}
    32  	ctx.process()
    33  	return ctx.threads
    34  }
    35  
    36  // DiscussionType extracts the specific discussion type from an email.
    37  func DiscussionType(msg *email.Email) dashapi.DiscussionType {
    38  	discType := dashapi.DiscussionMention
    39  	if msg.OwnEmail {
    40  		discType = dashapi.DiscussionReport
    41  	}
    42  	// This is very crude, but should work for now.
    43  	if patchSubjectRe.MatchString(strings.ToLower(msg.Subject)) {
    44  		discType = dashapi.DiscussionPatch
    45  	} else if strings.Contains(msg.Subject, "Monthly") {
    46  		discType = dashapi.DiscussionReminder
    47  	}
    48  	return discType
    49  }
    50  
    51  var patchSubjectRe = regexp.MustCompile(`\[(?:(?:rfc|resend)\s+)*patch`)
    52  
    53  type parseCtx struct {
    54  	threads  []*Thread
    55  	messages map[string]*email.Email
    56  	next     map[*email.Email][]*email.Email
    57  }
    58  
    59  func (c *parseCtx) record(msg *email.Email) {
    60  	c.messages[msg.MessageID] = msg
    61  }
    62  
    63  func (c *parseCtx) process() {
    64  	// List messages for which we dont't have ancestors.
    65  	nodes := []*email.Email{}
    66  	for _, msg := range c.messages {
    67  		if msg.InReplyTo == "" || c.messages[msg.InReplyTo] == nil {
    68  			nodes = append(nodes, msg)
    69  		} else {
    70  			parent := c.messages[msg.InReplyTo]
    71  			c.next[parent] = append(c.next[parent], msg)
    72  		}
    73  	}
    74  	// Iterate starting from these tree nodes.
    75  	for _, node := range nodes {
    76  		c.visit(node, nil)
    77  	}
    78  	// Collect BugIDs.
    79  	for _, thread := range c.threads {
    80  		unique := map[string]struct{}{}
    81  		for _, msg := range thread.Messages {
    82  			for _, id := range msg.BugIDs {
    83  				unique[id] = struct{}{}
    84  			}
    85  		}
    86  		var ids []string
    87  		for id := range unique {
    88  			ids = append(ids, id)
    89  		}
    90  		sort.Strings(ids)
    91  		thread.BugIDs = ids
    92  	}
    93  }
    94  
    95  func (c *parseCtx) visit(msg *email.Email, thread *Thread) {
    96  	var oldInfo *email.OldThreadInfo
    97  	if thread != nil {
    98  		oldInfo = &email.OldThreadInfo{
    99  			ThreadType: thread.Type,
   100  		}
   101  	}
   102  	msgType := DiscussionType(msg)
   103  	switch email.NewMessageAction(msg, msgType, oldInfo) {
   104  	case email.ActionIgnore:
   105  		thread = nil
   106  	case email.ActionAppend:
   107  		thread.Messages = append(thread.Messages, msg)
   108  	case email.ActionNewThread:
   109  		thread = &Thread{
   110  			MessageID: msg.MessageID,
   111  			Subject:   msg.Subject,
   112  			Type:      msgType,
   113  			Messages:  []*email.Email{msg},
   114  		}
   115  		c.threads = append(c.threads, thread)
   116  	}
   117  	for _, nextMsg := range c.next[msg] {
   118  		c.visit(nextMsg, thread)
   119  	}
   120  }