Skip to content

Commit 1ab44cb

Browse files
authored
Prevent merge messages from being sorted to the top of email chains (#18566)
* Prevent merge messages from being sorted to the top of email chains Gitea will currrently resend the same message-id for the closed/merged/reopened messages for issues. This will cause the merged message to leap to the top of an email chain and become out of sync. This PR adds specific suffices for these actions. Fix #18560 Signed-off-by: Andrew Thornton <[email protected]> * add test Signed-off-by: Andrew Thornton <[email protected]>
1 parent 9f9ca0a commit 1ab44cb

File tree

2 files changed

+131
-4
lines changed

2 files changed

+131
-4
lines changed

services/mailer/mail.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"strconv"
1515
"strings"
1616
texttmpl "text/template"
17+
"time"
1718

1819
"code.gitea.io/gitea/models"
1920
repo_model "code.gitea.io/gitea/models/repo"
@@ -298,13 +299,15 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
298299
}
299300

300301
// Make sure to compose independent messages to avoid leaking user emails
302+
msgID := createReference(ctx.Issue, ctx.Comment, ctx.ActionType)
303+
reference := createReference(ctx.Issue, nil, models.ActionType(0))
304+
301305
msgs := make([]*Message, 0, len(recipients))
302306
for _, recipient := range recipients {
303307
msg := NewMessageFrom([]string{recipient.Email}, ctx.Doer.DisplayName(), setting.MailService.FromEmail, subject, mailBody.String())
304308
msg.Info = fmt.Sprintf("Subject: %s, %s", subject, info)
305309

306-
msg.SetHeader("Message-ID", "<"+createReference(ctx.Issue, ctx.Comment)+">")
307-
reference := createReference(ctx.Issue, nil)
310+
msg.SetHeader("Message-ID", "<"+msgID+">")
308311
msg.SetHeader("In-Reply-To", "<"+reference+">")
309312
msg.SetHeader("References", "<"+reference+">")
310313

@@ -318,7 +321,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient
318321
return msgs, nil
319322
}
320323

321-
func createReference(issue *models.Issue, comment *models.Comment) string {
324+
func createReference(issue *models.Issue, comment *models.Comment, actionType models.ActionType) string {
322325
var path string
323326
if issue.IsPull {
324327
path = "pulls"
@@ -329,6 +332,17 @@ func createReference(issue *models.Issue, comment *models.Comment) string {
329332
var extra string
330333
if comment != nil {
331334
extra = fmt.Sprintf("/comment/%d", comment.ID)
335+
} else {
336+
switch actionType {
337+
case models.ActionCloseIssue, models.ActionClosePullRequest:
338+
extra = fmt.Sprintf("/close/%d", time.Now().UnixNano()/1e6)
339+
case models.ActionReopenIssue, models.ActionReopenPullRequest:
340+
extra = fmt.Sprintf("/reopen/%d", time.Now().UnixNano()/1e6)
341+
case models.ActionMergePullRequest:
342+
extra = fmt.Sprintf("/merge/%d", time.Now().UnixNano()/1e6)
343+
case models.ActionPullRequestReadyForReview:
344+
extra = fmt.Sprintf("/ready/%d", time.Now().UnixNano()/1e6)
345+
}
332346
}
333347

334348
return fmt.Sprintf("%s/%s/%d%s@%s", issue.Repo.FullName(), path, issue.Index, extra, setting.Domain)

services/mailer/mail_test.go

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package mailer
66

77
import (
88
"bytes"
9+
"fmt"
910
"html/template"
11+
"strings"
1012
"testing"
1113
texttmpl "text/template"
1214

@@ -15,7 +17,6 @@ import (
1517
"code.gitea.io/gitea/models/unittest"
1618
user_model "code.gitea.io/gitea/models/user"
1719
"code.gitea.io/gitea/modules/setting"
18-
1920
"github.com/stretchr/testify/assert"
2021
)
2122

@@ -250,3 +251,115 @@ func TestGenerateAdditionalHeaders(t *testing.T) {
250251
}
251252
}
252253
}
254+
255+
func Test_createReference(t *testing.T) {
256+
_, _, issue, comment := prepareMailerTest(t)
257+
_, _, pullIssue, _ := prepareMailerTest(t)
258+
pullIssue.IsPull = true
259+
260+
type args struct {
261+
issue *models.Issue
262+
comment *models.Comment
263+
actionType models.ActionType
264+
}
265+
tests := []struct {
266+
name string
267+
args args
268+
prefix string
269+
suffix string
270+
}{
271+
{
272+
name: "Open Issue",
273+
args: args{
274+
issue: issue,
275+
actionType: models.ActionCreateIssue,
276+
},
277+
prefix: fmt.Sprintf("%s/issues/%d@%s", issue.Repo.FullName(), issue.Index, setting.Domain),
278+
},
279+
{
280+
name: "Open Pull",
281+
args: args{
282+
issue: pullIssue,
283+
actionType: models.ActionCreatePullRequest,
284+
},
285+
prefix: fmt.Sprintf("%s/pulls/%d@%s", issue.Repo.FullName(), issue.Index, setting.Domain),
286+
},
287+
{
288+
name: "Comment Issue",
289+
args: args{
290+
issue: issue,
291+
comment: comment,
292+
actionType: models.ActionCommentIssue,
293+
},
294+
prefix: fmt.Sprintf("%s/issues/%d/comment/%d@%s", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
295+
},
296+
{
297+
name: "Comment Pull",
298+
args: args{
299+
issue: pullIssue,
300+
comment: comment,
301+
actionType: models.ActionCommentPull,
302+
},
303+
prefix: fmt.Sprintf("%s/pulls/%d/comment/%d@%s", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
304+
},
305+
{
306+
name: "Close Issue",
307+
args: args{
308+
issue: issue,
309+
actionType: models.ActionCloseIssue,
310+
},
311+
prefix: fmt.Sprintf("%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
312+
},
313+
{
314+
name: "Close Pull",
315+
args: args{
316+
issue: pullIssue,
317+
actionType: models.ActionClosePullRequest,
318+
},
319+
prefix: fmt.Sprintf("%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
320+
},
321+
{
322+
name: "Reopen Issue",
323+
args: args{
324+
issue: issue,
325+
actionType: models.ActionReopenIssue,
326+
},
327+
prefix: fmt.Sprintf("%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
328+
},
329+
{
330+
name: "Reopen Pull",
331+
args: args{
332+
issue: pullIssue,
333+
actionType: models.ActionReopenPullRequest,
334+
},
335+
prefix: fmt.Sprintf("%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
336+
},
337+
{
338+
name: "Merge Pull",
339+
args: args{
340+
issue: pullIssue,
341+
actionType: models.ActionMergePullRequest,
342+
},
343+
prefix: fmt.Sprintf("%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
344+
},
345+
{
346+
name: "Ready Pull",
347+
args: args{
348+
issue: pullIssue,
349+
actionType: models.ActionPullRequestReadyForReview,
350+
},
351+
prefix: fmt.Sprintf("%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
352+
},
353+
}
354+
for _, tt := range tests {
355+
t.Run(tt.name, func(t *testing.T) {
356+
got := createReference(tt.args.issue, tt.args.comment, tt.args.actionType)
357+
if !strings.HasPrefix(got, tt.prefix) {
358+
t.Errorf("createReference() = %v, want %v", got, tt.prefix)
359+
}
360+
if !strings.HasSuffix(got, tt.suffix) {
361+
t.Errorf("createReference() = %v, want %v", got, tt.prefix)
362+
}
363+
})
364+
}
365+
}

0 commit comments

Comments
 (0)