]> source.dussan.org Git - gitea.git/commitdiff
[refactor] mailer service (#15072)
author6543 <6543@obermui.de>
Fri, 2 Apr 2021 10:25:13 +0000 (12:25 +0200)
committerGitHub <noreply@github.com>
Fri, 2 Apr 2021 10:25:13 +0000 (11:25 +0100)
* 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>
15 files changed:
models/user.go
modules/notification/mail/mail.go
options/locale/locale_en-US.ini
routers/admin/users.go
routers/api/v1/admin/user.go
routers/user/auth.go
routers/user/setting/account.go
services/mailer/mail.go
services/mailer/mail_comment.go
services/mailer/mail_issue.go
services/mailer/mail_release.go
services/mailer/mail_repo.go
services/mailer/mail_test.go
services/mailer/mailer.go
templates/mail/notify/repo_transfer.tmpl

index 098f6af2b374a9f5ceab3e11072cd57872edcabe..aacf2957e3d4baee221ba7e5428eb81e4a91fea6 100644 (file)
@@ -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.
index f7192f5a52eff0d22d247cad80649ba88c62dba3..9c000da0f6c7134b9aab345d5cdbc9f194b748a7 100644 (file)
@@ -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)
        }
 }
index 3be209ffef908dfdebf4faccfc12dc756dcc2cba..c481414afb2a04d02e7ad004849c0f362d47fcd7 100644 (file)
@@ -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
index 2d40a883af3dbc1c7a6efc3c115e70261fe4cc4c..e3f569203075b3d5db8055613b6e4c32c786f2dc 100644 (file)
@@ -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))
index cbb9f6b21c4b388864f995fca1130ca3a49f6883..5a74c6ccd50811e17d4aa9b5ce1cdca8c1b5a246 100644 (file)
@@ -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))
 }
index 37181c68e7a248984406b1378a72b805cc6abd1c..9217885519f3c20dcbd31b51853b9eacf371a8e7 100644 (file)
@@ -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)
index 4900bba14ac0c2f3c180ec9cfdbc6f950ef7df5d..0bf6cf8b87283285787a2cc3490c5086290613b4 100644 (file)
@@ -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)
                }
index 7d6214c742a5c7aebb7071326d4fbac39cee6628..c50795968aa6419f8728ec5234d544ff387797dd 100644 (file)
@@ -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
index 2f166720dbd64539077072c63ff4c36c2024498b..f73c9fb6372f942104984417e60f99305d9309d0 100644 (file)
@@ -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
index b600060a67f5e82c7436531026df090d788dccbc..9786a06f62f3b967393f5d6b0713da74a4c8bb19 100644 (file)
@@ -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
+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
                }
 
-               checkUnit := models.UnitTypeIssues
-               if ctx.Issue.IsPull {
-                       checkUnit = models.UnitTypePullRequests
+               // if we have already visited this user we exclude them
+               if _, ok := visited[user.ID]; ok {
+                       continue
                }
-               // 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++
-                       }
+
+               // 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
                }
-               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
+               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]
                }
-               SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments"))
        }
+
        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
index f278c853aebd2e50994205fb6104f27687a72bdb..22efe2f0464b35302a6a2afa6fa27ef6feee289c 100644 (file)
@@ -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)
                }
        }
 
+       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 := fmt.Sprintf("%s in %s released", rel.TagName, rel.Repo.FullName())
 
+       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 {
index b9d24f4334d92029983d5488d66ed29e5ca55d50..c742101ee1961a7bc69bf47d628f84786c1b1827 100644 (file)
@@ -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)
+               }
+
+               for lang, tos := range langMap {
+                       if err := sendRepoTransferNotifyMailPerLang(lang, newOwner, doer, tos, repo); err != nil {
+                               return err
+                       }
                }
+
+               return nil
+       }
+
+       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()
-       } else {
-               emails = []string{newOwner.Email}
-               destination = "you"
+               subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
        }
 
-       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,
-
+               "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
        }
index d7d02d9dee82260f1d1cfc114308ea874d3a91cb..9eef084408dffae187ef87aaeceef1ce082efd6f 100644 (file)
@@ -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]
 }
index 2e7beffa151abc957d5d35cb8d864d95bf2436a0..6b86734bf845cba5f932c7a79f309c55300c0862 100644 (file)
@@ -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)
index 68ceded11673853a43589bce23a1088d938fe81d..e0dca8869d6692845ed0bbcddb92146eeb23c375 100644 (file)
@@ -11,7 +11,7 @@
        <p>
                ---
                <br>
-               <a href="{{.Link}}">View it on Gitea</a>.
+               <a href="{{.Link}}">View it on {{AppName}}</a>.
        </p>
 </body>
 </html>