[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>
tokarchuk/v1.17
6543 4 years ago committed by GitHub
parent cc2d540092
commit 80d6c6d7de
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      models/user.go
  2. 6
      modules/notification/mail/mail.go
  3. 8
      options/locale/locale_en-US.ini
  4. 2
      routers/admin/users.go
  5. 2
      routers/api/v1/admin/user.go
  6. 2
      routers/user/auth.go
  7. 4
      routers/user/setting/account.go
  8. 79
      services/mailer/mail.go
  9. 21
      services/mailer/mail_comment.go
  10. 95
      services/mailer/mail_issue.go
  11. 31
      services/mailer/mail_release.go
  12. 54
      services/mailer/mail_repo.go
  13. 6
      services/mailer/mail_test.go
  14. 9
      services/mailer/mailer.go
  15. 2
      templates/mail/notify/repo_transfer.tmpl

@ -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 { langMap := make(map[string][]string)
last = len(ids) for _, user := range users {
} // At this point we exclude:
unique := make([]int64, 0, last-i) // user that don't have all mails enabled or users only get mail on mention and this is one ...
for j := i; j < last; j++ { if !(user.EmailNotificationsPreference == models.EmailNotificationsEnabled ||
id := ids[j] fromMention && user.EmailNotificationsPreference == models.EmailNotificationsOnMention) {
if _, ok := visited[id]; !ok { continue
unique = append(unique, id)
visited[id] = true
}
}
recipients, err := models.GetMaileableUsersByIDs(unique, fromMention)
if err != nil {
return err
} }
checkUnit := models.UnitTypeIssues // if we have already visited this user we exclude them
if ctx.Issue.IsPull { if _, ok := visited[user.ID]; ok {
checkUnit = models.UnitTypePullRequests continue
} }
// Make sure all recipients can still see the issue
idx := 0 // now mark them as visited
for _, r := range recipients { visited[user.ID] = true
if ctx.Issue.Repo.CheckUnitUser(r, checkUnit) {
recipients[idx] = r // test if this user is allowed to see the issue/pull
idx++ if !ctx.Issue.Repo.CheckUnitUser(user, checkUnit) {
} continue
} }
recipients = recipients[:idx]
// TODO: Separate recipients by language for i18n mail templates langMap[user.Language] = append(langMap[user.Language], user.Email)
tos := make([]string, len(recipients)) }
for i := range recipients {
tos[i] = recipients[i].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 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)
} }
} }
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()) 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{}{ 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)
}
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() destination = newOwner.DisplayName()
} else { subject = locale.Tr("mail.repo.transfer.subject_to", doer.DisplayName(), repo.FullName(), destination)
emails = []string{newOwner.Email}
destination = "you"
} }
subject := fmt.Sprintf("%s would like to transfer \"%s\" to %s", doer.DisplayName(), repo.FullName(), destination)
data := map[string]interface{}{ data := map[string]interface{}{
"Doer": doer, "Doer": doer,
"User": repo.Owner, "User": repo.Owner,
"Repo": repo.FullName(), "Repo": repo.FullName(),
"Link": repo.HTMLURL(), "Link": repo.HTMLURL(),
"Subject": subject, "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>

Loading…
Cancel
Save