Rewrite delivery of issue and comment mails (#9009)

* Mail issue subscribers, rework the function

* Simplify a little more

* Fix unused variable

* Refactor mail delivery to avoid heavy load on server

* Avoid splitting into too many goroutines

* Fix comments and optimize GetMaileableUsersByIDs()

* Fix return on errors
This commit is contained in:
guillep2k 2019-11-18 05:08:20 -03:00 committed by zeripath
parent 9ff6312627
commit 08ae6bb7ed
10 changed files with 254 additions and 148 deletions

View file

@ -1219,6 +1219,19 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
return issues, nil return issues, nil
} }
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
// but skips joining with `user` for performance reasons.
// User permissions must be verified elsewhere if required.
func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) {
userIDs := make([]int64, 0, 5)
return userIDs, x.Table("comment").
Cols("poster_id").
Where("issue_id = ?", issueID).
And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
Distinct("poster_id").
Find(&userIDs)
}
// GetParticipantsByIssueID returns all users who are participated in comments of an issue. // GetParticipantsByIssueID returns all users who are participated in comments of an issue.
func GetParticipantsByIssueID(issueID int64) ([]*User, error) { func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
return getParticipantsByIssueID(x, issueID) return getParticipantsByIssueID(x, issueID)

View file

@ -41,6 +41,18 @@ func (issue *Issue) loadAssignees(e Engine) (err error) {
return return
} }
// GetAssigneeIDsByIssue returns the IDs of users assigned to an issue
// but skips joining with `user` for performance reasons.
// User permissions must be verified elsewhere if required.
func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) {
userIDs := make([]int64, 0, 5)
return userIDs, x.Table("issue_assignees").
Cols("assignee_id").
Where("issue_id = ?", issueID).
Distinct("assignee_id").
Find(&userIDs)
}
// GetAssigneesByIssue returns everyone assigned to that issue // GetAssigneesByIssue returns everyone assigned to that issue
func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) { func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
return getAssigneesByIssue(x, issue) return getAssigneesByIssue(x, issue)

View file

@ -60,6 +60,18 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
return return
} }
// GetIssueWatchersIDs returns IDs of subscribers to a given issue id
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetIssueWatchersIDs(issueID int64) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, x.Table("issue_watch").
Where("issue_id=?", issueID).
And("is_watching = ?", true).
Select("user_id").
Find(&ids)
}
// GetIssueWatchers returns watchers/unwatchers of a given issue // GetIssueWatchers returns watchers/unwatchers of a given issue
func GetIssueWatchers(issueID int64) (IssueWatchList, error) { func GetIssueWatchers(issueID int64) (IssueWatchList, error) {
return getIssueWatchers(x, issueID) return getIssueWatchers(x, issueID)

View file

@ -140,6 +140,18 @@ func GetWatchers(repoID int64) ([]*Watch, error) {
return getWatchers(x, repoID) return getWatchers(x, repoID)
} }
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetRepoWatchersIDs(repoID int64) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, x.Table("watch").
Where("watch.repo_id=?", repoID).
And("watch.mode<>?", RepoWatchModeDont).
Select("user_id").
Find(&ids)
}
// GetWatchers returns range of users watching given repository. // GetWatchers returns range of users watching given repository.
func (repo *Repository) GetWatchers(page int) ([]*User, error) { func (repo *Repository) GetWatchers(page int) ([]*User, error) {
users := make([]*User, 0, ItemsPerPage) users := make([]*User, 0, ItemsPerPage)

View file

@ -1307,6 +1307,20 @@ func getUserEmailsByNames(e Engine, names []string) []string {
return mails return mails
} }
// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails
func GetMaileableUsersByIDs(ids []int64) ([]*User, error) {
if len(ids) == 0 {
return nil, nil
}
ous := make([]*User, 0, len(ids))
return ous, x.In("id", ids).
Where("`type` = ?", UserTypeIndividual).
And("`prohibit_login` = ?", false).
And("`is_active` = ?", true).
And("`email_notifications_preference` = ?", EmailNotificationsEnabled).
Find(&ous)
}
// GetUsersByIDs returns all resolved users from a list of Ids. // GetUsersByIDs returns all resolved users from a list of Ids.
func GetUsersByIDs(ids []int64) ([]*User, error) { func GetUsersByIDs(ids []int64) ([]*User, error) {
ous := make([]*User, 0, len(ids)) ous := make([]*User, 0, len(ids))

View file

@ -164,13 +164,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
SendAsync(msg) SendAsync(msg)
} }
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool, func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMention bool, info string) []*Message {
content string, comment *models.Comment, tos []string, info string) *Message {
if err := issue.LoadPullRequest(); err != nil {
log.Error("LoadPullRequest: %v", err)
return nil
}
var ( var (
subject string subject string
@ -182,29 +176,29 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
) )
commentType := models.CommentTypeComment commentType := models.CommentTypeComment
if comment != nil { if ctx.Comment != nil {
prefix = "Re: " prefix = "Re: "
commentType = comment.Type commentType = ctx.Comment.Type
link = issue.HTMLURL() + "#" + comment.HashTag() link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag()
} else { } else {
link = issue.HTMLURL() link = ctx.Issue.HTMLURL()
} }
reviewType := models.ReviewTypeComment reviewType := models.ReviewTypeComment
if comment != nil && comment.Review != nil { if ctx.Comment != nil && ctx.Comment.Review != nil {
reviewType = comment.Review.Type reviewType = ctx.Comment.Review.Type
} }
fallback = prefix + fallbackMailSubject(issue) fallback = prefix + fallbackMailSubject(ctx.Issue)
// 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(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas())) body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas()))
actType, actName, tplName := actionToTemplate(issue, actionType, commentType, reviewType) actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
if comment != nil && comment.Review != nil { if ctx.Comment != nil && ctx.Comment.Review != nil {
reviewComments = make([]*models.Comment, 0, 10) reviewComments = make([]*models.Comment, 0, 10)
for _, lines := range comment.Review.CodeComments { for _, lines := range ctx.Comment.Review.CodeComments {
for _, comments := range lines { for _, comments := range lines {
reviewComments = append(reviewComments, comments...) reviewComments = append(reviewComments, comments...)
} }
@ -215,12 +209,12 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
"FallbackSubject": fallback, "FallbackSubject": fallback,
"Body": body, "Body": body,
"Link": link, "Link": link,
"Issue": issue, "Issue": ctx.Issue,
"Comment": comment, "Comment": ctx.Comment,
"IsPull": issue.IsPull, "IsPull": ctx.Issue.IsPull,
"User": issue.Repo.MustOwner(), "User": ctx.Issue.Repo.MustOwner(),
"Repo": issue.Repo.FullName(), "Repo": ctx.Issue.Repo.FullName(),
"Doer": doer, "Doer": ctx.Doer,
"IsMention": fromMention, "IsMention": fromMention,
"SubjectPrefix": prefix, "SubjectPrefix": prefix,
"ActionType": actType, "ActionType": actType,
@ -246,18 +240,23 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err) log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
} }
msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String()) // Make sure to compose independent messages to avoid leaking user emails
msgs := make([]*Message, 0, len(tos))
for _, to := range tos {
msg := NewMessageFrom([]string{to}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info) msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
// Set Message-ID on first message so replies know what to reference // Set Message-ID on first message so replies know what to reference
if comment == nil { if ctx.Comment == nil {
msg.SetHeader("Message-ID", "<"+issue.ReplyReference()+">") msg.SetHeader("Message-ID", "<"+ctx.Issue.ReplyReference()+">")
} else { } else {
msg.SetHeader("In-Reply-To", "<"+issue.ReplyReference()+">") msg.SetHeader("In-Reply-To", "<"+ctx.Issue.ReplyReference()+">")
msg.SetHeader("References", "<"+issue.ReplyReference()+">") msg.SetHeader("References", "<"+ctx.Issue.ReplyReference()+">")
}
msgs = append(msgs, msg)
} }
return msg return msgs
} }
func sanitizeSubject(subject string) string { func sanitizeSubject(subject string) string {
@ -269,21 +268,15 @@ func sanitizeSubject(subject string) string {
return mime.QEncoding.Encode("utf-8", string(runes)) return mime.QEncoding.Encode("utf-8", string(runes))
} }
// SendIssueCommentMail composes and sends issue comment emails to target receivers. // SendIssueAssignedMail composes and sends issue assigned email
func SendIssueCommentMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) { func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
if len(tos) == 0 { SendAsyncs(composeIssueCommentMessages(&mailCommentContext{
return Issue: issue,
} Doer: doer,
ActionType: models.ActionType(0),
SendAsync(composeIssueCommentMessage(issue, doer, actionType, false, content, comment, tos, "issue comment")) Content: content,
} Comment: comment,
}, tos, false, "issue assigned"))
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
func SendIssueMentionMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
if len(tos) == 0 {
return
}
SendAsync(composeIssueCommentMessage(issue, doer, actionType, true, content, comment, tos, "issue mention"))
} }
// actionToTemplate returns the type and name of the action facing the user // actionToTemplate returns the type and name of the action facing the user
@ -341,8 +334,3 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
} }
return return
} }
// SendIssueAssignedMail composes and sends issue assigned email
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
SendAsync(composeIssueCommentMessage(issue, doer, models.ActionType(0), false, content, comment, tos, "issue assigned"))
}

