You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

162 lines
5.1 KiB

  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package mailer
  5. import (
  6. "fmt"
  7. "code.gitea.io/gitea/models"
  8. "code.gitea.io/gitea/modules/log"
  9. "code.gitea.io/gitea/modules/references"
  10. "github.com/unknwon/com"
  11. )
  12. func mailSubject(issue *models.Issue) string {
  13. return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.FullName(), issue.Title, issue.Index)
  14. }
  15. // mailIssueCommentToParticipants can be used for both new issue creation and comment.
  16. // This function sends two list of emails:
  17. // 1. Repository watchers and users who are participated in comments.
  18. // 2. Users who are not in 1. but get mentioned in current issue/comment.
  19. func mailIssueCommentToParticipants(issue *models.Issue, doer *models.User, content string, comment *models.Comment, mentions []string) error {
  20. watchers, err := models.GetWatchers(issue.RepoID)
  21. if err != nil {
  22. return fmt.Errorf("getWatchers [repo_id: %d]: %v", issue.RepoID, err)
  23. }
  24. participants, err := models.GetParticipantsByIssueID(issue.ID)
  25. if err != nil {
  26. return fmt.Errorf("getParticipantsByIssueID [issue_id: %d]: %v", issue.ID, err)
  27. }
  28. // In case the issue poster is not watching the repository and is active,
  29. // even if we have duplicated in watchers, can be safely filtered out.
  30. err = issue.LoadPoster()
  31. if err != nil {
  32. return fmt.Errorf("GetUserByID [%d]: %v", issue.PosterID, err)
  33. }
  34. if issue.PosterID != doer.ID && issue.Poster.IsActive && !issue.Poster.ProhibitLogin {
  35. participants = append(participants, issue.Poster)
  36. }
  37. // Assignees must receive any communications
  38. assignees, err := models.GetAssigneesByIssue(issue)
  39. if err != nil {
  40. return err
  41. }
  42. for _, assignee := range assignees {
  43. if assignee.ID != doer.ID {
  44. participants = append(participants, assignee)
  45. }
  46. }
  47. tos := make([]string, 0, len(watchers)) // List of email addresses.
  48. names := make([]string, 0, len(watchers))
  49. for i := range watchers {
  50. if watchers[i].UserID == doer.ID {
  51. continue
  52. }
  53. to, err := models.GetUserByID(watchers[i].UserID)
  54. if err != nil {
  55. return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
  56. }
  57. if to.IsOrganization() || to.EmailNotifications() != models.EmailNotificationsEnabled {
  58. continue
  59. }
  60. tos = append(tos, to.Email)
  61. names = append(names, to.Name)
  62. }
  63. for i := range participants {
  64. if participants[i].ID == doer.ID ||
  65. com.IsSliceContainsStr(names, participants[i].Name) ||
  66. participants[i].EmailNotifications() != models.EmailNotificationsEnabled {
  67. continue
  68. }
  69. tos = append(tos, participants[i].Email)
  70. names = append(names, participants[i].Name)
  71. }
  72. if err := issue.LoadRepo(); err != nil {
  73. return err
  74. }
  75. for _, to := range tos {
  76. SendIssueCommentMail(issue, doer, content, comment, []string{to})
  77. }
  78. // Mail mentioned people and exclude watchers.
  79. names = append(names, doer.Name)
  80. tos = make([]string, 0, len(mentions)) // list of user names.
  81. for i := range mentions {
  82. if com.IsSliceContainsStr(names, mentions[i]) {
  83. continue
  84. }
  85. tos = append(tos, mentions[i])
  86. }
  87. emails := models.GetUserEmailsByNames(tos)
  88. for _, to := range emails {
  89. SendIssueMentionMail(issue, doer, content, comment, []string{to})
  90. }
  91. return nil
  92. }
  93. // MailParticipants sends new issue thread created emails to repository watchers
  94. // and mentioned people.
  95. func MailParticipants(issue *models.Issue, doer *models.User, opType models.ActionType) error {
  96. return mailParticipants(models.DefaultDBContext(), issue, doer, opType)
  97. }
  98. func mailParticipants(ctx models.DBContext, issue *models.Issue, doer *models.User, opType models.ActionType) (err error) {
  99. rawMentions := references.FindAllMentionsMarkdown(issue.Content)
  100. userMentions, err := issue.ResolveMentionsByVisibility(ctx, doer, rawMentions)
  101. if err != nil {
  102. return fmt.Errorf("ResolveMentionsByVisibility [%d]: %v", issue.ID, err)
  103. }
  104. if err = models.UpdateIssueMentions(ctx, issue.ID, userMentions); err != nil {
  105. return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
  106. }
  107. mentions := make([]string, len(userMentions))
  108. for i, u := range userMentions {
  109. mentions[i] = u.LowerName
  110. }
  111. if len(issue.Content) > 0 {
  112. if err = mailIssueCommentToParticipants(issue, doer, issue.Content, nil, mentions); err != nil {
  113. log.Error("mailIssueCommentToParticipants: %v", err)
  114. }
  115. }
  116. switch opType {
  117. case models.ActionCreateIssue, models.ActionCreatePullRequest:
  118. if len(issue.Content) == 0 {
  119. ct := fmt.Sprintf("Created #%d.", issue.Index)
  120. if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
  121. log.Error("mailIssueCommentToParticipants: %v", err)
  122. }
  123. }
  124. case models.ActionCloseIssue, models.ActionClosePullRequest:
  125. ct := fmt.Sprintf("Closed #%d.", issue.Index)
  126. if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
  127. log.Error("mailIssueCommentToParticipants: %v", err)
  128. }
  129. case models.ActionReopenIssue, models.ActionReopenPullRequest:
  130. ct := fmt.Sprintf("Reopened #%d.", issue.Index)
  131. if err = mailIssueCommentToParticipants(issue, doer, ct, nil, mentions); err != nil {
  132. log.Error("mailIssueCommentToParticipants: %v", err)
  133. }
  134. }
  135. return nil
  136. }