mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	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:
		@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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"))
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user