Skip to content

Commit 08ae6bb

Browse files
guillep2kzeripath
authored andcommitted
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
1 parent 9ff6312 commit 08ae6bb

File tree

10 files changed

+250
-144
lines changed

10 files changed

+250
-144
lines changed

models/issue.go

+13
Original file line numberDiff line numberDiff line change
@@ -1219,6 +1219,19 @@ func Issues(opts *IssuesOptions) ([]*Issue, error) {
12191219
return issues, nil
12201220
}
12211221

1222+
// GetParticipantsIDsByIssueID returns the IDs of all users who participated in comments of an issue,
1223+
// but skips joining with `user` for performance reasons.
1224+
// User permissions must be verified elsewhere if required.
1225+
func GetParticipantsIDsByIssueID(issueID int64) ([]int64, error) {
1226+
userIDs := make([]int64, 0, 5)
1227+
return userIDs, x.Table("comment").
1228+
Cols("poster_id").
1229+
Where("issue_id = ?", issueID).
1230+
And("type in (?,?,?)", CommentTypeComment, CommentTypeCode, CommentTypeReview).
1231+
Distinct("poster_id").
1232+
Find(&userIDs)
1233+
}
1234+
12221235
// GetParticipantsByIssueID returns all users who are participated in comments of an issue.
12231236
func GetParticipantsByIssueID(issueID int64) ([]*User, error) {
12241237
return getParticipantsByIssueID(x, issueID)

models/issue_assignees.go

+12
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 the IDs of users assigned to an issue
45+
// but skips joining with `user` for performance reasons.
46+
// User permissions must be verified elsewhere if 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

+12
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ func getIssueWatch(e Engine, userID, issueID int64) (iw *IssueWatch, exists bool
6060
return
6161
}
6262

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

models/repo_watch.go

+12
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,18 @@ func GetWatchers(repoID int64) ([]*Watch, error) {
140140
return getWatchers(x, repoID)
141141
}
142142

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

models/user.go

+14
Original file line numberDiff line numberDiff line change
@@ -1307,6 +1307,20 @@ 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+
if len(ids) == 0 {
1313+
return nil, nil
1314+
}
1315+
ous := make([]*User, 0, len(ids))
1316+
return ous, 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+
}
1323+
13101324
// GetUsersByIDs returns all resolved users from a list of Ids.
13111325
func GetUsersByIDs(ids []int64) ([]*User, error) {
13121326
ous := make([]*User, 0, len(ids))

services/mailer/mail.go

+42-54
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

+10-3
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)