Skip to content

Commit 77fa714

Browse files
KN4CK3Rlunny
andauthored
Add email headers (#15939)
* Added additional email headers. * Added tests. Co-authored-by: Lunny Xiao <[email protected]>
1 parent be745be commit 77fa714

File tree

3 files changed

+104
-61
lines changed

3 files changed

+104
-61
lines changed

services/mailer/mail.go

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"html/template"
1212
"mime"
1313
"regexp"
14+
"strconv"
1415
"strings"
1516
texttmpl "text/template"
1617

@@ -174,7 +175,7 @@ func SendCollaboratorMail(u, doer *models.User, repo *models.Repository) {
174175
SendAsync(msg)
175176
}
176177

177-
func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []string, fromMention bool, info string) ([]*Message, error) {
178+
func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipients []*models.User, fromMention bool, info string) ([]*Message, error) {
178179
var (
179180
subject string
180181
link string
@@ -265,9 +266,9 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str
265266
}
266267

267268
// Make sure to compose independent messages to avoid leaking user emails
268-
msgs := make([]*Message, 0, len(tos))
269-
for _, to := range tos {
270-
msg := NewMessageFrom([]string{to}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
269+
msgs := make([]*Message, 0, len(recipients))
270+
for _, recipient := range recipients {
271+
msg := NewMessageFrom([]string{recipient.Email}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
271272
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
272273

273274
// Set Message-ID on first message so replies know what to reference
@@ -277,12 +278,51 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, tos []str
277278
msg.SetHeader("In-Reply-To", "<"+ctx.Issue.ReplyReference()+">")
278279
msg.SetHeader("References", "<"+ctx.Issue.ReplyReference()+">")
279280
}
281+
282+
for key, value := range generateAdditionalHeaders(ctx, actType, recipient) {
283+
msg.SetHeader(key, value)
284+
}
285+
280286
msgs = append(msgs, msg)
281287
}
282288

283289
return msgs, nil
284290
}
285291

292+
func generateAdditionalHeaders(ctx *mailCommentContext, reason string, recipient *models.User) map[string]string {
293+
repo := ctx.Issue.Repo
294+
295+
return map[string]string{
296+
// https://datatracker.ietf.org/doc/html/rfc2919
297+
"List-ID": fmt.Sprintf("%s <%s.%s.%s>", repo.FullName(), repo.Name, repo.OwnerName, setting.Domain),
298+
299+
// https://datatracker.ietf.org/doc/html/rfc2369
300+
"List-Archive": fmt.Sprintf("<%s>", repo.HTMLURL()),
301+
//"List-Post": https://github.com/go-gitea/gitea/pull/13585
302+
//"List-Unsubscribe": https://github.com/go-gitea/gitea/issues/10808, https://github.com/go-gitea/gitea/issues/13283
303+
304+
"X-Gitea-Reason": reason,
305+
"X-Gitea-Sender": ctx.Doer.DisplayName(),
306+
"X-Gitea-Recipient": recipient.DisplayName(),
307+
"X-Gitea-Recipient-Address": recipient.Email,
308+
"X-Gitea-Repository": repo.Name,
309+
"X-Gitea-Repository-Path": repo.FullName(),
310+
"X-Gitea-Repository-Link": repo.HTMLURL(),
311+
"X-Gitea-Issue-ID": strconv.FormatInt(ctx.Issue.Index, 10),
312+
"X-Gitea-Issue-Link": ctx.Issue.HTMLURL(),
313+
314+
"X-GitHub-Reason": reason,
315+
"X-GitHub-Sender": ctx.Doer.DisplayName(),
316+
"X-GitHub-Recipient": recipient.DisplayName(),
317+
"X-GitHub-Recipient-Address": recipient.Email,
318+
319+
"X-GitLab-NotificationReason": reason,
320+
"X-GitLab-Project": repo.Name,
321+
"X-GitLab-Project-Path": repo.FullName(),
322+
"X-GitLab-Issue-IID": strconv.FormatInt(ctx.Issue.Index, 10),
323+
}
324+
}
325+
286326
func sanitizeSubject(subject string) string {
287327
runes := []rune(strings.TrimSpace(subjectRemoveSpaces.ReplaceAllLiteralString(subject, " ")))
288328
if len(runes) > mailMaxSubjectRunes {
@@ -294,9 +334,9 @@ func sanitizeSubject(subject string) string {
294334

295335
// SendIssueAssignedMail composes and sends issue assigned email
296336
func SendIssueAssignedMail(issue *models.Issue, doer *models.User, content string, comment *models.Comment, recipients []*models.User) error {
297-
langMap := make(map[string][]string)
337+
langMap := make(map[string][]*models.User)
298338
for _, user := range recipients {
299-
langMap[user.Language] = append(langMap[user.Language], user.Email)
339+
langMap[user.Language] = append(langMap[user.Language], user)
300340
}
301341

302342
for lang, tos := range langMap {

services/mailer/mail_issue.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visite
116116
checkUnit = models.UnitTypePullRequests
117117
}
118118

119-
langMap := make(map[string][]string)
119+
langMap := make(map[string][]*models.User)
120120
for _, user := range users {
121121
// At this point we exclude:
122122
// user that don't have all mails enabled or users only get mail on mention and this is one ...
@@ -138,7 +138,7 @@ func mailIssueCommentBatch(ctx *mailCommentContext, users []*models.User, visite
138138
continue
139139
}
140140

141-
langMap[user.Language] = append(langMap[user.Language], user.Email)
141+
langMap[user.Language] = append(langMap[user.Language], user)
142142
}
143143

144144
for lang, receivers := range langMap {

services/mailer/mail_test.go

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const bodyTpl = `
3939
</html>
4040
`
4141

42-
func TestComposeIssueCommentMessage(t *testing.T) {
42+
func prepareMailerTest(t *testing.T) (doer *models.User, repo *models.Repository, issue *models.Issue, comment *models.Comment) {
4343
assert.NoError(t, models.PrepareTestDatabase())
4444
var mailService = setting.Mailer{
4545
@@ -48,18 +48,24 @@ func TestComposeIssueCommentMessage(t *testing.T) {
4848
setting.MailService = &mailService
4949
setting.Domain = "localhost"
5050

51-
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
52-
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
53-
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
54-
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
51+
doer = models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
52+
repo = models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
53+
issue = models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
54+
assert.NoError(t, issue.LoadRepo())
55+
comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
56+
return
57+
}
58+
59+
func TestComposeIssueCommentMessage(t *testing.T) {
60+
doer, _, issue, comment := prepareMailerTest(t)
5561

5662
stpl := texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
5763
btpl := template.Must(template.New("issue/comment").Parse(bodyTpl))
5864
InitMailRender(stpl, btpl)
5965

60-
tos := []string{"[email protected]", "[email protected]"}
66+
recipients := []*models.User{{Name: "Test", Email: "[email protected]"}, {Name: "Test2", Email: "[email protected]"}}
6167
msgs, err := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
62-
Content: "test body", Comment: comment}, "en-US", tos, false, "issue comment")
68+
Content: "test body", Comment: comment}, "en-US", recipients, false, "issue comment")
6369
assert.NoError(t, err)
6470
assert.Len(t, msgs, 2)
6571
gomailMsg := msgs[0].ToMessage()
@@ -76,25 +82,15 @@ func TestComposeIssueCommentMessage(t *testing.T) {
7682
}
7783

7884
func TestComposeIssueMessage(t *testing.T) {
79-
assert.NoError(t, models.PrepareTestDatabase())
80-
var mailService = setting.Mailer{
81-
82-
}
83-
84-
setting.MailService = &mailService
85-
setting.Domain = "localhost"
86-
87-
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
88-
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
89-
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
85+
doer, _, issue, _ := prepareMailerTest(t)
9086

9187
stpl := texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
9288
btpl := template.Must(template.New("issue/new").Parse(bodyTpl))
9389
InitMailRender(stpl, btpl)
9490

95-
tos := []string{"[email protected]", "[email protected]"}
91+
recipients := []*models.User{{Name: "Test", Email: "[email protected]"}, {Name: "Test2", Email: "[email protected]"}}
9692
msgs, err := composeIssueCommentMessages(&mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
97-
Content: "test body"}, "en-US", tos, false, "issue create")
93+
Content: "test body"}, "en-US", recipients, false, "issue create")
9894
assert.NoError(t, err)
9995
assert.Len(t, msgs, 2)
10096

@@ -111,18 +107,8 @@ func TestComposeIssueMessage(t *testing.T) {
111107
}
112108

113109
func TestTemplateSelection(t *testing.T) {
114-
assert.NoError(t, models.PrepareTestDatabase())
115-
var mailService = setting.Mailer{
116-
117-
}
118-
119-
setting.MailService = &mailService
120-
setting.Domain = "localhost"
121-
122-
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
123-
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
124-
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
125-
tos := []string{"[email protected]"}
110+
doer, repo, issue, comment := prepareMailerTest(t)
111+
recipients := []*models.User{{Name: "Test", Email: "[email protected]"}}
126112

127113
stpl := texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
128114
texttmpl.Must(stpl.New("issue/new").Parse("issue/new/subject"))
@@ -146,38 +132,26 @@ func TestTemplateSelection(t *testing.T) {
146132
}
147133

148134
msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCreateIssue,
149-
Content: "test body"}, tos, false, "TestTemplateSelection")
135+
Content: "test body"}, recipients, false, "TestTemplateSelection")
150136
expect(t, msg, "issue/new/subject", "issue/new/body")
151137

152-
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
153138
msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCommentIssue,
154-
Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
139+
Content: "test body", Comment: comment}, recipients, false, "TestTemplateSelection")
155140
expect(t, msg, "issue/default/subject", "issue/default/body")
156141

