gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568
  1. // Copyright 2019 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package mailer
  4. import (
  5. "bytes"
  6. "context"
  7. "encoding/base64"
  8. "fmt"
  9. "html/template"
  10. "io"
  11. "mime/quotedprintable"
  12. "regexp"
  13. "strings"
  14. "testing"
  15. texttmpl "text/template"
  16. actions_model "code.gitea.io/gitea/models/actions"
  17. activities_model "code.gitea.io/gitea/models/activities"
  18. issues_model "code.gitea.io/gitea/models/issues"
  19. repo_model "code.gitea.io/gitea/models/repo"
  20. "code.gitea.io/gitea/models/unittest"
  21. user_model "code.gitea.io/gitea/models/user"
  22. "code.gitea.io/gitea/modules/markup"
  23. "code.gitea.io/gitea/modules/setting"
  24. "code.gitea.io/gitea/modules/storage"
  25. "code.gitea.io/gitea/modules/templates"
  26. "code.gitea.io/gitea/modules/test"
  27. "code.gitea.io/gitea/services/attachment"
  28. sender_service "code.gitea.io/gitea/services/mailer/sender"
  29. "github.com/stretchr/testify/assert"
  30. "github.com/stretchr/testify/require"
  31. )
  32. const subjectTpl = `
  33. {{.SubjectPrefix}}[{{.Repo}}] @{{.Doer.Name}} #{{.Issue.Index}} - {{.Issue.Title}}
  34. `
  35. const bodyTpl = `
  36. <!DOCTYPE html>
  37. <html>
  38. <head>
  39. <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  40. <title>{{.Subject}}</title>
  41. </head>
  42. <body>
  43. <p>{{.Body}}</p>
  44. <p>
  45. ---
  46. <br>
  47. <a href="{{.Link}}">View it on Gitea</a>.
  48. </p>
  49. </body>
  50. </html>
  51. `
  52. func prepareMailerTest(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, comment *issues_model.Comment) {
  53. assert.NoError(t, unittest.PrepareTestDatabase())
  54. setting.MailService = &setting.Mailer{From: "test@gitea.com"}
  55. setting.Domain = "localhost"
  56. setting.AppURL = "https://try.gitea.io/"
  57. doer = unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
  58. repo = unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1, Owner: doer})
  59. issue = unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 1, Repo: repo, Poster: doer})
  60. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 2, Issue: issue})
  61. require.NoError(t, issue.LoadRepo(t.Context()))
  62. return doer, repo, issue, comment
  63. }
  64. func prepareMailerBase64Test(t *testing.T) (doer *user_model.User, repo *repo_model.Repository, issue *issues_model.Issue, att1, att2 *repo_model.Attachment) {
  65. user, repo, issue, comment := prepareMailerTest(t)
  66. setting.MailService.EmbedAttachmentImages = true
  67. att1, err := attachment.NewAttachment(t.Context(), &repo_model.Attachment{
  68. RepoID: repo.ID,
  69. IssueID: issue.ID,
  70. UploaderID: user.ID,
  71. CommentID: comment.ID,
  72. Name: "test.png",
  73. }, bytes.NewReader([]byte("\x89\x50\x4e\x47\x0d\x0a\x1a\x0a")), 8)
  74. require.NoError(t, err)
  75. att2, err = attachment.NewAttachment(t.Context(), &repo_model.Attachment{
  76. RepoID: repo.ID,
  77. IssueID: issue.ID,
  78. UploaderID: user.ID,
  79. CommentID: comment.ID,
  80. Name: "test.png",
  81. }, bytes.NewReader([]byte("\x89\x50\x4e\x47\x0d\x0a\x1a\x0a"+strings.Repeat("\x00", 1024))), 8+1024)
  82. require.NoError(t, err)
  83. return user, repo, issue, att1, att2
  84. }
  85. func prepareMailTemplates(name, subjectTmpl, bodyTmpl string) {
  86. loadedTemplates.Store(&templates.MailTemplates{
  87. SubjectTemplates: texttmpl.Must(texttmpl.New(name).Parse(subjectTmpl)),
  88. BodyTemplates: template.Must(template.New(name).Parse(bodyTmpl)),
  89. })
  90. }
  91. func TestComposeIssueComment(t *testing.T) {
  92. doer, _, issue, comment := prepareMailerTest(t)
  93. markup.Init(&markup.RenderHelperFuncs{
  94. IsUsernameMentionable: func(ctx context.Context, username string) bool {
  95. return username == doer.Name
  96. },
  97. })
  98. setting.IncomingEmail.Enabled = true
  99. defer func() { setting.IncomingEmail.Enabled = false }()
  100. prepareMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)
  101. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
  102. msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
  103. Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
  104. Content: fmt.Sprintf("test @%s %s#%d body", doer.Name, issue.Repo.FullName(), issue.Index),
  105. Comment: comment,
  106. }, "en-US", recipients, false, "issue comment")
  107. assert.NoError(t, err)
  108. assert.Len(t, msgs, 2)
  109. gomailMsg := msgs[0].ToMessage()
  110. replyTo := gomailMsg.GetGenHeader("Reply-To")[0]
  111. subject := gomailMsg.GetGenHeader("Subject")[0]
  112. assert.Len(t, gomailMsg.GetAddrHeader("To"), 1, "exactly one recipient is expected in the To field")
  113. tokenRegex := regexp.MustCompile(`\Aincoming\+(.+)@localhost\z`)
  114. assert.Regexp(t, tokenRegex, replyTo)
  115. token := tokenRegex.FindAllStringSubmatch(replyTo, 1)[0][1]
  116. assert.Equal(t, "Re: ", subject[:4], "Comment reply subject should contain Re:")
  117. assert.Equal(t, "Re: [user2/repo1] @user2 #1 - issue1", subject)
  118. assert.Equal(t, "<user2/repo1/issues/1@localhost>", gomailMsg.GetGenHeader("In-Reply-To")[0], "In-Reply-To header doesn't match")
  119. assert.ElementsMatch(t, []string{"<user2/repo1/issues/1@localhost>", "<reply-" + token + "@localhost>"}, gomailMsg.GetGenHeader("References"), "References header doesn't match")
  120. assert.Equal(t, "<user2/repo1/issues/1/comment/2@localhost>", gomailMsg.GetGenHeader("Message-ID")[0], "Message-ID header doesn't match")
  121. assert.Equal(t, "<mailto:"+replyTo+">", gomailMsg.GetGenHeader("List-Post")[0])
  122. assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 2) // url + mailto
  123. var buf bytes.Buffer
  124. _, err = gomailMsg.WriteTo(&buf)
  125. require.NoError(t, err)
  126. b, err := io.ReadAll(quotedprintable.NewReader(&buf))
  127. assert.NoError(t, err)
  128. // text/plain
  129. assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, doer.HTMLURL(t.Context())))
  130. assert.Contains(t, string(b), fmt.Sprintf(`( %s )`, issue.HTMLURL(t.Context())))
  131. // text/html
  132. assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, doer.HTMLURL(t.Context())))
  133. assert.Contains(t, string(b), fmt.Sprintf(`href="%s"`, issue.HTMLURL(t.Context())))
  134. }
  135. func TestMailMentionsComment(t *testing.T) {
  136. doer, _, issue, comment := prepareMailerTest(t)
  137. comment.Poster = doer
  138. prepareMailTemplates("repo/issue/comment", subjectTpl, bodyTpl)
  139. mails := 0
  140. defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
  141. mails = len(msgs)
  142. })()
  143. err := MailParticipantsComment(t.Context(), comment, activities_model.ActionCommentIssue, issue, []*user_model.User{})
  144. require.NoError(t, err)
  145. assert.Equal(t, 3, mails)
  146. }
  147. func TestComposeIssueMessage(t *testing.T) {
  148. doer, _, issue, _ := prepareMailerTest(t)
  149. prepareMailTemplates("repo/issue/new", subjectTpl, bodyTpl)
  150. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
  151. msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
  152. Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
  153. Content: "test body",
  154. }, "en-US", recipients, false, "issue create")
  155. assert.NoError(t, err)
  156. assert.Len(t, msgs, 2)
  157. gomailMsg := msgs[0].ToMessage()
  158. mailto := gomailMsg.GetAddrHeader("To")
  159. subject := gomailMsg.GetGenHeader("Subject")
  160. messageID := gomailMsg.GetGenHeader("Message-ID")
  161. inReplyTo := gomailMsg.GetGenHeader("In-Reply-To")
  162. references := gomailMsg.GetGenHeader("References")
  163. assert.Len(t, mailto, 1, "exactly one recipient is expected in the To field")
  164. assert.Equal(t, "[user2/repo1] @user2 #1 - issue1", subject[0])
  165. assert.Equal(t, "<user2/repo1/issues/1@localhost>", inReplyTo[0], "In-Reply-To header doesn't match")
  166. assert.Equal(t, "<user2/repo1/issues/1@localhost>", references[0], "References header doesn't match")
  167. assert.Equal(t, "<user2/repo1/issues/1@localhost>", messageID[0], "Message-ID header doesn't match")
  168. assert.Empty(t, gomailMsg.GetGenHeader("List-Post")) // incoming mail feature disabled
  169. assert.Len(t, gomailMsg.GetGenHeader("List-Unsubscribe"), 1) // url without mailto
  170. }
  171. func TestTemplateSelection(t *testing.T) {
  172. doer, repo, issue, comment := prepareMailerTest(t)
  173. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
  174. prepareMailTemplates("repo/issue/default", "repo/issue/default/subject", "repo/issue/default/body")
  175. texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/issue/new").Parse("repo/issue/new/subject"))
  176. texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/pull/comment").Parse("repo/pull/comment/subject"))
  177. texttmpl.Must(LoadedTemplates().SubjectTemplates.New("repo/issue/close").Parse("")) // Must default to a fallback subject
  178. template.Must(LoadedTemplates().BodyTemplates.New("repo/issue/new").Parse("repo/issue/new/body"))
  179. template.Must(LoadedTemplates().BodyTemplates.New("repo/pull/comment").Parse("repo/pull/comment/body"))
  180. template.Must(LoadedTemplates().BodyTemplates.New("repo/issue/close").Parse("repo/issue/close/body"))
  181. expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) {
  182. subject := msg.ToMessage().GetGenHeader("Subject")
  183. msgbuf := new(bytes.Buffer)
  184. _, _ = msg.ToMessage().WriteTo(msgbuf)
  185. wholemsg := msgbuf.String()
  186. assert.Equal(t, []string{expSubject}, subject)
  187. assert.Contains(t, wholemsg, expBody)
  188. }
  189. msg := testComposeIssueCommentMessage(t, &mailComment{
  190. Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
  191. Content: "test body",
  192. }, recipients, false, "TestTemplateSelection")
  193. expect(t, msg, "repo/issue/new/subject", "repo/issue/new/body")
  194. msg = testComposeIssueCommentMessage(t, &mailComment{
  195. Issue: issue, Doer: doer, ActionType: activities_model.ActionCommentIssue,
  196. Content: "test body", Comment: comment,
  197. }, recipients, false, "TestTemplateSelection")
  198. expect(t, msg, "repo/issue/default/subject", "repo/issue/default/body")
  199. pull := unittest.AssertExistsAndLoadBean(t, &issues_model.Issue{ID: 2, Repo: repo, Poster: doer})
  200. comment = unittest.AssertExistsAndLoadBean(t, &issues_model.Comment{ID: 4, Issue: pull})
  201. msg = testComposeIssueCommentMessage(t, &mailComment{
  202. Issue: pull, Doer: doer, ActionType: activities_model.ActionCommentPull,
  203. Content: "test body", Comment: comment,
  204. }, recipients, false, "TestTemplateSelection")
  205. expect(t, msg, "repo/pull/comment/subject", "repo/pull/comment/body")
  206. msg = testComposeIssueCommentMessage(t, &mailComment{
  207. Issue: issue, Doer: doer, ActionType: activities_model.ActionCloseIssue,
  208. Content: "test body", Comment: comment,
  209. }, recipients, false, "TestTemplateSelection")
  210. expect(t, msg, "Re: [user2/repo1] issue1 (#1)", "repo/issue/close/body")
  211. }
  212. func TestTemplateServices(t *testing.T) {
  213. doer, _, issue, comment := prepareMailerTest(t)
  214. assert.NoError(t, issue.LoadRepo(t.Context()))
  215. expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
  216. actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
  217. ) {
  218. prepareMailTemplates("repo/issue/default", tplSubject, tplBody)
  219. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
  220. msg := testComposeIssueCommentMessage(t, &mailComment{
  221. Issue: issue, Doer: doer, ActionType: actionType,
  222. Content: "test body", Comment: comment,
  223. }, recipients, fromMention, "TestTemplateServices")
  224. subject := msg.ToMessage().GetGenHeader("Subject")
  225. msgbuf := new(bytes.Buffer)
  226. _, _ = msg.ToMessage().WriteTo(msgbuf)
  227. wholemsg := msgbuf.String()
  228. assert.Equal(t, []string{expSubject}, subject)
  229. assert.Contains(t, wholemsg, "\r\n"+expBody+"\r\n")
  230. }
  231. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, false,
  232. "{{.SubjectPrefix}}[{{.Repo}}]: @{{.Doer.Name}} commented on #{{.Issue.Index}} - {{.Issue.Title}}",
  233. "//{{.ActionType}},{{.ActionName}},{{if .IsMention}}norender{{end}}//",
  234. "Re: [user2/repo1]: @user2 commented on #1 - issue1",
  235. "//issue,comment,//")
  236. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
  237. "{{if .IsMention}}must render{{end}}",
  238. "//subject is: {{.Subject}}//",
  239. "must render",
  240. "//subject is: must render//")
  241. expect(t, issue, comment, doer, activities_model.ActionCommentIssue, true,
  242. "{{.FallbackSubject}}",
  243. "//{{.SubjectPrefix}}//",
  244. "Re: [user2/repo1] issue1 (#1)",
  245. "//Re: //")
  246. }
  247. func testComposeIssueCommentMessage(t *testing.T, ctx *mailComment, recipients []*user_model.User, fromMention bool, info string) *sender_service.Message {
  248. msgs, err := composeIssueCommentMessages(t.Context(), ctx, "en-US", recipients, fromMention, info)
  249. assert.NoError(t, err)
  250. assert.Len(t, msgs, 1)
  251. return msgs[0]
  252. }
  253. func TestGenerateAdditionalHeadersForIssue(t *testing.T) {
  254. doer, _, issue, _ := prepareMailerTest(t)
  255. comment := &mailComment{Issue: issue, Doer: doer}
  256. recipient := &user_model.User{Name: "test", Email: "test@gitea.com"}
  257. headers := generateAdditionalHeadersForIssue(comment, "dummy-reason", recipient)
  258. expected := map[string]string{
  259. "List-ID": "user2/repo1 <repo1.user2.localhost>",
  260. "List-Archive": "<https://try.gitea.io/user2/repo1>",
  261. "X-Gitea-Reason": "dummy-reason",
  262. "X-Gitea-Sender": "user2",
  263. "X-Gitea-Recipient": "test",
  264. "X-Gitea-Recipient-Address": "test@gitea.com",
  265. "X-Gitea-Repository": "repo1",
  266. "X-Gitea-Repository-Path": "user2/repo1",
  267. "X-Gitea-Repository-Link": "https://try.gitea.io/user2/repo1",
  268. "X-Gitea-Issue-ID": "1",
  269. "X-Gitea-Issue-Link": "https://try.gitea.io/user2/repo1/issues/1",
  270. }
  271. for key, value := range expected {
  272. if assert.Contains(t, headers, key) {
  273. assert.Equal(t, value, headers[key])
  274. }
  275. }
  276. }
  277. func TestGenerateMessageIDForIssue(t *testing.T) {
  278. _, _, issue, comment := prepareMailerTest(t)
  279. _, _, pullIssue, _ := prepareMailerTest(t)
  280. pullIssue.IsPull = true
  281. type args struct {
  282. issue *issues_model.Issue
  283. comment *issues_model.Comment
  284. actionType activities_model.ActionType
  285. }
  286. tests := []struct {
  287. name string
  288. args args
  289. prefix string
  290. }{
  291. {
  292. name: "Open Issue",
  293. args: args{
  294. issue: issue,
  295. actionType: activities_model.ActionCreateIssue,
  296. },
  297. prefix: fmt.Sprintf("<%s/issues/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
  298. },
  299. {
  300. name: "Open Pull",
  301. args: args{
  302. issue: pullIssue,
  303. actionType: activities_model.ActionCreatePullRequest,
  304. },
  305. prefix: fmt.Sprintf("<%s/pulls/%d@%s>", issue.Repo.FullName(), issue.Index, setting.Domain),
  306. },
  307. {
  308. name: "Comment Issue",
  309. args: args{
  310. issue: issue,
  311. comment: comment,
  312. actionType: activities_model.ActionCommentIssue,
  313. },
  314. prefix: fmt.Sprintf("<%s/issues/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
  315. },
  316. {
  317. name: "Comment Pull",
  318. args: args{
  319. issue: pullIssue,
  320. comment: comment,
  321. actionType: activities_model.ActionCommentPull,
  322. },
  323. prefix: fmt.Sprintf("<%s/pulls/%d/comment/%d@%s>", issue.Repo.FullName(), issue.Index, comment.ID, setting.Domain),
  324. },
  325. {
  326. name: "Close Issue",
  327. args: args{
  328. issue: issue,
  329. actionType: activities_model.ActionCloseIssue,
  330. },
  331. prefix: fmt.Sprintf("<%s/issues/%d/close/", issue.Repo.FullName(), issue.Index),
  332. },
  333. {
  334. name: "Close Pull",
  335. args: args{
  336. issue: pullIssue,
  337. actionType: activities_model.ActionClosePullRequest,
  338. },
  339. prefix: fmt.Sprintf("<%s/pulls/%d/close/", issue.Repo.FullName(), issue.Index),
  340. },
  341. {
  342. name: "Reopen Issue",
  343. args: args{
  344. issue: issue,
  345. actionType: activities_model.ActionReopenIssue,
  346. },
  347. prefix: fmt.Sprintf("<%s/issues/%d/reopen/", issue.Repo.FullName(), issue.Index),
  348. },
  349. {
  350. name: "Reopen Pull",
  351. args: args{
  352. issue: pullIssue,
  353. actionType: activities_model.ActionReopenPullRequest,
  354. },
  355. prefix: fmt.Sprintf("<%s/pulls/%d/reopen/", issue.Repo.FullName(), issue.Index),
  356. },
  357. {
  358. name: "Merge Pull",
  359. args: args{
  360. issue: pullIssue,
  361. actionType: activities_model.ActionMergePullRequest,
  362. },
  363. prefix: fmt.Sprintf("<%s/pulls/%d/merge/", issue.Repo.FullName(), issue.Index),
  364. },
  365. {
  366. name: "Ready Pull",
  367. args: args{
  368. issue: pullIssue,
  369. actionType: activities_model.ActionPullRequestReadyForReview,
  370. },
  371. prefix: fmt.Sprintf("<%s/pulls/%d/ready/", issue.Repo.FullName(), issue.Index),
  372. },
  373. }
  374. for _, tt := range tests {
  375. t.Run(tt.name, func(t *testing.T) {
  376. got := generateMessageIDForIssue(tt.args.issue, tt.args.comment, tt.args.actionType)
  377. assert.True(t, strings.HasPrefix(got, tt.prefix), "%v, want %v", got, tt.prefix)
  378. })
  379. }
  380. }
  381. func TestGenerateMessageIDForRelease(t *testing.T) {
  382. msgID := generateMessageIDForRelease(&repo_model.Release{
  383. ID: 1,
  384. Repo: &repo_model.Repository{OwnerName: "owner", Name: "repo"},
  385. })
  386. assert.Equal(t, "<owner/repo/releases/1@localhost>", msgID)
  387. }
  388. func TestGenerateMessageIDForActionsWorkflowRunStatusEmail(t *testing.T) {
  389. assert.NoError(t, unittest.PrepareTestDatabase())
  390. repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
  391. run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ID: 795, RepoID: repo.ID})
  392. assert.NoError(t, run.LoadAttributes(t.Context()))
  393. msgID := generateMessageIDForActionsWorkflowRunStatusEmail(repo, run)
  394. assert.Equal(t, "<user2/repo2/actions/runs/191@localhost>", msgID)
  395. }
  396. func TestFromDisplayName(t *testing.T) {
  397. tmpl, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}")
  398. assert.NoError(t, err)
  399. setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl}
  400. defer func() { setting.MailService = nil }()
  401. tests := []struct {
  402. userDisplayName string
  403. fromDisplayName string
  404. }{{
  405. userDisplayName: "test",
  406. fromDisplayName: "test",
  407. }, {
  408. userDisplayName: "Hi Its <Mee>",
  409. fromDisplayName: "Hi Its <Mee>",
  410. }, {
  411. userDisplayName: "Æsir",
  412. fromDisplayName: "=?utf-8?q?=C3=86sir?=",
  413. }, {
  414. userDisplayName: "new😀user",
  415. fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=",
  416. }}
  417. for _, tc := range tests {
  418. t.Run(tc.userDisplayName, func(t *testing.T) {
  419. user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"}
  420. got := fromDisplayName(user)
  421. assert.Equal(t, tc.fromDisplayName, got)
  422. })
  423. }
  424. t.Run("template with all available vars", func(t *testing.T) {
  425. tmpl, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])")
  426. assert.NoError(t, err)
  427. setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: tmpl}
  428. oldAppName := setting.AppName
  429. setting.AppName = "Code IT"
  430. oldDomain := setting.Domain
  431. setting.Domain = "code.it"
  432. defer func() {
  433. setting.AppName = oldAppName
  434. setting.Domain = oldDomain
  435. }()
  436. assert.Equal(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"}))
  437. })
  438. }
  439. func TestEmbedBase64Images(t *testing.T) {
  440. user, repo, issue, att1, att2 := prepareMailerBase64Test(t)
  441. // comment := &mailComment{Issue: issue, Doer: user}
  442. imgExternalURL := "https://via.placeholder.com/10"
  443. imgExternalImg := fmt.Sprintf(`<img src="%s"/>`, imgExternalURL)
  444. att1URL := setting.AppURL + repo.Owner.Name + "/" + repo.Name + "/attachments/" + att1.UUID
  445. att1Img := fmt.Sprintf(`<img src="%s"/>`, att1URL)
  446. att1Base64 := "data:image/png;base64,iVBORw0KGgo="
  447. att1ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att1Base64)
  448. att2URL := setting.AppURL + repo.Owner.Name + "/" + repo.Name + "/attachments/" + att2.UUID
  449. att2Img := fmt.Sprintf(`<img src="%s"/>`, att2URL)
  450. att2File, err := storage.Attachments.Open(att2.RelativePath())
  451. require.NoError(t, err)
  452. defer att2File.Close()
  453. att2Bytes, err := io.ReadAll(att2File)
  454. require.NoError(t, err)
  455. require.Greater(t, len(att2Bytes), 1024)
  456. att2Base64 := "data:image/png;base64," + base64.StdEncoding.EncodeToString(att2Bytes)
  457. att2ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att2Base64)
  458. t.Run("ComposeMessage", func(t *testing.T) {
  459. prepareMailTemplates("repo/issue/new", subjectTpl, bodyTpl)
  460. issue.Content = fmt.Sprintf(`MSG-BEFORE <image src="attachments/%s"> MSG-AFTER`, att1.UUID)
  461. require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content"))
  462. recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
  463. msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
  464. Issue: issue,
  465. Doer: user,
  466. ActionType: activities_model.ActionCreateIssue,
  467. Content: issue.Content,
  468. }, "en-US", recipients, false, "issue create")
  469. require.NoError(t, err)
  470. mailBody := msgs[0].Body
  471. assert.Regexp(t, `MSG-BEFORE <a[^>]+><img src="data:image/png;base64,iVBORw0KGgo=".*/></a> MSG-AFTER`, mailBody)
  472. })
  473. t.Run("EmbedInstanceImageSkipExternalImage", func(t *testing.T) {
  474. mailBody := "<html><head></head><body><p>Test1</p>" + imgExternalImg + "<p>Test2</p>" + att1Img + "<p>Test3</p></body></html>"
  475. expectedMailBody := "<html><head></head><body><p>Test1</p>" + imgExternalImg + "<p>Test2</p>" + att1ImgBase64 + "<p>Test3</p></body></html>"
  476. b64embedder := newMailAttachmentBase64Embedder(user, repo, 1024)
  477. resultMailBody, err := b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
  478. require.NoError(t, err)
  479. assert.Equal(t, expectedMailBody, string(resultMailBody))
  480. })
  481. t.Run("LimitedEmailBodySize", func(t *testing.T) {
  482. mailBody := fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1Img, att2Img)
  483. b64embedder := newMailAttachmentBase64Embedder(user, repo, 1024)
  484. resultMailBody, err := b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
  485. require.NoError(t, err)
  486. expected := fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1ImgBase64, att2Img)
  487. assert.Equal(t, expected, string(resultMailBody))
  488. b64embedder = newMailAttachmentBase64Embedder(user, repo, 4096)
  489. resultMailBody, err = b64embedder.Base64InlineImages(t.Context(), template.HTML(mailBody))
  490. require.NoError(t, err)
  491. expected = fmt.Sprintf("<html><head></head><body>%s%s</body></html>", att1ImgBase64, att2ImgBase64)
  492. assert.Equal(t, expected, string(resultMailBody))
  493. })
  494. }