github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/syz-cluster/email-reporter/handler.go (about) 1 // Copyright 2025 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 main 5 6 import ( 7 "context" 8 "fmt" 9 "log" 10 "strings" 11 "time" 12 13 "github.com/google/syzkaller/pkg/email" 14 15 "github.com/google/syzkaller/syz-cluster/pkg/api" 16 "github.com/google/syzkaller/syz-cluster/pkg/app" 17 "github.com/google/syzkaller/syz-cluster/pkg/emailclient" 18 "github.com/google/syzkaller/syz-cluster/pkg/report" 19 ) 20 21 type Handler struct { 22 reporter string 23 apiClient *api.ReporterClient 24 emailConfig *app.EmailConfig 25 sender emailclient.Sender 26 } 27 28 func (h *Handler) PollReportsLoop(ctx context.Context, pollPeriod time.Duration) { 29 defer log.Printf("reporter server polling aborted") 30 log.Printf("reporter server polling started") 31 32 for { 33 _, err := h.PollAndReport(ctx) 34 if err != nil { 35 app.Errorf("%v", err) 36 } 37 select { 38 case <-ctx.Done(): 39 return 40 case <-time.After(pollPeriod): 41 } 42 } 43 } 44 45 func (h *Handler) PollAndReport(ctx context.Context) (*api.SessionReport, error) { 46 reply, err := h.apiClient.GetNextReport(ctx, h.reporter) 47 if err != nil { 48 return nil, fmt.Errorf("failed to poll the next report: %w", err) 49 } else if reply == nil || reply.Report == nil { 50 return nil, nil 51 } 52 report := reply.Report 53 log.Printf("report %q is to be sent", report.ID) 54 if err := h.report(ctx, report); err != nil { 55 // TODO: consider retrying if the error happened before we attempted 56 // to actually send the message. 57 return nil, fmt.Errorf("failed to report %q: %w", report.ID, err) 58 } 59 return report, nil 60 } 61 62 func (h *Handler) report(ctx context.Context, rep *api.SessionReport) error { 63 // Start by confirming the report - it's better to not send an email at all than to send it multiple times. 64 err := h.apiClient.ConfirmReport(ctx, rep.ID) 65 if err != nil { 66 return fmt.Errorf("failed to confirm: %w", err) 67 } 68 69 // Construct and send the message. 70 body, err := report.Render(rep, h.emailConfig) 71 if err != nil { 72 // This should never be happening.. 73 return fmt.Errorf("failed to render the template: %w", err) 74 } 75 toSend := &emailclient.Email{ 76 Subject: "Re: " + rep.Series.Title, // TODO: use the original rather than the stripped title. 77 To: rep.Series.Cc, 78 Body: body, 79 Cc: []string{h.emailConfig.ArchiveList}, 80 BugID: rep.ID, 81 } 82 if rep.Moderation { 83 toSend.To = []string{h.emailConfig.ModerationList} 84 toSend.Subject = "[moderation/CI] " + toSend.Subject 85 } else { 86 if h.emailConfig.Name != "" { 87 toSend.Subject = fmt.Sprintf("[%s] %s", h.emailConfig.Name, toSend.Subject) 88 } 89 // We assume that email reporting is used for series received over emails. 90 toSend.InReplyTo = rep.Series.ExtID 91 toSend.To = rep.Series.Cc 92 toSend.Cc = append(toSend.Cc, h.emailConfig.ReportCC...) 93 } 94 msgID, err := h.sender(ctx, toSend) 95 if err != nil { 96 return fmt.Errorf("failed to send: %w", err) 97 } 98 // Senders may not always know the MessageID of the newly sent messages (that's the case of dashapi). 99 if msgID != "" { 100 // Record MessageID so that we could later trace user replies back to it. 101 _, err = h.apiClient.RecordReply(ctx, &api.RecordReplyReq{ 102 // TODO: for Lore emails, set Link = lore.Link(msgID). 103 MessageID: msgID, 104 Time: time.Now(), 105 ReportID: rep.ID, 106 Reporter: h.reporter, 107 }) 108 if err != nil { 109 return fmt.Errorf("failed to record the reply: %w", err) 110 } 111 } 112 return nil 113 } 114 115 // IncomingEmail assumes that the related report ID is already extracted and resides in msg.BugIDs. 116 func (h *Handler) IncomingEmail(ctx context.Context, msg *email.Email) error { 117 if len(msg.BugIDs) == 0 { 118 // Unrelated email. 119 return nil 120 } 121 if msg.OwnEmail && !strings.HasPrefix(msg.Subject, email.ForwardedPrefix) { 122 // We normally ignore our own emails, with the exception of the emails forwarded from the dashboard. 123 return nil 124 } 125 reportID := msg.BugIDs[0] 126 127 var reply string 128 for _, command := range msg.Commands { 129 var err error 130 switch command.Command { 131 case email.CmdUpstream: 132 // Reply nothing on success. 133 err = h.apiClient.UpstreamReport(ctx, reportID, &api.UpstreamReportReq{ 134 User: msg.Author, 135 }) 136 case email.CmdInvalid: 137 // Reply nothing on success. 138 err = h.apiClient.InvalidateReport(ctx, reportID) 139 default: 140 reply = "Unknown command" 141 } 142 if err != nil { 143 reply = fmt.Sprintf("Failed to process the command. Contact %s.", 144 h.emailConfig.SupportEmail) 145 } 146 } 147 148 if reply == "" { 149 return nil 150 } 151 _, err := h.sender(ctx, &emailclient.Email{ 152 To: []string{msg.Author}, 153 Cc: msg.Cc, 154 Subject: "Re: " + msg.Subject, 155 InReplyTo: msg.MessageID, 156 Body: []byte(email.FormReply(msg, reply)), 157 }) 158 return err 159 }