Skip to content

Commit 8ca7cd6

Browse files
committed
Refactor mail delivery to avoid heavy load on server
1 parent 5f5e3b3 commit 8ca7cd6

File tree

10 files changed

+241
-148
lines changed

10 files changed

+241
-148
lines changed

models/issue.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1197,6 +1197,19 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
11971197
return issues, nil
11981198
}
11991199

1200+
// GetParticipantsIDsByIssueID returns all users who are participated in comments of an issue,
1201+
// but skips joining with `user` for performance reasons.
1202+
// User permissions must be verified elsewhere as required.
1203+
func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) {
1204+
userIDs := make([]int64, 0, 5)
1205+
return userIDs, x.Table("comment").
1206+
Cols("poster_id").
1207+
Where("issue_id = ?", issueID).
1208+
And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
1209+
Distinct("poster_id").
1210+
Find(&userIDs)
1211+
}
1212+
12001213
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
12011214
func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
12021215
return getParticipantsByIssueID(x, issueID)

models/issue_assignees.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,18 @@ func (issue *Issue) loadAssignees(e Engine) (err error) {
4141
return
4242
}
4343

44+
// GetAssigneeIDsByIssue returns everyone assigned to that issue
45+
// but skips joining with `user` for performance reasons.
46+
// User permissions must be verified elsewhere as required.
47+
func GetAssigneeIDsByIssue(issueID int64) ([]int64, error) {
48+
userIDs := make([]int64, 0, 5)
49+
return userIDs, x.Table("issue_assignees").
50+
Cols("assignee_id").
51+
Where("issue_id = ?", issueID).
52+
Distinct("assignee_id").
53+
Find(&userIDs)
54+
}
55+
4456
// GetAssigneesByIssue returns everyone assigned to that issue
4557
func GetAssigneesByIssue(issue *Issue) (assignees []*User, err error) {
4658
return getAssigneesByIssue(x, issue)

models/issue_watch.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,17 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
6060
return
6161
}
6262

