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.

mailer.go 3.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. // Copyright 2022 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package templates
  4. import (
  5. "context"
  6. "fmt"
  7. "html/template"
  8. "regexp"
  9. "strings"
  10. texttmpl "text/template"
  11. "code.gitea.io/gitea/modules/base"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. )
  15. var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
  16. // mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
  17. func mailSubjectTextFuncMap() texttmpl.FuncMap {
  18. return texttmpl.FuncMap{
  19. "dict": dict,
  20. "Eval": Eval,
  21. "EllipsisString": base.EllipsisString,
  22. "AppName": func() string {
  23. return setting.AppName
  24. },
  25. "AppDomain": func() string { // documented in mail-templates.md
  26. return setting.Domain
  27. },
  28. }
  29. }
  30. func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
  31. // Split template into subject and body
  32. var subjectContent []byte
  33. bodyContent := content
  34. loc := mailSubjectSplit.FindIndex(content)
  35. if loc != nil {
  36. subjectContent = content[0:loc[0]]
  37. bodyContent = content[loc[1]:]
  38. }
  39. if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
  40. return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
  41. }
  42. if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
  43. return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
  44. }
  45. return nil
  46. }
  47. // Mailer provides the templates required for sending notification mails.
  48. func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
  49. subjectTemplates := texttmpl.New("")
  50. bodyTemplates := template.New("")
  51. subjectTemplates.Funcs(mailSubjectTextFuncMap())
  52. bodyTemplates.Funcs(NewFuncMap())
  53. assetFS := AssetFS()
  54. refreshTemplates := func(firstRun bool) {
  55. if !firstRun {
  56. log.Trace("Reloading mail templates")
  57. }
  58. assetPaths, err := ListMailTemplateAssetNames(assetFS)
  59. if err != nil {
  60. log.Error("Failed to list mail templates: %v", err)
  61. return
  62. }
  63. for _, assetPath := range assetPaths {
  64. content, layerName, err := assetFS.ReadLayeredFile(assetPath)
  65. if err != nil {
  66. log.Warn("Failed to read mail template %s by %s: %v", assetPath, layerName, err)
  67. continue
  68. }
  69. tmplName := strings.TrimPrefix(strings.TrimSuffix(assetPath, ".tmpl"), "mail/")
  70. if firstRun {
  71. log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
  72. }
  73. if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
  74. if firstRun {
  75. log.Fatal("Failed to parse mail template, err: %v", err)
  76. } else {
  77. log.Error("Failed to parse mail template, err: %v", err)
  78. }
  79. }
  80. }
  81. }
  82. refreshTemplates(true)
  83. if !setting.IsProd {
  84. // Now subjectTemplates and bodyTemplates are both synchronized
  85. // thus it is safe to call refresh from a different goroutine
  86. go assetFS.WatchLocalChanges(ctx, func() {
  87. refreshTemplates(false)
  88. })
  89. }
  90. return subjectTemplates, bodyTemplates
  91. }