github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/dashboard/app/reporting_email.go (about)

     1  // Copyright 2017 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  	"bytes"
     8  	"context"
     9  	"encoding/json"
    10  	"errors"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/mail"
    15  	"regexp"
    16  	"sort"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"text/tabwriter"
    21  	"time"
    22  
    23  	"github.com/google/syzkaller/dashboard/dashapi"
    24  	"github.com/google/syzkaller/pkg/email"
    25  	"github.com/google/syzkaller/pkg/email/lore"
    26  	"github.com/google/syzkaller/pkg/html"
    27  	"github.com/google/syzkaller/sys/targets"
    28  	"google.golang.org/appengine/v2"
    29  	db "google.golang.org/appengine/v2/datastore"
    30  	"google.golang.org/appengine/v2/log"
    31  	aemail "google.golang.org/appengine/v2/mail"
    32  )
    33  
    34  // Email reporting interface.
    35  
    36  func initEmailReporting() {
    37  	http.HandleFunc("/cron/email_poll", handleEmailPoll)
    38  	http.HandleFunc("/_ah/mail/", handleIncomingMail)
    39  	http.HandleFunc("/_ah/bounce", handleEmailBounce)
    40  
    41  	mailingLists = make(map[string]bool)
    42  	for _, cfg := range getConfig(context.Background()).Namespaces {
    43  		for _, reporting := range cfg.Reporting {
    44  			if cfg, ok := reporting.Config.(*EmailConfig); ok {
    45  				mailingLists[email.CanonicalEmail(cfg.Email)] = true
    46  			}
    47  		}
    48  	}
    49  }
    50  
    51  const (
    52  	emailType = "email"
    53  	// This plays an important role at least for job replies.
    54  	// If we CC a kernel mailing list and it uses Patchwork,
    55  	// then any emails with a patch attached create a new patch
    56  	// entry pending for review. The prefix makes Patchwork
    57  	// treat it as a comment for a previous patch.
    58  	replySubjectPrefix = "Re: "
    59  
    60  	replyNoBugID = "I see the command but can't find the corresponding bug.\n" +
    61  		"Please resend the email to %[1]v address\n" +
    62  		"that is the sender of the bug report (also present in the Reported-by tag)."
    63  	replyAmbiguousBugID = "I see the command, but I cannot identify the bug that was meant.\n" +
    64  		"Several bugs with the exact same title were earlier sent to the mailing list.\n" +
    65  		"Please resend the email to %[1]v address\n" +
    66  		"that is the sender of the original bug report (also present in the Reported-by tag)."
    67  	replyBadBugID = "I see the command but can't find the corresponding bug.\n" +
    68  		"The email is sent to  %[1]v address\n" +
    69  		"but the HASH does not correspond to any known bug.\n" +
    70  		"Please double check the address."
    71  )
    72  
    73  var mailingLists map[string]bool
    74  
    75  type EmailConfig struct {
    76  	Email              string
    77  	HandleListEmails   bool // This is a temporary option to simplify the feature deployment.
    78  	MailMaintainers    bool
    79  	DefaultMaintainers []string
    80  	SubjectPrefix      string
    81  }
    82  
    83  func (cfg *EmailConfig) Type() string {
    84  	return emailType
    85  }
    86  
    87  func (cfg *EmailConfig) Validate() error {
    88  	if _, err := mail.ParseAddress(cfg.Email); err != nil {
    89  		return fmt.Errorf("bad email address %q: %w", cfg.Email, err)
    90  	}
    91  	for _, email := range cfg.DefaultMaintainers {
    92  		if _, err := mail.ParseAddress(email); err != nil {
    93  			return fmt.Errorf("bad email address %q: %w", email, err)
    94  		}
    95  	}
    96  	if cfg.MailMaintainers && len(cfg.DefaultMaintainers) == 0 {
    97  		return fmt.Errorf("email config: MailMaintainers is set but no DefaultMaintainers")
    98  	}
    99  	if cfg.SubjectPrefix != strings.TrimSpace(cfg.SubjectPrefix) {
   100  		return fmt.Errorf("email config: subject prefix %q contains leading/trailing spaces", cfg.SubjectPrefix)
   101  	}
   102  	return nil
   103  }
   104  
   105  // handleEmailPoll is called by cron and sends emails for new bugs, if any.
   106  func handleEmailPoll(w http.ResponseWriter, r *http.Request) {
   107  	c := appengine.NewContext(r)
   108  	stop, err := emergentlyStopped(c)
   109  	if err != nil {
   110  		log.Errorf(c, "emergency stop querying failed: %v", err)
   111  		http.Error(w, err.Error(), http.StatusInternalServerError)
   112  		return
   113  	}
   114  	if stop {
   115  		log.Errorf(c, "aborting email poll due to an emergency stop")
   116  		return
   117  	}
   118  	if err := emailPollJobs(c); err != nil {
   119  		log.Errorf(c, "job poll failed: %v", err)
   120  		http.Error(w, err.Error(), http.StatusInternalServerError)
   121  		return
   122  	}
   123  	if err := emailPollNotifications(c); err != nil {
   124  		log.Errorf(c, "notif poll failed: %v", err)
   125  		http.Error(w, err.Error(), http.StatusInternalServerError)
   126  		return
   127  	}
   128  	if err := emailPollBugs(c); err != nil {
   129  		log.Errorf(c, "bug poll failed: %v", err)
   130  		http.Error(w, err.Error(), http.StatusInternalServerError)
   131  		return
   132  	}
   133  	if err := emailPollBugLists(c); err != nil {
   134  		log.Errorf(c, "bug list poll failed: %v", err)
   135  		http.Error(w, err.Error(), http.StatusInternalServerError)
   136  		return
   137  	}
   138  	w.Write([]byte("OK"))
   139  }
   140  
   141  func emailPollBugLists(c context.Context) error {
   142  	reports := reportingPollBugLists(c, emailType)
   143  	for _, rep := range reports {
   144  		if err := emailSendBugListReport(c, rep); err != nil {
   145  			log.Errorf(c, "emailPollBugLists: %v", err)
   146  		}
   147  	}
   148  	return nil
   149  }
   150  
   151  func emailPollBugs(c context.Context) error {
   152  	reports := reportingPollBugs(c, emailType)
   153  	for _, rep := range reports {
   154  		if err := emailSendBugReport(c, rep); err != nil {
   155  			log.Errorf(c, "emailPollBugs: %v", err)
   156  		}
   157  	}
   158  	return nil
   159  }
   160  
   161  func emailSendBugReport(c context.Context, rep *dashapi.BugReport) error {
   162  	cfg := new(EmailConfig)
   163  	if err := json.Unmarshal(rep.Config, cfg); err != nil {
   164  		return fmt.Errorf("failed to unmarshal email config: %w", err)
   165  	}
   166  	if err := emailReport(c, rep); err != nil {
   167  		return fmt.Errorf("failed to report bug: %w", err)
   168  	}
   169  	cmd := &dashapi.BugUpdate{
   170  		ID:         rep.ID,
   171  		Status:     dashapi.BugStatusOpen,
   172  		ReproLevel: dashapi.ReproLevelNone,
   173  		CrashID:    rep.CrashID,
   174  	}
   175  	if len(rep.ReproC) != 0 {
   176  		cmd.ReproLevel = dashapi.ReproLevelC
   177  	} else if len(rep.ReproSyz) != 0 {
   178  		cmd.ReproLevel = dashapi.ReproLevelSyz
   179  	}
   180  	for label := range rep.LabelMessages {
   181  		cmd.Labels = append(cmd.Labels, label)
   182  	}
   183  	ok, reason, err := incomingCommand(c, cmd)
   184  	if !ok || err != nil {
   185  		return fmt.Errorf("failed to update reported bug: ok=%v reason=%v err=%w", ok, reason, err)
   186  	}
   187  	return nil
   188  }
   189  
   190  func emailSendBugListReport(c context.Context, rep *dashapi.BugListReport) error {
   191  	cfg := new(EmailConfig)
   192  	if err := json.Unmarshal(rep.Config, cfg); err != nil {
   193  		return fmt.Errorf("failed to unmarshal email config: %w", err)
   194  	}
   195  	err := emailListReport(c, rep, cfg)
   196  	if err != nil {
   197  		return fmt.Errorf("failed to send the bug list message: %w", err)
   198  	}
   199  	upd := &dashapi.BugListUpdate{
   200  		ID:      rep.ID,
   201  		Command: dashapi.BugListSentCmd,
   202  	}
   203  	_, err = reportingBugListCommand(c, upd)
   204  	if err != nil {
   205  		return fmt.Errorf("failed to update the bug list: %w", err)
   206  	}
   207  	return nil
   208  }
   209  
   210  func emailPollNotifications(c context.Context) error {
   211  	notifs := reportingPollNotifications(c, emailType)
   212  	for _, notif := range notifs {
   213  		if err := emailSendBugNotif(c, notif); err != nil {
   214  			log.Errorf(c, "emailPollNotifications: %v", err)
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  func emailSendBugNotif(c context.Context, notif *dashapi.BugNotification) error {
   221  	status, body := dashapi.BugStatusOpen, ""
   222  	var statusReason dashapi.BugStatusReason
   223  	switch notif.Type {
   224  	case dashapi.BugNotifUpstream:
   225  		body = "Sending this report to the next reporting stage."
   226  		status = dashapi.BugStatusUpstream
   227  	case dashapi.BugNotifBadCommit:
   228  		var err error
   229  		body, err = buildBadCommitMessage(c, notif)
   230  		if err != nil {
   231  			return err
   232  		}
   233  	case dashapi.BugNotifObsoleted:
   234  		body = "Auto-closing this bug as obsolete.\n"
   235  		statusReason = dashapi.BugStatusReason(notif.Text)
   236  		if statusReason == dashapi.InvalidatedByRevokedRepro {
   237  			body += "No recent activity, existing reproducers are no longer triggering the issue."
   238  		} else {
   239  			body += "Crashes did not happen for a while, no reproducer and no activity."
   240  		}
   241  		status = dashapi.BugStatusInvalid
   242  	case dashapi.BugNotifLabel:
   243  		bodyBuf := new(bytes.Buffer)
   244  		if err := mailTemplates.ExecuteTemplate(bodyBuf, "mail_label_notif.txt", notif); err != nil {
   245  			return fmt.Errorf("failed to execute mail_label_notif.txt: %w", err)
   246  		}
   247  		body = bodyBuf.String()
   248  	default:
   249  		return fmt.Errorf("bad notification type %v", notif.Type)
   250  	}
   251  	cfg := new(EmailConfig)
   252  	if err := json.Unmarshal(notif.Config, cfg); err != nil {
   253  		return fmt.Errorf("failed to unmarshal email config: %w", err)
   254  	}
   255  	to := email.MergeEmailLists([]string{cfg.Email}, notif.CC)
   256  	if cfg.MailMaintainers && notif.Public {
   257  		to = email.MergeEmailLists(to, notif.Maintainers, cfg.DefaultMaintainers)
   258  	}
   259  	from, err := email.AddAddrContext(fromAddr(c), notif.ID)
   260  	if err != nil {
   261  		return err
   262  	}
   263  	log.Infof(c, "sending notif %v for %q to %q: %v", notif.Type, notif.Title, to, body)
   264  	if err := sendMailText(c, cfg, notif.Title, from, to, notif.ExtID, body); err != nil {
   265  		return err
   266  	}
   267  	cmd := &dashapi.BugUpdate{
   268  		ID:           notif.ID,
   269  		Status:       status,
   270  		StatusReason: statusReason,
   271  		Notification: true,
   272  	}
   273  	if notif.Label != "" {
   274  		cmd.Labels = []string{notif.Label}
   275  	}
   276  	ok, reason, err := incomingCommand(c, cmd)
   277  	if !ok || err != nil {
   278  		return fmt.Errorf("notif update failed: ok=%v reason=%v err=%w", ok, reason, err)
   279  	}
   280  	return nil
   281  }
   282  
   283  func buildBadCommitMessage(c context.Context, notif *dashapi.BugNotification) (string, error) {
   284  	var sb strings.Builder
   285  	days := int(notifyAboutBadCommitPeriod / time.Hour / 24)
   286  	nsConfig := getNsConfig(c, notif.Namespace)
   287  	fmt.Fprintf(&sb, `This bug is marked as fixed by commit:
   288  %v
   289  
   290  But I can't find it in the tested trees[1] for more than %v days.
   291  Is it a correct commit? Please update it by replying:
   292  
   293  #syz fix: exact-commit-title
   294  
   295  Until then the bug is still considered open and new crashes with
   296  the same signature are ignored.
   297  
   298  Kernel: %s
   299  Dashboard link: %s
   300  
   301  ---
   302  [1] I expect the commit to be present in:
   303  `, notif.Text, days, nsConfig.DisplayTitle, notif.Link)
   304  
   305  	repos, err := loadRepos(c, notif.Namespace)
   306  	if err != nil {
   307  		return "", err
   308  	}
   309  	const maxShow = 4
   310  	for i, repo := range repos {
   311  		if i >= maxShow {
   312  			break
   313  		}
   314  		fmt.Fprintf(&sb, "\n%d. %s branch of\n%s\n", i+1, repo.Branch, repo.URL)
   315  	}
   316  	if len(repos) > maxShow {
   317  		fmt.Fprintf(&sb, "\nThe full list of %d trees can be found at\n%s\n",
   318  			len(repos), fmt.Sprintf("%v/%v/repos", appURL(c), notif.Namespace))
   319  	}
   320  	return sb.String(), nil
   321  }
   322  
   323  func emailPollJobs(c context.Context) error {
   324  	jobs, err := pollCompletedJobs(c, emailType)
   325  	if err != nil {
   326  		return err
   327  	}
   328  	for _, job := range jobs {
   329  		if err := emailReport(c, job); err != nil {
   330  			log.Errorf(c, "failed to report job: %v", err)
   331  			continue
   332  		}
   333  		if err := jobReported(c, job.JobID); err != nil {
   334  			log.Errorf(c, "failed to mark job reported: %v", err)
   335  			continue
   336  		}
   337  	}
   338  	return nil
   339  }
   340  
   341  func emailReport(c context.Context, rep *dashapi.BugReport) error {
   342  	cfg := new(EmailConfig)
   343  	if err := json.Unmarshal(rep.Config, cfg); err != nil {
   344  		return fmt.Errorf("failed to unmarshal email config: %w", err)
   345  	}
   346  	if rep.UserSpaceArch == targets.AMD64 {
   347  		// This is default, so don't include the info.
   348  		rep.UserSpaceArch = ""
   349  	}
   350  	templ := ""
   351  	switch rep.Type {
   352  	case dashapi.ReportNew, dashapi.ReportRepro:
   353  		templ = "mail_bug.txt"
   354  	case dashapi.ReportTestPatch:
   355  		templ = "mail_test_result.txt"
   356  		cfg.MailMaintainers = false
   357  	case dashapi.ReportBisectCause:
   358  		templ = "mail_bisect_result.txt"
   359  	case dashapi.ReportBisectFix:
   360  		if rep.BisectFix.CrossTree {
   361  			templ = "mail_fix_candidate.txt"
   362  			if rep.BisectFix.Commit == nil {
   363  				return fmt.Errorf("reporting failed fix candidate bisection for %s", rep.ID)
   364  			}
   365  		} else {
   366  			templ = "mail_bisect_result.txt"
   367  		}
   368  	default:
   369  		return fmt.Errorf("unknown report type %v", rep.Type)
   370  	}
   371  	return sendMailTemplate(c, &mailSendParams{
   372  		templateName: templ,
   373  		templateArg:  rep,
   374  		cfg:          cfg,
   375  		title:        generateEmailBugTitle(rep, cfg),
   376  		reportID:     rep.ID,
   377  		replyTo:      rep.ExtID,
   378  		cc:           rep.CC,
   379  		maintainers:  rep.Maintainers,
   380  	})
   381  }
   382  
   383  func emailListReport(c context.Context, rep *dashapi.BugListReport, cfg *EmailConfig) error {
   384  	if rep.Moderation {
   385  		cfg.MailMaintainers = false
   386  	}
   387  	args := struct {
   388  		*dashapi.BugListReport
   389  		Table string
   390  	}{BugListReport: rep}
   391  
   392  	var b bytes.Buffer
   393  	w := tabwriter.NewWriter(&b, 0, 0, 1, ' ', 0)
   394  	fmt.Fprintln(w, "Ref\tCrashes\tRepro\tTitle")
   395  	for i, bug := range rep.Bugs {
   396  		repro := "No"
   397  		if bug.ReproLevel > dashapi.ReproLevelNone {
   398  			repro = "Yes"
   399  		}
   400  		fmt.Fprintf(w, "<%d>\t%d\t%s\t%s\n", i+1, bug.Hits, repro, bug.Title)
   401  		fmt.Fprintf(w, "\t\t\t%s\n", bug.Link)
   402  	}
   403  	w.Flush()
   404  	args.Table = b.String()
   405  
   406  	return sendMailTemplate(c, &mailSendParams{
   407  		templateName: "mail_subsystem.txt",
   408  		templateArg:  args,
   409  		cfg:          cfg,
   410  		title: fmt.Sprintf("Monthly %s report (%s)",
   411  			rep.Subsystem, rep.Created.Format("Jan 2006")),
   412  		reportID:    rep.ID,
   413  		maintainers: rep.Maintainers,
   414  	})
   415  }
   416  
   417  type mailSendParams struct {
   418  	templateName string
   419  	templateArg  any
   420  	cfg          *EmailConfig
   421  	title        string
   422  	reportID     string
   423  	replyTo      string
   424  	cc           []string
   425  	maintainers  []string
   426  }
   427  
   428  func sendMailTemplate(c context.Context, params *mailSendParams) error {
   429  	cfg := params.cfg
   430  	to := email.MergeEmailLists([]string{cfg.Email}, params.cc)
   431  	if cfg.MailMaintainers {
   432  		to = email.MergeEmailLists(to, params.maintainers, cfg.DefaultMaintainers)
   433  	}
   434  	from, err := email.AddAddrContext(fromAddr(c), params.reportID)
   435  	if err != nil {
   436  		return err
   437  	}
   438  	body := new(bytes.Buffer)
   439  	if err := mailTemplates.ExecuteTemplate(body, params.templateName, params.templateArg); err != nil {
   440  		return fmt.Errorf("failed to execute %v template: %w", params.templateName, err)
   441  	}
   442  	log.Infof(c, "sending email %q to %q", params.title, to)
   443  	return sendMailText(c, params.cfg, params.title, from, to, params.replyTo, body.String())
   444  }
   445  func generateEmailBugTitle(rep *dashapi.BugReport, emailConfig *EmailConfig) string {
   446  	title := ""
   447  	for i := len(rep.Subsystems) - 1; i >= 0; i-- {
   448  		question := ""
   449  		if rep.Subsystems[i].SetBy == "" {
   450  			// Include the question mark for automatically created tags.
   451  			question = "?"
   452  		}
   453  		title = fmt.Sprintf("[%s%s] %s", rep.Subsystems[i].Name, question, title)
   454  	}
   455  	return title + rep.Title
   456  }
   457  
   458  // handleIncomingMail is the entry point for incoming emails.
   459  func handleIncomingMail(w http.ResponseWriter, r *http.Request) {
   460  	c := appengine.NewContext(r)
   461  	url := r.URL.RequestURI()
   462  	myEmail := ""
   463  	if index := strings.LastIndex(url, "/"); index >= 0 {
   464  		myEmail = url[index+1:]
   465  	} else {
   466  		log.Errorf(c, "invalid email handler URL: %s", url)
   467  		return
   468  	}
   469  	source := dashapi.NoDiscussion
   470  	for _, item := range getConfig(c).DiscussionEmails {
   471  		if item.ReceiveAddress != myEmail {
   472  			continue
   473  		}
   474  		source = item.Source
   475  		break
   476  	}
   477  	msg, err := email.Parse(r.Body, ownEmails(c), ownMailingLists(c), []string{
   478  		appURL(c),
   479  	})
   480  	if err != nil {
   481  		// Malformed emails constantly appear from spammers.
   482  		// But we have not seen errors parsing legit emails.
   483  		// These errors are annoying. Warn and ignore them.
   484  		log.Warningf(c, "failed to parse email: %v", err)
   485  		return
   486  	}
   487  	log.Infof(c, "received email at %q, source %q", myEmail, source)
   488  	if source == dashapi.NoDiscussion {
   489  		if stop, err := emergentlyStopped(c); err != nil || stop {
   490  			log.Errorf(c, "abort email processing due to emergency stop (stop %v, err %v)",
   491  				stop, err)
   492  			return
   493  		}
   494  		err = processIncomingEmail(c, msg)
   495  	} else {
   496  		// Discussions are safe to handle even during an emergency stop.
   497  		err = processDiscussionEmail(c, msg, source)
   498  	}
   499  	if err != nil {
   500  		log.Errorf(c, "email processing failed: %s", err)
   501  	}
   502  }
   503  
   504  // nolint: gocyclo
   505  func processIncomingEmail(c context.Context, msg *email.Email) error {
   506  	// Ignore any incoming emails from syzbot itself.
   507  	if ownEmail(c) == msg.Author {
   508  		// But we still want to remember the id of our own message, so just neutralize the command.
   509  		msg.Commands = nil
   510  	}
   511  	log.Infof(c, "received email: subject %q, author %q, cc %q, msg %q, bug %v, %d cmds, link %q, list %q",
   512  		msg.Subject, msg.Author, msg.Cc, msg.MessageID, msg.BugIDs, len(msg.Commands), msg.Link, msg.MailingList)
   513  	excludeSampleCommands(msg)
   514  	bugInfo, bugListInfo, emailConfig := identifyEmail(c, msg)
   515  	if bugInfo == nil && bugListInfo == nil {
   516  		return nil // error was already logged
   517  	}
   518  	// A mailing list can send us a duplicate email, to not process/reply
   519  	// to such duplicate emails, we ignore emails coming from our mailing lists.
   520  	fromMailingList := msg.MailingList != ""
   521  	missingLists := missingMailingLists(c, msg, emailConfig)
   522  	log.Infof(c, "from/cc mailing list: %v (missing: %v)", fromMailingList, missingLists)
   523  	if fromMailingList && len(msg.BugIDs) > 0 && len(msg.Commands) > 0 {
   524  		// Note that if syzbot was not directly mentioned in To or Cc, this is not really
   525  		// a duplicate message, so it must be processed. We detect it by looking at BugID.
   526  
   527  		// There's also a chance that the user mentioned syzbot directly, but without BugID.
   528  		// We don't need to worry about this case, as we won't recognize the bug anyway.
   529  		log.Infof(c, "duplicate email from mailing list, ignoring")
   530  		return nil
   531  	}
   532  
   533  	var replies []string
   534  	if bugListInfo != nil {
   535  		const maxCommands = 10
   536  		if len(msg.Commands) > maxCommands {
   537  			return replyTo(c, msg, bugListInfo.id,
   538  				fmt.Sprintf("Too many commands (%d > %d)", len(msg.Commands), maxCommands))
   539  		}
   540  		for _, command := range msg.Commands {
   541  			replies = append(replies, handleBugListCommand(c, bugListInfo, msg, command))
   542  		}
   543  		if reply := groupEmailReplies(replies); reply != "" {
   544  			return replyTo(c, msg, bugListInfo.id, reply)
   545  		}
   546  	} else {
   547  		const maxCommands = 3
   548  		if len(msg.Commands) > maxCommands {
   549  			return replyTo(c, msg, bugInfo.bugReporting.ID,
   550  				fmt.Sprintf("Too many commands (%d > %d)", len(msg.Commands), maxCommands))
   551  		}
   552  		unCc := false
   553  		for _, command := range msg.Commands {
   554  			if command.Command == email.CmdUnCC {
   555  				unCc = true
   556  			}
   557  			replies = append(replies, handleBugCommand(c, bugInfo, msg, command))
   558  		}
   559  		if len(msg.Commands) == 0 {
   560  			// Even if there are 0 commands we'd still like to just ping the bug.
   561  			replies = append(replies, handleBugCommand(c, bugInfo, msg, nil))
   562  		}
   563  		reply := groupEmailReplies(replies)
   564  		if reply == "" && len(msg.Commands) > 0 && len(missingLists) > 0 && !unCc {
   565  			return forwardEmail(c, emailConfig, msg, bugInfo, missingLists)
   566  		}
   567  		if reply != "" {
   568  			return replyTo(c, msg, bugInfo.bugReporting.ID, reply)
   569  		}
   570  	}
   571  	return nil
   572  }
   573  
   574  func excludeSampleCommands(msg *email.Email) {
   575  	// Sometimes it happens that somebody sends us our own text back, ignore it.
   576  	var newCommands []*email.SingleCommand
   577  	for _, cmd := range msg.Commands {
   578  		ok := true
   579  		switch cmd.Command {
   580  		case email.CmdFix:
   581  			ok = cmd.Args != "exact-commit-title"
   582  		case email.CmdTest:
   583  			ok = cmd.Args != "git://repo/address.git branch-or-commit-hash"
   584  		case email.CmdSet:
   585  			ok = cmd.Args != "subsystems: new-subsystem"
   586  		case email.CmdUnset:
   587  			ok = cmd.Args != "some-label"
   588  		case email.CmdDup:
   589  			ok = cmd.Args != "exact-subject-of-another-report"
   590  		}
   591  		if ok {
   592  			newCommands = append(newCommands, cmd)
   593  		}
   594  	}
   595  	msg.Commands = newCommands
   596  }
   597  
   598  func groupEmailReplies(replies []string) string {
   599  	// If there's just one reply, return it.
   600  	if len(replies) == 1 {
   601  		return replies[0]
   602  	}
   603  	var totalReply strings.Builder
   604  	for i, reply := range replies {
   605  		if reply == "" {
   606  			continue
   607  		}
   608  		if totalReply.Len() > 0 {
   609  			totalReply.WriteString("\n\n")
   610  		}
   611  		totalReply.WriteString(fmt.Sprintf("Command #%d:\n", i+1))
   612  		totalReply.WriteString(reply)
   613  	}
   614  	return totalReply.String()
   615  }
   616  
   617  func handleBugCommand(c context.Context, bugInfo *bugInfoResult, msg *email.Email,
   618  	command *email.SingleCommand) string {
   619  	status := dashapi.BugStatusUpdate
   620  	if command != nil {
   621  		status = emailCmdToStatus[command.Command]
   622  	}
   623  	cmd := &dashapi.BugUpdate{
   624  		Status: status,
   625  		ID:     bugInfo.bugReporting.ID,
   626  		ExtID:  msg.MessageID,
   627  		Link:   msg.Link,
   628  		CC:     msg.Cc,
   629  	}
   630  	if command != nil {
   631  		switch command.Command {
   632  		case email.CmdTest:
   633  			return handleTestCommand(c, bugInfo, msg, command)
   634  		case email.CmdSet:
   635  			return handleSetCommand(c, bugInfo.bug, msg, command)
   636  		case email.CmdUnset:
   637  			return handleUnsetCommand(c, bugInfo.bug, msg, command)
   638  		case email.CmdUpstream, email.CmdInvalid, email.CmdUnDup:
   639  		case email.CmdFix:
   640  			if command.Args == "" {
   641  				return "no commit title"
   642  			}
   643  			cmd.FixCommits = []string{command.Args}
   644  		case email.CmdUnFix:
   645  			cmd.ResetFixCommits = true
   646  		case email.CmdDup:
   647  			if command.Args == "" {
   648  				return "no dup title"
   649  			}
   650  			var err error
   651  			cmd.DupOf, err = getSubjectParser(c).parseFullTitle(command.Args)
   652  			if err != nil {
   653  				return "failed to parse the dup title"
   654  			}
   655  		case email.CmdUnCC:
   656  			cmd.CC = []string{msg.Author}
   657  		default:
   658  			if command.Command != email.CmdUnknown {
   659  				log.Errorf(c, "unknown email command %v %q", command.Command, command.Str)
   660  			}
   661  			return fmt.Sprintf("unknown command %q", command.Str)
   662  		}
   663  	}
   664  	ok, reply, err := incomingCommand(c, cmd)
   665  	if err != nil {
   666  		return "" // the error was already logged
   667  	}
   668  	if !ok && reply != "" {
   669  		return reply
   670  	}
   671  	return ""
   672  }
   673  
   674  func processDiscussionEmail(c context.Context, msg *email.Email, source dashapi.DiscussionSource) error {
   675  	log.Debugf(c, "processDiscussionEmail %s from source %v", msg.MessageID, source)
   676  	if len(msg.BugIDs) == 0 {
   677  		return nil
   678  	}
   679  	const limitIDs = 10
   680  	if len(msg.BugIDs) > limitIDs {
   681  		msg.BugIDs = msg.BugIDs[:limitIDs]
   682  	}
   683  	log.Debugf(c, "saving to discussions for %q", msg.BugIDs)
   684  	dType := dashapi.DiscussionMention
   685  	if source == dashapi.DiscussionLore {
   686  		dType = lore.DiscussionType(msg)
   687  	}
   688  	extIDs := []string{}
   689  	for _, id := range msg.BugIDs {
   690  		if isBugListHash(id) {
   691  			dType = dashapi.DiscussionReminder
   692  			continue
   693  		}
   694  		_, _, err := findBugByReportingID(c, id)
   695  		if err == nil {
   696  			extIDs = append(extIDs, id)
   697  		}
   698  	}
   699  	msg.BugIDs = extIDs
   700  	err := saveDiscussionMessage(c, msg, source, dType)
   701  	if err != nil {
   702  		return fmt.Errorf("failed to save in discussions: %w", err)
   703  	}
   704  	return nil
   705  }
   706  
   707  var emailCmdToStatus = map[email.Command]dashapi.BugStatus{
   708  	email.CmdUpstream: dashapi.BugStatusUpstream,
   709  	email.CmdInvalid:  dashapi.BugStatusInvalid,
   710  	email.CmdUnDup:    dashapi.BugStatusOpen,
   711  	email.CmdFix:      dashapi.BugStatusOpen,
   712  	email.CmdUnFix:    dashapi.BugStatusUpdate,
   713  	email.CmdDup:      dashapi.BugStatusDup,
   714  	email.CmdUnCC:     dashapi.BugStatusUnCC,
   715  }
   716  
   717  func handleTestCommand(c context.Context, info *bugInfoResult,
   718  	msg *email.Email, command *email.SingleCommand) string {
   719  	args := strings.Fields(command.Args)
   720  	if len(args) != 0 && len(args) != 2 {
   721  		return fmt.Sprintf("want either no args or 2 args (repo, branch), got %v", len(args))
   722  	}
   723  	repo, branch := "", ""
   724  	if len(args) == 2 {
   725  		repo, branch = args[0], args[1]
   726  	}
   727  	if info.bug.sanitizeAccess(c, AccessPublic) != AccessPublic {
   728  		log.Warningf(c, "%v: bug is not AccessPublic, patch testing request is denied", info.bug.Title)
   729  		return ""
   730  	}
   731  	reply := ""
   732  	err := handleTestRequest(c, &testReqArgs{
   733  		bug: info.bug, bugKey: info.bugKey, bugReporting: info.bugReporting,
   734  		user: msg.Author, extID: msg.MessageID, link: msg.Link,
   735  		patch: []byte(msg.Patch), repo: repo, branch: branch, jobCC: msg.Cc})
   736  	if err != nil {
   737  		var testDenied *TestRequestDeniedError
   738  		var badTest *BadTestRequestError
   739  		switch {
   740  		case errors.As(err, &testDenied):
   741  			// Don't send a reply in this case.
   742  			log.Errorf(c, "patch test request denied: %v", testDenied)
   743  		case errors.As(err, &badTest):
   744  			reply = badTest.Error()
   745  		default:
   746  			// Don't leak any details to the reply email.
   747  			reply = "Processing failed due to an internal error"
   748  			// .. but they are useful for debugging, so we'd like to see it on the Admin page.
   749  			log.Errorf(c, "handleTestRequest error: %v", err)
   750  		}
   751  	}
   752  	return reply
   753  }
   754  
   755  var (
   756  	// The supported formats are:
   757  	// For bugs:
   758  	// #syz set LABEL[: value_1, [value_2, ....]]
   759  	// For bug lists:
   760  	// #syz set <N> LABEL[: value_1, [value_2, ....]]
   761  	setCmdRe         = regexp.MustCompile(`(?m)\s*([-\w]+)\s*(?:\:\s*([,\-\w\s]*?))?$`)
   762  	setCmdArgSplitRe = regexp.MustCompile(`[\s,]+`)
   763  	setBugCmdFormat  = `I've failed to parse your command. Please use the following format(s):
   764  #syz set some-flag
   765  #syz set label: value
   766  #syz set subsystems: one-subsystem, another-subsystem
   767  
   768  Or, for bug lists,
   769  #syz set <Ref> some-flag
   770  #syz set <Ref> label: value
   771  #syz set <Ref> subsystems: one-subsystem, another-subsystem
   772  
   773  The following labels are suported:
   774  %s`
   775  	setCmdUnknownLabel = `The specified label %q is unknown.
   776  Please use one of the supported labels.
   777  
   778  The following labels are suported:
   779  %s`
   780  	setCmdUnknownValue = `The specified label value is incorrect.
   781  %s.
   782  Please use one of the supported label values.
   783  
   784  The following labels are suported:
   785  %s`
   786  	cmdInternalErrorReply = `The command was not executed due to an internal error.
   787  Please contact the bot's maintainers.`
   788  )
   789  
   790  func handleSetCommand(c context.Context, bug *Bug, msg *email.Email,
   791  	command *email.SingleCommand) string {
   792  	labelSet := makeLabelSet(c, bug.Namespace)
   793  
   794  	match := setCmdRe.FindStringSubmatch(command.Args)
   795  	if match == nil {
   796  		return fmt.Sprintf(setBugCmdFormat, labelSet.Help())
   797  	}
   798  	label, values := BugLabelType(match[1]), match[2]
   799  	log.Infof(c, "bug=%q label=%s values=%s", bug.displayTitle(), label, values)
   800  	if !labelSet.FindLabel(label) {
   801  		return fmt.Sprintf(setCmdUnknownLabel, label, labelSet.Help())
   802  	}
   803  	var labels []BugLabel
   804  	for _, value := range unique(setCmdArgSplitRe.Split(values, -1)) {
   805  		labels = append(labels, BugLabel{
   806  			Label: label,
   807  			Value: value,
   808  			SetBy: msg.Author,
   809  			Link:  msg.Link,
   810  		})
   811  	}
   812  	var setError error
   813  	err := updateSingleBug(c, bug.key(c), func(bug *Bug) error {
   814  		setError = bug.SetLabels(labelSet, labels)
   815  		return setError
   816  	})
   817  	if setError != nil {
   818  		return fmt.Sprintf(setCmdUnknownValue, setError, labelSet.Help())
   819  	}
   820  	if err != nil {
   821  		log.Errorf(c, "failed to set bug tags: %s", err)
   822  		return cmdInternalErrorReply
   823  	}
   824  	return ""
   825  }
   826  
   827  var (
   828  	unsetBugCmdFormat = `I've failed to parse your command. Please use the following format(s):
   829  #syz unset any-label
   830  
   831  Or, for bug lists,
   832  #syz unset <Ref> any-label
   833  `
   834  	unsetLabelsNotFound = `The following labels did not exist: %s`
   835  )
   836  
   837  func handleUnsetCommand(c context.Context, bug *Bug, msg *email.Email,
   838  	command *email.SingleCommand) string {
   839  	match := setCmdRe.FindStringSubmatch(command.Args)
   840  	if match == nil {
   841  		return unsetBugCmdFormat
   842  	}
   843  	var labels []BugLabelType
   844  	for _, name := range unique(setCmdArgSplitRe.Split(command.Args, -1)) {
   845  		labels = append(labels, BugLabelType(name))
   846  	}
   847  
   848  	var notFound map[BugLabelType]struct{}
   849  	var notFoundErr = fmt.Errorf("some labels were not found")
   850  	err := updateSingleBug(c, bug.key(c), func(bug *Bug) error {
   851  		notFound = bug.UnsetLabels(labels...)
   852  		if len(notFound) > 0 {
   853  			return notFoundErr
   854  		}
   855  		return nil
   856  	})
   857  	if err == notFoundErr {
   858  		var names []string
   859  		for label := range notFound {
   860  			names = append(names, string(label))
   861  		}
   862  		return fmt.Sprintf(unsetLabelsNotFound, strings.Join(names, ", "))
   863  	} else if err != nil {
   864  		log.Errorf(c, "failed to unset bug labels: %s", err)
   865  		return cmdInternalErrorReply
   866  	}
   867  	return ""
   868  }
   869  
   870  func handleEmailBounce(w http.ResponseWriter, r *http.Request) {
   871  	c := appengine.NewContext(r)
   872  	body, err := io.ReadAll(r.Body)
   873  	if err != nil {
   874  		log.Errorf(c, "email bounced: failed to read body: %v", err)
   875  		return
   876  	}
   877  	if nonCriticalBounceRe.Match(body) {
   878  		log.Infof(c, "email bounced: address not found")
   879  	} else {
   880  		log.Errorf(c, "email bounced")
   881  	}
   882  	log.Infof(c, "%s", body)
   883  }
   884  
   885  var (
   886  	setGroupCmdRe     = regexp.MustCompile(`(?m)\s*<(\d+)>\s*(.*)$`)
   887  	setGroupCmdFormat = `I've failed to parse your command. Please use the following format(s):
   888  #syz set <Ref> some-label, another-label
   889  #syz set <Ref> subsystems: one-subsystem, another-subsystem
   890  #syz unset <Ref> some-label
   891  `
   892  	setGroupCmdBadRef = `The specified <Ref> number is invalid. It must be one of the <NUM> values
   893  listed in the bug list table.
   894  `
   895  )
   896  
   897  func handleBugListCommand(c context.Context, bugListInfo *bugListInfoResult,
   898  	msg *email.Email, command *email.SingleCommand) string {
   899  	upd := &dashapi.BugListUpdate{
   900  		ID:    bugListInfo.id,
   901  		ExtID: msg.MessageID,
   902  		Link:  msg.Link,
   903  	}
   904  	switch command.Command {
   905  	case email.CmdUpstream:
   906  		upd.Command = dashapi.BugListUpstreamCmd
   907  	case email.CmdRegenerate:
   908  		upd.Command = dashapi.BugListRegenerateCmd
   909  	case email.CmdSet, email.CmdUnset:
   910  		// Extract and cut the <Ref> part.
   911  		match := setGroupCmdRe.FindStringSubmatch(command.Args)
   912  		if match == nil {
   913  			return setGroupCmdFormat
   914  		}
   915  		ref, args := match[1], match[2]
   916  		numRef, err := strconv.Atoi(ref)
   917  		if err != nil {
   918  			return setGroupCmdFormat
   919  		}
   920  		if numRef < 1 || numRef > len(bugListInfo.keys) {
   921  			return setGroupCmdBadRef
   922  		}
   923  		bugKey := bugListInfo.keys[numRef-1]
   924  		bug := new(Bug)
   925  		if err := db.Get(c, bugKey, bug); err != nil {
   926  			log.Errorf(c, "failed to fetch bug by key %s: %s", bugKey, err)
   927  			return cmdInternalErrorReply
   928  		}
   929  		command.Args = args
   930  		switch command.Command {
   931  		case email.CmdSet:
   932  			return handleSetCommand(c, bug, msg, command)
   933  		case email.CmdUnset:
   934  			return handleUnsetCommand(c, bug, msg, command)
   935  		}
   936  	default:
   937  		upd.Command = dashapi.BugListUpdateCmd
   938  	}
   939  	log.Infof(c, "bug list update: id=%s, cmd=%v", upd.ID, upd.Command)
   940  	reply, err := reportingBugListCommand(c, upd)
   941  	if err != nil {
   942  		log.Errorf(c, "bug list command failed: %s", err)
   943  		return cmdInternalErrorReply
   944  	}
   945  	return reply
   946  }
   947  
   948  // These are just stale emails in MAINTAINERS.
   949  var nonCriticalBounceRe = regexp.MustCompile(`\*\* Address not found \*\*|550 #5\.1\.0 Address rejected`)
   950  
   951  type bugListInfoResult struct {
   952  	id     string
   953  	config *EmailConfig
   954  	keys   []*db.Key
   955  }
   956  
   957  func identifyEmail(c context.Context, msg *email.Email) (*bugInfoResult, *bugListInfoResult, *EmailConfig) {
   958  	bugID := ""
   959  	if len(msg.BugIDs) > 0 {
   960  		// For now let's only consider one of them.
   961  		bugID = msg.BugIDs[0]
   962  	}
   963  	if isBugListHash(bugID) {
   964  		subsystem, report, stage, err := findSubsystemReportByID(c, bugID)
   965  		if err != nil {
   966  			log.Errorf(c, "findBugListByID failed: %s", err)
   967  			return nil, nil, nil
   968  		}
   969  		if subsystem == nil {
   970  			log.Errorf(c, "no bug list with the %v ID found", bugID)
   971  			return nil, nil, nil
   972  		}
   973  		reminderConfig := getNsConfig(c, subsystem.Namespace).Subsystems.Reminder
   974  		if reminderConfig == nil {
   975  			log.Errorf(c, "reminder configuration is empty")
   976  			return nil, nil, nil
   977  		}
   978  		emailConfig, ok := bugListReportingConfig(c, subsystem.Namespace, stage).(*EmailConfig)
   979  		if !ok {
   980  			log.Errorf(c, "bug list's reporting config is not EmailConfig (id=%v)", bugID)
   981  			return nil, nil, nil
   982  		}
   983  		keys, err := report.getBugKeys()
   984  		if err != nil {
   985  			log.Errorf(c, "failed to extract keys from bug list: %s", err)
   986  			return nil, nil, nil
   987  		}
   988  		return nil, &bugListInfoResult{
   989  			id:     bugID,
   990  			config: emailConfig,
   991  			keys:   keys,
   992  		}, emailConfig
   993  	}
   994  	bugInfo := loadBugInfo(c, msg)
   995  	if bugInfo == nil {
   996  		return nil, nil, nil
   997  	}
   998  	return bugInfo, nil, bugInfo.reporting.Config.(*EmailConfig)
   999  }
  1000  
  1001  type bugInfoResult struct {
  1002  	bug          *Bug
  1003  	bugKey       *db.Key
  1004  	bugReporting *BugReporting
  1005  	reporting    *Reporting
  1006  }
  1007  
  1008  func loadBugInfo(c context.Context, msg *email.Email) *bugInfoResult {
  1009  	bugID := ""
  1010  	if len(msg.BugIDs) > 0 {
  1011  		// For now let's only consider one of them.
  1012  		bugID = msg.BugIDs[0]
  1013  	}
  1014  	if bugID == "" {
  1015  		var matchingErr error
  1016  		// Give it one more try -- maybe we can determine the bug from the subject + mailing list.
  1017  		if msg.MailingList != "" {
  1018  			var ret *bugInfoResult
  1019  			ret, matchingErr = matchBugFromList(c, msg.MailingList, msg.Subject)
  1020  			if matchingErr == nil {
  1021  				return ret
  1022  			}
  1023  			log.Infof(c, "mailing list matching failed: %s", matchingErr)
  1024  		}
  1025  		if len(msg.Commands) == 0 {
  1026  			// This happens when people CC syzbot on unrelated emails.
  1027  			log.Infof(c, "no bug ID (%q)", msg.Subject)
  1028  		} else {
  1029  			log.Errorf(c, "no bug ID (%q)", msg.Subject)
  1030  			from, err := email.AddAddrContext(ownEmail(c), "HASH")
  1031  			if err != nil {
  1032  				log.Errorf(c, "failed to format sender email address: %v", err)
  1033  				from = "ERROR"
  1034  			}
  1035  			message := fmt.Sprintf(replyNoBugID, from)
  1036  			if matchingErr == errAmbiguousTitle {
  1037  				message = fmt.Sprintf(replyAmbiguousBugID, from)
  1038  			}
  1039  			if err := replyTo(c, msg, "", message); err != nil {
  1040  				log.Errorf(c, "failed to send reply: %v", err)
  1041  			}
  1042  		}
  1043  		return nil
  1044  	}
  1045  	bug, bugKey, err := findBugByReportingID(c, bugID)
  1046  	if err != nil {
  1047  		log.Errorf(c, "can't find bug: %v", err)
  1048  		from, err := email.AddAddrContext(ownEmail(c), "HASH")
  1049  		if err != nil {
  1050  			log.Errorf(c, "failed to format sender email address: %v", err)
  1051  			from = "ERROR"
  1052  		}
  1053  		if err := replyTo(c, msg, "", fmt.Sprintf(replyBadBugID, from)); err != nil {
  1054  			log.Errorf(c, "failed to send reply: %v", err)
  1055  		}
  1056  		return nil
  1057  	}
  1058  	bugReporting, _ := bugReportingByID(bug, bugID)
  1059  	if bugReporting == nil {
  1060  		log.Errorf(c, "can't find bug reporting: %v", err)
  1061  		if err := replyTo(c, msg, "", "Can't find the corresponding bug."); err != nil {
  1062  			log.Errorf(c, "failed to send reply: %v", err)
  1063  		}
  1064  		return nil
  1065  	}
  1066  	reporting := getNsConfig(c, bug.Namespace).ReportingByName(bugReporting.Name)
  1067  	if reporting == nil {
  1068  		log.Errorf(c, "can't find reporting for this bug: namespace=%q reporting=%q",
  1069  			bug.Namespace, bugReporting.Name)
  1070  		return nil
  1071  	}
  1072  	if reporting.Config.Type() != emailType {
  1073  		log.Errorf(c, "reporting is not email: namespace=%q reporting=%q config=%q",
  1074  			bug.Namespace, bugReporting.Name, reporting.Config.Type())
  1075  		return nil
  1076  	}
  1077  	return &bugInfoResult{bug, bugKey, bugReporting, reporting}
  1078  }
  1079  
  1080  func ownMailingLists(c context.Context) []string {
  1081  	configs := []ReportingType{}
  1082  	for _, ns := range getConfig(c).Namespaces {
  1083  		for _, rep := range ns.Reporting {
  1084  			configs = append(configs, rep.Config)
  1085  		}
  1086  		if ns.Subsystems.Reminder == nil {
  1087  			continue
  1088  		}
  1089  		reminderConfig := ns.Subsystems.Reminder
  1090  		if reminderConfig.ModerationConfig != nil {
  1091  			configs = append(configs, reminderConfig.ModerationConfig)
  1092  		}
  1093  		if reminderConfig.Config != nil {
  1094  			configs = append(configs, reminderConfig.Config)
  1095  		}
  1096  	}
  1097  	ret := []string{}
  1098  	for _, config := range configs {
  1099  		emailConfig, ok := config.(*EmailConfig)
  1100  		if !ok {
  1101  			continue
  1102  		}
  1103  		ret = append(ret, emailConfig.Email)
  1104  	}
  1105  	return ret
  1106  }
  1107  
  1108  var (
  1109  	// Use getSubjectParser(c) instead.
  1110  	defaultSubjectParser *subjectTitleParser
  1111  	subjectParserInit    sync.Once
  1112  	errAmbiguousTitle    = errors.New("ambiguous bug title")
  1113  )
  1114  
  1115  func getSubjectParser(c context.Context) *subjectTitleParser {
  1116  	if getConfig(c) != getConfig(context.Background()) {
  1117  		// For the non-default config, do not cache the parser.
  1118  		return makeSubjectTitleParser(c)
  1119  	}
  1120  	subjectParserInit.Do(func() {
  1121  		defaultSubjectParser = makeSubjectTitleParser(c)
  1122  	})
  1123  	return defaultSubjectParser
  1124  }
  1125  
  1126  func matchBugFromList(c context.Context, sender, subject string) (*bugInfoResult, error) {
  1127  	title, seq, err := getSubjectParser(c).parseTitle(subject)
  1128  	if err != nil {
  1129  		return nil, err
  1130  	}
  1131  	// Query all bugs with this title.
  1132  	var bugs []*Bug
  1133  	bugKeys, err := db.NewQuery("Bug").
  1134  		Filter("Title=", title).
  1135  		GetAll(c, &bugs)
  1136  	if err != nil {
  1137  		return nil, fmt.Errorf("failed to fetch bugs: %w", err)
  1138  	}
  1139  	// Filter the bugs by the email.
  1140  	candidates := []*bugInfoResult{}
  1141  	for i, bug := range bugs {
  1142  		log.Infof(c, "processing bug %v", bug.displayTitle())
  1143  		// We could add it to the query, but it's probably not worth it - we already have
  1144  		// tons of db indexes while the number of matching bugs should not be large anyway.
  1145  		if bug.Seq != seq {
  1146  			log.Infof(c, "bug's seq is %v, wanted %d", bug.Seq, seq)
  1147  			continue
  1148  		}
  1149  		if bug.sanitizeAccess(c, AccessPublic) != AccessPublic {
  1150  			log.Infof(c, "access denied")
  1151  			continue
  1152  		}
  1153  		reporting, bugReporting, _, _, err := currentReporting(c, bug)
  1154  		if err != nil || reporting == nil {
  1155  			log.Infof(c, "could not query reporting: %s", err)
  1156  			continue
  1157  		}
  1158  		emailConfig, ok := reporting.Config.(*EmailConfig)
  1159  		if !ok {
  1160  			log.Infof(c, "reporting is not EmailConfig (%q)", subject)
  1161  			continue
  1162  		}
  1163  		if !emailConfig.HandleListEmails {
  1164  			log.Infof(c, "the feature is disabled for the config")
  1165  			continue
  1166  		}
  1167  		if emailConfig.Email != sender {
  1168  			log.Infof(c, "config's Email is %v, wanted %v", emailConfig.Email, sender)
  1169  			continue
  1170  		}
  1171  		candidates = append(candidates, &bugInfoResult{
  1172  			bug: bug, bugKey: bugKeys[i],
  1173  			bugReporting: bugReporting, reporting: reporting,
  1174  		})
  1175  	}
  1176  	if len(candidates) > 1 {
  1177  		return nil, errAmbiguousTitle
  1178  	} else if len(candidates) == 0 {
  1179  		return nil, fmt.Errorf("unable to determine the bug")
  1180  	}
  1181  	return candidates[0], nil
  1182  }
  1183  
  1184  type subjectTitleParser struct {
  1185  	pattern *regexp.Regexp
  1186  }
  1187  
  1188  func makeSubjectTitleParser(c context.Context) *subjectTitleParser {
  1189  	stripPrefixes := []string{`R[eE]:`}
  1190  	for _, ns := range getConfig(c).Namespaces {
  1191  		for _, rep := range ns.Reporting {
  1192  			emailConfig, ok := rep.Config.(*EmailConfig)
  1193  			if !ok {
  1194  				continue
  1195  			}
  1196  			if ok && emailConfig.SubjectPrefix != "" {
  1197  				stripPrefixes = append(stripPrefixes,
  1198  					regexp.QuoteMeta(emailConfig.SubjectPrefix))
  1199  			}
  1200  		}
  1201  	}
  1202  	rePrefixes := `^(?:(?:` + strings.Join(stripPrefixes, "|") + `)\s*)*`
  1203  	pattern := regexp.MustCompile(rePrefixes + `(?:\[[^\]]+\]\s*)*\s*(.*)$`)
  1204  	return &subjectTitleParser{pattern}
  1205  }
  1206  
  1207  func (p *subjectTitleParser) parseTitle(subject string) (string, int64, error) {
  1208  	rawTitle, err := p.parseFullTitle(subject)
  1209  	if err != nil {
  1210  		return "", 0, err
  1211  	}
  1212  	return splitDisplayTitle(rawTitle)
  1213  }
  1214  
  1215  func (p *subjectTitleParser) parseFullTitle(subject string) (string, error) {
  1216  	subject = strings.TrimSpace(subject)
  1217  	parts := p.pattern.FindStringSubmatch(subject)
  1218  	if parts == nil || parts[len(parts)-1] == "" {
  1219  		return "", fmt.Errorf("failed to extract the title")
  1220  	}
  1221  	return parts[len(parts)-1], nil
  1222  }
  1223  
  1224  func missingMailingLists(c context.Context, msg *email.Email, emailConfig *EmailConfig) []string {
  1225  	// We want to ensure that the incoming message is recorded on both our mailing list
  1226  	// and the archive mailing list (in case of Linux -- linux-kernel@vger.kernel.org).
  1227  	mailingLists := []string{
  1228  		email.CanonicalEmail(emailConfig.Email),
  1229  	}
  1230  	if emailConfig.MailMaintainers {
  1231  		mailingLists = append(mailingLists, emailConfig.DefaultMaintainers...)
  1232  	}
  1233  	// Consider all recipients.
  1234  	exists := map[string]struct{}{}
  1235  	if msg.MailingList != "" {
  1236  		exists[msg.MailingList] = struct{}{}
  1237  	}
  1238  	for _, email := range msg.Cc {
  1239  		exists[email] = struct{}{}
  1240  	}
  1241  	var missing []string
  1242  	for _, list := range mailingLists {
  1243  		if _, ok := exists[list]; !ok {
  1244  			missing = append(missing, list)
  1245  		}
  1246  	}
  1247  	sort.Strings(missing)
  1248  	msg.Cc = append(msg.Cc, missing...)
  1249  	return missing
  1250  }
  1251  
  1252  func forwardEmail(c context.Context, cfg *EmailConfig, msg *email.Email,
  1253  	info *bugInfoResult, mailingLists []string) error {
  1254  	log.Infof(c, "forwarding email: id=%q from=%q to=%q", msg.MessageID, msg.Author, mailingLists)
  1255  	body := fmt.Sprintf(`For archival purposes, forwarding an incoming command email to
  1256  %v.
  1257  
  1258  ***
  1259  
  1260  Subject: %s
  1261  Author: %s
  1262  
  1263  %s`, strings.Join(mailingLists, ", "), msg.Subject, msg.Author, msg.Body)
  1264  	from, err := email.AddAddrContext(fromAddr(c), info.bugReporting.ID)
  1265  	if err != nil {
  1266  		return err
  1267  	}
  1268  	return sendMailText(c, cfg, msg.Subject, from, mailingLists, info.bugReporting.ExtID, body)
  1269  }
  1270  
  1271  func sendMailText(c context.Context, cfg *EmailConfig, subject, from string, to []string, replyTo, body string) error {
  1272  	msg := &aemail.Message{
  1273  		Sender:  from,
  1274  		To:      to,
  1275  		Subject: subject,
  1276  		Body:    body,
  1277  	}
  1278  	if cfg.SubjectPrefix != "" {
  1279  		msg.Subject = cfg.SubjectPrefix + " " + msg.Subject
  1280  	}
  1281  	if replyTo != "" {
  1282  		msg.Headers = mail.Header{"In-Reply-To": []string{replyTo}}
  1283  		msg.Subject = replySubject(msg.Subject)
  1284  	}
  1285  	return sendEmail(c, msg)
  1286  }
  1287  
  1288  func replyTo(c context.Context, msg *email.Email, bugID, reply string) error {
  1289  	from, err := email.AddAddrContext(fromAddr(c), bugID)
  1290  	if err != nil {
  1291  		log.Errorf(c, "failed to build the From address: %v", err)
  1292  		return err
  1293  	}
  1294  	log.Infof(c, "sending reply: to=%q cc=%q subject=%q reply=%q",
  1295  		msg.Author, msg.Cc, msg.Subject, reply)
  1296  	replyMsg := &aemail.Message{
  1297  		Sender:  from,
  1298  		To:      []string{msg.Author},
  1299  		Cc:      msg.Cc,
  1300  		Subject: replySubject(msg.Subject),
  1301  		Body:    email.FormReply(msg, reply),
  1302  		Headers: mail.Header{"In-Reply-To": []string{msg.MessageID}},
  1303  	}
  1304  	return sendEmail(c, replyMsg)
  1305  }
  1306  
  1307  // Sends email, can be stubbed for testing.
  1308  var sendEmail = func(c context.Context, msg *aemail.Message) error {
  1309  	if err := aemail.Send(c, msg); err != nil {
  1310  		return fmt.Errorf("failed to send email: %w", err)
  1311  	}
  1312  	return nil
  1313  }
  1314  
  1315  func replySubject(subject string) string {
  1316  	if !strings.HasPrefix(subject, replySubjectPrefix) {
  1317  		return replySubjectPrefix + subject
  1318  	}
  1319  	return subject
  1320  }
  1321  
  1322  func ownEmail(c context.Context) string {
  1323  	if getConfig(c).OwnEmailAddress != "" {
  1324  		return getConfig(c).OwnEmailAddress
  1325  	}
  1326  	return fmt.Sprintf("syzbot@%v.appspotmail.com", appengine.AppID(c))
  1327  }
  1328  
  1329  func fromAddr(c context.Context) string {
  1330  	return fmt.Sprintf("\"syzbot\" <%v>", ownEmail(c))
  1331  }
  1332  
  1333  func ownEmails(c context.Context) []string {
  1334  	emails := []string{ownEmail(c)}
  1335  	config := getConfig(c)
  1336  	if config.ExtraOwnEmailAddresses != nil {
  1337  		emails = append(emails, config.ExtraOwnEmailAddresses...)
  1338  	} else if config.OwnEmailAddress == "" {
  1339  		// Now we use syzbot@ but we used to use bot@, so we add them both.
  1340  		emails = append(emails, fmt.Sprintf("bot@%v.appspotmail.com", appengine.AppID(c)))
  1341  	}
  1342  	return emails
  1343  }
  1344  
  1345  func sanitizeCC(c context.Context, cc []string) []string {
  1346  	var res []string
  1347  	for _, addr := range cc {
  1348  		mail, err := mail.ParseAddress(addr)
  1349  		if err != nil {
  1350  			continue
  1351  		}
  1352  		if email.CanonicalEmail(mail.Address) == ownEmail(c) {
  1353  			continue
  1354  		}
  1355  		res = append(res, mail.Address)
  1356  	}
  1357  	return res
  1358  }
  1359  
  1360  func externalLink(c context.Context, tag string, id int64) string {
  1361  	if id == 0 {
  1362  		return ""
  1363  	}
  1364  	return fmt.Sprintf("%v/x/%v?x=%v", appURL(c), textFilename(tag), strconv.FormatUint(uint64(id), 16))
  1365  }
  1366  
  1367  func appURL(c context.Context) string {
  1368  	appURL := getConfig(c).AppURL
  1369  	if appURL != "" {
  1370  		return appURL
  1371  	}
  1372  	return fmt.Sprintf("https://%v.appspot.com", appengine.AppID(c))
  1373  }
  1374  
  1375  var mailTemplates = html.CreateTextGlob("mail_*.txt")