63+
// GetIssueWatchersIDs returns IDs of watchers but avoids joining with `user` for performance reasons
64+
// User permissions must be verified elsewhere as required
65+
func GetIssueWatchersIDs(issueID int64) ([]int64, error) {
66+
ids := make([]int64, 0, 64)
67+
return ids, x.Table("issue_watch").
68+
Where("issue_id=?", issueID).
69+
And("is_watching = ?", true).
70+
Select("user_id").
71+
Find(&ids)
72+
}
73+
6374
// GetIssueWatchers returns watchers/unwatchers of a given issue
6475
func GetIssueWatchers(issueID int64) (IssueWatchList, error) {
6576
return getIssueWatchers(x, issueID)

models/repo_watch.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,17 @@ func GetWatchers(repoID int64) ([]*Watch, error) {
140140
return getWatchers(x, repoID)
141141
}
142142

143+
// GetRepoWatchersIDs returns IDs of watchers but avoids joining with `user` for performance reasons
144+
// User permissions must be verified elsewhere as required
145+
func GetRepoWatchersIDs(repoID int64) ([]int64, error) {
146+
ids := make([]int64, 0, 64)
147+
return ids, x.Table("watch").
148+
Where("watch.repo_id=?", repoID).
149+
And("watch.mode<>?", RepoWatchModeDont).
150+
Select("user_id").
151+
Find(&ids)
152+
}
153+
143154
// GetWatchers returns range of users watching given repository.
144155
func (repo *Repository) GetWatchers(page int) ([]*User, error) {
145156
users := make([]*User, 0, ItemsPerPage)

models/user.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,21 @@ func getUserEmailsByNames(e Engine, names []string) []string {
13071307
return mails
13081308
}
13091309

1310+
// GetMaileableUsersByIDs gets users from ids, but only if they can receive mails
1311+
func GetMaileableUsersByIDs(ids []int64) ([]*User, error) {
1312+
ous := make([]*User, 0, len(ids))
1313+
if len(ids) == 0 {
1314+
return ous, nil
1315+
}
1316+
err := x.In("id", ids).
1317+
Where("`type` = ?", UserTypeIndividual).
1318+
And("`prohibit_login` = ?", false).
1319+
And("`is_active` = ?", true).
1320+
And("`email_notifications_preference` = ?", EmailNotificationsEnabled).
1321+
Find(&ous)
1322+
return ous, err
1323+
}
1324+
13101325
// GetUsersByIDs returns all resolved users from a list of Ids.
13111326
func GetUsersByIDs(ids []int64) ([]*User, error) {
13121327
ous := make([]*User, 0, len(ids))

services/mailer/mail.go

Lines changed: 42 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -164,13 +164,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
164164
SendAsync(msg)
165165
}
166166

167-
func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionType models.ActionType, fromMention bool,
168-
content string, comment *models.Comment, tos []string, info string) *Message {
169-
170-
if err := issue.LoadPullRequest(); err != nil {
171-
log.Error("LoadPullRequest: %v", err)
172-
return nil
173-
}
167+
func composeIssueCommentMessages(ctx *mailCommentContext, tos []string, fromMention bool, info string) []*Message {
174168

175169
var (
176170
subject string
@@ -182,29 +176,29 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
182176
)
183177

184178
commentType := models.CommentTypeComment
185-
if comment != nil {
179+
if ctx.Comment != nil {
186180
prefix = "Re: "
187-
commentType = comment.Type
188-
link = issue.HTMLURL() + "#" + comment.HashTag()
181+
commentType = ctx.Comment.Type
182+
link = ctx.Issue.HTMLURL() + "#" + ctx.Comment.HashTag()
189183
} else {
190-
link = issue.HTMLURL()
184+
link = ctx.Issue.HTMLURL()
191185
}
192186

193187
reviewType := models.ReviewTypeComment
194-
if comment != nil && comment.Review != nil {
195-
reviewType = comment.Review.Type
188+
if ctx.Comment != nil && ctx.Comment.Review != nil {
189+
reviewType = ctx.Comment.Review.Type
196190
}
197191

198-
fallback = prefix + fallbackMailSubject(issue)
192+
fallback = prefix + fallbackMailSubject(ctx.Issue)
199193

200194
// This is the body of the new issue or comment, not the mail body
201-
body := string(markup.RenderByType(markdown.MarkupName, []byte(content), issue.Repo.HTMLURL(), issue.Repo.ComposeMetas()))
195+
body := string(markup.RenderByType(markdown.MarkupName, []byte(ctx.Content), ctx.Issue.Repo.HTMLURL(), ctx.Issue.Repo.ComposeMetas()))
202196

203-
actType, actName, tplName := actionToTemplate(issue, actionType, commentType, reviewType)
197+
actType, actName, tplName := actionToTemplate(ctx.Issue, ctx.ActionType, commentType, reviewType)
204198

205-
if comment != nil && comment.Review != nil {
199+
if ctx.Comment != nil && ctx.Comment.Review != nil {
206200
reviewComments = make([]*models.Comment, 0, 10)
207-
for _, lines := range comment.Review.CodeComments {
201+
for _, lines := range ctx.Comment.Review.CodeComments {
208202
for _, comments := range lines {
209203
reviewComments = append(reviewComments, comments...)
210204
}
@@ -215,12 +209,12 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
215209
"FallbackSubject": fallback,
216210
"Body": body,
217211
"Link": link,
218-
"Issue": issue,
219-
"Comment": comment,
220-
"IsPull": issue.IsPull,
221-
"User": issue.Repo.MustOwner(),
222-
"Repo": issue.Repo.FullName(),
223-
"Doer": doer,
212+
"Issue": ctx.Issue,
213+
"Comment": ctx.Comment,
214+
"IsPull": ctx.Issue.IsPull,
215+
"User": ctx.Issue.Repo.MustOwner(),
216+
"Repo": ctx.Issue.Repo.FullName(),
217+
"Doer": ctx.Doer,
224218
"IsMention": fromMention,
225219
"SubjectPrefix": prefix,
226220
"ActionType": actType,
@@ -246,18 +240,23 @@ func composeIssueCommentMessage(issue *models.Issue, doer *models.User, actionTy
246240
log.Error("ExecuteTemplate [%s]: %v", string(tplName)+"/body", err)
247241
}
248242

249-
msg := NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
250-
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
251-
252-
// Set Message-ID on first message so replies know what to reference
253-
if comment == nil {
254-
msg.SetHeader("Message-ID", "<"+issue.ReplyReference()+">")
255-
} else {
256-
msg.SetHeader("In-Reply-To", "<"+issue.ReplyReference()+">")
257-
msg.SetHeader("References", "<"+issue.ReplyReference()+">")
243+
// Make sure to compose independent messages to avoid leaking user emails
244+
msgs := make([]*Message, 0, len(tos))
245+
for _, to := range tos {
246+
msg := NewMessageFrom([]string{to}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
247+
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
248+
249+
// Set Message-ID on first message so replies know what to reference
250+
if ctx.Comment == nil {
251+
msg.SetHeader("Message-ID", "<"+ctx.Issue.ReplyReference()+">")
252+
} else {
253+
msg.SetHeader("In-Reply-To", "<"+ctx.Issue.ReplyReference()+">")
254+
msg.SetHeader("References", "<"+ctx.Issue.ReplyReference()+">")
255+
}
256+
msgs = append(msgs, msg)
258257
}
259258

260-
return msg
259+
return msgs
261260
}
262261

263262
func sanitizeSubject(subject string) string {
@@ -269,21 +268,15 @@ func sanitizeSubject(subject string) string {
269268
return mime.QEncoding.Encode("utf-8", string(runes))
270269
}
271270

272-
// SendIssueCommentMail composes and sends issue comment emails to target receivers.
273-
func SendIssueCommentMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
274-
if len(tos) == 0 {
275-
return
276-
}
277-
278-
SendAsync(composeIssueCommentMessage(issue, doer, actionType, false, content, comment, tos, "issue comment"))
279-
}
280-
281-
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
282-
func SendIssueMentionMail(issue *models.Issue, doer *models.User, actionType models.ActionType, content string, comment *models.Comment, tos []string) {
283-
if len(tos) == 0 {
284-
return
285-
}
286-
SendAsync(composeIssueCommentMessage(issue, doer, actionType, true, content, comment, tos, "issue mention"))
271+
// SendIssueAssignedMail composes and sends issue assigned email
272+
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
273+
SendAsyncs(composeIssueCommentMessages(&mailCommentContext{
274+
Issue: issue,
275+
Doer: doer,
276+
ActionType: models.ActionType(0),
277+
Content: content,
278+
Comment: comment,
279+
}, tos, false, "issue assigned"))
287280
}
288281

289282
// actionToTemplate returns the type and name of the action facing the user
@@ -341,8 +334,3 @@ func actionToTemplate(issue *models.Issue, actionType models.ActionType,
341334
}
342335
return
343336
}
344-
345-
// SendIssueAssignedMail composes and sends issue assigned email
346-
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, tos []string) {
347-
SendAsync(composeIssueCommentMessage(issue, doer, models.ActionType(0), false, content, comment, tos, "issue assigned"))
348-
}

services/mailer/mail_comment.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,18 @@ func mailParticipantsComment(ctx models.DBContext, c *models.Comment, opType mod
2727
if err = models.UpdateIssueMentions(ctx, c.IssueID, userMentions); err != nil {
2828
return fmt.Errorf("UpdateIssueMentions [%d]: %v", c.IssueID, err)
2929
}
30-
mentions := make([]string, len(userMentions))
30+
mentions := make([]int64, len(userMentions))
3131
for i, u := range userMentions {
32-
mentions[i] = u.LowerName
32+
mentions[i] = u.ID
3333
}
34-
if err = mailIssueCommentToParticipants(issue, c.Poster, opType, c.Content, c, mentions); err != nil {
34+
if err = mailIssueCommentToParticipants(
35+
&mailCommentContext{
36+
Issue: issue,
37+
Doer: c.Poster,
38+
ActionType: opType,
39+
Content: c.Content,
40+
Comment: c,
41+
}, mentions); err != nil {
3542
log.Error("mailIssueCommentToParticipants: %v", err)
3643
}
3744
return nil

0 commit comments

Comments
 (0)