From 80d6c6d7deb3f6a38ff6d09ec38ffb04de9da726 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 2 Apr 2021 12:25:13 +0200 Subject: [PATCH] [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 * add i18n to datas Signed-off-by: Andrew Thornton * a comment Co-authored-by: Andrew Thornton --- models/user.go | 5 -- modules/notification/mail/mail.go | 6 +- options/locale/locale_en-US.ini | 8 ++ routers/admin/users.go | 2 +- routers/api/v1/admin/user.go | 2 +- routers/user/auth.go | 2 +- routers/user/setting/account.go | 4 +- services/mailer/mail.go | 79 ++++++++++------- services/mailer/mail_comment.go | 21 +---- services/mailer/mail_issue.go | 107 +++++++++++------------ services/mailer/mail_release.go | 33 ++++--- services/mailer/mail_repo.go | 56 ++++++++---- services/mailer/mail_test.go | 6 +- services/mailer/mailer.go | 9 +- templates/mail/notify/repo_transfer.tmpl | 2 +- 15 files changed, 191 insertions(+), 151 deletions(-) diff --git a/models/user.go b/models/user.go index 098f6af2b..aacf2957e 100644 --- a/models/user.go +++ b/models/user.go @@ -331,11 +331,6 @@ func (u *User) GenerateEmailActivateCode(email string) string { 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. func (u *User) GetFollowers(listOptions ListOptions) ([]*User, error) { sess := x. diff --git a/modules/notification/mail/mail.go b/modules/notification/mail/mail.go index f7192f5a5..9c000da0f 100644 --- a/modules/notification/mail/mail.go +++ b/modules/notification/mail/mail.go @@ -104,14 +104,14 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *model // mail only sent to added assignees and not self-assignee if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { 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) { if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { 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) { - 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) } } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 3be209ffe..c481414af 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -320,6 +320,14 @@ reset_password = Recover your account register_success = Registration successful 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] yes = Yes no = No diff --git a/routers/admin/users.go b/routers/admin/users.go index 2d40a883a..e3f569203 100644 --- a/routers/admin/users.go +++ b/routers/admin/users.go @@ -154,7 +154,7 @@ func NewUserPost(ctx *context.Context) { // Send email notification. if form.SendNotify { - mailer.SendRegisterNotifyMail(ctx.Locale, u) + mailer.SendRegisterNotifyMail(u) } ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name)) diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go index cbb9f6b21..5a74c6ccd 100644 --- a/routers/api/v1/admin/user.go +++ b/routers/api/v1/admin/user.go @@ -114,7 +114,7 @@ func CreateUser(ctx *context.APIContext) { // Send email notification. if form.SendNotify { - mailer.SendRegisterNotifyMail(ctx.Locale, u) + mailer.SendRegisterNotifyMail(u) } ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.User)) } diff --git a/routers/user/auth.go b/routers/user/auth.go index 37181c68e..921788551 100644 --- a/routers/user/auth.go +++ b/routers/user/auth.go @@ -1397,7 +1397,7 @@ func ForgotPasswdPost(ctx *context.Context) { return } - mailer.SendResetPasswordMail(ctx.Locale, u) + mailer.SendResetPasswordMail(u) if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { log.Error("Set cache(MailResendLimit) fail: %v", err) diff --git a/routers/user/setting/account.go b/routers/user/setting/account.go index 4900bba14..0bf6cf8b8 100644 --- a/routers/user/setting/account.go +++ b/routers/user/setting/account.go @@ -132,7 +132,7 @@ func EmailPost(ctx *context.Context) { ctx.Redirect(setting.AppSubURL + "/user/settings/account") return } - mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email) + mailer.SendActivateEmailMail(ctx.User, email) address = email.Email } @@ -194,7 +194,7 @@ func EmailPost(ctx *context.Context) { // Send confirmation email 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 { log.Error("Set cache(MailResendLimit) fail: %v", err) } diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 7d6214c74..c50795968 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -22,6 +22,7 @@ import ( "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/translation" "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()) } -// SendUserMail sends a mail to the user -func SendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { +// sendUserMail sends a mail to the user +func sendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { + locale := translation.NewLocale(language) data := map[string]interface{}{ "DisplayName": u.DisplayName(), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language), "ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language), "Code": code, + "i18n": locale, + "Language": locale.Language(), } var content bytes.Buffer + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { log.Error("Template: %v", err) return @@ -79,33 +84,32 @@ func SendUserMail(language string, u *models.User, tpl base.TplName, code, subje 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) -func SendActivateAccountMail(locale Locale, u *models.User) { - SendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateActivateCode(), locale.Tr("mail.activate_account"), "activate account") +func SendActivateAccountMail(locale translation.Locale, u *models.User) { + 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 -func SendResetPasswordMail(locale Locale, u *models.User) { - SendUserMail(locale.Language(), u, mailAuthResetPassword, u.GenerateActivateCode(), locale.Tr("mail.reset_password"), "recover account") +func SendResetPasswordMail(u *models.User) { + 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 -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{}{ "DisplayName": u.DisplayName(), "ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()), "Code": u.GenerateEmailActivateCode(email.Email), "Email": email.Email, + "i18n": locale, + "Language": locale.Language(), } var content bytes.Buffer + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { log.Error("Template: %v", err) 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. -func SendRegisterNotifyMail(locale Locale, u *models.User) { - if setting.MailService == nil { - log.Warn("SendRegisterNotifyMail is being invoked but mail service hasn't been initialized") - return - } +func SendRegisterNotifyMail(u *models.User) { + locale := translation.NewLocale(u.Language) data := map[string]interface{}{ "DisplayName": u.DisplayName(), "Username": u.Name, + "i18n": locale, + "Language": locale.Language(), } var content bytes.Buffer + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil { log.Error("Template: %v", err) return @@ -144,17 +148,21 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) { // SendCollaboratorMail sends mail notification to new collaborator. func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { + locale := translation.NewLocale(u.Language) 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{}{ "Subject": subject, "RepoName": repoName, "Link": repo.HTMLURL(), + "i18n": locale, + "Language": locale.Language(), } var content bytes.Buffer + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil { log.Error("Template: %v", err) return @@ -166,7 +174,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { 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 ( 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 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) if actName != "new" { @@ -208,6 +215,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent } } } + locale := translation.NewLocale(lang) mailMeta := map[string]interface{}{ "FallbackSubject": fallback, @@ -224,13 +232,16 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent "ActionType": actType, "ActionName": actName, "ReviewComments": reviewComments, + "i18n": locale, + "Language": locale.Language(), } var mailSubject bytes.Buffer + // TODO: i18n templates? if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { subject = sanitizeSubject(mailSubject.String()) } else { - log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/subject", err) + log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err) } if subject == "" { @@ -243,6 +254,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent var mailBody bytes.Buffer + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil { 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 -func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) { - SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ - Issue: issue, - Doer: doer, - ActionType: models.ActionType(0), - Content: content, - Comment: comment, - }, tos, false, "issue assigned")) +func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) { + langMap := make(map[string][]string) + for _, user := range recipients { + langMap[user.Language] = append(langMap[user.Language], user.Email) + } + + for lang, tos := range langMap { + 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 diff --git a/services/mailer/mail_comment.go b/services/mailer/mail_comment.go index 2f166720d..f73c9fb63 100644 --- a/services/mailer/mail_comment.go +++ b/services/mailer/mail_comment.go @@ -9,25 +9,16 @@ import ( "code.gitea.io/gitea/modules/log" ) -// MailParticipantsComment sends new comment emails to repository watchers -// and mentioned people. +// MailParticipantsComment sends new comment emails to repository watchers and mentioned people. func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) error { - return mailParticipantsComment(c, opType, issue, mentions) -} - -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( + if err := mailIssueCommentToParticipants( &mailCommentContext{ Issue: issue, Doer: c.Poster, ActionType: opType, Content: c.Content, Comment: c, - }, mentionedIDs); err != nil { + }, mentions); err != nil { log.Error("mailIssueCommentToParticipants: %v", err) } 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 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[c.Poster.ID] = true if err = mailIssueCommentBatch( @@ -48,7 +35,7 @@ func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []* ActionType: models.ActionCommentPull, Content: c.Content, Comment: c, - }, mentionedIDs, visited, true); err != nil { + }, mentions, visited, true); err != nil { log.Error("mailIssueCommentBatch: %v", err) } return nil diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go index b600060a6..9786a06f6 100644 --- a/services/mailer/mail_issue.go +++ b/services/mailer/mail_issue.go @@ -23,11 +23,16 @@ type mailCommentContext struct { 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. // This function sends two list of emails: // 1. Repository watchers and users who are participated in comments. // 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 if err := ctx.Issue.LoadRepo(); err != nil { @@ -94,78 +99,72 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e 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 nil } -func mailIssueCommentBatch(ctx *mailCommentContext, ids []int64, visited map[int64]bool, fromMention bool) error { - const batchSize = 100 - for i := 0; i < len(ids); i += batchSize { - var last int - 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")) +func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visited map[int64]bool, fromMention bool) error { + checkUnit := models.UnitTypeIssues + if ctx.Issue.IsPull { + checkUnit = models.UnitTypePullRequests } + + 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 } // MailParticipants sends new issue thread created emails to repository watchers // and mentioned people. func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error { - return mailParticipants(issue, doer, opType, mentions) -} - -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( + if err := mailIssueCommentToParticipants( &mailCommentContext{ Issue: issue, Doer: doer, ActionType: opType, Content: issue.Content, Comment: nil, - }, mentionedIDs); err != nil { + }, mentions); err != nil { log.Error("mailIssueCommentToParticipants: %v", err) } return nil diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index f278c853a..22efe2f04 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -6,13 +6,13 @@ package mailer import ( "bytes" - "fmt" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup/markdown" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/translation" ) const ( @@ -33,29 +33,40 @@ func MailNewRelease(rel *models.Release) { return } - tos := make([]string, 0, len(recipients)) - for _, to := range recipients { - if to.ID != rel.PublisherID { - tos = append(tos, to.Email) + langMap := make(map[string][]string) + for _, user := range recipients { + if user.ID != rel.PublisherID { + langMap[user.Language] = append(langMap[user.Language], user.Email) } } - rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) - subject := fmt.Sprintf("%s in %s released", rel.TagName, rel.Repo.FullName()) + for lang, tos := range langMap { + 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{}{ - "Release": rel, - "Subject": subject, + "Release": rel, + "Subject": subject, + "i18n": locale, + "Language": locale.Language(), } 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) return } - msgs := make([]*Message, 0, len(recipients)) + msgs := make([]*Message, 0, len(tos)) publisherName := rel.Publisher.DisplayName() relURL := "<" + rel.HTMLURL() + ">" for _, to := range tos { diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index b9d24f433..c742101ee 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -9,42 +9,60 @@ import ( "fmt" "code.gitea.io/gitea/models" + "code.gitea.io/gitea/modules/translation" ) // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created func SendRepoTransferNotifyMail(doer, newOwner *models.User, repo *models.Repository) error { - var ( - emails []string - destination string - content bytes.Buffer - ) - if newOwner.IsOrganization() { users, err := models.GetUsersWhoCanCreateOrgRepo(newOwner.ID) if err != nil { return err } - for i := range users { - emails = append(emails, users[i].Email) + langMap := make(map[string][]string) + for _, user := range users { + langMap[user.Language] = append(langMap[user.Language], user.Email) } - destination = newOwner.DisplayName() - } else { - emails = []string{newOwner.Email} - destination = "you" + + for lang, tos := range langMap { + if err := sendRepoTransferNotifyMailPerLang(lang, newOwner, doer, tos, repo); err != nil { + return err + } + } + + return nil } - subject := fmt.Sprintf("%s would like to transfer \"%s\" to %s", doer.DisplayName(), repo.FullName(), destination) - data := map[string]interface{}{ - "Doer": doer, - "User": repo.Owner, - "Repo": repo.FullName(), - "Link": repo.HTMLURL(), - "Subject": subject, + return sendRepoTransferNotifyMailPerLang(newOwner.Language, newOwner, doer, []string{newOwner.Email}, repo) +} +// 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, } + // TODO: i18n templates? if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil { return err } diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index d7d02d9de..9eef08440 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -59,7 +59,7 @@ func TestComposeIssueCommentMessage(t *testing.T) { tos := []string{"test@gitea.com", "test2@gitea.com"} 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) gomailMsg := msgs[0].ToMessage() mailto := gomailMsg.GetHeader("To") @@ -93,7 +93,7 @@ func TestComposeIssueMessage(t *testing.T) { tos := []string{"test@gitea.com", "test2@gitea.com"} 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) 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 { - msgs := composeIssueCommentMessages(ctx, tos, fromMention, info) + msgs := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info) assert.Len(t, msgs, 1) return msgs[0] } diff --git a/services/mailer/mailer.go b/services/mailer/mailer.go index 2e7beffa1..6b86734bf 100644 --- a/services/mailer/mailer.go +++ b/services/mailer/mailer.go @@ -337,13 +337,16 @@ func NewContext() { // SendAsync send mail asynchronously func SendAsync(msg *Message) { - go func() { - _ = mailQueue.Push(msg) - }() + SendAsyncs([]*Message{msg}) } // SendAsyncs send mails asynchronously 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() { for _, msg := range msgs { _ = mailQueue.Push(msg) diff --git a/templates/mail/notify/repo_transfer.tmpl b/templates/mail/notify/repo_transfer.tmpl index 68ceded11..e0dca8869 100644 --- a/templates/mail/notify/repo_transfer.tmpl +++ b/templates/mail/notify/repo_transfer.tmpl @@ -11,7 +11,7 @@

---
- View it on Gitea. + View it on {{AppName}}.