[refactor] mailer service (#15072)
* Unexport SendUserMail * Instead of "[]*models.User" or "[]string" lists infent "[]*MailRecipient" for mailer * adopt * code format * TODOs for "i18n" * clean * no fallback for lang -> just use english * lint * exec testComposeIssueCommentMessage per lang and use only emails * rm MailRecipient * Dont reload from users from db if you alredy have in ram * nits * minimize diff Signed-off-by: 6543 <6543@obermui.de> * localize subjects * linter ... * Tr extend * start tmpl edit ... * Apply suggestions from code review * use translation.Locale * improve mailIssueCommentBatch Signed-off-by: Andrew Thornton <art27@cantab.net> * add i18n to datas Signed-off-by: Andrew Thornton <art27@cantab.net> * a comment Co-authored-by: Andrew Thornton <art27@cantab.net>
This commit is contained in:
parent
cc2d540092
commit
80d6c6d7de
15 changed files with 191 additions and 151 deletions
|
@ -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) {
|
||||||
|
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{
|
SendAsyncs(composeIssueCommentMessages(&mailCommentContext{
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
Doer: doer,
|
Doer: doer,
|
||||||
ActionType: models.ActionType(0),
|
ActionType: models.ActionType(0),
|
||||||
Content: content,
|
Content: content,
|
||||||
Comment: comment,
|
Comment: comment,
|
||||||
}, tos, false, "issue assigned"))
|
}, 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
|
|
||||||
for i := 0; i < len(ids); i += batchSize {
|
|
||||||
var last int
|
|
||||||
if i+batchSize < len(ids) {
|
|
||||||
last = i + batchSize
|
|
||||||
} else {
|
|
||||||
last = len(ids)
|
|
||||||
}
|
|
||||||
unique := make([]int64, 0, last-i)
|
|
||||||
for j := i; j < last; j++ {
|
|
||||||
id := ids[j]
|
|
||||||
if _, ok := visited[id]; !ok {
|
|
||||||
unique = append(unique, id)
|
|
||||||
visited[id] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipients, err := models.GetMaileableUsersByIDs(unique, fromMention)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
checkUnit := models.UnitTypeIssues
|
checkUnit := models.UnitTypeIssues
|
||||||
if ctx.Issue.IsPull {
|
if ctx.Issue.IsPull {
|
||||||
checkUnit = models.UnitTypePullRequests
|
checkUnit = models.UnitTypePullRequests
|
||||||
}
|
}
|
||||||
// Make sure all recipients can still see the issue
|
|
||||||
idx := 0
|
|
||||||
for _, r := range recipients {
|
|
||||||
if ctx.Issue.Repo.CheckUnitUser(r, checkUnit) {
|
|
||||||
recipients[idx] = r
|
|
||||||
idx++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recipients = recipients[:idx]
|
|
||||||
|
|
||||||
// TODO: Separate recipients by language for i18n mail templates
|
langMap := make(map[string][]string)
|
||||||
tos := make([]string, len(recipients))
|
for _, user := range users {
|
||||||
for i := range recipients {
|
// At this point we exclude:
|
||||||
tos[i] = recipients[i].Email
|
// 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
|
||||||
}
|
}
|
||||||
SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments"))
|
|
||||||
|
// if we have already visited this user we exclude them
|
||||||
|
if _, ok := visited[user.ID]; ok {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// now mark them as visited
|
||||||
|
visited[user.ID] = true
|
||||||
|
|
||||||
|
// test if this user is allowed to see the issue/pull
|
||||||
|
if !ctx.Issue.Repo.CheckUnitUser(user, checkUnit) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
langMap[user.Language] = append(langMap[user.Language], user.Email)
|
||||||
|
}
|
||||||
|
|
||||||
|
for lang, receivers := range langMap {
|
||||||
|
// because we know that the len(receivers) > 0 and we don't care about the order particularly
|
||||||
|
// working backwards from the last (possibly) incomplete batch. If len(receivers) can be 0 this
|
||||||
|
// starting condition will need to be changed slightly
|
||||||
|
for i := ((len(receivers) - 1) / MailBatchSize) * MailBatchSize; i >= 0; i -= MailBatchSize {
|
||||||
|
SendAsyncs(composeIssueCommentMessages(ctx, lang, receivers[i:], fromMention, "issue comments"))
|
||||||
|
receivers = receivers[:i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// MailParticipants sends new issue thread created emails to repository watchers
|
// MailParticipants sends new issue thread created emails to repository watchers
|
||||||
// and mentioned people.
|
// and mentioned people.
|
||||||
func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error {
|
func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error {
|
||||||
return mailParticipants(issue, doer, opType, mentions)
|
if err := mailIssueCommentToParticipants(
|
||||||
}
|
|
||||||
|
|
||||||
func mailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) (err error) {
|
|
||||||
mentionedIDs := make([]int64, len(mentions))
|
|
||||||
for i, u := range mentions {
|
|
||||||
mentionedIDs[i] = u.ID
|
|
||||||
}
|
|
||||||
if err = mailIssueCommentToParticipants(
|
|
||||||
&mailCommentContext{
|
&mailCommentContext{
|
||||||
Issue: issue,
|
Issue: issue,
|
||||||
Doer: doer,
|
Doer: doer,
|
||||||
ActionType: opType,
|
ActionType: opType,
|
||||||
Content: issue.Content,
|
Content: issue.Content,
|
||||||
Comment: nil,
|
Comment: nil,
|
||||||
}, mentionedIDs); err != nil {
|
}, mentions); err != nil {
|
||||||
log.Error("mailIssueCommentToParticipants: %v", err)
|
log.Error("mailIssueCommentToParticipants: %v", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -6,13 +6,13 @@ package mailer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -33,29 +33,40 @@ func MailNewRelease(rel *models.Release) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tos := make([]string, 0, len(recipients))
|
langMap := make(map[string][]string)
|
||||||
for _, to := range recipients {
|
for _, user := range recipients {
|
||||||
if to.ID != rel.PublisherID {
|
if user.ID != rel.PublisherID {
|
||||||
tos = append(tos, to.Email)
|
langMap[user.Language] = append(langMap[user.Language], user.Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
destination = newOwner.DisplayName()
|
}
|
||||||
} else {
|
|
||||||
emails = []string{newOwner.Email}
|
for lang, tos := range langMap {
|
||||||
destination = "you"
|
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()
|
||||||
|
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{}{
|
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>
|
||||||
|
|
Reference in a new issue