1
1
mirror of https://github.com/go-gitea/gitea synced 2025-07-25 19:58:36 +00:00

#2854 fix no mail notification when issue is closed/reopened

This commit is contained in:
Unknwon
2016-07-16 00:36:39 +08:00
parent 7ca5f8f119
commit f1b8d52eb3
26 changed files with 438 additions and 347 deletions

View File

@@ -73,6 +73,15 @@ func (i *Issue) BeforeUpdate() {
i.DeadlineUnix = i.Deadline.UTC().Unix()
}
func (issue *Issue) loadAttributes() (err error) {
issue.Repo, err = GetRepositoryByID(issue.RepoID)
if err != nil {
return fmt.Errorf("GetRepositoryByID: %v", err)
}
return nil
}
func (i *Issue) AfterSet(colName string, _ xorm.Cell) {
var err error
switch colName {
@@ -146,6 +155,10 @@ func (i *Issue) State() string {
return "open"
}
func (issue *Issue) FullLink() string {
return fmt.Sprintf("%s/issues/%d", issue.Repo.FullLink(), issue.Index)
}
// IsPoster returns true if given user by ID is the poster.
func (i *Issue) IsPoster(uid int64) bool {
return i.PosterID == uid
@@ -390,7 +403,7 @@ func newIssue(e *xorm.Session, repo *Repository, issue *Issue, labelIDs []int64,
}
}
return nil
return issue.loadAttributes()
}
// NewIssue creates new issue with labels for repository.
@@ -422,7 +435,9 @@ func NewIssue(repo *Repository, issue *Issue, labelIDs []int64, uuids []string)
IsPrivate: repo.IsPrivate,
}
if err = NotifyWatchers(act); err != nil {
log.Error(4, "notifyWatchers: %v", err)
log.Error(4, "NotifyWatchers: %v", err)
} else if err = issue.MailParticipants(); err != nil {
log.Error(4, "MailParticipants: %v", err)
}
return nil
@@ -451,8 +466,7 @@ func GetIssueByRef(ref string) (*Issue, error) {
return nil, err
}
issue.Repo = repo
return issue, nil
return issue, issue.loadAttributes()
}
// GetIssueByIndex returns issue by given index in repository.
@@ -467,7 +481,7 @@ func GetIssueByIndex(repoID, index int64) (*Issue, error) {
} else if !has {
return nil, ErrIssueNotExist{0, repoID, index}
}
return issue, nil
return issue, issue.loadAttributes()
}
// GetIssueByID returns an issue by given ID.
@@ -479,7 +493,7 @@ func GetIssueByID(id int64) (*Issue, error) {
} else if !has {
return nil, ErrIssueNotExist{id, 0, 0}
}
return issue, nil
return issue, issue.loadAttributes()
}
type IssuesOptions struct {
@@ -700,42 +714,44 @@ func GetIssueUserPairsByMode(uid, rid int64, isClosed bool, page, filterMode int
return ius, err
}
func UpdateMentions(userNames []string, issueId int64) error {
for i := range userNames {
userNames[i] = strings.ToLower(userNames[i])
}
users := make([]*User, 0, len(userNames))
if err := x.Where("lower_name IN (?)", strings.Join(userNames, "\",\"")).OrderBy("lower_name ASC").Find(&users); err != nil {
return err
// UpdateIssueMentions extracts mentioned people from content and
// updates issue-user relations for them.
func UpdateIssueMentions(issueID int64, mentions []string) error {
if len(mentions) == 0 {
return nil
}
ids := make([]int64, 0, len(userNames))
for i := range mentions {
mentions[i] = strings.ToLower(mentions[i])
}
users := make([]*User, 0, len(mentions))
if err := x.In("lower_name", mentions).Asc("lower_name").Find(&users); err != nil {
return fmt.Errorf("find mentioned users: %v", err)
}
ids := make([]int64, 0, len(mentions))
for _, user := range users {
ids = append(ids, user.Id)
if !user.IsOrganization() {
if !user.IsOrganization() || user.NumMembers == 0 {
continue
}
if user.NumMembers == 0 {
continue
}
tempIds := make([]int64, 0, user.NumMembers)
orgUsers, err := GetOrgUsersByOrgId(user.Id)
memberIDs := make([]int64, 0, user.NumMembers)
orgUsers, err := GetOrgUsersByOrgID(user.Id)
if err != nil {
return err
return fmt.Errorf("GetOrgUsersByOrgID [%d]: %v", user.Id, err)
}
for _, orgUser := range orgUsers {
tempIds = append(tempIds, orgUser.ID)
memberIDs = append(memberIDs, orgUser.ID)
}
ids = append(ids, tempIds...)
ids = append(ids, memberIDs...)
}
if err := UpdateIssueUsersByMentions(ids, issueId); err != nil {
return err
if err := UpdateIssueUsersByMentions(issueID, ids); err != nil {
return fmt.Errorf("UpdateIssueUsersByMentions: %v", err)
}
return nil
@@ -973,9 +989,12 @@ func UpdateIssueUserByRead(uid, issueID int64) error {
}
// UpdateIssueUsersByMentions updates issue-user pairs by mentioning.
func UpdateIssueUsersByMentions(uids []int64, iid int64) error {
func UpdateIssueUsersByMentions(issueID int64, uids []int64) error {
for _, uid := range uids {
iu := &IssueUser{UID: uid, IssueID: iid}
iu := &IssueUser{
UID: uid,
IssueID: issueID,
}
has, err := x.Get(iu)
if err != nil {
return err

View File

@@ -13,6 +13,7 @@ import (
"github.com/go-xorm/xorm"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/markdown"
)
// CommentType defines whether a comment is just a simple comment, an action (like close) or a reference.
@@ -113,10 +114,34 @@ func (c *Comment) EventTag() string {
return "event-" + com.ToStr(c.ID)
}
// MailParticipants sends new comment emails to repository watchers
// and mentioned people.
func (cmt *Comment) MailParticipants(opType ActionType, issue *Issue) (err error) {
mentions := markdown.FindAllMentions(cmt.Content)
if err = UpdateIssueMentions(cmt.IssueID, mentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", cmt.IssueID, err)
}
switch opType {
case ACTION_COMMENT_ISSUE:
issue.Content = cmt.Content
case ACTION_CLOSE_ISSUE:
issue.Content = fmt.Sprintf("Closed #%d", issue.Index)
case ACTION_REOPEN_ISSUE:
issue.Content = fmt.Sprintf("Reopened #%d", issue.Index)
}
if err = mailIssueCommentToParticipants(issue, cmt.Poster, mentions); err != nil {
log.Error(4, "mailIssueCommentToParticipants: %v", err)
}
return nil
}
func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err error) {
comment := &Comment{
Type: opts.Type,
PosterID: opts.Doer.Id,
Poster: opts.Doer,
IssueID: opts.Issue.ID,
CommitID: opts.CommitID,
CommitSHA: opts.CommitSHA,
@@ -157,7 +182,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
if IsErrAttachmentNotExist(err) {
continue
}
return nil, fmt.Errorf("getAttachmentByUUID[%s]: %v", uuid, err)
return nil, fmt.Errorf("getAttachmentByUUID [%s]: %v", uuid, err)
}
attachments = append(attachments, attach)
}
@@ -167,7 +192,7 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
attachments[i].CommentID = comment.ID
// No assign value could be 0, so ignore AllCols().
if _, err = e.Id(attachments[i].ID).Update(attachments[i]); err != nil {
return nil, fmt.Errorf("update attachment[%d]: %v", attachments[i].ID, err)
return nil, fmt.Errorf("update attachment [%d]: %v", attachments[i].ID, err)
}
}
@@ -200,13 +225,15 @@ func createComment(e *xorm.Session, opts *CreateCommentOptions) (_ *Comment, err
if err != nil {
return nil, err
}
}
// Notify watchers for whatever action comes in, ignore if no action type
// Notify watchers for whatever action comes in, ignore if no action type.
if act.OpType > 0 {
if err = notifyWatchers(e, act); err != nil {
return nil, fmt.Errorf("notifyWatchers: %v", err)
log.Error(4, "notifyWatchers: %v", err)
}
comment.MailParticipants(act.OpType, opts.Issue)
}
return comment, nil

81
models/issue_mail.go Normal file
View File

@@ -0,0 +1,81 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"fmt"
"github.com/Unknwon/com"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/markdown"
"github.com/gogits/gogs/modules/setting"
)
func (issue *Issue) MailSubject() string {
return fmt.Sprintf("[%s] %s (#%d)", issue.Repo.Name, issue.Name, issue.Index)
}
// mailIssueCommentToParticipants can be used for both new issue creation and comment.
func mailIssueCommentToParticipants(issue *Issue, doer *User, mentions []string) error {
if !setting.Service.EnableNotifyMail {
return nil
}
// Mail wahtcers.
watchers, err := GetWatchers(issue.RepoID)
if err != nil {
return fmt.Errorf("GetWatchers [%d]: %v", issue.RepoID, err)
}
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 := GetUserByID(watchers[i].UserID)
if err != nil {
return fmt.Errorf("GetUserByID [%d]: %v", watchers[i].UserID, err)
}
if to.IsOrganization() {
continue
}
tos = append(tos, to.Email)
names = append(names, to.Name)
}
SendIssueCommentMail(issue, doer, tos)
// 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])
}
SendIssueMentionMail(issue, doer, GetUserEmailsByNames(tos))
return nil
}
// MailParticipants sends new issue thread created emails to repository watchers
// and mentioned people.
func (issue *Issue) MailParticipants() (err error) {
mentions := markdown.FindAllMentions(issue.Content)
if err = UpdateIssueMentions(issue.ID, mentions); err != nil {
return fmt.Errorf("UpdateIssueMentions [%d]: %v", issue.ID, err)
}
if err = mailIssueCommentToParticipants(issue, issue.Poster, mentions); err != nil {
log.Error(4, "mailIssueCommentToParticipants: %v", err)
}
return nil
}

183
models/mail.go Normal file
View File

@@ -0,0 +1,183 @@
// Copyright 2016 The Gogs Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package models
import (
"fmt"
"html/template"
"path"
"gopkg.in/gomail.v2"
"gopkg.in/macaron.v1"
"github.com/gogits/gogs/modules/base"
"github.com/gogits/gogs/modules/log"
"github.com/gogits/gogs/modules/mailer"
"github.com/gogits/gogs/modules/markdown"
"github.com/gogits/gogs/modules/setting"
)
const (
MAIL_AUTH_ACTIVATE base.TplName = "auth/activate"
MAIL_AUTH_ACTIVATE_EMAIL base.TplName = "auth/activate_email"
MAIL_AUTH_RESET_PASSWORD base.TplName = "auth/reset_passwd"
MAIL_AUTH_REGISTER_NOTIFY base.TplName = "auth/register_notify"
MAIL_ISSUE_COMMENT base.TplName = "issue/comment"
MAIL_ISSUE_MENTION base.TplName = "issue/mention"
MAIL_NOTIFY_COLLABORATOR base.TplName = "notify/collaborator"
)
type MailRender interface {
HTMLString(string, interface{}, ...macaron.HTMLOptions) (string, error)
}
var mailRender MailRender
func InitMailRender(dir, appendDir string, funcMap []template.FuncMap) {
opt := &macaron.RenderOptions{
Directory: dir,
AppendDirectories: []string{appendDir},
Funcs: funcMap,
Extensions: []string{".tmpl", ".html"},
}
ts := macaron.NewTemplateSet()
ts.Set(macaron.DEFAULT_TPL_SET_NAME, opt)
mailRender = &macaron.TplRender{
TemplateSet: ts,
Opt: opt,
}
}
func SendTestMail(email string) error {
return gomail.Send(&mailer.Sender{}, mailer.NewMessage([]string{email}, "Gogs Test Email!", "Gogs Test Email!").Message)
}
func SendUserMail(c *macaron.Context, u *User, tpl base.TplName, code, subject, info string) {
data := map[string]interface{}{
"Username": u.DisplayName(),
"ActiveCodeLives": setting.Service.ActiveCodeLives / 60,
"ResetPwdCodeLives": setting.Service.ResetPwdCodeLives / 60,
"Code": code,
}
body, err := mailRender.HTMLString(string(tpl), data)
if err != nil {
log.Error(3, "HTMLString: %v", err)
return
}
msg := mailer.NewMessage([]string{u.Email}, subject, body)
msg.Info = fmt.Sprintf("UID: %d, %s", u.Id, info)
mailer.SendAsync(msg)
}
func SendActivateAccountMail(c *macaron.Context, u *User) {
SendUserMail(c, u, MAIL_AUTH_ACTIVATE, u.GenerateActivateCode(), c.Tr("mail.activate_account"), "activate account")
}
func SendResetPasswordMail(c *macaron.Context, u *User) {
SendUserMail(c, u, MAIL_AUTH_RESET_PASSWORD, u.GenerateActivateCode(), c.Tr("mail.reset_password"), "reset password")
}
// SendActivateAccountMail sends confirmation email.
func SendActivateEmailMail(c *macaron.Context, u *User, email *EmailAddress) {
data := map[string]interface{}{
"Username": u.DisplayName(),
"ActiveCodeLives": setting.Service.ActiveCodeLives / 60,
"Code": u.GenerateEmailActivateCode(email.Email),
"Email": email.Email,
}
body, err := mailRender.HTMLString(string(MAIL_AUTH_ACTIVATE_EMAIL), data)
if err != nil {
log.Error(3, "HTMLString: %v", err)
return
}
msg := mailer.NewMessage([]string{email.Email}, c.Tr("mail.activate_email"), body)
msg.Info = fmt.Sprintf("UID: %d, activate email", u.Id)
mailer.SendAsync(msg)
}
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
func SendRegisterNotifyMail(c *macaron.Context, u *User) {
data := map[string]interface{}{
"Username": u.DisplayName(),
}
body, err := mailRender.HTMLString(string(MAIL_AUTH_REGISTER_NOTIFY), data)
if err != nil {
log.Error(3, "HTMLString: %v", err)
return
}
msg := mailer.NewMessage([]string{u.Email}, c.Tr("mail.register_notify"), body)
msg.Info = fmt.Sprintf("UID: %d, registration notify", u.Id)
mailer.SendAsync(msg)
}
// SendCollaboratorMail sends mail notification to new collaborator.
func SendCollaboratorMail(u, doer *User, repo *Repository) {
repoName := path.Join(repo.Owner.Name, repo.Name)
subject := fmt.Sprintf("%s added you to %s", doer.DisplayName(), repoName)
data := map[string]interface{}{
"Subject": subject,
"RepoName": repoName,
"Link": repo.FullLink(),
}
body, err := mailRender.HTMLString(string(MAIL_NOTIFY_COLLABORATOR), data)
if err != nil {
log.Error(3, "HTMLString: %v", err)
return
}
msg := mailer.NewMessage([]string{u.Email}, subject, body)
msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.Id)
mailer.SendAsync(msg)
}
func composeTplData(subject, body, link string) map[string]interface{} {
data := make(map[string]interface{}, 10)
data["Subject"] = subject
data["Body"] = body
data["Link"] = link
return data
}
func composeIssueMessage(issue *Issue, doer *User, tplName base.TplName, tos []string, info string) *mailer.Message {
subject := issue.MailSubject()
body := string(markdown.RenderSpecialLink([]byte(issue.Content), issue.Repo.FullLink(), issue.Repo.ComposeMetas()))
data := composeTplData(subject, body, issue.FullLink())
data["Doer"] = doer
content, err := mailRender.HTMLString(string(tplName), data)
if err != nil {
log.Error(3, "HTMLString (%s): %v", tplName, err)
}
msg := mailer.NewMessage(tos, subject, content)
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
return msg
}
// SendIssueCommentMail composes and sends issue comment emails to target receivers.
func SendIssueCommentMail(issue *Issue, doer *User, tos []string) {
if len(tos) == 0 {
return
}
mailer.SendAsync(composeIssueMessage(issue, doer, MAIL_ISSUE_COMMENT, tos, "issue comment"))
}
// SendIssueMentionMail composes and sends issue mention emails to target receivers.
func SendIssueMentionMail(issue *Issue, doer *User, tos []string) {
if len(tos) == 0 {
return
}
mailer.SendAsync(composeIssueMessage(issue, doer, MAIL_ISSUE_MENTION, tos, "issue mention"))
}

View File

@@ -58,7 +58,7 @@ func (org *User) GetTeams() error {
// GetMembers returns all members of organization.
func (org *User) GetMembers() error {
ous, err := GetOrgUsersByOrgId(org.Id)
ous, err := GetOrgUsersByOrgID(org.Id)
if err != nil {
return err
}
@@ -306,10 +306,10 @@ func GetOrgUsersByUserID(uid int64, all bool) ([]*OrgUser, error) {
return ous, err
}
// GetOrgUsersByOrgId returns all organization-user relations by organization ID.
func GetOrgUsersByOrgId(orgId int64) ([]*OrgUser, error) {
// GetOrgUsersByOrgID returns all organization-user relations by organization ID.
func GetOrgUsersByOrgID(orgID int64) ([]*OrgUser, error) {
ous := make([]*OrgUser, 0, 10)
err := x.Where("org_id=?", orgId).Find(&ous)
err := x.Where("org_id=?", orgID).Find(&ous)
return ous, err
}

View File

@@ -342,9 +342,6 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
RepoName: repo.Name,
IsPrivate: repo.IsPrivate,
}
if err = notifyWatchers(sess, act); err != nil {
return err
}
pr.Index = pull.Index
if err = repo.SavePatch(pr.Index, patch); err != nil {
@@ -364,7 +361,17 @@ func NewPullRequest(repo *Repository, pull *Issue, labelIDs []int64, uuids []str
return fmt.Errorf("insert pull repo: %v", err)
}
return sess.Commit()
if err = sess.Commit(); err != nil {
return fmt.Errorf("Commit: %v", err)
}
if err = NotifyWatchers(act); err != nil {
log.Error(4, "NotifyWatchers: %v", err)
} else if err = pull.MailParticipants(); err != nil {
log.Error(4, "MailParticipants: %v", err)
}
return nil
}
// GetUnmergedPullRequest returnss a pull request that is open and has not been merged