157142
pull := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 2, Repo: repo, Poster: doer}).(*models.Issue)
158143
comment = models.AssertExistsAndLoadBean(t, &models.Comment{ID: 4, Issue: pull}).(*models.Comment)
159144
msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: pull, Doer: doer, ActionType: models.ActionCommentPull,
160-
Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
145+
Content: "test body", Comment: comment}, recipients, false, "TestTemplateSelection")
161146
expect(t, msg, "pull/comment/subject", "pull/comment/body")
162147

163148
msg = testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: models.ActionCloseIssue,
164-
Content: "test body", Comment: comment}, tos, false, "TestTemplateSelection")
149+
Content: "test body", Comment: comment}, recipients, false, "TestTemplateSelection")
165150
expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
166151
}
167152

168153
func TestTemplateServices(t *testing.T) {
169-
assert.NoError(t, models.PrepareTestDatabase())
170-
var mailService = setting.Mailer{
171-
172-
}
173-
174-
setting.MailService = &mailService
175-
setting.Domain = "localhost"
176-
177-
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
178-
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1, Owner: doer}).(*models.Repository)
179-
issue := models.AssertExistsAndLoadBean(t, &models.Issue{ID: 1, Repo: repo, Poster: doer}).(*models.Issue)
180-
comment := models.AssertExistsAndLoadBean(t, &models.Comment{ID: 2, Issue: issue}).(*models.Comment)
154+
doer, _, issue, comment := prepareMailerTest(t)
181155
assert.NoError(t, issue.LoadRepo())
182156

