aboutsummaryrefslogtreecommitdiffstats
path: root/syz-cluster/email-reporter/handler.go
diff options
context:
space:
mode:
authorAleksandr Nogikh <nogikh@google.com>2025-04-17 15:53:07 +0200
committerTaras Madan <tarasmadan@google.com>2025-05-09 08:56:10 +0000
commit6f914b33a42faaa48f4d49115e69fb8bfd0e58eb (patch)
tree0556d464e9b681adb6a2fa190edd7253582e5662 /syz-cluster/email-reporter/handler.go
parent807b3db2ee615b6c4ab080661c9a02d61d148c3e (diff)
syz-cluster: add email-reporter component
Add the high-level logic for the email-reporter component that will be responsible for sending bug reports and reacting to incoming emails.
Diffstat (limited to 'syz-cluster/email-reporter/handler.go')
-rw-r--r--syz-cluster/email-reporter/handler.go143
1 files changed, 143 insertions, 0 deletions
diff --git a/syz-cluster/email-reporter/handler.go b/syz-cluster/email-reporter/handler.go
new file mode 100644
index 000000000..a956b77f9
--- /dev/null
+++ b/syz-cluster/email-reporter/handler.go
@@ -0,0 +1,143 @@
+// Copyright 2025 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 main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/google/syzkaller/pkg/email"
+ "github.com/google/syzkaller/syz-cluster/pkg/api"
+ "github.com/google/syzkaller/syz-cluster/pkg/app"
+ "github.com/google/syzkaller/syz-cluster/pkg/report"
+)
+
+type EmailToSend struct {
+ To []string
+ Cc []string
+ Subject string
+ InReplyTo string
+ Body []byte
+}
+
+type SendEmailCb func(context.Context, *EmailToSend) (string, error)
+
+type Handler struct {
+ apiClient *api.ReporterClient
+ emailConfig *app.EmailConfig
+ sender SendEmailCb
+}
+
+func (h *Handler) PollReportsLoop(ctx context.Context, pollPeriod time.Duration) {
+ for {
+ _, err := h.PollAndReport(ctx)
+ if err != nil {
+ app.Errorf("%v", err)
+ }
+ select {
+ case <-ctx.Done():
+ return
+ case <-time.After(pollPeriod):
+ }
+ }
+}
+
+func (h *Handler) PollAndReport(ctx context.Context) (*api.SessionReport, error) {
+ reply, err := h.apiClient.GetNextReport(ctx, api.EmailReporter)
+ if err != nil {
+ return nil, fmt.Errorf("failed to poll the next report: %w", err)
+ } else if reply == nil || reply.Report == nil {
+ return nil, nil
+ }
+ report := reply.Report
+ log.Printf("report %q is to be sent", report.ID)
+ if err := h.report(ctx, report); err != nil {
+ // TODO: consider retrying if the error happened before we attempted
+ // to actually send the message.
+ return nil, fmt.Errorf("failed to report %q: %w", report.ID, err)
+ }
+ return report, nil
+}
+
+func (h *Handler) report(ctx context.Context, rep *api.SessionReport) error {
+ // Start by confirming the report - it's better to not send an email at all than to send it multiple times.
+ err := h.apiClient.ConfirmReport(ctx, rep.ID)
+ if err != nil {
+ return fmt.Errorf("failed to confirm: %w", err)
+ }
+
+ // Construct and send the message.
+ body, err := report.Render(rep, h.emailConfig)
+ if err != nil {
+ // This should never be happening..
+ return fmt.Errorf("failed to render the template: %w", err)
+ }
+ toSend := &EmailToSend{
+ Subject: "Re: " + rep.Series.Title, // TODO: use the original rather than the stripped title.
+ To: rep.Cc,
+ Body: body,
+ }
+ if rep.Moderation {
+ toSend.To = []string{h.emailConfig.ModerationList}
+ } else {
+ // We assume that email reporting is used for series received over emails.
+ toSend.InReplyTo = rep.Series.ExtID
+ toSend.To = rep.Cc
+ toSend.Cc = []string{h.emailConfig.ArchiveList}
+ }
+ msgID, err := h.sender(ctx, toSend)
+ if err != nil {
+ return fmt.Errorf("failed to send: %w", err)
+ }
+
+ // Now that the report is sent, update the link to the email discussion.
+ err = h.apiClient.UpdateReport(ctx, rep.ID, &api.UpdateReportReq{
+ // TODO: for Lore emails, set Link = lore.Link(msgID).
+ MessageID: msgID,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to update: %w", err)
+ }
+ return nil
+}
+
+// IncomingEmail assumes that the related report ID is already extracted and resides in msg.BugIDs.
+func (h *Handler) IncomingEmail(ctx context.Context, msg *email.Email) error {
+ if len(msg.BugIDs) == 0 {
+ // Unrelated email.
+ return nil
+ }
+ reportID := msg.BugIDs[0]
+
+ var reply string
+ for _, command := range msg.Commands {
+ switch command.Command {
+ case email.CmdUpstream:
+ err := h.apiClient.UpstreamReport(ctx, reportID, &api.UpstreamReportReq{
+ User: msg.Author,
+ })
+ if err != nil {
+ reply = fmt.Sprintf("Failed to process the command. Contact %s.",
+ h.emailConfig.SupportEmail)
+ }
+ // Reply nothing on success.
+ default:
+ reply = "Unknown command"
+ }
+ }
+
+ if reply == "" {
+ return nil
+ }
+ _, err := h.sender(ctx, &EmailToSend{
+ To: []string{msg.Author},
+ Cc: msg.Cc,
+ Subject: "Re: " + msg.Subject,
+ InReplyTo: msg.MessageID,
+ Body: []byte(email.FormReply(msg, reply)),
+ })
+ return err
+}