View file

@ -27,11 +27,18 @@ func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType mod
if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil { if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err) return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
} }
mentions := make([]string, len(userMentions)) mentions := make([]int64, len(userMentions))
for i, u := range userMentions { for i, u := range userMentions {
mentions[i] = u.LowerName mentions[i] = u.ID
} }
if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil { if err = mailIssueCommentToParticipants(
&mailCommentContext{
Issue: issue,
Doer: c.Poster,
ActionType: opType,
Content: c.Content,
Comment: c,
}, mentions); err != nil {
log.Error("mailIssueCommentToParticipants: %v", err) log.Error("mailIssueCommentToParticipants: %v", err)
} }
return nil return nil

View file

@ -10,105 +10,118 @@ import (
"code.gitea.io/gitea/models" "code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references" "code.gitea.io/gitea/modules/references"
"github.com/unknwon/com"
) )
func fallbackMailSubject(issue *models.Issue) string { func fallbackMailSubject(issue *models.Issue) string {
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index) return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
} }
type mailCommentContext struct {
Issue *models.Issue
Doer *models.User
ActionType models.ActionType
Content string
Comment *models.Comment
}
// 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(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, mentions []string) error { func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []int64) error {
watchers, err := models.GetWatchers(issue.RepoID) // Required by the mail composer; make sure to load these before calling the async function
if err != nil { if err := ctx.Issue.LoadRepo(); err != nil {
return fmt.Errorf("getWatchers [repo_id: %d]: %v", issue.RepoID, err) return fmt.Errorf("LoadRepo(): %v", err)
} }
participants, err := models.GetParticipantsByIssueID(issue.ID) if err := ctx.Issue.LoadPoster(); err != nil {
if err != nil { return fmt.Errorf("LoadPoster(): %v", err)
return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err) }
if err := ctx.Issue.LoadPullRequest(); err != nil {
return fmt.Errorf("LoadPullRequest(): %v", err)
} }
// In case the issue poster is not watching the repository and is active, // Enough room to avoid reallocations
// even if we have duplicated in watchers, can be safely filtered out. unfiltered := make([]int64, 1, 64)
err = issue.LoadPoster()
// =========== Original poster ===========
unfiltered[0] = ctx.Issue.PosterID
// =========== Assignees ===========
ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID)
if err != nil { if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err) return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err)
} }
if issue.PosterID != doer.ID && issue.Poster.IsActive && !issue.Poster.ProhibitLogin { unfiltered = append(unfiltered, ids...)
participants = append(participants, issue.Poster)
// =========== Participants (i.e. commenters, reviewers) ===========
ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID)
if err != nil {
return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err)
}
unfiltered = append(unfiltered, ids...)
// =========== Issue watchers ===========
ids, err = models.GetIssueWatchersIDs(ctx.Issue.ID)
if err != nil {
return fmt.Errorf("GetIssueWatchersIDs(%d): %v", ctx.Issue.ID, err)
}
unfiltered = append(unfiltered, ids...)
// =========== Repo watchers ===========
// Make repo watchers last, since it's likely the list with the most users
ids, err = models.GetRepoWatchersIDs(ctx.Issue.RepoID)
if err != nil {
return fmt.Errorf("GetRepoWatchersIDs(%d): %v", ctx.Issue.RepoID, err)
}
unfiltered = append(ids, unfiltered...)
visited := make(map[int64]bool, len(unfiltered)+len(mentions)+1)
// Avoid mailing the doer
visited[ctx.Doer.ID] = true
if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil {
return fmt.Errorf("mailIssueCommentBatch(): %v", err)
} }
// Assignees must receive any communications // =========== Mentions ===========
assignees, err := models.GetAssigneesByIssue(issue) if err = mailIssueCommentBatch(ctx, mentions, visited, true); err != nil {
return fmt.Errorf("mailIssueCommentBatch() mentions: %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)
if err != nil { if err != nil {
return err return err
} }
// TODO: Check issue visibility for each user
for _, assignee := range assignees { // TODO: Separate recipients by language for i18n mail templates
if assignee.ID != doer.ID { tos := make([]string, len(recipients))
participants = append(participants, assignee) for i := range recipients {
tos[i] = recipients[i].Email
} }
SendAsyncs(composeIssueCommentMessages(ctx, tos, fromMention, "issue comments"))
} }
tos := make([]string, 0, len(watchers)) // List of email addresses.
names := make([]string, 0, len(watchers))
for i := range watchers {
if watchers[i].UserID == doer.ID {
continue
}
to, err := models.GetUserByID(watchers[i].UserID)
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
}
if to.IsOrganization() || to.EmailNotifications() != models.EmailNotificationsEnabled {
continue
}
tos = append(tos, to.Email)
names = append(names, to.Name)
}
for i := range participants {
if participants[i].ID == doer.ID ||
com.IsSliceContainsStr(names, participants[i].Name) ||
participants[i].EmailNotifications() != models.EmailNotificationsEnabled {
continue
}
tos = append(tos, participants[i].Email)
names = append(names, participants[i].Name)
}
if err := issue.LoadRepo(); err != nil {
return err
}
for _, to := range tos {
SendIssueCommentMail(issue, doer, actionType, content, comment, []string{to})
}
// Mail mentioned people and exclude watchers.
names = append(names, doer.Name)
tos = make([]string, 0, len(mentions)) // list of user names.
for i := range mentions {
if com.IsSliceContainsStr(names, mentions[i]) {
continue
}
tos = append(tos, mentions[i])
}
emails := models.GetUserEmailsByNames(tos)
for _, to := range emails {
SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
}
return nil return nil
} }
@ -127,11 +140,18 @@ func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.Us
if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil { if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err) return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
} }
mentions := make([]string, len(userMentions)) mentions := make([]int64, len(userMentions))
for i, u := range userMentions { for i, u := range userMentions {
mentions[i] = u.LowerName mentions[i] = u.ID
} }
if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil { if err = mailIssueCommentToParticipants(
&mailCommentContext{
Issue: issue,
Doer: doer,
ActionType: opType,
Content: issue.Content,
Comment: nil,
}, mentions); err != nil {
log.Error("mailIssueCommentToParticipants: %v", err) log.Error("mailIssueCommentToParticipants: %v", err)
} }
return nil return nil

