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  }