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")