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