mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09:00 
			
		
		
		
	[refactor] mailer service (#15072)
* Unexport SendUserMail * Instead of "[]*models.User" or "[]string" lists infent "[]*MailRecipient" for mailer * adopt * code format * TODOs for "i18n" * clean * no fallback for lang -> just use english * lint * exec testComposeIssueCommentMessage per lang and use only emails * rm MailRecipient * Dont reload from users from db if you alredy have in ram * nits * minimize diff Signed-off-by: 6543 <6543@obermui.de> * localize subjects * linter ... * Tr extend * start tmpl edit ... * Apply suggestions from code review * use translation.Locale * improve mailIssueCommentBatch Signed-off-by: Andrew Thornton <art27@cantab.net> * add i18n to datas Signed-off-by: Andrew Thornton <art27@cantab.net> * a comment Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
		| @@ -331,11 +331,6 @@ func (u *User) GenerateEmailActivateCode(email string) string { | |||||||
| 	return code | 	return code | ||||||
| } | } | ||||||
|  |  | ||||||
| // GenerateActivateCode generates an activate code based on user information. |  | ||||||
| func (u *User) GenerateActivateCode() string { |  | ||||||
| 	return u.GenerateEmailActivateCode(u.Email) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // GetFollowers returns range of user's followers. | // GetFollowers returns range of user's followers. | ||||||
| func (u *User) GetFollowers(listOptions ListOptions) ([]*User, error) { | func (u *User) GetFollowers(listOptions ListOptions) ([]*User, error) { | ||||||
| 	sess := x. | 	sess := x. | ||||||
|   | |||||||
| @@ -104,14 +104,14 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *model | |||||||
| 	// mail only sent to added assignees and not self-assignee | 	// mail only sent to added assignees and not self-assignee | ||||||
| 	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { | 	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { | ||||||
| 		ct := fmt.Sprintf("Assigned #%d.", issue.Index) | 		ct := fmt.Sprintf("Assigned #%d.", issue.Index) | ||||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []string{assignee.Email}) | 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{assignee}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func (m *mailNotifier) NotifyPullReviewRequest(doer *models.User, issue *models.Issue, reviewer *models.User, isRequest bool, comment *models.Comment) { | func (m *mailNotifier) NotifyPullReviewRequest(doer *models.User, issue *models.Issue, reviewer *models.User, isRequest bool, comment *models.Comment) { | ||||||
| 	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { | 	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { | ||||||
| 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) | 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) | ||||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []string{reviewer.Email}) | 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{reviewer}) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -153,7 +153,7 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *model | |||||||
| } | } | ||||||
|  |  | ||||||
| func (m *mailNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) { | func (m *mailNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) { | ||||||
| 	if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, []*models.User{}); err != nil { | 	if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, nil); err != nil { | ||||||
| 		log.Error("MailParticipantsComment: %v", err) | 		log.Error("MailParticipantsComment: %v", err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -320,6 +320,14 @@ reset_password = Recover your account | |||||||
| register_success = Registration successful | register_success = Registration successful | ||||||
| register_notify = Welcome to Gitea | register_notify = Welcome to Gitea | ||||||
|  |  | ||||||
|  | release.new.subject = %s in %s released | ||||||
|  |  | ||||||
|  | repo.transfer.subject_to = %s would like to transfer "%s" to %s | ||||||
|  | repo.transfer.subject_to_you = %s would like to transfer "%s" to you | ||||||
|  | repo.transfer.to_you = you | ||||||
|  |  | ||||||
|  | repo.collaborator.added.subject = %s added you to %s | ||||||
|  |  | ||||||
| [modal] | [modal] | ||||||
| yes = Yes | yes = Yes | ||||||
| no = No | no = No | ||||||
|   | |||||||
| @@ -154,7 +154,7 @@ func NewUserPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	// Send email notification. | 	// Send email notification. | ||||||
| 	if form.SendNotify { | 	if form.SendNotify { | ||||||
| 		mailer.SendRegisterNotifyMail(ctx.Locale, u) | 		mailer.SendRegisterNotifyMail(u) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name)) | 	ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name)) | ||||||
|   | |||||||
| @@ -114,7 +114,7 @@ func CreateUser(ctx *context.APIContext) { | |||||||
|  |  | ||||||
| 	// Send email notification. | 	// Send email notification. | ||||||
| 	if form.SendNotify { | 	if form.SendNotify { | ||||||
| 		mailer.SendRegisterNotifyMail(ctx.Locale, u) | 		mailer.SendRegisterNotifyMail(u) | ||||||
| 	} | 	} | ||||||
| 	ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.User)) | 	ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.User)) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1397,7 +1397,7 @@ func ForgotPasswdPost(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	mailer.SendResetPasswordMail(ctx.Locale, u) | 	mailer.SendResetPasswordMail(u) | ||||||
|  |  | ||||||
| 	if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { | 	if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { | ||||||
| 		log.Error("Set cache(MailResendLimit) fail: %v", err) | 		log.Error("Set cache(MailResendLimit) fail: %v", err) | ||||||
|   | |||||||
| @@ -132,7 +132,7 @@ func EmailPost(ctx *context.Context) { | |||||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings/account") | 				ctx.Redirect(setting.AppSubURL + "/user/settings/account") | ||||||
| 				return | 				return | ||||||
| 			} | 			} | ||||||
| 			mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email) | 			mailer.SendActivateEmailMail(ctx.User, email) | ||||||
| 			address = email.Email | 			address = email.Email | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -194,7 +194,7 @@ func EmailPost(ctx *context.Context) { | |||||||
|  |  | ||||||
| 	// Send confirmation email | 	// Send confirmation email | ||||||
| 	if setting.Service.RegisterEmailConfirm { | 	if setting.Service.RegisterEmailConfirm { | ||||||
| 		mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email) | 		mailer.SendActivateEmailMail(ctx.User, email) | ||||||
| 		if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { | 		if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { | ||||||
| 			log.Error("Set cache(MailResendLimit) fail: %v", err) | 			log.Error("Set cache(MailResendLimit) fail: %v", err) | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| 	"code.gitea.io/gitea/modules/timeutil" | 	"code.gitea.io/gitea/modules/timeutil" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation" | ||||||
|  |  | ||||||
| 	"gopkg.in/gomail.v2" | 	"gopkg.in/gomail.v2" | ||||||
| ) | ) | ||||||
| @@ -57,17 +58,21 @@ func SendTestMail(email string) error { | |||||||
| 	return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) | 	return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendUserMail sends a mail to the user | // sendUserMail sends a mail to the user | ||||||
| func SendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { | func sendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { | ||||||
|  | 	locale := translation.NewLocale(language) | ||||||
| 	data := map[string]interface{}{ | 	data := map[string]interface{}{ | ||||||
| 		"DisplayName":       u.DisplayName(), | 		"DisplayName":       u.DisplayName(), | ||||||
| 		"ActiveCodeLives":   timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language), | 		"ActiveCodeLives":   timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language), | ||||||
| 		"ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language), | 		"ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language), | ||||||
| 		"Code":              code, | 		"Code":              code, | ||||||
|  | 		"i18n":              locale, | ||||||
|  | 		"Language":          locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var content bytes.Buffer | 	var content bytes.Buffer | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { | ||||||
| 		log.Error("Template: %v", err) | 		log.Error("Template: %v", err) | ||||||
| 		return | 		return | ||||||
| @@ -79,33 +84,32 @@ func SendUserMail(language string, u *models.User, tpl base.TplName, code, subje | |||||||
| 	SendAsync(msg) | 	SendAsync(msg) | ||||||
| } | } | ||||||
|  |  | ||||||
| // Locale represents an interface to translation |  | ||||||
| type Locale interface { |  | ||||||
| 	Language() string |  | ||||||
| 	Tr(string, ...interface{}) string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // SendActivateAccountMail sends an activation mail to the user (new user registration) | // SendActivateAccountMail sends an activation mail to the user (new user registration) | ||||||
| func SendActivateAccountMail(locale Locale, u *models.User) { | func SendActivateAccountMail(locale translation.Locale, u *models.User) { | ||||||
| 	SendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateActivateCode(), locale.Tr("mail.activate_account"), "activate account") | 	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account") | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendResetPasswordMail sends a password reset mail to the user | // SendResetPasswordMail sends a password reset mail to the user | ||||||
| func SendResetPasswordMail(locale Locale, u *models.User) { | func SendResetPasswordMail(u *models.User) { | ||||||
| 	SendUserMail(locale.Language(), u, mailAuthResetPassword, u.GenerateActivateCode(), locale.Tr("mail.reset_password"), "recover account") | 	locale := translation.NewLocale(u.Language) | ||||||
|  | 	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account") | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendActivateEmailMail sends confirmation email to confirm new email address | // SendActivateEmailMail sends confirmation email to confirm new email address | ||||||
| func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAddress) { | func SendActivateEmailMail(u *models.User, email *models.EmailAddress) { | ||||||
|  | 	locale := translation.NewLocale(u.Language) | ||||||
| 	data := map[string]interface{}{ | 	data := map[string]interface{}{ | ||||||
| 		"DisplayName":     u.DisplayName(), | 		"DisplayName":     u.DisplayName(), | ||||||
| 		"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()), | 		"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()), | ||||||
| 		"Code":            u.GenerateEmailActivateCode(email.Email), | 		"Code":            u.GenerateEmailActivateCode(email.Email), | ||||||
| 		"Email":           email.Email, | 		"Email":           email.Email, | ||||||
|  | 		"i18n":            locale, | ||||||
|  | 		"Language":        locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var content bytes.Buffer | 	var content bytes.Buffer | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { | ||||||
| 		log.Error("Template: %v", err) | 		log.Error("Template: %v", err) | ||||||
| 		return | 		return | ||||||
| @@ -118,19 +122,19 @@ func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAdd | |||||||
| } | } | ||||||
|  |  | ||||||
| // SendRegisterNotifyMail triggers a notify e-mail by admin created a account. | // SendRegisterNotifyMail triggers a notify e-mail by admin created a account. | ||||||
| func SendRegisterNotifyMail(locale Locale, u *models.User) { | func SendRegisterNotifyMail(u *models.User) { | ||||||
| 	if setting.MailService == nil { | 	locale := translation.NewLocale(u.Language) | ||||||
| 		log.Warn("SendRegisterNotifyMail is being invoked but mail service hasn't been initialized") |  | ||||||
| 		return |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	data := map[string]interface{}{ | 	data := map[string]interface{}{ | ||||||
| 		"DisplayName": u.DisplayName(), | 		"DisplayName": u.DisplayName(), | ||||||
| 		"Username":    u.Name, | 		"Username":    u.Name, | ||||||
|  | 		"i18n":        locale, | ||||||
|  | 		"Language":    locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var content bytes.Buffer | 	var content bytes.Buffer | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil { | ||||||
| 		log.Error("Template: %v", err) | 		log.Error("Template: %v", err) | ||||||
| 		return | 		return | ||||||
| @@ -144,17 +148,21 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) { | |||||||
|  |  | ||||||
| // SendCollaboratorMail sends mail notification to new collaborator. | // SendCollaboratorMail sends mail notification to new collaborator. | ||||||
| func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | ||||||
|  | 	locale := translation.NewLocale(u.Language) | ||||||
| 	repoName := repo.FullName() | 	repoName := repo.FullName() | ||||||
| 	subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repoName) |  | ||||||
|  |  | ||||||
|  | 	subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName) | ||||||
| 	data := map[string]interface{}{ | 	data := map[string]interface{}{ | ||||||
| 		"Subject":  subject, | 		"Subject":  subject, | ||||||
| 		"RepoName": repoName, | 		"RepoName": repoName, | ||||||
| 		"Link":     repo.HTMLURL(), | 		"Link":     repo.HTMLURL(), | ||||||
|  | 		"i18n":     locale, | ||||||
|  | 		"Language": locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var content bytes.Buffer | 	var content bytes.Buffer | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil { | ||||||
| 		log.Error("Template: %v", err) | 		log.Error("Template: %v", err) | ||||||
| 		return | 		return | ||||||
| @@ -166,7 +174,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | |||||||
| 	SendAsync(msg) | 	SendAsync(msg) | ||||||
| } | } | ||||||
|  |  | ||||||
| func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMention bool, info string) []*Message { | func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []string, fromMention bool, info string) []*Message { | ||||||
|  |  | ||||||
| 	var ( | 	var ( | ||||||
| 		subject string | 		subject string | ||||||
| @@ -192,7 +200,6 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | |||||||
|  |  | ||||||
| 	// This is the body of the new issue or comment, not the mail body | 	// This is the body of the new issue or comment, not the mail body | ||||||
| 	body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas())) | 	body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas())) | ||||||
|  |  | ||||||
| 	actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) | 	actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) | ||||||
|  |  | ||||||
| 	if actName != "new" { | 	if actName != "new" { | ||||||
| @@ -208,6 +215,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | 	locale := translation.NewLocale(lang) | ||||||
|  |  | ||||||
| 	mailMeta := map[string]interface{}{ | 	mailMeta := map[string]interface{}{ | ||||||
| 		"FallbackSubject": fallback, | 		"FallbackSubject": fallback, | ||||||
| @@ -224,13 +232,16 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | |||||||
| 		"ActionType":      actType, | 		"ActionType":      actType, | ||||||
| 		"ActionName":      actName, | 		"ActionName":      actName, | ||||||
| 		"ReviewComments":  reviewComments, | 		"ReviewComments":  reviewComments, | ||||||
|  | 		"i18n":            locale, | ||||||
|  | 		"Language":        locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var mailSubject bytes.Buffer | 	var mailSubject bytes.Buffer | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { | 	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { | ||||||
| 		subject = sanitizeSubject(mailSubject.String()) | 		subject = sanitizeSubject(mailSubject.String()) | ||||||
| 	} else { | 	} else { | ||||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/subject", err) | 		log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if subject == "" { | 	if subject == "" { | ||||||
| @@ -243,6 +254,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | |||||||
|  |  | ||||||
| 	var mailBody bytes.Buffer | 	var mailBody bytes.Buffer | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil { | ||||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err) | 		log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err) | ||||||
| 	} | 	} | ||||||
| @@ -276,14 +288,21 @@ func sanitizeSubject(subject string) string { | |||||||
| } | } | ||||||
|  |  | ||||||
| // SendIssueAssignedMail composes and sends issue assigned email | // SendIssueAssignedMail composes and sends issue assigned email | ||||||
| func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) { | func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) { | ||||||
| 	SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ | 	langMap := make(map[string][]string) | ||||||
| 		Issue:      issue, | 	for _, user := range recipients { | ||||||
| 		Doer:       doer, | 		langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||||
| 		ActionType: models.ActionType(0), | 	} | ||||||
| 		Content:    content, |  | ||||||
| 		Comment:    comment, | 	for lang, tos := range langMap { | ||||||
| 	}, tos, false, "issue assigned")) | 		SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ | ||||||
|  | 			Issue:      issue, | ||||||
|  | 			Doer:       doer, | ||||||
|  | 			ActionType: models.ActionType(0), | ||||||
|  | 			Content:    content, | ||||||
|  | 			Comment:    comment, | ||||||
|  | 		}, lang, tos, false, "issue assigned")) | ||||||
|  | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // actionToTemplate returns the type and name of the action facing the user | // actionToTemplate returns the type and name of the action facing the user | ||||||
|   | |||||||
| @@ -9,25 +9,16 @@ import ( | |||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // MailParticipantsComment sends new comment emails to repository watchers | // MailParticipantsComment sends new comment emails to repository watchers and mentioned people. | ||||||
| // and mentioned people. |  | ||||||
| func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) error { | func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) error { | ||||||
| 	return mailParticipantsComment(c, opType, issue, mentions) | 	if err := mailIssueCommentToParticipants( | ||||||
| } |  | ||||||
|  |  | ||||||
| func mailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) (err error) { |  | ||||||
| 	mentionedIDs := make([]int64, len(mentions)) |  | ||||||
| 	for i, u := range mentions { |  | ||||||
| 		mentionedIDs[i] = u.ID |  | ||||||
| 	} |  | ||||||
| 	if err = mailIssueCommentToParticipants( |  | ||||||
| 		&mailCommentContext{ | 		&mailCommentContext{ | ||||||
| 			Issue:      issue, | 			Issue:      issue, | ||||||
| 			Doer:       c.Poster, | 			Doer:       c.Poster, | ||||||
| 			ActionType: opType, | 			ActionType: opType, | ||||||
| 			Content:    c.Content, | 			Content:    c.Content, | ||||||
| 			Comment:    c, | 			Comment:    c, | ||||||
| 		}, mentionedIDs); err != nil { | 		}, mentions); err != nil { | ||||||
| 		log.Error("mailIssueCommentToParticipants: %v", err) | 		log.Error("mailIssueCommentToParticipants: %v", err) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| @@ -35,10 +26,6 @@ func mailParticipantsComment(c *models.Comment, opType models.ActionType, issue | |||||||
|  |  | ||||||
| // MailMentionsComment sends email to users mentioned in a code comment | // MailMentionsComment sends email to users mentioned in a code comment | ||||||
| func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []*models.User) (err error) { | func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []*models.User) (err error) { | ||||||
| 	mentionedIDs := make([]int64, len(mentions)) |  | ||||||
| 	for i, u := range mentions { |  | ||||||
| 		mentionedIDs[i] = u.ID |  | ||||||
| 	} |  | ||||||
| 	visited := make(map[int64]bool, len(mentions)+1) | 	visited := make(map[int64]bool, len(mentions)+1) | ||||||
| 	visited[c.Poster.ID] = true | 	visited[c.Poster.ID] = true | ||||||
| 	if err = mailIssueCommentBatch( | 	if err = mailIssueCommentBatch( | ||||||
| @@ -48,7 +35,7 @@ func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []* | |||||||
| 			ActionType: models.ActionCommentPull, | 			ActionType: models.ActionCommentPull, | ||||||
| 			Content:    c.Content, | 			Content:    c.Content, | ||||||
| 			Comment:    c, | 			Comment:    c, | ||||||
| 		}, mentionedIDs, visited, true); err != nil { | 		}, mentions, visited, true); err != nil { | ||||||
| 		log.Error("mailIssueCommentBatch: %v", err) | 		log.Error("mailIssueCommentBatch: %v", err) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -23,11 +23,16 @@ type mailCommentContext struct { | |||||||
| 	Comment    *models.Comment | 	Comment    *models.Comment | ||||||
| } | } | ||||||
|  |  | ||||||
|  | const ( | ||||||
|  | 	// MailBatchSize set the batch size used in mailIssueCommentBatch | ||||||
|  | 	MailBatchSize = 100 | ||||||
|  | ) | ||||||
|  |  | ||||||
| // mailIssueCommentToParticipants can be used for both new issue creation and comment. | // mailIssueCommentToParticipants can be used for both new issue creation and comment. | ||||||
| // This function sends two list of emails: | // This function sends two list of emails: | ||||||
| // 1. Repository watchers and users who are participated in comments. | // 1. Repository watchers and users who are participated in comments. | ||||||
| // 2. Users who are not in 1. but get mentioned in current issue/comment. | // 2. Users who are not in 1. but get mentioned in current issue/comment. | ||||||
| func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) error { | func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*models.User) error { | ||||||
|  |  | ||||||
| 	// Required by the mail composer; make sure to load these before calling the async function | 	// Required by the mail composer; make sure to load these before calling the async function | ||||||
| 	if err := ctx.Issue.LoadRepo(); err != nil { | 	if err := ctx.Issue.LoadRepo(); err != nil { | ||||||
| @@ -94,78 +99,72 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e | |||||||
| 		visited[i] = true | 		visited[i] = true | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil { | 	unfilteredUsers, err := models.GetMaileableUsersByIDs(unfiltered, false) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 	if err = mailIssueCommentBatch(ctx, unfilteredUsers, visited, false); err != nil { | ||||||
| 		return fmt.Errorf("mailIssueCommentBatch(): %v", err) | 		return fmt.Errorf("mailIssueCommentBatch(): %v", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func mailIssueCommentBatch(ctx *mailCommentContext, ids []int64, visited map[int64]bool, fromMention bool) error { | func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visited map[int64]bool, fromMention bool) error { | ||||||
| 	const batchSize = 100 | 	checkUnit := models.UnitTypeIssues | ||||||
| 	for i := 0; i < len(ids); i += batchSize { | 	if ctx.Issue.IsPull { | ||||||
| 		var last int | 		checkUnit = models.UnitTypePullRequests | ||||||
| 		if i+batchSize < len(ids) { |  | ||||||
| 			last = i + batchSize |  | ||||||
| 		} else { |  | ||||||
| 			last = len(ids) |  | ||||||
| 		} |  | ||||||
| 		unique := make([]int64, 0, last-i) |  | ||||||
| 		for j := i; j < last; j++ { |  | ||||||
| 			id := ids[j] |  | ||||||
| 			if _, ok := visited[id]; !ok { |  | ||||||
| 				unique = append(unique, id) |  | ||||||
| 				visited[id] = true |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		recipients, err := models.GetMaileableUsersByIDs(unique, fromMention) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		checkUnit := models.UnitTypeIssues |  | ||||||
| 		if ctx.Issue.IsPull { |  | ||||||
| 			checkUnit = models.UnitTypePullRequests |  | ||||||
| 		} |  | ||||||
| 		// Make sure all recipients can still see the issue |  | ||||||
| 		idx := 0 |  | ||||||
| 		for _, r := range recipients { |  | ||||||
| 			if ctx.Issue.Repo.CheckUnitUser(r, checkUnit) { |  | ||||||
| 				recipients[idx] = r |  | ||||||
| 				idx++ |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 		recipients = recipients[:idx] |  | ||||||
|  |  | ||||||
| 		// TODO: Separate recipients by language for i18n mail templates |  | ||||||
| 		tos := make([]string, len(recipients)) |  | ||||||
| 		for i := range recipients { |  | ||||||
| 			tos[i] = recipients[i].Email |  | ||||||
| 		} |  | ||||||
| 		SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments")) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	langMap := make(map[string][]string) | ||||||
|  | 	for _, user := range users { | ||||||
|  | 		// At this point we exclude: | ||||||
|  | 		// user that don't have all mails enabled or users only get mail on mention and this is one ... | ||||||
|  | 		if !(user.EmailNotificationsPreference == models.EmailNotificationsEnabled || | ||||||
|  | 			fromMention && user.EmailNotificationsPreference == models.EmailNotificationsOnMention) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// if we have already visited this user we exclude them | ||||||
|  | 		if _, ok := visited[user.ID]; ok { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// now mark them as visited | ||||||
|  | 		visited[user.ID] = true | ||||||
|  |  | ||||||
|  | 		// test if this user is allowed to see the issue/pull | ||||||
|  | 		if !ctx.Issue.Repo.CheckUnitUser(user, checkUnit) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for lang, receivers := range langMap { | ||||||
|  | 		// because we know that the len(receivers) > 0 and we don't care about the order particularly | ||||||
|  | 		// working backwards from the last (possibly) incomplete batch. If len(receivers) can be 0 this | ||||||
|  | 		// starting condition will need to be changed slightly | ||||||
|  | 		for i := ((len(receivers) - 1) / MailBatchSize) * MailBatchSize; i >= 0; i -= MailBatchSize { | ||||||
|  | 			SendAsyncs(composeIssueCommentMessages(ctx, lang, receivers[i:], fromMention, "issue comments")) | ||||||
|  | 			receivers = receivers[:i] | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // MailParticipants sends new issue thread created emails to repository watchers | // MailParticipants sends new issue thread created emails to repository watchers | ||||||
| // and mentioned people. | // and mentioned people. | ||||||
| func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error { | func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error { | ||||||
| 	return mailParticipants(issue, doer, opType, mentions) | 	if err := mailIssueCommentToParticipants( | ||||||
| } |  | ||||||
|  |  | ||||||
| func mailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) (err error) { |  | ||||||
| 	mentionedIDs := make([]int64, len(mentions)) |  | ||||||
| 	for i, u := range mentions { |  | ||||||
| 		mentionedIDs[i] = u.ID |  | ||||||
| 	} |  | ||||||
| 	if err = mailIssueCommentToParticipants( |  | ||||||
| 		&mailCommentContext{ | 		&mailCommentContext{ | ||||||
| 			Issue:      issue, | 			Issue:      issue, | ||||||
| 			Doer:       doer, | 			Doer:       doer, | ||||||
| 			ActionType: opType, | 			ActionType: opType, | ||||||
| 			Content:    issue.Content, | 			Content:    issue.Content, | ||||||
| 			Comment:    nil, | 			Comment:    nil, | ||||||
| 		}, mentionedIDs); err != nil { | 		}, mentions); err != nil { | ||||||
| 		log.Error("mailIssueCommentToParticipants: %v", err) | 		log.Error("mailIssueCommentToParticipants: %v", err) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -6,13 +6,13 @@ package mailer | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
| 	"fmt" |  | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
| 	"code.gitea.io/gitea/modules/base" | 	"code.gitea.io/gitea/modules/base" | ||||||
| 	"code.gitea.io/gitea/modules/log" | 	"code.gitea.io/gitea/modules/log" | ||||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | 	"code.gitea.io/gitea/modules/markup/markdown" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| const ( | const ( | ||||||
| @@ -33,29 +33,40 @@ func MailNewRelease(rel *models.Release) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	tos := make([]string, 0, len(recipients)) | 	langMap := make(map[string][]string) | ||||||
| 	for _, to := range recipients { | 	for _, user := range recipients { | ||||||
| 		if to.ID != rel.PublisherID { | 		if user.ID != rel.PublisherID { | ||||||
| 			tos = append(tos, to.Email) | 			langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) | 	for lang, tos := range langMap { | ||||||
| 	subject := fmt.Sprintf("%s in %s released", rel.TagName, rel.Repo.FullName()) | 		mailNewRelease(lang, tos, rel) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func mailNewRelease(lang string, tos []string, rel *models.Release) { | ||||||
|  | 	locale := translation.NewLocale(lang) | ||||||
|  |  | ||||||
|  | 	rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) | ||||||
|  |  | ||||||
|  | 	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) | ||||||
| 	mailMeta := map[string]interface{}{ | 	mailMeta := map[string]interface{}{ | ||||||
| 		"Release": rel, | 		"Release":  rel, | ||||||
| 		"Subject": subject, | 		"Subject":  subject, | ||||||
|  | 		"i18n":     locale, | ||||||
|  | 		"Language": locale.Language(), | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var mailBody bytes.Buffer | 	var mailBody bytes.Buffer | ||||||
|  |  | ||||||
| 	if err = bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil { | 	// TODO: i18n templates? | ||||||
|  | 	if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil { | ||||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err) | 		log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	msgs := make([]*Message, 0, len(recipients)) | 	msgs := make([]*Message, 0, len(tos)) | ||||||
| 	publisherName := rel.Publisher.DisplayName() | 	publisherName := rel.Publisher.DisplayName() | ||||||
| 	relURL := "<" + rel.HTMLURL() + ">" | 	relURL := "<" + rel.HTMLURL() + ">" | ||||||
| 	for _, to := range tos { | 	for _, to := range tos { | ||||||
|   | |||||||
| @@ -9,42 +9,60 @@ import ( | |||||||
| 	"fmt" | 	"fmt" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/models" | 	"code.gitea.io/gitea/models" | ||||||
|  | 	"code.gitea.io/gitea/modules/translation" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created | // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created | ||||||
| func SendRepoTransferNotifyMail(doer, newOwner *models.User, repo *models.Repository) error { | func SendRepoTransferNotifyMail(doer, newOwner *models.User, repo *models.Repository) error { | ||||||
| 	var ( |  | ||||||
| 		emails      []string |  | ||||||
| 		destination string |  | ||||||
| 		content     bytes.Buffer |  | ||||||
| 	) |  | ||||||
|  |  | ||||||
| 	if newOwner.IsOrganization() { | 	if newOwner.IsOrganization() { | ||||||
| 		users, err := models.GetUsersWhoCanCreateOrgRepo(newOwner.ID) | 		users, err := models.GetUsersWhoCanCreateOrgRepo(newOwner.ID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		for i := range users { | 		langMap := make(map[string][]string) | ||||||
| 			emails = append(emails, users[i].Email) | 		for _, user := range users { | ||||||
|  | 			langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||||
| 		} | 		} | ||||||
| 		destination = newOwner.DisplayName() |  | ||||||
| 	} else { | 		for lang, tos := range langMap { | ||||||
| 		emails = []string{newOwner.Email} | 			if err := sendRepoTransferNotifyMailPerLang(lang, newOwner, doer, tos, repo); err != nil { | ||||||
| 		destination = "you" | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	subject := fmt.Sprintf("%s would like to transfer \"%s\" to %s", doer.DisplayName(), repo.FullName(), destination) | 	return sendRepoTransferNotifyMailPerLang(newOwner.Language, newOwner, doer, []string{newOwner.Email}, repo) | ||||||
| 	data := map[string]interface{}{ | } | ||||||
| 		"Doer":    doer, |  | ||||||
| 		"User":    repo.Owner, |  | ||||||
| 		"Repo":    repo.FullName(), |  | ||||||
| 		"Link":    repo.HTMLURL(), |  | ||||||
| 		"Subject": subject, |  | ||||||
|  |  | ||||||
|  | // sendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created for each language | ||||||
|  | func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *models.User, emails []string, repo *models.Repository) error { | ||||||
|  | 	var ( | ||||||
|  | 		locale  = translation.NewLocale(lang) | ||||||
|  | 		content bytes.Buffer | ||||||
|  | 	) | ||||||
|  |  | ||||||
|  | 	destination := locale.Tr("mail.repo.transfer.to_you") | ||||||
|  | 	subject := locale.Tr("mail.repo.transfer.subject_to_you", doer.DisplayName(), repo.FullName()) | ||||||
|  | 	if newOwner.IsOrganization() { | ||||||
|  | 		destination = newOwner.DisplayName() | ||||||
|  | 		subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data := map[string]interface{}{ | ||||||
|  | 		"Doer":        doer, | ||||||
|  | 		"User":        repo.Owner, | ||||||
|  | 		"Repo":        repo.FullName(), | ||||||
|  | 		"Link":        repo.HTMLURL(), | ||||||
|  | 		"Subject":     subject, | ||||||
|  | 		"i18n":        locale, | ||||||
|  | 		"Language":    locale.Language(), | ||||||
| 		"Destination": destination, | 		"Destination": destination, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// TODO: i18n templates? | ||||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil { | 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -59,7 +59,7 @@ func TestComposeIssueCommentMessage(t *testing.T) { | |||||||
|  |  | ||||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | ||||||
| 		Content: "test body", Comment: comment}, tos, false, "issue comment") | 		Content: "test body", Comment: comment}, "en-US", tos, false, "issue comment") | ||||||
| 	assert.Len(t, msgs, 2) | 	assert.Len(t, msgs, 2) | ||||||
| 	gomailMsg := msgs[0].ToMessage() | 	gomailMsg := msgs[0].ToMessage() | ||||||
| 	mailto := gomailMsg.GetHeader("To") | 	mailto := gomailMsg.GetHeader("To") | ||||||
| @@ -93,7 +93,7 @@ func TestComposeIssueMessage(t *testing.T) { | |||||||
|  |  | ||||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue, | 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue, | ||||||
| 		Content: "test body"}, tos, false, "issue create") | 		Content: "test body"}, "en-US", tos, false, "issue create") | ||||||
| 	assert.Len(t, msgs, 2) | 	assert.Len(t, msgs, 2) | ||||||
|  |  | ||||||
| 	gomailMsg := msgs[0].ToMessage() | 	gomailMsg := msgs[0].ToMessage() | ||||||
| @@ -218,7 +218,7 @@ func TestTemplateServices(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message { | func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message { | ||||||
| 	msgs := composeIssueCommentMessages(ctx, tos, fromMention, info) | 	msgs := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info) | ||||||
| 	assert.Len(t, msgs, 1) | 	assert.Len(t, msgs, 1) | ||||||
| 	return msgs[0] | 	return msgs[0] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -337,13 +337,16 @@ func NewContext() { | |||||||
|  |  | ||||||
| // SendAsync send mail asynchronously | // SendAsync send mail asynchronously | ||||||
| func SendAsync(msg *Message) { | func SendAsync(msg *Message) { | ||||||
| 	go func() { | 	SendAsyncs([]*Message{msg}) | ||||||
| 		_ = mailQueue.Push(msg) |  | ||||||
| 	}() |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // SendAsyncs send mails asynchronously | // SendAsyncs send mails asynchronously | ||||||
| func SendAsyncs(msgs []*Message) { | func SendAsyncs(msgs []*Message) { | ||||||
|  | 	if setting.MailService == nil { | ||||||
|  | 		log.Error("Mailer: SendAsyncs is being invoked but mail service hasn't been initialized") | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	go func() { | 	go func() { | ||||||
| 		for _, msg := range msgs { | 		for _, msg := range msgs { | ||||||
| 			_ = mailQueue.Push(msg) | 			_ = mailQueue.Push(msg) | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ | |||||||
| 	<p> | 	<p> | ||||||
| 		--- | 		--- | ||||||
| 		<br> | 		<br> | ||||||
| 		<a href="{{.Link}}">View it on Gitea</a>. | 		<a href="{{.Link}}">View it on {{AppName}}</a>. | ||||||
| 	</p> | 	</p> | ||||||
| </body> | </body> | ||||||
| </html> | </html> | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user