Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.

mail_issue.go 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package mailer
  4. import (
  5. "context"
  6. "fmt"
  7. activities_model "code.gitea.io/gitea/models/activities"
  8. issues_model "code.gitea.io/gitea/models/issues"
  9. access_model "code.gitea.io/gitea/models/perm/access"
  10. repo_model "code.gitea.io/gitea/models/repo"
  11. "code.gitea.io/gitea/models/unit"
  12. user_model "code.gitea.io/gitea/models/user"
  13. "code.gitea.io/gitea/modules/container"
  14. "code.gitea.io/gitea/modules/log"
  15. "code.gitea.io/gitea/modules/setting"
  16. )
  17. func fallbackMailSubject(issue *issues_model.Issue) string {
  18. return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
  19. }
  20. type mailCommentContext struct {
  21. context.Context
  22. Issue *issues_model.Issue
  23. Doer *user_model.User
  24. ActionType activities_model.ActionType
  25. Content string
  26. Comment *issues_model.Comment
  27. ForceDoerNotification bool
  28. }
  29. const (
  30. // MailBatchSize set the batch size used in mailIssueCommentBatch
  31. MailBatchSize = 100
  32. )
  33. // mailIssueCommentToParticipants can be used for both new issue creation and comment.
  34. // This function sends two list of emails:
  35. // 1. Repository watchers (except for WIP pull requests) and users who are participated in comments.
  36. // 2. Users who are not in 1. but get mentioned in current issue/comment.
  37. func mailIssueCommentToParticipants(ctx *mailCommentContext, mentions []*user_model.User) error {
  38. // Required by the mail composer; make sure to load these before calling the async function
  39. if err := ctx.Issue.LoadRepo(ctx); err != nil {
  40. return fmt.Errorf("LoadRepo: %w", err)
  41. }
  42. if err := ctx.Issue.LoadPoster(ctx); err != nil {
  43. return fmt.Errorf("LoadPoster: %w", err)
  44. }
  45. if err := ctx.Issue.LoadPullRequest(ctx); err != nil {
  46. return fmt.Errorf("LoadPullRequest: %w", err)
  47. }
  48. // Enough room to avoid reallocations
  49. unfiltered := make([]int64, 1, 64)
  50. // =========== Original poster ===========
  51. unfiltered[0] = ctx.Issue.PosterID
  52. // =========== Assignees ===========
  53. ids, err := issues_model.GetAssigneeIDsByIssue(ctx, ctx.Issue.ID)
  54. if err != nil {
  55. return fmt.Errorf("GetAssigneeIDsByIssue(%d): %w", ctx.Issue.ID, err)
  56. }
  57. unfiltered = append(unfiltered, ids...)
  58. // =========== Participants (i.e. commenters, reviewers) ===========
  59. ids, err = issues_model.GetParticipantsIDsByIssueID(ctx, ctx.Issue.ID)
  60. if err != nil {
  61. return fmt.Errorf("GetParticipantsIDsByIssueID(%d): %w", ctx.Issue.ID, err)
  62. }
  63. unfiltered = append(unfiltered, ids...)
  64. // =========== Issue watchers ===========
  65. ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, true)
  66. if err != nil {
  67. return fmt.Errorf("GetIssueWatchersIDs(%d): %w", ctx.Issue.ID, err)
  68. }
  69. unfiltered = append(unfiltered, ids...)
  70. // =========== Repo watchers ===========
  71. // Make repo watchers last, since it's likely the list with the most users
  72. if !(ctx.Issue.IsPull && ctx.Issue.PullRequest.IsWorkInProgress() && ctx.ActionType != activities_model.ActionCreatePullRequest) {
  73. ids, err = repo_model.GetRepoWatchersIDs(ctx, ctx.Issue.RepoID)
  74. if err != nil {
  75. return fmt.Errorf("GetRepoWatchersIDs(%d): %w", ctx.Issue.RepoID, err)
  76. }
  77. unfiltered = append(ids, unfiltered...)
  78. }
  79. visited := make(container.Set[int64], len(unfiltered)+len(mentions)+1)
  80. // Avoid mailing the doer
  81. if ctx.Doer.EmailNotificationsPreference != user_model.EmailNotificationsAndYourOwn && !ctx.ForceDoerNotification {
  82. visited.Add(ctx.Doer.ID)
  83. }
  84. // =========== Mentions ===========
  85. if err = mailIssueCommentBatch(ctx, mentions, visited, true); err != nil {
  86. return fmt.Errorf("mailIssueCommentBatch() mentions: %w", err)
  87. }
  88. // Avoid mailing explicit unwatched
  89. ids, err = issues_model.GetIssueWatchersIDs(ctx, ctx.Issue.ID, false)
  90. if err != nil {
  91. return fmt.Errorf("GetIssueWatchersIDs(%d): %w", ctx.Issue.ID, err)
  92. }
  93. visited.AddMultiple(ids...)
  94. unfilteredUsers, err := user_model.GetMaileableUsersByIDs(ctx, unfiltered, false)
  95. if err != nil {
  96. return err
  97. }
  98. if err = mailIssueCommentBatch(ctx, unfilteredUsers, visited, false); err != nil {
  99. return fmt.Errorf("mailIssueCommentBatch(): %w", err)
  100. }
  101. return nil
  102. }
  103. func mailIssueCommentBatch(ctx *mailCommentContext, users []*user_model.User, visited container.Set[int64], fromMention bool) error {
  104. checkUnit := unit.TypeIssues
  105. if ctx.Issue.IsPull {
  106. checkUnit = unit.TypePullRequests
  107. }
  108. langMap := make(map[string][]*user_model.User)
  109. for _, user := range users {
  110. if !user.IsActive {
  111. // Exclude deactivated users
  112. continue
  113. }
  114. // At this point we exclude:
  115. // user that don't have all mails enabled or users only get mail on mention and this is one ...
  116. if !(user.EmailNotificationsPreference == user_model.EmailNotificationsEnabled ||
  117. user.EmailNotificationsPreference == user_model.EmailNotificationsAndYourOwn ||
  118. fromMention && user.EmailNotificationsPreference == user_model.EmailNotificationsOnMention) {
  119. continue
  120. }
  121. // if we have already visited this user we exclude them
  122. if !visited.Add(user.ID) {
  123. continue
  124. }
  125. // test if this user is allowed to see the issue/pull
  126. if !access_model.CheckRepoUnitUser(ctx, ctx.Issue.Repo, user, checkUnit) {
  127. continue
  128. }
  129. langMap[user.Language] = append(langMap[user.Language], user)
  130. }
  131. for lang, receivers := range langMap {
  132. // because we know that the len(receivers) > 0 and we don't care about the order particularly
  133. // working backwards from the last (possibly) incomplete batch. If len(receivers) can be 0 this
  134. // starting condition will need to be changed slightly
  135. for i := ((len(receivers) - 1) / MailBatchSize) * MailBatchSize; i >= 0; i -= MailBatchSize {
  136. msgs, err := composeIssueCommentMessages(ctx, lang, receivers[i:], fromMention, "issue comments")
  137. if err != nil {
  138. return err
  139. }
  140. SendAsyncs(msgs)
  141. receivers = receivers[:i]
  142. }
  143. }
  144. return nil
  145. }
  146. // MailParticipants sends new issue thread created emails to repository watchers
  147. // and mentioned people.
  148. func MailParticipants(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, opType activities_model.ActionType, mentions []*user_model.User) error {
  149. if setting.MailService == nil {
  150. // No mail service configured
  151. return nil
  152. }
  153. content := issue.Content
  154. if opType == activities_model.ActionCloseIssue || opType == activities_model.ActionClosePullRequest ||
  155. opType == activities_model.ActionReopenIssue || opType == activities_model.ActionReopenPullRequest ||
  156. opType == activities_model.ActionMergePullRequest || opType == activities_model.ActionAutoMergePullRequest {
  157. content = ""
  158. }
  159. forceDoerNotification := opType == activities_model.ActionAutoMergePullRequest
  160. if err := mailIssueCommentToParticipants(
  161. &mailCommentContext{
  162. Context: ctx,
  163. Issue: issue,
  164. Doer: doer,
  165. ActionType: opType,
  166. Content: content,
  167. Comment: nil,
  168. ForceDoerNotification: forceDoerNotification,
  169. }, mentions); err != nil {
  170. log.Error("mailIssueCommentToParticipants: %v", err)
  171. }
  172. return nil
  173. }