Skip to content

Commit 944d904

Browse files
mrsdizzietechknowlogick
authored andcommitted
Include thread related headers in issue/coment mail (#7484)
* Include thread related headers in issue/coment mail Make it so mail programs will group comments from an issue into the same thread by setting Message-ID on initial issue and then using In-Reply-To and References headers to reference that later on. * Add tests * more tests * fix typo
1 parent 5d3e351 commit 944d904

File tree

3 files changed

+115
-1
lines changed

3 files changed

+115
-1
lines changed

models/issue.go

+12
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,18 @@ func (issue *Issue) sendLabelUpdatedWebhook(doer *User) {
472472
}
473473
}
474474

475+
// ReplyReference returns tokenized address to use for email reply headers
476+
func (issue *Issue) ReplyReference() string {
477+
var path string
478+
if issue.IsPull {
479+
path = "pulls"
480+
} else {
481+
path = "issues"
482+
}
483+
484+
return fmt.Sprintf("%s/%s/%d@%s", issue.Repo.FullName(), path, issue.Index, setting.Domain)
485+
}
486+
475487
func (issue *Issue) addLabel(e *xorm.Session, label *Label, doer *User) error {
476488
return newIssueLabel(e, issue, label, doer)
477489
}

models/mail.go

+16-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,13 @@ func composeTplData(subject, body, link string) map[string]interface{} {
156156
}
157157

158158
func composeIssueCommentMessage(issue *Issue, doer *User, content string, comment *Comment, tplName base.TplName, tos []string, info string) *mailer.Message {
159-
subject := issue.mailSubject()
159+
var subject string
160+
if comment != nil {
161+
subject = "Re: " + issue.mailSubject()
162+
} else {
163+
subject = issue.mailSubject()
164+
}
165+
160166
err := issue.LoadRepo()
161167
if err != nil {
162168
log.Error("LoadRepo: %v", err)
@@ -179,6 +185,15 @@ func composeIssueCommentMessage(issue *Issue, doer *User, content string, commen
179185

180186
msg := mailer.NewMessageFrom(tos, doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
181187
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
188+
189+
// Set Message-ID on first message so replies know what to reference
190+
if comment == nil {
191+
msg.SetHeader("Message-ID", "<"+issue.ReplyReference()+">")
192+
} else {
193+
msg.SetHeader("In-Reply-To", "<"+issue.ReplyReference()+">")
194+
msg.SetHeader("References", "<"+issue.ReplyReference()+">")
195+
}
196+
182197
return msg
183198
}
184199

models/mail_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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+
5+
package models
6+
7+
import (
8+
"html/template"
9+
"testing"
10+
11+
"code.gitea.io/gitea/modules/setting"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
const tmpl = `
17+
<!DOCTYPE html>
18+
<html>
19+
<head>
20+
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
21+
<title>{{.Subject}}</title>
22+
</head>
23+
24+
<body>
25+
<p>{{.Body}}</p>
26+
<p>
27+
---
28+
<br>
29+
<a href="{{.Link}}">View it on Gitea</a>.
30+
</p>
31+
</body>
32+
</html>
33+
`
34+
35+
func TestComposeIssueCommentMessage(t *testing.T) {
36+
assert.NoError(t, PrepareTestDatabase())
37+
var MailService setting.Mailer
38+
39+
MailService.From = "[email protected]"
40+
setting.MailService = &MailService
41+
42+
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
43+
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, Owner: doer}).(*Repository)
44+
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1, Repo: repo, Poster: doer}).(*Issue)
45+
comment := AssertExistsAndLoadBean(t, &Comment{ID: 2, Issue: issue}).(*Comment)
46+
47+
email := template.Must(template.New("issue/comment").Parse(tmpl))
48+
InitMailRender(email)
49+
50+
tos := []string{"[email protected]", "[email protected]"}
51+
msg := composeIssueCommentMessage(issue, doer, "test body", comment, mailIssueComment, tos, "issue comment")
52+
53+
subject := msg.GetHeader("Subject")
54+
inreplyTo := msg.GetHeader("In-Reply-To")
55+
references := msg.GetHeader("References")
56+
57+
assert.Equal(t, subject[0], "Re: "+issue.mailSubject(), "Comment reply subject should contain Re:")
58+
assert.Equal(t, inreplyTo[0], "<user2/repo1/issues/1@localhost>", "In-Reply-To header doesn't match")
59+
assert.Equal(t, references[0], "<user2/repo1/issues/1@localhost>", "References header doesn't match")
60+
61+
}
62+
63+
func TestComposeIssueMessage(t *testing.T) {
64+
assert.NoError(t, PrepareTestDatabase())
65+
var MailService setting.Mailer
66+
67+
MailService.From = "[email protected]"
68+
setting.MailService = &MailService
69+
70+
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
71+
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1, Owner: doer}).(*Repository)
72+
issue := AssertExistsAndLoadBean(t, &Issue{ID: 1, Repo: repo, Poster: doer}).(*Issue)
73+
74+
email := template.Must(template.New("issue/comment").Parse(tmpl))
75+
InitMailRender(email)
76+
77+
tos := []string{"[email protected]", "[email protected]"}
78+
msg := composeIssueCommentMessage(issue, doer, "test body", nil, mailIssueComment, tos, "issue create")
79+
80+
subject := msg.GetHeader("Subject")
81+
messageID := msg.GetHeader("Message-ID")
82+
83+
assert.Equal(t, subject[0], issue.mailSubject(), "Subject not equal to issue.mailSubject()")
84+
assert.Nil(t, msg.GetHeader("In-Reply-To"))
85+
assert.Nil(t, msg.GetHeader("References"))
86+
assert.Equal(t, messageID[0], "<user2/repo1/issues/1@localhost>", "Message-ID header doesn't match")
87+
}

0 commit comments

Comments
 (0)