View file

@ -58,12 +58,16 @@ func TestComposeIssueCommentMessage(t *testing.T) {
InitMailRender(stpl, btpl) InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com", "test2@gitea.com"} tos := []string{"test@gitea.com", "test2@gitea.com"}
msg := composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "issue comment") msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
Content: "test body", Comment: comment}, tos, false, "issue comment")
assert.Len(t, msgs, 2)
subject := msg.GetHeader("Subject") mailto := msgs[0].GetHeader("To")
inreplyTo := msg.GetHeader("In-Reply-To") subject := msgs[0].GetHeader("Subject")
references := msg.GetHeader("References") inreplyTo := msgs[0].GetHeader("In-Reply-To")
references := msgs[0].GetHeader("References")
assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:") assert.Equal(t, "Re: ", subject[0][:4], "Comment reply subject should contain Re:")
assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject[0]) assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject[0])
assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match") assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match")
@ -88,14 +92,18 @@ func TestComposeIssueMessage(t *testing.T) {
InitMailRender(stpl, btpl) InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com", "test2@gitea.com"} tos := []string{"test@gitea.com", "test2@gitea.com"}
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "issue create") msgs := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
Content: "test body"}, tos, false, "issue create")
assert.Len(t, msgs, 2)
subject := msg.GetHeader("Subject") mailto := msgs[0].GetHeader("To")
messageID := msg.GetHeader("Message-ID") subject := msgs[0].GetHeader("Subject")
messageID := msgs[0].GetHeader("Message-ID")
assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0]) assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
assert.Nil(t, msg.GetHeader("In-Reply-To")) assert.Nil(t, msgs[0].GetHeader("In-Reply-To"))
assert.Nil(t, msg.GetHeader("References")) assert.Nil(t, msgs[0].GetHeader("References"))
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match") assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
} }
@ -134,20 +142,24 @@ func TestTemplateSelection(t *testing.T) {
assert.Contains(t, wholemsg, expBody) assert.Contains(t, wholemsg, expBody)
} }
msg := composeIssueCommentMessage(issue, doer, models.ActionCreateIssue, false, "test body", nil, tos, "TestTemplateSelection") msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
Content: "test body"}, tos, false, "TestTemplateSelection")
expect(t, msg, "issue/new/subject", "issue/new/body") expect(t, msg, "issue/new/subject", "issue/new/body")
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment) comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
msg = composeIssueCommentMessage(issue, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection") msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
expect(t, msg, "issue/default/subject", "issue/default/body") expect(t, msg, "issue/default/subject", "issue/default/body")
pull := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue) pull := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment) comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
msg = composeIssueCommentMessage(pull, doer, models.ActionCommentIssue, false, "test body", comment, tos, "TestTemplateSelection") msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: pull, Doer: doer, ActionType: models.ActionCommentIssue,
Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
expect(t, msg, "pull/comment/subject", "pull/comment/body") expect(t, msg, "pull/comment/subject", "pull/comment/body")
msg = composeIssueCommentMessage(issue, doer, models.ActionCloseIssue, false, "test body", nil, tos, "TestTemplateSelection") msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCloseIssue,
expect(t, msg, "[user2/repo1] issue1 (#1)", "issue/close/body") Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
} }
func TestTemplateServices(t *testing.T) { func TestTemplateServices(t *testing.T) {
@ -173,7 +185,8 @@ func TestTemplateServices(t *testing.T) {
InitMailRender(stpl, btpl) InitMailRender(stpl, btpl)
tos := []string{"test@gitea.com"} tos := []string{"test@gitea.com"}
msg := composeIssueCommentMessage(issue, doer, actionType, fromMention, "test body", comment, tos, "TestTemplateServices") msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: actionType,
Content: "test body", Comment: comment}, tos, fromMention, "TestTemplateServices")
subject := msg.GetHeader("Subject") subject := msg.GetHeader("Subject")
msgbuf := new(bytes.Buffer) msgbuf := new(bytes.Buffer)
@ -202,3 +215,9 @@ func TestTemplateServices(t *testing.T) {
"Re: [user2/repo1] issue1 (#1)", "Re: [user2/repo1] issue1 (#1)",
"//Re: //") "//Re: //")
} }
func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message {
msgs := composeIssueCommentMessages(ctx, tos, fromMention, info)
assert.Len(t, msgs, 1)
return msgs[0]
}

View file

@ -295,9 +295,18 @@ func NewContext() {
go processMailQueue() go processMailQueue()
} }
// SendAsync send mail asynchronous // SendAsync send mail asynchronously
func SendAsync(msg *Message) { func SendAsync(msg *Message) {
go func() { go func() {
mailQueue <- msg mailQueue <- msg
}() }()
} }
// SendAsyncs send mails asynchronously
func SendAsyncs(msgs []*Message) {
go func() {
for _, msg := range msgs {
mailQueue <- msg
}
}()
}