You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

mail_test.go 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package mailer
  4. import (
  5. "bytes"
  6. "context"
  7. "fmt"
  8. "html/template"
  9. "io"
  10. "mime/quotedprintable"
  11. "regexp"
  12. "strings"
  13. "testing"
  14. texttmpl "text/template"
  15. activities_model "code.gitea.io/gitea/models/activities"
  16. "code.gitea.io/gitea/models/db"
  17. issues_model "code.gitea.io/gitea/models/issues"
  18. repo_model "code.gitea.io/gitea/models/repo"
  19. "code.gitea.io/gitea/models/unittest"
  20. user_model "code.gitea.io/gitea/models/user"
  21. "code.gitea.io/gitea/modules/markup"
  22. "code.gitea.io/gitea/modules/setting"
  23. "github.com/stretchr/testify/assert"
  24. )
  25. const subjectTpl = `
  26. {{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
  27. `
  28. const bodyTpl = `
  29. <!DOCTYPE html>
  30. <html>
  31. <head>
  32. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  33. <title>{{.Subject}}</title>
  34. </head>
  35. <body>
  36. <p>{{.Body}}</p>
  37. <p>
  38. ---
  39. <br>
  40. <a href="{{.Link}}">View it on Gitea</a>.
  41. </p>
  42. </body>
  43. </html>
  44. `
  45. func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
  46. assert.NoError(t, unittest.PrepareTestDatabase())
  47. mailService := setting.Mailer{
  48. From: "test@gitea.com",
  49. }
  50. setting.MailService = &mailService
  51. setting.Domain = "localhost"
  52. doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  53. repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer})
  54. issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer})
  55. assert.NoError(t, issue.LoadRepo(db.DefaultContext))
  56. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue})
  57. return doer, repo, issue, comment
  58. }
  59. func TestComposeIssueCommentMessage(t *testing.T) {
  60. doer, _, issue, comment := prepareMailerTest(t)
  61. markup.Init(&markup.ProcessorHelper{
  62. IsUsernameMentionable: func(ctx context.Context, username string) bool {
  63. return username == doer.Name
  64. },
  65. })
  66. setting.IncomingEmail.Enabled = true
  67. defer func() { setting.IncomingEmail.Enabled = false }()
  68. subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
  69. bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
  70. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
  71. msgs, err := composeIssueCommentMessages(&mailCommentContext{
  72. Context: context.TODO(), // TODO: use a correct context
  73. Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
  74. Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
  75. Comment: comment,
  76. }, "en-US", recipients, false, "issue comment")
  77. assert.NoError(t, err)
  78. assert.Len(t, msgs, 2)
  79. gomailMsg := msgs[0].ToMessage()
  80. replyTo := gomailMsg.GetHeader("Reply-To")[0]
  81. subject := gomailMsg.GetHeader("Subject")[0]
  82. assert.Len(t, gomailMsg.GetHeader("To"), 1, "exactly one recipient is expected in the To field")
  83. tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`)
  84. assert.Regexp(t, tokenRegex, replyTo)
  85. token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1]
  86. assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:")
  87. assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject)
  88. assert.Equal(t, "<user2/repo1/issues/1@localhost>", gomailMsg.GetHeader("In-Reply-To")[0], "In-Reply-To header doesn't match")
  89. assert.ElementsMatch(t, []string{"<user2/repo1/issues/1@localhost>", "<reply-" + token + "@localhost>"}, gomailMsg.GetHeader("References"), "References header doesn't match")
  90. assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetHeader("Message-ID")[0], "Message-ID header doesn't match")
  91. assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetHeader("List-Post")[0])
  92. assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 2) // url + mailto
  93. var buf bytes.Buffer
  94. gomailMsg.WriteTo(&buf)
  95. b, err := io.ReadAll(quotedprintable.NewReader(&buf))
  96. assert.NoError(t, err)
  97. // text/plain
  98. assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL()))
  99. assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL()))
  100. // text/html
  101. assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL()))
  102. assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL()))
  103. }
  104. func TestComposeIssueMessage(t *testing.T) {
  105. doer, _, issue, _ := prepareMailerTest(t)
  106. subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
  107. bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
  108. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
  109. msgs, err := composeIssueCommentMessages(&mailCommentContext{
  110. Context: context.TODO(), // TODO: use a correct context
  111. Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
  112. Content: "test body",
  113. }, "en-US", recipients, false, "issue create")
  114. assert.NoError(t, err)
  115. assert.Len(t, msgs, 2)
  116. gomailMsg := msgs[0].ToMessage()
  117. mailto := gomailMsg.GetHeader("To")
  118. subject := gomailMsg.GetHeader("Subject")
  119. messageID := gomailMsg.GetHeader("Message-ID")
  120. inReplyTo := gomailMsg.GetHeader("In-Reply-To")
  121. references := gomailMsg.GetHeader("References")
  122. assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
  123. assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
  124. assert.Equal(t, "<user2/repo1/issues/1@localhost>", inReplyTo[0], "In-Reply-To header doesn't match")
  125. assert.Equal(t, "<user2/repo1/issues/1@localhost>", references[0], "References header doesn't match")
  126. assert.Equal(t, "<user2/repo1/issues/1@localhost>", messageID[0], "Message-ID header doesn't match")
  127. assert.Empty(t, gomailMsg.GetHeader("List-Post")) // incoming mail feature disabled
  128. assert.Len(t, gomailMsg.GetHeader("List-Unsubscribe"), 1) // url without mailto
  129. }
  130. func TestTemplateSelection(t *testing.T) {
  131. doer, repo, issue, comment := prepareMailerTest(t)
  132. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
  133. subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
  134. texttmpl.Must(subjectTemplates.New("issue/new").Parse("issue/new/subject"))
  135. texttmpl.Must(subjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
  136. texttmpl.Must(subjectTemplates.New("issue/close").Parse("")) // Must default to fallback subject
  137. bodyTemplates = template.Must(template.New("issue/default").Parse("issue/default/body"))
  138. template.Must(bodyTemplates.New("issue/new").Parse("issue/new/body"))
  139. template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body"))
  140. template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body"))
  141. expect := func(t *testing.T, msg *Message, expSubject, expBody string) {
  142. subject := msg.ToMessage().GetHeader("Subject")
  143. msgbuf := new(bytes.Buffer)
  144. _, _ = msg.ToMessage().WriteTo(msgbuf)
  145. wholemsg := msgbuf.String()
  146. assert.Equal(t, []string{expSubject}, subject)
  147. assert.Contains(t, wholemsg, expBody)
  148. }
  149. msg := testComposeIssueCommentMessage(t, &mailCommentContext{
  150. Context: context.TODO(), // TODO: use a correct context
  151. Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
  152. Content: "test body",
  153. }, recipients, false, "TestTemplateSelection")
  154. expect(t, msg, "issue/new/subject", "issue/new/body")
  155. msg = testComposeIssueCommentMessage(t, &mailCommentContext{
  156. Context: context.TODO(), // TODO: use a correct context
  157. Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
  158. Content: "test body", Comment: comment,
  159. }, recipients, false, "TestTemplateSelection")
  160. expect(t, msg, "issue/default/subject", "issue/default/body")
  161. pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
  162. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
  163. msg = testComposeIssueCommentMessage(t, &mailCommentContext{
  164. Context: context.TODO(), // TODO: use a correct context
  165. Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
  166. Content: "test body", Comment: comment,
  167. }, recipients, false, "TestTemplateSelection")
  168. expect(t, msg, "pull/comment/subject", "pull/comment/body")
  169. msg = testComposeIssueCommentMessage(t, &mailCommentContext{
  170. Context: context.TODO(), // TODO: use a correct context
  171. Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
  172. Content: "test body", Comment: comment,
  173. }, recipients, false, "TestTemplateSelection")
  174. expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "issue/close/body")
  175. }
  176. func TestTemplateServices(t *testing.T) {
  177. doer, _, issue, comment := prepareMailerTest(t)
  178. assert.NoError(t, issue.LoadRepo(db.DefaultContext))
  179. expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
  180. actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
  181. ) {
  182. subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
  183. bodyTemplates = template.Must(template.New("issue/default").Parse(tplBody))
  184. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
  185. msg := testComposeIssueCommentMessage(t, &mailCommentContext{
  186. Context: context.TODO(), // TODO: use a correct context
  187. Issue: issue, Doer: doer, ActionType: actionType,
  188. Content: "test body", Comment: comment,
  189. }, recipients, fromMention, "TestTemplateServices")
  190. subject := msg.ToMessage().GetHeader("Subject")
  191. msgbuf := new(bytes.Buffer)
  192. _, _ = msg.ToMessage().WriteTo(msgbuf)
  193. wholemsg := msgbuf.String()
  194. assert.Equal(t, []string{expSubject}, subject)
  195. assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
  196. }
  197. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, false,
  198. "{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
  199. "//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
  200. "Re: [user2/repo1]: @user2 commented on #1 - issue1",
  201. "//issue,comment,//")
  202. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
  203. "{{if .IsMention}}must render{{end}}",
  204. "//subject is: {{.Subject}}//",
  205. "must render",
  206. "//subject is: must render//")
  207. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
  208. "{{.FallbackSubject}}",
  209. "//{{.SubjectPrefix}}//",
  210. "Re: [user2/repo1] issue1 (#1)",
  211. "//Re: //")
  212. }
  213. func testComposeIssueCommentMessage(t *testing.T, ctx *mailCommentContext, recipients []*user_model.User, fromMention bool, info string) *Message {
  214. msgs, err := composeIssueCommentMessages(ctx, "en-US", recipients, fromMention, info)
  215. assert.NoError(t, err)
  216. assert.Len(t, msgs, 1)
  217. return msgs[0]
  218. }
  219. func TestGenerateAdditionalHeaders(t *testing.T) {
  220. doer, _, issue, _ := prepareMailerTest(t)
  221. ctx := &mailCommentContext{Context: context.TODO() /* TODO: use a correct context */, Issue: issue, Doer: doer}
  222. recipient := &user_model.User{Name: "test", Email: "test@gitea.com"}
  223. headers := generateAdditionalHeaders(ctx, "dummy-reason", recipient)
  224. expected := map[string]string{
  225. "List-ID": "user2/repo1 <repo1.user2.localhost>",
  226. "List-Archive": "<https://try.gitea.io/user2/repo1>",
  227. "X-Gitea-Reason": "dummy-reason",
  228. "X-Gitea-Sender": "user2",
  229. "X-Gitea-Recipient": "test",
  230. "X-Gitea-Recipient-Address": "test@gitea.com",
  231. "X-Gitea-Repository": "repo1",
  232. "X-Gitea-Repository-Path": "user2/repo1",
  233. "X-Gitea-Repository-Link": "https://try.gitea.io/user2/repo1",
  234. "X-Gitea-Issue-ID": "1",
  235. "X-Gitea-Issue-Link": "https://try.gitea.io/user2/repo1/issues/1",
  236. }
  237. for key, value := range expected {
  238. if assert.Contains(t, headers, key) {
  239. assert.Equal(t, value, headers[key])
  240. }
  241. }
  242. }
  243. func TestGenerateMessageIDForIssue(t *testing.T) {
  244. _, _, issue, comment := prepareMailerTest(t)
  245. _, _, pullIssue, _ := prepareMailerTest(t)
  246. pullIssue.IsPull = true
  247. type args struct {
  248. issue *issues_model.Issue
  249. comment *issues_model.Comment
  250. actionType activities_model.ActionType
  251. }
  252. tests := []struct {
  253. name string
  254. args args
  255. prefix string
  256. }{
  257. {
  258. name: "Open Issue",
  259. args: args{
  260. issue: issue,
  261. actionType: activities_model.ActionCreateIssue,
  262. },
  263. prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
  264. },
  265. {
  266. name: "Open Pull",
  267. args: args{
  268. issue: pullIssue,
  269. actionType: activities_model.ActionCreatePullRequest,
  270. },
  271. prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
  272. },
  273. {
  274. name: "Comment Issue",
  275. args: args{
  276. issue: issue,
  277. comment: comment,
  278. actionType: activities_model.ActionCommentIssue,
  279. },
  280. prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
  281. },
  282. {
  283. name: "Comment Pull",
  284. args: args{
  285. issue: pullIssue,
  286. comment: comment,
  287. actionType: activities_model.ActionCommentPull,
  288. },
  289. prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
  290. },
  291. {
  292. name: "Close Issue",
  293. args: args{
  294. issue: issue,
  295. actionType: activities_model.ActionCloseIssue,
  296. },
  297. prefix: fmt.Sprintf("<%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
  298. },
  299. {
  300. name: "Close Pull",
  301. args: args{
  302. issue: pullIssue,
  303. actionType: activities_model.ActionClosePullRequest,
  304. },
  305. prefix: fmt.Sprintf("<%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
  306. },
  307. {
  308. name: "Reopen Issue",
  309. args: args{
  310. issue: issue,
  311. actionType: activities_model.ActionReopenIssue,
  312. },
  313. prefix: fmt.Sprintf("<%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
  314. },
  315. {
  316. name: "Reopen Pull",
  317. args: args{
  318. issue: pullIssue,
  319. actionType: activities_model.ActionReopenPullRequest,
  320. },
  321. prefix: fmt.Sprintf("<%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
  322. },
  323. {
  324. name: "Merge Pull",
  325. args: args{
  326. issue: pullIssue,
  327. actionType: activities_model.ActionMergePullRequest,
  328. },
  329. prefix: fmt.Sprintf("<%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
  330. },
  331. {
  332. name: "Ready Pull",
  333. args: args{
  334. issue: pullIssue,
  335. actionType: activities_model.ActionPullRequestReadyForReview,
  336. },
  337. prefix: fmt.Sprintf("<%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
  338. },
  339. }
  340. for _, tt := range tests {
  341. t.Run(tt.name, func(t *testing.T) {
  342. got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType)
  343. if !strings.HasPrefix(got, tt.prefix) {
  344. t.Errorf("generateMessageIDForIssue() = %v, want %v", got, tt.prefix)
  345. }
  346. })
  347. }
  348. }
  349. func TestGenerateMessageIDForRelease(t *testing.T) {
  350. msgID := generateMessageIDForRelease(&repo_model.Release{
  351. ID: 1,
  352. Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"},
  353. })
  354. assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
  355. }