From fab3f9b37dca65849a3c1dfdd0fb43b564fe526c Mon Sep 17 00:00:00 2001 From: Mathieu Velten Date: Wed, 16 Sep 2020 11:25:56 +0200 Subject: [PATCH] Add email support (#35) Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com> --- README.md | 8 +++ changelog.d/35.feature | 1 + go.mod | 1 + go.sum | 3 + main.go | 21 +++++- rageshake.sample.yaml | 13 ++++ submit.go | 154 ++++++++++++++++++++++++++++------------- 7 files changed, 148 insertions(+), 53 deletions(-) create mode 100644 changelog.d/35.feature diff --git a/README.md b/README.md index 7a2fe9b..f507468 100644 --- a/README.md +++ b/README.md @@ -91,3 +91,11 @@ The response (if successful) will be a JSON object with the following fields: * `report_url`: A URL where the user can track their bug report. Omitted if issue submission was disabled. + +## Notifications + +You can get notifications when a new rageshake arrives on the server. + +Currently this tool supports pushing notifications as GitHub issues in a repo, +through a Slack webhook or by email, cf sample config file for how to +configure them. \ No newline at end of file diff --git a/changelog.d/35.feature b/changelog.d/35.feature new file mode 100644 index 0000000..3b78de1 --- /dev/null +++ b/changelog.d/35.feature @@ -0,0 +1 @@ +Add email support. diff --git a/go.mod b/go.mod index 56a27c0..0e007e8 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/go-github v0.0.0-20170401000335-12363ffc1001 github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171 + github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7 golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b golang.org/x/oauth2 v0.0.0-20170321013421-7fdf09982454 diff --git a/go.sum b/go.sum index 7476b54..f049b8d 100644 --- a/go.sum +++ b/go.sum @@ -3,9 +3,12 @@ github.com/golang/protobuf v0.0.0-20170331031902-2bba0603135d/go.mod h1:6lQm79b+ github.com/google/go-genproto v0.0.0-20170404132009-411e09b969b1/go.mod h1:3Rcd9jSoLVkV/osPrt5CogLvLiarfI8U9/x78NwhuDU= github.com/google/go-github v0.0.0-20170401000335-12363ffc1001 h1:OK4gfzCBCtPg14E4sYsczwFhjVu1jQJZI+OEOpiTigw= github.com/google/go-github v0.0.0-20170401000335-12363ffc1001/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135 h1:zLTLjkaOFEFIOxY5BWLFLwh+cL8vOBW4XJ2aqLE/Tf0= github.com/google/go-querystring v0.0.0-20170111101155-53e6ce116135/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/googleapis/gax-go v0.0.0-20170321005343-9af46dd5a171/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible h1:d60x4RsAHk/UX/0OT8Gc6D7scVvhBbEANpTAWrDhA/I= +github.com/jordan-wright/email v4.0.1-0.20200824153738-3f5bafa1cd84+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/pkg/errors v0.0.0-20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b h1:Co3zyosPfwWowmu8+roHGC+aDgizpCPH3ukhubZ0Ttg= golang.org/x/net v0.0.0-20170329170435-ffcf1bedda3b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= diff --git a/main.go b/main.go index 15de9d6..4dc140a 100644 --- a/main.go +++ b/main.go @@ -21,8 +21,6 @@ import ( "crypto/subtle" "flag" "fmt" - "github.com/google/go-github/github" - "golang.org/x/oauth2" "io/ioutil" "log" "net" @@ -31,6 +29,9 @@ import ( "strings" "time" + "github.com/google/go-github/github" + "golang.org/x/oauth2" + yaml "gopkg.in/yaml.v2" ) @@ -51,6 +52,16 @@ type config struct { GithubProjectMappings map[string]string `yaml:"github_project_mappings"` SlackWebhookURL string `yaml:"slack_webhook_url"` + + EmailAddresses []string `yaml:"email_addresses"` + + EmailFrom string `yaml:"email_from"` + + SMTPServer string `yaml:"smtp_server"` + + SMTPUsername string `yaml:"smtp_username"` + + SMTPPassword string `yaml:"smtp_password"` } func basicAuth(handler http.Handler, username, password, realm string) http.Handler { @@ -99,6 +110,10 @@ func main() { slack = newSlackClient(cfg.SlackWebhookURL) } + if len(cfg.EmailAddresses) > 0 && cfg.SMTPServer == "" { + log.Fatal("Email address(es) specified but no smtp_server configured. Wrong configuration, aborting...") + } + apiPrefix := cfg.APIPrefix if apiPrefix == "" { _, port, err := net.SplitHostPort(*bindAddr) @@ -112,7 +127,7 @@ func main() { } log.Printf("Using %s/listing as public URI", apiPrefix) - http.Handle("/api/submit", &submitServer{ghClient, apiPrefix, cfg.GithubProjectMappings, slack}) + http.Handle("/api/submit", &submitServer{ghClient, apiPrefix, slack, cfg}) // Make sure bugs directory exists _ = os.Mkdir("bugs", os.ModePerm) diff --git a/rageshake.sample.yaml b/rageshake.sample.yaml index ea68887..18b55d8 100644 --- a/rageshake.sample.yaml +++ b/rageshake.sample.yaml @@ -20,3 +20,16 @@ github_project_mappings: # a Slack personal webhook URL (https://api.slack.com/incoming-webhooks), which # will be used to post a notification on Slack for each report. slack_webhook_url: https://hooks.slack.com/services/TTTTTTT/XXXXXXXXXX/YYYYYYYYYYY + +# notification can also be pushed by email. +# this param controls the target emails +email_addresses: + - support@matrix.org + +# this is the from field that will be used in the email notifications +email_from: Rageshake + +# SMTP server configuration +smtp_server: localhost:25 +smtp_username: myemailuser +smtp_password: myemailpass diff --git a/submit.go b/submit.go index d6b5d0a..42c7bed 100644 --- a/submit.go +++ b/submit.go @@ -28,6 +28,7 @@ import ( "mime" "mime/multipart" "net/http" + "net/smtp" "os" "path/filepath" "regexp" @@ -37,6 +38,7 @@ import ( "time" "github.com/google/go-github/github" + "github.com/jordan-wright/email" ) var maxPayloadSize = 1024 * 1024 * 55 // 55 MB @@ -49,10 +51,9 @@ type submitServer struct { // External URI to /api apiPrefix string - // mappings from application to github owner/project - githubProjectMappings map[string]string - slack *slackClient + + cfg *config } // the type of payload which can be uploaded as JSON to the submit endpoint @@ -470,70 +471,77 @@ func (s *submitServer) saveReport(ctx context.Context, p parsedPayload, reportDi return nil, err } + if err := s.sendEmail(p, reportDir); err != nil { + return nil, err + } + return &resp, nil } func (s *submitServer) submitGithubIssue(ctx context.Context, p parsedPayload, listingURL string, resp *submitResponse) error { if s.ghClient == nil { - log.Println("GH issue submission disabled") - } else { - // submit a github issue - ghProj := s.githubProjectMappings[p.AppName] - if ghProj == "" { - log.Println("Not creating GH issue for unknown app", p.AppName) - return nil - } - splits := strings.SplitN(ghProj, "/", 2) - if len(splits) < 2 { - log.Println("Can't create GH issue for invalid repo", ghProj) - } - owner, repo := splits[0], splits[1] - - issueReq := buildGithubIssueRequest(p, listingURL) - - issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, &issueReq) - if err != nil { - return err - } - - log.Println("Created issue:", *issue.HTMLURL) - - resp.ReportURL = *issue.HTMLURL + return nil } + + // submit a github issue + ghProj := s.cfg.GithubProjectMappings[p.AppName] + if ghProj == "" { + log.Println("Not creating GH issue for unknown app", p.AppName) + return nil + } + splits := strings.SplitN(ghProj, "/", 2) + if len(splits) < 2 { + log.Println("Can't create GH issue for invalid repo", ghProj) + } + owner, repo := splits[0], splits[1] + + issueReq := buildGithubIssueRequest(p, listingURL) + + issue, _, err := s.ghClient.Issues.Create(ctx, owner, repo, &issueReq) + if err != nil { + return err + } + + log.Println("Created issue:", *issue.HTMLURL) + + resp.ReportURL = *issue.HTMLURL + return nil } func (s *submitServer) submitSlackNotification(p parsedPayload, listingURL string) error { if s.slack == nil { - log.Println("Slack notifications disabled") - } else { - slackBuf := fmt.Sprintf( - "%s\nApplication: %s\nReport: %s", - p.UserText, p.AppName, listingURL, - ) - - err := s.slack.Notify(slackBuf) - if err != nil { - return err - } + return nil } + + slackBuf := fmt.Sprintf( + "%s\nApplication: %s\nReport: %s", + p.UserText, p.AppName, listingURL, + ) + + err := s.slack.Notify(slackBuf) + if err != nil { + return err + } + return nil } -func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueRequest { +func buildReportTitle(p parsedPayload) string { // set the title to the first (non-empty) line of the user's report, if any - var title string trimmedUserText := strings.TrimSpace(p.UserText) if trimmedUserText == "" { - title = "Untitled report" - } else { - if i := strings.IndexAny(trimmedUserText, "\r\n"); i < 0 { - title = trimmedUserText - } else { - title = trimmedUserText[0:i] - } + return "Untitled report" } + if i := strings.IndexAny(trimmedUserText, "\r\n"); i >= 0 { + return trimmedUserText[0:i] + } + + return trimmedUserText +} + +func buildReportBody(p parsedPayload, quoteChar string) *bytes.Buffer { var bodyBuf bytes.Buffer fmt.Fprintf(&bodyBuf, "User message:\n\n%s\n\n", p.UserText) var dataKeys []string @@ -543,19 +551,29 @@ func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueReq sort.Strings(dataKeys) for _, k := range dataKeys { v := p.Data[k] - fmt.Fprintf(&bodyBuf, "%s: `%s`\n", k, v) + fmt.Fprintf(&bodyBuf, "%s: %s%s%s\n", k, quoteChar, v, quoteChar) } - fmt.Fprintf(&bodyBuf, "[Logs](%s)", listingURL) + + return &bodyBuf +} + +func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueRequest { + bodyBuf := buildReportBody(p, "`") + + // Add log links to the body + fmt.Fprintf(bodyBuf, "[Logs](%s)", listingURL) for _, file := range p.Files { fmt.Fprintf( - &bodyBuf, + bodyBuf, " / [%s](%s)", file, listingURL+"/"+file, ) } + title := buildReportTitle(p) + body := bodyBuf.String() labels := p.Labels @@ -570,6 +588,42 @@ func buildGithubIssueRequest(p parsedPayload, listingURL string) github.IssueReq } } +func (s *submitServer) sendEmail(p parsedPayload, reportDir string) error { + if len(s.cfg.EmailAddresses) == 0 { + return nil + } + + e := email.NewEmail() + + e.From = "Rageshake " + if s.cfg.EmailFrom != "" { + e.From = s.cfg.EmailFrom + } + + e.To = s.cfg.EmailAddresses + + e.Subject = fmt.Sprintf("[%s] %s", p.AppName, buildReportTitle(p)) + + e.Text = buildReportBody(p, "\"").Bytes() + + allFiles := append(p.Files, p.Logs...) + for _, file := range allFiles { + fullPath := filepath.Join(reportDir, file) + e.AttachFile(fullPath) + } + + var auth smtp.Auth = nil + if s.cfg.SMTPPassword != "" || s.cfg.SMTPUsername != "" { + auth = smtp.PlainAuth("", s.cfg.SMTPUsername, s.cfg.SMTPPassword, s.cfg.SMTPServer) + } + err := e.Send(s.cfg.SMTPServer, auth) + if err != nil { + return err + } + + return nil +} + func respond(code int, w http.ResponseWriter) { w.WriteHeader(code) w.Write([]byte("{}"))