diff options
| author | Dmitry Vyukov <dvyukov@google.com> | 2019-03-21 18:18:14 +0100 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2019-03-22 12:00:50 +0100 |
| commit | ca3ffbc9f365dab1816871c776e673e30a86bcdf (patch) | |
| tree | 978a0825c46ccb63a0521d08c89128e867aa1e22 | |
| parent | 40fdabdc24442daa8f380fad4c880a056a074222 (diff) | |
dashboard/app: add uncc command
Add "#syz uncc" command as a safety handle.
The command allows sender to unsubscribe from all future communication on the bug.
Linus mentioned possibility of saying "I'm not the right person for this report"
in the context of bug reminders:
https://groups.google.com/d/msg/syzkaller/zYlQ-b-QPHQ/AJzpeObcBAAJ
| -rw-r--r-- | dashboard/app/email_test.go | 58 | ||||
| -rw-r--r-- | dashboard/app/entities.go | 1 | ||||
| -rw-r--r-- | dashboard/app/jobs.go | 59 | ||||
| -rw-r--r-- | dashboard/app/jobs_test.go | 9 | ||||
| -rw-r--r-- | dashboard/app/notifications_test.go | 30 | ||||
| -rw-r--r-- | dashboard/app/reporting.go | 103 | ||||
| -rw-r--r-- | dashboard/app/reporting_email.go | 5 | ||||
| -rw-r--r-- | dashboard/app/util_test.go | 8 | ||||
| -rw-r--r-- | dashboard/dashapi/dashapi.go | 1 | ||||
| -rw-r--r-- | pkg/email/parser.go | 11 |
10 files changed, 142 insertions, 143 deletions
diff --git a/dashboard/app/email_test.go b/dashboard/app/email_test.go index 4cb254854..07a242e22 100644 --- a/dashboard/app/email_test.go +++ b/dashboard/app/email_test.go @@ -22,7 +22,7 @@ func TestEmailReport(t *testing.T) { c.client2.UploadBuild(build) crash := testCrash(build, 1) - crash.Maintainers = []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`} + crash.Maintainers = []string{`"Foo Bar" <foo@bar.com>`, `bar@foo.com`, `idont@want.EMAILS`} c.client2.ReportCrash(crash) // Report the crash over email and check all fields. @@ -52,7 +52,7 @@ console output: %[2]v kernel config: %[3]v dashboard link: https://testapp.appspot.com/bug?extid=%[1]v compiler: compiler1 -CC: [bar@foo.com foo@bar.com] +CC: [bar@foo.com foo@bar.com idont@want.EMAILS] Unfortunately, I don't have any reproducer for this crash yet. @@ -109,6 +109,14 @@ For more options, visit https://groups.google.com/d/optout. // We used to extract "#syz fix: exact-commit-title" from it. c.incomingEmail(sender0, body0) + c.incomingEmail(sender0, "I don't want emails", EmailOptFrom(`"idont" <idont@WANT.emails>`)) + c.expectNoEmail() + + // This person sends an email and is listed as a maintainer, but opt-out of emails. + // We should not send anything else to them for this bug. Also don't warn about no mailing list in CC. + c.incomingEmail(sender0, "#syz uncc", EmailOptFrom(`"IDONT" <Idont@want.emails>`), EmailOptCC(nil)) + c.expectNoEmail() + // Now report syz reproducer and check updated email. build2 := testBuild(10) build2.Arch = "386" @@ -310,8 +318,7 @@ unknown command "bad-command" // Now mark the bug as fixed. c.incomingEmail(sender1, "#syz fix: some: commit title") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Check that the commit is now passed to builders. builderPollResp, _ := c.client2.BuilderPoll(build.Manager) @@ -361,9 +368,6 @@ Content-Type: text/plain #syz upstream `, sender) c.expectOK(c.POST("/_ah/mail/", incoming1)) - - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) } // Basic dup scenario: mark one bug as dup of another. @@ -383,24 +387,25 @@ func TestEmailDup(t *testing.T) { c.client2.ReportCrash(crash2) c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 2) - msg1 := <-c.emailSink - msg2 := <-c.emailSink + msg1 := c.pollEmailBug() + msg2 := c.pollEmailBug() // Dup crash2 to crash1. c.incomingEmail(msg2.Sender, "#syz dup: BUG: slightly more elaborate title") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Second crash happens again crash2.ReproC = []byte("int main() {}") c.client2.ReportCrash(crash2) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Now close the original bug, and check that new bugs for dup are now created. c.incomingEmail(msg1.Sender, "#syz invalid") + // uncc command must not trugger error reply even for closed bug. + c.incomingEmail(msg1.Sender, "#syz uncc", EmailOptCC(nil)) + c.expectNoEmail() + // New crash must produce new bug in the first reporting. c.client2.ReportCrash(crash2) { @@ -425,25 +430,21 @@ func TestEmailUndup(t *testing.T) { c.client2.ReportCrash(crash2) c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 2) - msg1 := <-c.emailSink - msg2 := <-c.emailSink + msg1 := c.pollEmailBug() + msg2 := c.pollEmailBug() // Dup crash2 to crash1. c.incomingEmail(msg2.Sender, "#syz dup: BUG: slightly more elaborate title") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Undup crash2. c.incomingEmail(msg2.Sender, "#syz undup") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Now close the original bug, and check that new crashes for the dup does not create bugs. c.incomingEmail(msg1.Sender, "#syz invalid") c.client2.ReportCrash(crash2) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() } func TestEmailCrossReportingDup(t *testing.T) { @@ -491,13 +492,9 @@ func TestEmailCrossReportingDup(t *testing.T) { c.incomingEmail(bugSender, "#syz dup: "+crash2.Title) if test.result { - if len(c.emailSink) != 0 { - msg := <-c.emailSink - t.Fatalf("unexpected reply: %s\n%s\n", msg.Subject, msg.Body) - } + c.expectNoEmail() } else { - c.expectEQ(len(c.emailSink), 1) - msg := <-c.emailSink + msg := c.pollEmailBug() if !strings.Contains(msg.Body, "> #syz dup:") || !strings.Contains(msg.Body, "Can't dup bug to a bug in different reporting") { c.t.Fatalf("bad reply body:\n%v", msg.Body) @@ -512,8 +509,7 @@ func TestEmailErrors(t *testing.T) { // No reply for email without bug hash and no commands. c.incomingEmail("syzbot@testapp.appspotmail.com", "Investment Proposal") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // If email contains a command we need to reply. c.incomingEmail("syzbot@testapp.appspotmail.com", "#syz invalid") diff --git a/dashboard/app/entities.go b/dashboard/app/entities.go index 32cf37926..a3ab5c79b 100644 --- a/dashboard/app/entities.go +++ b/dashboard/app/entities.go @@ -89,6 +89,7 @@ type Bug struct { CommitInfo []Commit // additional info for commits (for historical reasons parallel array to Commits) HappenedOn []string `datastore:",noindex"` // list of managers PatchedOn []string `datastore:",noindex"` // list of managers + UNCC []string // don't CC these emails on this bug } type Commit struct { diff --git a/dashboard/app/jobs.go b/dashboard/app/jobs.go index 1bb67a991..7bff92b85 100644 --- a/dashboard/app/jobs.go +++ b/dashboard/app/jobs.go @@ -572,10 +572,6 @@ func createBugReportForJob(c context.Context, job *Job, jobKey *datastore.Key, c if err != nil { return nil, err } - kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig) - if err != nil { - return nil, err - } bugKey := jobKey.Parent() crashKey := datastore.NewKey(c, "Crash", "", job.CrashID, bugKey) crash := new(Crash) @@ -590,10 +586,6 @@ func createBugReportForJob(c context.Context, job *Job, jobKey *datastore.Key, c if bugReporting == nil { return nil, fmt.Errorf("job bug has no reporting %q", job.Reporting) } - creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID) - if err != nil { - return nil, err - } var typ dashapi.ReportType switch job.Type { case JobTestPatch: @@ -606,39 +598,21 @@ func createBugReportForJob(c context.Context, job *Job, jobKey *datastore.Key, c return nil, fmt.Errorf("unknown job type %v", job.Type) } rep := &dashapi.BugReport{ - Type: typ, - Namespace: job.Namespace, - Config: reportingConfig, - ID: bugReporting.ID, - JobID: extJobID(jobKey), - ExtID: job.ExtID, - Title: bug.displayTitle(), - Link: fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID), - CreditEmail: creditEmail, - CC: job.CC, - Log: crashLog, - LogLink: externalLink(c, textCrashLog, job.CrashLog), - Report: report, - ReportLink: externalLink(c, textCrashReport, job.CrashReport), - OS: build.OS, - Arch: build.Arch, - VMArch: build.VMArch, - UserSpaceArch: kernelArch(build.Arch), - CompilerID: build.CompilerID, - KernelRepo: build.KernelRepo, - KernelRepoAlias: kernelRepoInfo(build).Alias, - KernelBranch: build.KernelBranch, - KernelCommit: build.KernelCommit, - KernelCommitTitle: build.KernelCommitTitle, - KernelCommitDate: build.KernelCommitDate, - KernelConfig: kernelConfig, - KernelConfigLink: externalLink(c, textKernelConfig, build.KernelConfig), - ReproCLink: externalLink(c, textReproC, crash.ReproC), - ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz), - CrashTitle: job.CrashTitle, - Error: jobError, - ErrorLink: externalLink(c, textError, job.Error), - PatchLink: externalLink(c, textPatch, job.Patch), + Type: typ, + Config: reportingConfig, + JobID: extJobID(jobKey), + ExtID: job.ExtID, + CC: job.CC, + Log: crashLog, + LogLink: externalLink(c, textCrashLog, job.CrashLog), + Report: report, + ReportLink: externalLink(c, textCrashReport, job.CrashReport), + ReproCLink: externalLink(c, textReproC, crash.ReproC), + ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz), + CrashTitle: job.CrashTitle, + Error: jobError, + ErrorLink: externalLink(c, textError, job.Error), + PatchLink: externalLink(c, textPatch, job.Patch), } if job.Type == JobBisectCause || job.Type == JobBisectFix { kernelRepo := kernelRepoInfo(build) @@ -659,6 +633,9 @@ func createBugReportForJob(c context.Context, job *Job, jobKey *datastore.Key, c rep.Error = rep.Error[len(rep.Error)-maxInlineError:] rep.ErrorTruncated = true } + if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil { + return nil, err + } return rep, nil } diff --git a/dashboard/app/jobs_test.go b/dashboard/app/jobs_test.go index a5783b63b..73e324733 100644 --- a/dashboard/app/jobs_test.go +++ b/dashboard/app/jobs_test.go @@ -74,8 +74,7 @@ func TestJob(t *testing.T) { c.incomingEmail(sender, "#syz test: git://git.git/git.git kernel-branch\n"+patch, EmailOptFrom("\"foo\" <blAcklisteD@dOmain.COM>")) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() pollResp := c.client2.pollJobs(build.Manager) c.expectEQ(pollResp.ID, "") @@ -83,15 +82,13 @@ func TestJob(t *testing.T) { c.incomingEmail(sender, "#syz test: git://git.git/git.git kernel-branch\n"+patch, EmailOptMessageID(1), EmailOptFrom("test@requester.com"), EmailOptCC([]string{"somebody@else.com"})) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // A dup of the same request with the same Message-ID. c.incomingEmail(sender, "#syz test: git://git.git/git.git kernel-branch\n"+patch, EmailOptMessageID(1), EmailOptFrom("test@requester.com"), EmailOptCC([]string{"somebody@else.com"})) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() pollResp = c.client2.pollJobs("foobar") c.expectEQ(pollResp.ID, "") diff --git a/dashboard/app/notifications_test.go b/dashboard/app/notifications_test.go index d59465e57..12e9aae73 100644 --- a/dashboard/app/notifications_test.go +++ b/dashboard/app/notifications_test.go @@ -27,8 +27,7 @@ func TestEmailNotifUpstreamEmbargo(t *testing.T) { // Upstreaming happens after 14 days, so no emails yet. c.advanceTime(13 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Now we should get notification about upstreaming and upstream report: c.advanceTime(2 * 24 * time.Hour) @@ -54,8 +53,7 @@ func TestEmailNotifUpstreamSkip(t *testing.T) { c.expectEQ(report.To, []string{"test@syzkaller.com"}) // No emails yet. - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Now upload repro and it should be auto-upstreamed. crash.ReproOpts = []byte("repro opts") @@ -82,16 +80,13 @@ func TestEmailNotifBadFix(t *testing.T) { c.expectEQ(report.To, []string{"test@syzkaller.com"}) c.incomingEmail(report.Sender, "#syz fix: some: commit title") - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Notification about bad fixing commit should be send after 90 days. c.advanceTime(50 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() c.advanceTime(35 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() c.advanceTime(10 * 24 * time.Hour) notif := c.pollEmailBug() if !strings.Contains(notif.Body, "This bug is marked as fixed by commit:\nsome: commit title\n") { @@ -99,8 +94,7 @@ func TestEmailNotifBadFix(t *testing.T) { } // No notifications for another 14 days, then another one. c.advanceTime(13 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() c.advanceTime(2 * 24 * time.Hour) notif = c.pollEmailBug() if !strings.Contains(notif.Body, "This bug is marked as fixed by commit:\nsome: commit title\n") { @@ -127,13 +121,11 @@ func TestEmailNotifObsoleted(t *testing.T) { // Bug is open, new crashes don't create new bug. c.client2.ReportCrash(crash) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Not yet. c.advanceTime(179 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Now! c.advanceTime(2 * 24 * time.Hour) @@ -199,8 +191,7 @@ func TestEmailNotifNotObsoleted(t *testing.T) { report4 = c.pollEmailBug() c.advanceTime(179 * 24 * time.Hour) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() c.client2.ReportCrash(crash2) c.incomingEmail(report3.Sender, "I am looking at it") @@ -209,8 +200,7 @@ func TestEmailNotifNotObsoleted(t *testing.T) { // Only crash 4 is obsoleted. notif := c.pollEmailBug() c.expectEQ(notif.Sender, report4.Sender) - c.expectOK(c.GET("/email_poll")) - c.expectEQ(len(c.emailSink), 0) + c.expectNoEmail() // Crash 3 also obsoleted after some time. c.advanceTime(20 * 24 * time.Hour) diff --git a/dashboard/app/reporting.go b/dashboard/app/reporting.go index 1467f63f6..31306b53a 100644 --- a/dashboard/app/reporting.go +++ b/dashboard/app/reporting.go @@ -343,14 +343,6 @@ func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *datast if err != nil { return nil, err } - kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig) - if err != nil { - return nil, err - } - creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID) - if err != nil { - return nil, err - } typ := dashapi.ReportNew if !bugReporting.Reported.IsZero() { typ = dashapi.ReportRepro @@ -358,41 +350,23 @@ func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *datast kernelRepo := kernelRepoInfo(build) rep := &dashapi.BugReport{ - Type: typ, - Namespace: bug.Namespace, - Config: reportingConfig, - ID: bugReporting.ID, - ExtID: bugReporting.ExtID, - First: bugReporting.Reported.IsZero(), - Moderation: reporting.moderation, - Title: bug.displayTitle(), - Link: fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID), - CreditEmail: creditEmail, - Log: crashLog, - LogLink: externalLink(c, textCrashLog, crash.Log), - Report: report, - ReportLink: externalLink(c, textCrashReport, crash.Report), - Maintainers: append(crash.Maintainers, kernelRepo.CC...), - OS: build.OS, - Arch: build.Arch, - VMArch: build.VMArch, - UserSpaceArch: kernelArch(build.Arch), - CompilerID: build.CompilerID, - KernelRepo: build.KernelRepo, - KernelRepoAlias: kernelRepo.Alias, - KernelBranch: build.KernelBranch, - KernelCommit: build.KernelCommit, - KernelCommitTitle: build.KernelCommitTitle, - KernelCommitDate: build.KernelCommitDate, - KernelConfig: kernelConfig, - KernelConfigLink: externalLink(c, textKernelConfig, build.KernelConfig), - ReproC: reproC, - ReproCLink: externalLink(c, textReproC, crash.ReproC), - ReproSyz: reproSyz, - ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz), - CrashID: crashKey.IntID(), - NumCrashes: bug.NumCrashes, - HappenedOn: managersToRepos(c, bug.Namespace, bug.HappenedOn), + Type: typ, + Config: reportingConfig, + ExtID: bugReporting.ExtID, + First: bugReporting.Reported.IsZero(), + Moderation: reporting.moderation, + Log: crashLog, + LogLink: externalLink(c, textCrashLog, crash.Log), + Report: report, + ReportLink: externalLink(c, textCrashReport, crash.Report), + Maintainers: append(crash.Maintainers, kernelRepo.CC...), + ReproC: reproC, + ReproCLink: externalLink(c, textReproC, crash.ReproC), + ReproSyz: reproSyz, + ReproSyzLink: externalLink(c, textReproSyz, crash.ReproSyz), + CrashID: crashKey.IntID(), + NumCrashes: bug.NumCrashes, + HappenedOn: managersToRepos(c, bug.Namespace, bug.HappenedOn), } if bugReporting.CC != "" { rep.CC = strings.Split(bugReporting.CC, "|") @@ -400,9 +374,48 @@ func createBugReport(c context.Context, bug *Bug, crash *Crash, crashKey *datast if bug.BisectCause == BisectYes { rep.BisectCause = bisectFromJob(c, rep, job) } + if err := fillBugReport(c, rep, bug, bugReporting, build); err != nil { + return nil, err + } return rep, nil } +// fillBugReport fills common report fields for bug and job reports. +func fillBugReport(c context.Context, rep *dashapi.BugReport, bug *Bug, bugReporting *BugReporting, + build *Build) error { + kernelConfig, _, err := getText(c, textKernelConfig, build.KernelConfig) + if err != nil { + return err + } + creditEmail, err := email.AddAddrContext(ownEmail(c), bugReporting.ID) + if err != nil { + return err + } + rep.Namespace = bug.Namespace + rep.ID = bugReporting.ID + rep.Title = bug.displayTitle() + rep.Link = fmt.Sprintf("%v/bug?extid=%v", appURL(c), bugReporting.ID) + rep.CreditEmail = creditEmail + rep.OS = build.OS + rep.Arch = build.Arch + rep.VMArch = build.VMArch + rep.UserSpaceArch = kernelArch(build.Arch) + rep.CompilerID = build.CompilerID + rep.KernelRepo = build.KernelRepo + rep.KernelRepoAlias = kernelRepoInfo(build).Alias + rep.KernelBranch = build.KernelBranch + rep.KernelCommit = build.KernelCommit + rep.KernelCommitTitle = build.KernelCommitTitle + rep.KernelCommitDate = build.KernelCommitDate + rep.KernelConfig = kernelConfig + rep.KernelConfigLink = externalLink(c, textKernelConfig, build.KernelConfig) + for _, addr := range bug.UNCC { + rep.CC = email.RemoveFromEmailList(rep.CC, addr) + rep.Maintainers = email.RemoveFromEmailList(rep.Maintainers, addr) + } + return nil +} + func loadBisectJob(c context.Context, bug *Bug) (*Job, *Crash, *datastore.Key, *datastore.Key, error) { bugKey := bug.key(c) var jobs []*Job @@ -679,7 +692,7 @@ func incomingCommandTx(c context.Context, now time.Time, cmd *dashapi.BugUpdate, if bugReporting.Link == "" { bugReporting.Link = cmd.Link } - if len(cmd.CC) != 0 { + if len(cmd.CC) != 0 && cmd.Status != dashapi.BugStatusUnCC { merged := email.MergeEmailLists(strings.Split(bugReporting.CC, "|"), cmd.CC) bugReporting.CC = strings.Join(merged, "|") } @@ -755,6 +768,8 @@ func incomingCommandCmd(c context.Context, now time.Time, cmd *dashapi.BugUpdate bug.DupOf = dupHash case dashapi.BugStatusUpdate: // Just update Link, Commits, etc below. + case dashapi.BugStatusUnCC: + bug.UNCC = email.MergeEmailLists(bug.UNCC, cmd.CC) default: return false, internalError, fmt.Errorf("unknown bug status %v", cmd.Status) } diff --git a/dashboard/app/reporting_email.go b/dashboard/app/reporting_email.go index 7d06eeae3..d05b0cd59 100644 --- a/dashboard/app/reporting_email.go +++ b/dashboard/app/reporting_email.go @@ -326,6 +326,9 @@ func incomingMail(c context.Context, r *http.Request) error { } cmd.Status = dashapi.BugStatusDup cmd.DupOf = msg.CommandArgs + case "uncc", "uncc:": + cmd.Status = dashapi.BugStatusUnCC + cmd.CC = []string{email.CanonicalEmail(msg.From)} default: return replyTo(c, msg, fmt.Sprintf("unknown command %q", msg.Command), nil) } @@ -336,7 +339,7 @@ func incomingMail(c context.Context, r *http.Request) error { if !ok && reply != "" { return replyTo(c, msg, reply, nil) } - if !mailingListInCC && msg.Command != "" { + if !mailingListInCC && msg.Command != "" && cmd.Status != dashapi.BugStatusUnCC { warnMailingListInCC(c, msg, mailingList) } return nil diff --git a/dashboard/app/util_test.go b/dashboard/app/util_test.go index 74540d5b1..fbe11b017 100644 --- a/dashboard/app/util_test.go +++ b/dashboard/app/util_test.go @@ -252,6 +252,14 @@ func (c *Ctx) pollEmailBug() *aemail.Message { return <-c.emailSink } +func (c *Ctx) expectNoEmail() { + c.expectOK(c.GET("/email_poll")) + if len(c.emailSink) != 0 { + msg := <-c.emailSink + c.t.Fatalf("\n%v: got expected email: %v\n%s", caller(0), msg.Subject, msg.Body) + } +} + type apiClient struct { *Ctx *dashapi.Dashboard diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index 42200eb15..d53786476 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -469,6 +469,7 @@ const ( BugStatusInvalid BugStatusDup BugStatusUpdate // aux info update (i.e. ExtID/Link/CC) + BugStatusUnCC // don't CC sender on any future communication ) const ( diff --git a/pkg/email/parser.go b/pkg/email/parser.go index 1b2dbc747..ce652c5da 100644 --- a/pkg/email/parser.go +++ b/pkg/email/parser.go @@ -330,3 +330,14 @@ func MergeEmailLists(lists ...[]string) []string { } return result } + +func RemoveFromEmailList(list []string, toRemove string) []string { + var result []string + toRemove = CanonicalEmail(toRemove) + for _, email := range list { + if CanonicalEmail(email) != toRemove { + result = append(result, email) + } + } + return result +} |
