@ -10,105 +10,118 @@ import (
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/references"
"github.com/unknwon/com"
)
func fallbackMailSubject ( issue * models . Issue ) string {
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.
// This function sends two list of emails:
// 1. Repository watchers and users who are participated in comments.
// 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 * mailCommentContex t, mentions [ ] int64 ) error {
watchers , err := models . GetWatchers ( issue . RepoID )
if err != nil {
return fmt . Errorf ( "getWatchers [repo_id: %d]: %v" , issue . RepoID , err )
// Required by the mail composer; make sure to load these before calling the async function
if err := ctx . Issue . LoadRepo ( ) ; err != nil {
return fmt . Errorf ( "LoadRepo(): %v" , err )
}
participants , err := models . GetParticipantsByIssueID ( issue . ID )
if err != nil {
return fmt . Errorf ( "getParticipantsByIssueID [issue_id: %d]: %v" , issue . ID , err )
if err := ctx . Issue . LoadPoster ( ) ; err != nil {
return fmt . Errorf ( "LoadPoster(): %v" , 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,
// even if we have duplicated in watchers, can be safely filtered out.
err = issue . LoadPoster ( )
// Enough room to avoid reallocations
unfiltered := make ( [ ] int64 , 1 , 64 )
// =========== Original poster ===========
unfiltered [ 0 ] = ctx . Issue . PosterID
// =========== Assignees ===========
ids , err := models . GetAssigneeIDsByIssue ( ctx . Issue . ID )
if err != nil {
return fmt . Errorf ( "GetUserByID [%d]: %v" , issue . PosterID , err )
}
if issue . PosterID != doer . ID && issue . Poster . IsActive && ! issue . Poster . ProhibitLogin {
participants = append ( participants , issue . Poster )
return fmt . Errorf ( "GetAssigneeIDsByIssue(%d): %v" , ctx . Issue . ID , err )
}
unfiltered = append ( unfiltered , ids ... )
// Assignees must receive any communications
assignees , err := models . GetAssigneesByIssue ( issue )
// =========== Participants (i.e. commenters, reviewers) ===========
ids , err = models . GetParticipantsIDsByIssueID ( ctx . Issue . ID )
if err != nil {
return err
return fmt . Errorf ( "GetParticipantsIDsByIssueID(%d): %v" , ctx . Issue . ID , err )
}
unfiltered = append ( unfiltered , ids ... )
for _ , assignee := range assignees {
if assignee . ID != doer . ID {
participants = append ( participants , assignee )
}
// =========== 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 ... )
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 )
// =========== 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 )
}
for i := range participants {
if participants [ i ] . ID == doer . ID ||
com . IsSliceContainsStr ( names , participants [ i ] . Name ) ||
participants [ i ] . EmailNotifications ( ) != models . EmailNotificationsEnabled {
continue
}
unfiltered = append ( ids , unfiltered ... )
tos = append ( tos , participants [ i ] . Email )
names = append ( names , participants [ i ] . Name )
}
visited := make ( map [ int64 ] bool , len ( unfiltered ) + len ( mentions ) + 1 )
if err := issue . LoadRepo ( ) ; err != nil {
return err
}
// Avoid mailing the doer
visited [ ctx . Doer . ID ] = true
for _ , to := range tos {
SendIssueCommentMail ( issue , doer , actionType , content , comment , [ ] string { to } )
if err = mailIssueCommentBatch ( ctx , unfiltered , visited , false ) ; err != nil {
return fmt . Errorf ( "mailIssueCommentBatch(): %v" , err )
}
// 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 ] )
// =========== Mentions ===========
if err = mailIssueCommentBatch ( ctx , mentions , visited , true ) ; err != nil {
return fmt . Errorf ( "mailIssueCommentBatch() mentions: %v" , err )
}
emails := models . GetUserEmailsByNames ( tos )
return nil
}
for _ , to := range emails {
SendIssueMentionMail ( issue , doer , actionType , content , comment , [ ] string { to } )
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 {
return err
}
// TODO: Check issue visibility for each user
// TODO: Separate recipients by language for i18n mail templates
tos := make ( [ ] string , len ( recipients ) )
for i := range recipients {
tos [ i ] = recipients [ i ] . Email
}
SendAsyncs ( composeIssueCommentMessages ( ctx , tos , fromMention , "issue comments" ) )
}
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 {
return fmt . Errorf ( "UpdateIssueMentions [%d]: %v" , issue . ID , err )
}
mentions := make ( [ ] string , len ( userMentions ) )
mentions := make ( [ ] int64 , len ( userMentions ) )
for i , u := range userMentions {
mentions [ i ] = u . LowerName
}
if err = mailIssueCommentToParticipants ( issue , doer , opType , issue . Content , nil , mentions ) ; err != nil {
mentions [ i ] = u . ID
}
if err = mailIssueCommentToParticipants (
& mailCommentContext {
Issue : issue ,
Doer : doer ,
ActionType : opType ,
Content : issue . Content ,
Comment : nil ,
} , mentions ) ; err != nil {
log . Error ( "mailIssueCommentToParticipants: %v" , err )
}
return nil