mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 16:40:24 +08:00 
			
		
		
		
	[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:
		@@ -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 New Issue
	
	Block a user