summaryrefslogtreecommitdiffstats
path: root/services/mailer/mail_issue.go
diff options
context:
space:
mode:
Diffstat (limited to 'services/mailer/mail_issue.go')
-rw-r--r--services/mailer/mail_issue.go162
1 files changed, 91 insertions, 71 deletions
diff --git a/services/mailer/mail_issue.go b/services/mailer/mail_issue.go
index 32b21b1324..696adfadda 100644
--- a/services/mailer/mail_issue.go
+++ b/services/mailer/mail_issue.go
@@ -10,105 +10,118 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
-
- "github.com/unknwon/com"
)
func fallbackMailSubject(issue *models.Issue) string {
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.
// 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(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)
- if err != nil {
- return fmt.Errorf("getWatchers [repo_id: %d]: %v", issue.RepoID, err)
+ // Required by the mail composer; make sure to load these before calling the async function
+ if err := ctx.Issue.LoadRepo(); err != nil {
+ return fmt.Errorf("LoadRepo(): %v", err)
}
- participants, err := models.GetParticipantsByIssueID(issue.ID)
- if err != nil {
- return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
+ if err := ctx.Issue.LoadPoster(); err != nil {
+ return fmt.Errorf("LoadPoster(): %v", 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,
- // even if we have duplicated in watchers, can be safely filtered out.
- err = issue.LoadPoster()
+ // Enough room to avoid reallocations
+ unfiltered := make([]int64, 1, 64)
+
+ // =========== Original poster ===========
+ unfiltered[0] = ctx.Issue.PosterID
+
+ // =========== Assignees ===========
+ ids, err := models.GetAssigneeIDsByIssue(ctx.Issue.ID)
if err != nil {
- return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err)
- }
- if issue.PosterID != doer.ID && issue.Poster.IsActive && !issue.Poster.ProhibitLogin {
- participants = append(participants, issue.Poster)
+ return fmt.Errorf("GetAssigneeIDsByIssue(%d): %v", ctx.Issue.ID, err)
}
+ unfiltered = append(unfiltered, ids...)
- // Assignees must receive any communications
- assignees, err := models.GetAssigneesByIssue(issue)
+ // =========== Participants (i.e. commenters, reviewers) ===========
+ ids, err = models.GetParticipantsIDsByIssueID(ctx.Issue.ID)
if err != nil {
- return err
+ return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %v", ctx.Issue.ID, err)
}
+ unfiltered = append(unfiltered, ids...)
- for _, assignee := range assignees {
- if assignee.ID != doer.ID {
- participants = append(participants, assignee)
- }
+ // =========== 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...)
- 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)
+ // =========== 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)
}
- for i := range participants {
- if participants[i].ID == doer.ID ||
- com.IsSliceContainsStr(names, participants[i].Name) ||
- participants[i].EmailNotifications() != models.EmailNotificationsEnabled {
- continue
- }
+ unfiltered = append(ids, unfiltered...)
- tos = append(tos, participants[i].Email)
- names = append(names, participants[i].Name)
- }
+ visited := make(map[int64]bool, len(unfiltered)+len(mentions)+1)
- if err := issue.LoadRepo(); err != nil {
- return err
- }
+ // Avoid mailing the doer
+ visited[ctx.Doer.ID] = true
- for _, to := range tos {
- SendIssueCommentMail(issue, doer, actionType, content, comment, []string{to})
+ if err = mailIssueCommentBatch(ctx, unfiltered, visited, false); err != nil {
+ return fmt.Errorf("mailIssueCommentBatch(): %v", err)
}
- // 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])
+ // =========== Mentions ===========
+ if err = mailIssueCommentBatch(ctx, mentions, visited, true); err != nil {
+ return fmt.Errorf("mailIssueCommentBatch() mentions: %v", err)
}
- emails := models.GetUserEmailsByNames(tos)
+ return nil
+}
- for _, to := range emails {
- SendIssueMentionMail(issue, doer, actionType, content, comment, []string{to})
+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 {
+ return err
+ }
+ // TODO: Check issue visibility for each user
+ // 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"))
}
-
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 {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
}
- mentions := make([]string, len(userMentions))
+ mentions := make([]int64, len(userMentions))
for i, u := range userMentions {
- mentions[i] = u.LowerName
- }
- if err = mailIssueCommentToParticipants(issue, doer, opType, issue.Content, nil, mentions); err != nil {
+ mentions[i] = u.ID
+ }
+ if err = mailIssueCommentToParticipants(
+ &mailCommentContext{
+ Issue: issue,
+ Doer: doer,
+ ActionType: opType,
+ Content: issue.Content,
+ Comment: nil,
+ }, mentions); err != nil {
log.Error("mailIssueCommentToParticipants: %v", err)
}
return nil