183157
expect := func(t *testing.T, issue *models.Issue, comment *models.Comment, doer *models.User,
@@ -187,9 +161,9 @@ func TestTemplateServices(t *testing.T) {
187161
btpl := template.Must(template.New("issue/default").Parse(tplBody))
188162
InitMailRender(stpl, btpl)
189163

190-
tos := []string{"[email protected]"}
164+
recipients := []*models.User{{Name: "Test", Email: "[email protected]"}}
191165
msg := testComposeIssueCommentMessage(t, &mailCommentContext{Issue: issue, Doer: doer, ActionType: actionType,
192-
Content: "test body", Comment: comment}, tos, fromMention, "TestTemplateServices")
166+
Content: "test body", Comment: comment}, recipients, fromMention, "TestTemplateServices")
193167

194168
subject := msg.ToMessage().GetHeader("Subject")
195169
msgbuf := new(bytes.Buffer)
@@ -219,9 +193,38 @@ func TestTemplateServices(t *testing.T) {
219193
"//Re: //")
220194
}
221195

222-
func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, tos []string, fromMention bool, info string) *Message {
223-
msgs, err := composeIssueCommentMessages(ctx, "en-US", tos, fromMention, info)
196+
func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*models.User, fromMention bool, info string) *Message {
197+
msgs, err := composeIssueCommentMessages(ctx, "en-US", recipients, fromMention, info)
224198
assert.NoError(t, err)
225199
assert.Len(t, msgs, 1)
226200
return msgs[0]
227201
}
202+
203+
func TestGenerateAdditionalHeaders(t *testing.T) {
204+
doer, _, issue, _ := prepareMailerTest(t)
205+
206+
ctx := &mailCommentContext{Issue: issue, Doer: doer}
207+
recipient := &models.User{Name: "Test", Email: "[email protected]"}
208+
209+
headers := generateAdditionalHeaders(ctx, "dummy-reason", recipient)
210+
211+
expected := map[string]string{
212+
"List-ID": "user2/repo1 <repo1.user2.localhost>",
213+
"List-Archive": "<https://try.gitea.io/user2/repo1>",
214+
"X-Gitea-Reason": "dummy-reason",
215+
"X-Gitea-Sender": "< U<se>r Tw<o > ><",
216+
"X-Gitea-Recipient": "Test",
217+
"X-Gitea-Recipient-Address": "[email protected]",
218+
"X-Gitea-Repository": "repo1",
219+
"X-Gitea-Repository-Path": "user2/repo1",
220+
"X-Gitea-Repository-Link": "https://try.gitea.io/user2/repo1",
221+
"X-Gitea-Issue-ID": "1",
222+
"X-Gitea-Issue-Link": "https://try.gitea.io/user2/repo1/issues/1",
223+
}
224+
225+
for key, value := range expected {
226+
if assert.Contains(t, headers, key) {
227+
assert.Equal(t, value, headers[key])
228+
}
229+
}
230+
}

0 commit comments

Comments
 (0)