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 <zhiburt@gmail.com> * Send emails one by one instead of as one chunck Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com> * Send messages concurantly Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com> * Use SendAsync+Flush instead of SendSync Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com> * Add timeout parameter to sendemail command Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com> * Fix spelling mistake Signed-off-by: Maxim Zhiburt <zhiburt@gmail.com> * 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 <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
		
							parent
							
								
									c5020cff3d
								
							
						
					
					
						commit
						a1952afc38
					
				
					 6 changed files with 212 additions and 0 deletions
				
			
		
							
								
								
									
										23
									
								
								cmd/admin.go
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								cmd/admin.go
									
									
									
									
									
								
							|  | @ -34,6 +34,7 @@ var ( | ||||||
| 			subcmdRepoSyncReleases, | 			subcmdRepoSyncReleases, | ||||||
| 			subcmdRegenerate, | 			subcmdRegenerate, | ||||||
| 			subcmdAuth, | 			subcmdAuth, | ||||||
|  | 			subcmdSendMail, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -282,6 +283,28 @@ var ( | ||||||
| 		Action: runAddOauth, | 		Action: runAddOauth, | ||||||
| 		Flags:  oauthCLIFlags, | 		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 { | func runChangePassword(c *cli.Context) error { | ||||||
|  |  | ||||||
							
								
								
									
										20
									
								
								cmd/cmd.go
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								cmd/cmd.go
									
									
									
									
									
								
							|  | @ -9,6 +9,7 @@ package cmd | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | @ -32,6 +33,25 @@ func argsSet(c *cli.Context, args ...string) error { | ||||||
| 	return nil | 	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 { | func initDB() error { | ||||||
| 	return initDBDisableConsole(false) | 	return initDBDisableConsole(false) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										48
									
								
								cmd/mailer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								cmd/mailer.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										53
									
								
								modules/private/mail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								modules/private/mail.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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)) | ||||||
|  | } | ||||||
|  | @ -47,5 +47,6 @@ func RegisterRoutes(m *macaron.Macaron) { | ||||||
| 		m.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging) | 		m.Post("/manager/release-and-reopen-logging", ReleaseReopenLogging) | ||||||
| 		m.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger) | 		m.Post("/manager/add-logger", bind(private.LoggerOptions{}), AddLogger) | ||||||
| 		m.Post("/manager/remove-logger/:group/:name", RemoveLogger) | 		m.Post("/manager/remove-logger/:group/:name", RemoveLogger) | ||||||
|  | 		m.Post("/mail/send", SendEmail) | ||||||
| 	}, CheckInternalToken) | 	}, CheckInternalToken) | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										67
									
								
								routers/private/mail.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								routers/private/mail.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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)) | ||||||
|  | } | ||||||
		Loading…
	
		Reference in a new issue