From a1952afc382bf0773094cbf36b1e3528d89c958b Mon Sep 17 00:00:00 2001 From: Maxim Zhiburt Date: Sat, 24 Oct 2020 23:38:14 +0300 Subject: [PATCH] Sendmail command (#13079) * Add SendSync method Usefull to have when you need to be confident that message was sent. * Add sendmail command * add checks that if either title or content is empty then error out * Add a confirmation step * Add --force option to bypass confirm step * Move implementation of runSendMail to a different file * Add copyrighting comment * Make content optional Print waring if it's empty or haven't been set up. The warning will be skiped if there's a `--force` flag. * Fix import style Co-authored-by: 6543 <6543@obermui.de> * Use batch when getting all users IterateUsers uses batching by default. Signed-off-by: Maxim Zhiburt * Send emails one by one instead of as one chunck Signed-off-by: Maxim Zhiburt * Send messages concurantly Signed-off-by: Maxim Zhiburt * Use SendAsync+Flush instead of SendSync Signed-off-by: Maxim Zhiburt * Add timeout parameter to sendemail command Signed-off-by: Maxim Zhiburt * Fix spelling mistake Signed-off-by: Maxim Zhiburt * Update cmd/admin.go Co-authored-by: 6543 <6543@obermui.de> * Connect to a running Gitea instance * Fix mispelling * Add copyright comment Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao Co-authored-by: techknowlogick --- cmd/admin.go | 23 +++++++++++++ cmd/cmd.go | 20 +++++++++++ cmd/mailer.go | 48 ++++++++++++++++++++++++++ modules/private/mail.go | 53 +++++++++++++++++++++++++++++ routers/private/internal.go | 1 + routers/private/mail.go | 67 +++++++++++++++++++++++++++++++++++++ 6 files changed, 212 insertions(+) create mode 100644 cmd/mailer.go create mode 100644 modules/private/mail.go create mode 100644 routers/private/mail.go diff --git a/cmd/admin.go b/cmd/admin.go index d50365725..8989ec2eb 100644 --- a/cmd/admin.go +++ b/cmd/admin.go @@ -34,6 +34,7 @@ var ( subcmdRepoSyncReleases, subcmdRegenerate, subcmdAuth, + subcmdSendMail, }, } @@ -282,6 +283,28 @@ var ( Action: runAddOauth, Flags: oauthCLIFlags, } + + subcmdSendMail = cli.Command{ + Name: "sendmail", + Usage: "Send a message to all users", + Action: runSendMail, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "title", + Usage: `a title of a message`, + Value: "", + }, + cli.StringFlag{ + Name: "content", + Usage: "a content of a message", + Value: "", + }, + cli.BoolFlag{ + Name: "force,f", + Usage: "A flag to bypass a confirmation step", + }, + }, + } ) func runChangePassword(c *cli.Context) error { diff --git a/cmd/cmd.go b/cmd/cmd.go index d05eb8b1a..bb768cc15 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -9,6 +9,7 @@ package cmd import ( "errors" "fmt" + "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/setting" @@ -32,6 +33,25 @@ func argsSet(c *cli.Context, args ...string) error { return nil } +// confirm waits for user input which confirms an action +func confirm() (bool, error) { + var response string + + _, err := fmt.Scanln(&response) + if err != nil { + return false, err + } + + switch strings.ToLower(response) { + case "y", "yes": + return true, nil + case "n", "no": + return false, nil + default: + return false, errors.New(response + " isn't a correct confirmation string") + } +} + func initDB() error { return initDBDisableConsole(false) } diff --git a/cmd/mailer.go b/cmd/mailer.go new file mode 100644 index 000000000..a9a9048a5 --- /dev/null +++ b/cmd/mailer.go @@ -0,0 +1,48 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package cmd + +import ( + "fmt" + "net/http" + + "code.gitea.io/gitea/modules/private" + "github.com/urfave/cli" +) + +func runSendMail(c *cli.Context) error { + if err := argsSet(c, "title"); err != nil { + return err + } + + subject := c.String("title") + confirmSkiped := c.Bool("force") + body := c.String("content") + + if !confirmSkiped { + if len(body) == 0 { + fmt.Print("warning: Content is empty") + } + + fmt.Print("Proceed with sending email? [Y/n] ") + isConfirmed, err := confirm() + if err != nil { + return err + } else if !isConfirmed { + fmt.Println("The mail was not sent") + return nil + } + } + + status, message := private.SendEmail(subject, body, nil) + if status != http.StatusOK { + fmt.Printf("error: %s", message) + return nil + } + + fmt.Printf("Succseded: %s", message) + + return nil +} diff --git a/modules/private/mail.go b/modules/private/mail.go new file mode 100644 index 000000000..db56009bb --- /dev/null +++ b/modules/private/mail.go @@ -0,0 +1,53 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package private + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "code.gitea.io/gitea/modules/setting" +) + +// Email structure holds a data for sending general emails +type Email struct { + Subject string + Message string + To []string +} + +// SendEmail calls the internal SendEmail function +// +// It accepts a list of usernames. +// If DB contains these users it will send the email to them. +// +// If to list == nil its supposed to send an email to every +// user present in DB +func SendEmail(subject, message string, to []string) (int, string) { + reqURL := setting.LocalURL + "api/internal/mail/send" + + req := newInternalRequest(reqURL, "POST") + req = req.Header("Content-Type", "application/json") + jsonBytes, _ := json.Marshal(Email{ + Subject: subject, + Message: message, + To: to, + }) + req.Body(jsonBytes) + resp, err := req.Response() + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Unable to contact gitea: %v", err.Error()) + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return http.StatusInternalServerError, fmt.Sprintf("Response body error: %v", err.Error()) + } + + return http.StatusOK, fmt.Sprintf("Was sent %s from %d", body, len(to)) +} diff --git a/routers/private/internal.go b/routers/private/internal.go index 821cf62a6..4fb267a49 100644 --- a/routers/private/internal.go +++ b/routers/private/internal.go @@ -47,5 +47,6 @@ func RegisterRoutes(m *macaron.Macaron) { m.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging) m.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger) m.Post("/manager/remove-logger/:group/:name", RemoveLogger) + m.Post("/mail/send", SendEmail) }, CheckInternalToken) } diff --git a/routers/private/mail.go b/routers/private/mail.go new file mode 100644 index 000000000..8d0975248 --- /dev/null +++ b/routers/private/mail.go @@ -0,0 +1,67 @@ +// Copyright 2020 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package private + +import ( + "fmt" + "net/http" + "strconv" + + "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/private" + "code.gitea.io/gitea/services/mailer" + "gitea.com/macaron/macaron" +) + +// SendEmail pushes messages to mail queue +// +// It doesn't wait before each message will be processed +func SendEmail(ctx *macaron.Context, mail private.Email) { + var emails []string + if len(mail.To) > 0 { + for _, uname := range mail.To { + user, err := models.GetUserByName(uname) + if err != nil { + err := fmt.Sprintf("Failed to get user information: %v", err) + log.Error(err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err, + }) + return + } + + if user != nil { + emails = append(emails, user.Email) + } + } + } else { + err := models.IterateUser(func(user *models.User) error { + emails = append(emails, user.Email) + return nil + }) + if err != nil { + err := fmt.Sprintf("Failed to find users: %v", err) + log.Error(err) + ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ + "err": err, + }) + return + } + } + + sendEmail(ctx, mail.Subject, mail.Message, emails) +} + +func sendEmail(ctx *macaron.Context, subject, message string, to []string) { + for _, email := range to { + msg := mailer.NewMessage([]string{email}, subject, message) + mailer.SendAsync(msg) + } + + wasSent := strconv.Itoa(len(to)) + + ctx.PlainText(http.StatusOK, []byte(wasSent)) +}