gitea源码

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