* 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>tags/v1.15.0-rc1
@@ -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. |
@@ -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) | |||
} | |||
} |
@@ -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 |
@@ -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)) |
@@ -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)) | |||
} |
@@ -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) |
@@ -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) | |||
} |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 { |
@@ -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 | |||
} |
@@ -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] | |||
} |
@@ -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) |
@@ -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> |