mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09: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 | ||||
| } | ||||
|  | ||||
| // GenerateActivateCode generates an activate code based on user information. | ||||
| func (u *User) GenerateActivateCode() string { | ||||
| 	return u.GenerateEmailActivateCode(u.Email) | ||||
| } | ||||
|  | ||||
| // GetFollowers returns range of user's followers. | ||||
| func (u *User) GetFollowers(listOptions ListOptions) ([]*User, error) { | ||||
| 	sess := x. | ||||
|   | ||||
| @@ -104,14 +104,14 @@ func (m *mailNotifier) NotifyIssueChangeAssignee(doer *models.User, issue *model | ||||
| 	// mail only sent to added assignees and not self-assignee | ||||
| 	if !removed && doer.ID != assignee.ID && assignee.EmailNotifications() == models.EmailNotificationsEnabled { | ||||
| 		ct := fmt.Sprintf("Assigned #%d.", issue.Index) | ||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []string{assignee.Email}) | ||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{assignee}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (m *mailNotifier) NotifyPullReviewRequest(doer *models.User, issue *models.Issue, reviewer *models.User, isRequest bool, comment *models.Comment) { | ||||
| 	if isRequest && doer.ID != reviewer.ID && reviewer.EmailNotifications() == models.EmailNotificationsEnabled { | ||||
| 		ct := fmt.Sprintf("Requested to review %s.", issue.HTMLURL()) | ||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []string{reviewer.Email}) | ||||
| 		mailer.SendIssueAssignedMail(issue, doer, ct, comment, []*models.User{reviewer}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @@ -153,7 +153,7 @@ func (m *mailNotifier) NotifyPullRequestPushCommits(doer *models.User, pr *model | ||||
| } | ||||
|  | ||||
| func (m *mailNotifier) NotifyPullRevieweDismiss(doer *models.User, review *models.Review, comment *models.Comment) { | ||||
| 	if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, []*models.User{}); err != nil { | ||||
| 	if err := mailer.MailParticipantsComment(comment, models.ActionPullReviewDismissed, review.Issue, nil); err != nil { | ||||
| 		log.Error("MailParticipantsComment: %v", err) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -320,6 +320,14 @@ reset_password = Recover your account | ||||
| register_success = Registration successful | ||||
| register_notify = Welcome to Gitea | ||||
|  | ||||
| release.new.subject = %s in %s released | ||||
|  | ||||
| repo.transfer.subject_to = %s would like to transfer "%s" to %s | ||||
| repo.transfer.subject_to_you = %s would like to transfer "%s" to you | ||||
| repo.transfer.to_you = you | ||||
|  | ||||
| repo.collaborator.added.subject = %s added you to %s | ||||
|  | ||||
| [modal] | ||||
| yes = Yes | ||||
| no = No | ||||
|   | ||||
| @@ -154,7 +154,7 @@ func NewUserPost(ctx *context.Context) { | ||||
|  | ||||
| 	// Send email notification. | ||||
| 	if form.SendNotify { | ||||
| 		mailer.SendRegisterNotifyMail(ctx.Locale, u) | ||||
| 		mailer.SendRegisterNotifyMail(u) | ||||
| 	} | ||||
|  | ||||
| 	ctx.Flash.Success(ctx.Tr("admin.users.new_success", u.Name)) | ||||
|   | ||||
| @@ -114,7 +114,7 @@ func CreateUser(ctx *context.APIContext) { | ||||
|  | ||||
| 	// Send email notification. | ||||
| 	if form.SendNotify { | ||||
| 		mailer.SendRegisterNotifyMail(ctx.Locale, u) | ||||
| 		mailer.SendRegisterNotifyMail(u) | ||||
| 	} | ||||
| 	ctx.JSON(http.StatusCreated, convert.ToUser(u, ctx.User)) | ||||
| } | ||||
|   | ||||
| @@ -1397,7 +1397,7 @@ func ForgotPasswdPost(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	mailer.SendResetPasswordMail(ctx.Locale, u) | ||||
| 	mailer.SendResetPasswordMail(u) | ||||
|  | ||||
| 	if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil { | ||||
| 		log.Error("Set cache(MailResendLimit) fail: %v", err) | ||||
|   | ||||
| @@ -132,7 +132,7 @@ func EmailPost(ctx *context.Context) { | ||||
| 				ctx.Redirect(setting.AppSubURL + "/user/settings/account") | ||||
| 				return | ||||
| 			} | ||||
| 			mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email) | ||||
| 			mailer.SendActivateEmailMail(ctx.User, email) | ||||
| 			address = email.Email | ||||
| 		} | ||||
|  | ||||
| @@ -194,7 +194,7 @@ func EmailPost(ctx *context.Context) { | ||||
|  | ||||
| 	// Send confirmation email | ||||
| 	if setting.Service.RegisterEmailConfirm { | ||||
| 		mailer.SendActivateEmailMail(ctx.Locale, ctx.User, email) | ||||
| 		mailer.SendActivateEmailMail(ctx.User, email) | ||||
| 		if err := ctx.Cache.Put("MailResendLimit_"+ctx.User.LowerName, ctx.User.LowerName, 180); err != nil { | ||||
| 			log.Error("Set cache(MailResendLimit) fail: %v", err) | ||||
| 		} | ||||
|   | ||||
| @@ -22,6 +22,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/timeutil" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
|  | ||||
| 	"gopkg.in/gomail.v2" | ||||
| ) | ||||
| @@ -57,17 +58,21 @@ func SendTestMail(email string) error { | ||||
| 	return gomail.Send(Sender, NewMessage([]string{email}, "Gitea Test Email!", "Gitea Test Email!").ToMessage()) | ||||
| } | ||||
|  | ||||
| // SendUserMail sends a mail to the user | ||||
| func SendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { | ||||
| // sendUserMail sends a mail to the user | ||||
| func sendUserMail(language string, u *models.User, tpl base.TplName, code, subject, info string) { | ||||
| 	locale := translation.NewLocale(language) | ||||
| 	data := map[string]interface{}{ | ||||
| 		"DisplayName":       u.DisplayName(), | ||||
| 		"ActiveCodeLives":   timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, language), | ||||
| 		"ResetPwdCodeLives": timeutil.MinutesToFriendly(setting.Service.ResetPwdCodeLives, language), | ||||
| 		"Code":              code, | ||||
| 		"i18n":              locale, | ||||
| 		"Language":          locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var content bytes.Buffer | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil { | ||||
| 		log.Error("Template: %v", err) | ||||
| 		return | ||||
| @@ -79,33 +84,32 @@ func SendUserMail(language string, u *models.User, tpl base.TplName, code, subje | ||||
| 	SendAsync(msg) | ||||
| } | ||||
|  | ||||
| // Locale represents an interface to translation | ||||
| type Locale interface { | ||||
| 	Language() string | ||||
| 	Tr(string, ...interface{}) string | ||||
| } | ||||
|  | ||||
| // SendActivateAccountMail sends an activation mail to the user (new user registration) | ||||
| func SendActivateAccountMail(locale Locale, u *models.User) { | ||||
| 	SendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateActivateCode(), locale.Tr("mail.activate_account"), "activate account") | ||||
| func SendActivateAccountMail(locale translation.Locale, u *models.User) { | ||||
| 	sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.activate_account"), "activate account") | ||||
| } | ||||
|  | ||||
| // SendResetPasswordMail sends a password reset mail to the user | ||||
| func SendResetPasswordMail(locale Locale, u *models.User) { | ||||
| 	SendUserMail(locale.Language(), u, mailAuthResetPassword, u.GenerateActivateCode(), locale.Tr("mail.reset_password"), "recover account") | ||||
| func SendResetPasswordMail(u *models.User) { | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 	sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.Tr("mail.reset_password"), "recover account") | ||||
| } | ||||
|  | ||||
| // SendActivateEmailMail sends confirmation email to confirm new email address | ||||
| func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAddress) { | ||||
| func SendActivateEmailMail(u *models.User, email *models.EmailAddress) { | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 	data := map[string]interface{}{ | ||||
| 		"DisplayName":     u.DisplayName(), | ||||
| 		"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale.Language()), | ||||
| 		"Code":            u.GenerateEmailActivateCode(email.Email), | ||||
| 		"Email":           email.Email, | ||||
| 		"i18n":            locale, | ||||
| 		"Language":        locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var content bytes.Buffer | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil { | ||||
| 		log.Error("Template: %v", err) | ||||
| 		return | ||||
| @@ -118,19 +122,19 @@ func SendActivateEmailMail(locale Locale, u *models.User, email *models.EmailAdd | ||||
| } | ||||
|  | ||||
| // SendRegisterNotifyMail triggers a notify e-mail by admin created a account. | ||||
| func SendRegisterNotifyMail(locale Locale, u *models.User) { | ||||
| 	if setting.MailService == nil { | ||||
| 		log.Warn("SendRegisterNotifyMail is being invoked but mail service hasn't been initialized") | ||||
| 		return | ||||
| 	} | ||||
| func SendRegisterNotifyMail(u *models.User) { | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
|  | ||||
| 	data := map[string]interface{}{ | ||||
| 		"DisplayName": u.DisplayName(), | ||||
| 		"Username":    u.Name, | ||||
| 		"i18n":        locale, | ||||
| 		"Language":    locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var content bytes.Buffer | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil { | ||||
| 		log.Error("Template: %v", err) | ||||
| 		return | ||||
| @@ -144,17 +148,21 @@ func SendRegisterNotifyMail(locale Locale, u *models.User) { | ||||
|  | ||||
| // SendCollaboratorMail sends mail notification to new collaborator. | ||||
| func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | ||||
| 	locale := translation.NewLocale(u.Language) | ||||
| 	repoName := repo.FullName() | ||||
| 	subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repoName) | ||||
|  | ||||
| 	subject := locale.Tr("mail.repo.collaborator.added.subject", doer.DisplayName(), repoName) | ||||
| 	data := map[string]interface{}{ | ||||
| 		"Subject":  subject, | ||||
| 		"RepoName": repoName, | ||||
| 		"Link":     repo.HTMLURL(), | ||||
| 		"i18n":     locale, | ||||
| 		"Language": locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var content bytes.Buffer | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil { | ||||
| 		log.Error("Template: %v", err) | ||||
| 		return | ||||
| @@ -166,7 +174,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) { | ||||
| 	SendAsync(msg) | ||||
| } | ||||
|  | ||||
| func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMention bool, info string) []*Message { | ||||
| func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []string, fromMention bool, info string) []*Message { | ||||
|  | ||||
| 	var ( | ||||
| 		subject string | ||||
| @@ -192,7 +200,6 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | ||||
|  | ||||
| 	// This is the body of the new issue or comment, not the mail body | ||||
| 	body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas())) | ||||
|  | ||||
| 	actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType) | ||||
|  | ||||
| 	if actName != "new" { | ||||
| @@ -208,6 +215,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	locale := translation.NewLocale(lang) | ||||
|  | ||||
| 	mailMeta := map[string]interface{}{ | ||||
| 		"FallbackSubject": fallback, | ||||
| @@ -224,13 +232,16 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | ||||
| 		"ActionType":      actType, | ||||
| 		"ActionName":      actName, | ||||
| 		"ReviewComments":  reviewComments, | ||||
| 		"i18n":            locale, | ||||
| 		"Language":        locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var mailSubject bytes.Buffer | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := subjectTemplates.ExecuteTemplate(&mailSubject, string(tplName), mailMeta); err == nil { | ||||
| 		subject = sanitizeSubject(mailSubject.String()) | ||||
| 	} else { | ||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/subject", err) | ||||
| 		log.Error("ExecuteTemplate [%s]: %v", tplName+"/subject", err) | ||||
| 	} | ||||
|  | ||||
| 	if subject == "" { | ||||
| @@ -243,6 +254,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMent | ||||
|  | ||||
| 	var mailBody bytes.Buffer | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplName), mailMeta); err != nil { | ||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err) | ||||
| 	} | ||||
| @@ -276,14 +288,21 @@ func sanitizeSubject(subject string) string { | ||||
| } | ||||
|  | ||||
| // SendIssueAssignedMail composes and sends issue assigned email | ||||
| func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) { | ||||
| 	SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ | ||||
| 		Issue:      issue, | ||||
| 		Doer:       doer, | ||||
| 		ActionType: models.ActionType(0), | ||||
| 		Content:    content, | ||||
| 		Comment:    comment, | ||||
| 	}, tos, false, "issue assigned")) | ||||
| func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) { | ||||
| 	langMap := make(map[string][]string) | ||||
| 	for _, user := range recipients { | ||||
| 		langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||
| 	} | ||||
|  | ||||
| 	for lang, tos := range langMap { | ||||
| 		SendAsyncs(composeIssueCommentMessages(&mailCommentContext{ | ||||
| 			Issue:      issue, | ||||
| 			Doer:       doer, | ||||
| 			ActionType: models.ActionType(0), | ||||
| 			Content:    content, | ||||
| 			Comment:    comment, | ||||
| 		}, lang, tos, false, "issue assigned")) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| // actionToTemplate returns the type and name of the action facing the user | ||||
|   | ||||
| @@ -9,25 +9,16 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| ) | ||||
|  | ||||
| // MailParticipantsComment sends new comment emails to repository watchers | ||||
| // and mentioned people. | ||||
| // MailParticipantsComment sends new comment emails to repository watchers and mentioned people. | ||||
| func MailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) error { | ||||
| 	return mailParticipantsComment(c, opType, issue, mentions) | ||||
| } | ||||
|  | ||||
| func mailParticipantsComment(c *models.Comment, opType models.ActionType, issue *models.Issue, mentions []*models.User) (err error) { | ||||
| 	mentionedIDs := make([]int64, len(mentions)) | ||||
| 	for i, u := range mentions { | ||||
| 		mentionedIDs[i] = u.ID | ||||
| 	} | ||||
| 	if err = mailIssueCommentToParticipants( | ||||
| 	if err := mailIssueCommentToParticipants( | ||||
| 		&mailCommentContext{ | ||||
| 			Issue:      issue, | ||||
| 			Doer:       c.Poster, | ||||
| 			ActionType: opType, | ||||
| 			Content:    c.Content, | ||||
| 			Comment:    c, | ||||
| 		}, mentionedIDs); err != nil { | ||||
| 		}, mentions); err != nil { | ||||
| 		log.Error("mailIssueCommentToParticipants: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
| @@ -35,10 +26,6 @@ func mailParticipantsComment(c *models.Comment, opType models.ActionType, issue | ||||
|  | ||||
| // MailMentionsComment sends email to users mentioned in a code comment | ||||
| func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []*models.User) (err error) { | ||||
| 	mentionedIDs := make([]int64, len(mentions)) | ||||
| 	for i, u := range mentions { | ||||
| 		mentionedIDs[i] = u.ID | ||||
| 	} | ||||
| 	visited := make(map[int64]bool, len(mentions)+1) | ||||
| 	visited[c.Poster.ID] = true | ||||
| 	if err = mailIssueCommentBatch( | ||||
| @@ -48,7 +35,7 @@ func MailMentionsComment(pr *models.PullRequest, c *models.Comment, mentions []* | ||||
| 			ActionType: models.ActionCommentPull, | ||||
| 			Content:    c.Content, | ||||
| 			Comment:    c, | ||||
| 		}, mentionedIDs, visited, true); err != nil { | ||||
| 		}, mentions, visited, true); err != nil { | ||||
| 		log.Error("mailIssueCommentBatch: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
|   | ||||
| @@ -23,11 +23,16 @@ type mailCommentContext struct { | ||||
| 	Comment    *models.Comment | ||||
| } | ||||
|  | ||||
| const ( | ||||
| 	// MailBatchSize set the batch size used in mailIssueCommentBatch | ||||
| 	MailBatchSize = 100 | ||||
| ) | ||||
|  | ||||
| // mailIssueCommentToParticipants can be used for both new issue creation and comment. | ||||
| // This function sends two list of emails: | ||||
| // 1. Repository watchers and users who are participated in comments. | ||||
| // 2. Users who are not in 1. but get mentioned in current issue/comment. | ||||
| func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) error { | ||||
| func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*models.User) error { | ||||
|  | ||||
| 	// Required by the mail composer; make sure to load these before calling the async function | ||||
| 	if err := ctx.Issue.LoadRepo(); err != nil { | ||||
| @@ -94,78 +99,72 @@ func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) e | ||||
| 		visited[i] = true | ||||
| 	} | ||||
|  | ||||
| 	if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil { | ||||
| 	unfilteredUsers, err := models.GetMaileableUsersByIDs(unfiltered, false) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	if err = mailIssueCommentBatch(ctx, unfilteredUsers, visited, false); err != nil { | ||||
| 		return fmt.Errorf("mailIssueCommentBatch(): %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func mailIssueCommentBatch(ctx *mailCommentContext, ids []int64, visited map[int64]bool, fromMention bool) error { | ||||
| 	const batchSize = 100 | ||||
| 	for i := 0; i < len(ids); i += batchSize { | ||||
| 		var last int | ||||
| 		if i+batchSize < len(ids) { | ||||
| 			last = i + batchSize | ||||
| 		} else { | ||||
| 			last = len(ids) | ||||
| 		} | ||||
| 		unique := make([]int64, 0, last-i) | ||||
| 		for j := i; j < last; j++ { | ||||
| 			id := ids[j] | ||||
| 			if _, ok := visited[id]; !ok { | ||||
| 				unique = append(unique, id) | ||||
| 				visited[id] = true | ||||
| 			} | ||||
| 		} | ||||
| 		recipients, err := models.GetMaileableUsersByIDs(unique, fromMention) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		checkUnit := models.UnitTypeIssues | ||||
| 		if ctx.Issue.IsPull { | ||||
| 			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 | ||||
| 		tos := make([]string, len(recipients)) | ||||
| 		for i := range recipients { | ||||
| 			tos[i] = recipients[i].Email | ||||
| 		} | ||||
| 		SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments")) | ||||
| func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visited map[int64]bool, fromMention bool) error { | ||||
| 	checkUnit := models.UnitTypeIssues | ||||
| 	if ctx.Issue.IsPull { | ||||
| 		checkUnit = models.UnitTypePullRequests | ||||
| 	} | ||||
|  | ||||
| 	langMap := make(map[string][]string) | ||||
| 	for _, user := range users { | ||||
| 		// At this point we exclude: | ||||
| 		// user that don't have all mails enabled or users only get mail on mention and this is one ... | ||||
| 		if !(user.EmailNotificationsPreference == models.EmailNotificationsEnabled || | ||||
| 			fromMention && user.EmailNotificationsPreference == models.EmailNotificationsOnMention) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// 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 | ||||
| } | ||||
|  | ||||
| // MailParticipants sends new issue thread created emails to repository watchers | ||||
| // and mentioned people. | ||||
| func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) error { | ||||
| 	return mailParticipants(issue, doer, opType, mentions) | ||||
| } | ||||
|  | ||||
| func mailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType, mentions []*models.User) (err error) { | ||||
| 	mentionedIDs := make([]int64, len(mentions)) | ||||
| 	for i, u := range mentions { | ||||
| 		mentionedIDs[i] = u.ID | ||||
| 	} | ||||
| 	if err = mailIssueCommentToParticipants( | ||||
| 	if err := mailIssueCommentToParticipants( | ||||
| 		&mailCommentContext{ | ||||
| 			Issue:      issue, | ||||
| 			Doer:       doer, | ||||
| 			ActionType: opType, | ||||
| 			Content:    issue.Content, | ||||
| 			Comment:    nil, | ||||
| 		}, mentionedIDs); err != nil { | ||||
| 		}, mentions); err != nil { | ||||
| 		log.Error("mailIssueCommentToParticipants: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
|   | ||||
| @@ -6,13 +6,13 @@ package mailer | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/markup/markdown" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| @@ -33,29 +33,40 @@ func MailNewRelease(rel *models.Release) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	tos := make([]string, 0, len(recipients)) | ||||
| 	for _, to := range recipients { | ||||
| 		if to.ID != rel.PublisherID { | ||||
| 			tos = append(tos, to.Email) | ||||
| 	langMap := make(map[string][]string) | ||||
| 	for _, user := range recipients { | ||||
| 		if user.ID != rel.PublisherID { | ||||
| 			langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) | ||||
| 	subject := fmt.Sprintf("%s in %s released", rel.TagName, rel.Repo.FullName()) | ||||
| 	for lang, tos := range langMap { | ||||
| 		mailNewRelease(lang, tos, rel) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func mailNewRelease(lang string, tos []string, rel *models.Release) { | ||||
| 	locale := translation.NewLocale(lang) | ||||
|  | ||||
| 	rel.RenderedNote = markdown.RenderString(rel.Note, rel.Repo.Link(), rel.Repo.ComposeMetas()) | ||||
|  | ||||
| 	subject := locale.Tr("mail.release.new.subject", rel.TagName, rel.Repo.FullName()) | ||||
| 	mailMeta := map[string]interface{}{ | ||||
| 		"Release": rel, | ||||
| 		"Subject": subject, | ||||
| 		"Release":  rel, | ||||
| 		"Subject":  subject, | ||||
| 		"i18n":     locale, | ||||
| 		"Language": locale.Language(), | ||||
| 	} | ||||
|  | ||||
| 	var mailBody bytes.Buffer | ||||
|  | ||||
| 	if err = bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil { | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil { | ||||
| 		log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	msgs := make([]*Message, 0, len(recipients)) | ||||
| 	msgs := make([]*Message, 0, len(tos)) | ||||
| 	publisherName := rel.Publisher.DisplayName() | ||||
| 	relURL := "<" + rel.HTMLURL() + ">" | ||||
| 	for _, to := range tos { | ||||
|   | ||||
| @@ -9,42 +9,60 @@ import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/translation" | ||||
| ) | ||||
|  | ||||
| // SendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created | ||||
| func SendRepoTransferNotifyMail(doer, newOwner *models.User, repo *models.Repository) error { | ||||
| 	var ( | ||||
| 		emails      []string | ||||
| 		destination string | ||||
| 		content     bytes.Buffer | ||||
| 	) | ||||
|  | ||||
| 	if newOwner.IsOrganization() { | ||||
| 		users, err := models.GetUsersWhoCanCreateOrgRepo(newOwner.ID) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		for i := range users { | ||||
| 			emails = append(emails, users[i].Email) | ||||
| 		langMap := make(map[string][]string) | ||||
| 		for _, user := range users { | ||||
| 			langMap[user.Language] = append(langMap[user.Language], user.Email) | ||||
| 		} | ||||
| 		destination = newOwner.DisplayName() | ||||
| 	} else { | ||||
| 		emails = []string{newOwner.Email} | ||||
| 		destination = "you" | ||||
|  | ||||
| 		for lang, tos := range langMap { | ||||
| 			if err := sendRepoTransferNotifyMailPerLang(lang, newOwner, doer, tos, repo); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	subject := fmt.Sprintf("%s would like to transfer \"%s\" to %s", doer.DisplayName(), repo.FullName(), destination) | ||||
| 	data := map[string]interface{}{ | ||||
| 		"Doer":    doer, | ||||
| 		"User":    repo.Owner, | ||||
| 		"Repo":    repo.FullName(), | ||||
| 		"Link":    repo.HTMLURL(), | ||||
| 		"Subject": subject, | ||||
| 	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) | ||||
| 	} | ||||
|  | ||||
| 	data := map[string]interface{}{ | ||||
| 		"Doer":        doer, | ||||
| 		"User":        repo.Owner, | ||||
| 		"Repo":        repo.FullName(), | ||||
| 		"Link":        repo.HTMLURL(), | ||||
| 		"Subject":     subject, | ||||
| 		"i18n":        locale, | ||||
| 		"Language":    locale.Language(), | ||||
| 		"Destination": destination, | ||||
| 	} | ||||
|  | ||||
| 	// TODO: i18n templates? | ||||
| 	if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|   | ||||
| @@ -59,7 +59,7 @@ func TestComposeIssueCommentMessage(t *testing.T) { | ||||
|  | ||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue, | ||||
| 		Content: "test body", Comment: comment}, tos, false, "issue comment") | ||||
| 		Content: "test body", Comment: comment}, "en-US", tos, false, "issue comment") | ||||
| 	assert.Len(t, msgs, 2) | ||||
| 	gomailMsg := msgs[0].ToMessage() | ||||
| 	mailto := gomailMsg.GetHeader("To") | ||||
| @@ -93,7 +93,7 @@ func TestComposeIssueMessage(t *testing.T) { | ||||
|  | ||||
| 	tos := []string{"test@gitea.com", "test2@gitea.com"} | ||||
| 	msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue, | ||||
| 		Content: "test body"}, tos, false, "issue create") | ||||
| 		Content: "test body"}, "en-US", tos, false, "issue create") | ||||
| 	assert.Len(t, msgs, 2) | ||||
|  | ||||
| 	gomailMsg := msgs[0].ToMessage() | ||||
| @@ -218,7 +218,7 @@ func TestTemplateServices(t *testing.T) { | ||||
| } | ||||
|  | ||||
| func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message { | ||||
| 	msgs := composeIssueCommentMessages(ctx, tos, fromMention, info) | ||||
| 	msgs := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info) | ||||
| 	assert.Len(t, msgs, 1) | ||||
| 	return msgs[0] | ||||
| } | ||||
|   | ||||
| @@ -337,13 +337,16 @@ func NewContext() { | ||||
|  | ||||
| // SendAsync send mail asynchronously | ||||
| func SendAsync(msg *Message) { | ||||
| 	go func() { | ||||
| 		_ = mailQueue.Push(msg) | ||||
| 	}() | ||||
| 	SendAsyncs([]*Message{msg}) | ||||
| } | ||||
|  | ||||
| // SendAsyncs send mails asynchronously | ||||
| func SendAsyncs(msgs []*Message) { | ||||
| 	if setting.MailService == nil { | ||||
| 		log.Error("Mailer: SendAsyncs is being invoked but mail service hasn't been initialized") | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	go func() { | ||||
| 		for _, msg := range msgs { | ||||
| 			_ = mailQueue.Push(msg) | ||||
|   | ||||
| @@ -11,7 +11,7 @@ | ||||
| 	<p> | ||||
| 		--- | ||||
| 		<br> | ||||
| 		<a href="{{.Link}}">View it on Gitea</a>. | ||||
| 		<a href="{{.Link}}">View it on {{AppName}}</a>. | ||||
| 	</p> | ||||
| </body> | ||||
| </html> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user