summaryrefslogtreecommitdiffstats
path: root/modules/templates/mailer.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/templates/mailer.go')
-rw-r--r--modules/templates/mailer.go110
1 files changed, 110 insertions, 0 deletions
diff --git a/modules/templates/mailer.go b/modules/templates/mailer.go
new file mode 100644
index 00000000..ee79755d
--- /dev/null
+++ b/modules/templates/mailer.go
@@ -0,0 +1,110 @@
+// Copyright 2022 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package templates
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "regexp"
+ "strings"
+ texttmpl "text/template"
+
+ "code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
+
+// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
+func mailSubjectTextFuncMap() texttmpl.FuncMap {
+ return texttmpl.FuncMap{
+ "dict": dict,
+ "Eval": Eval,
+
+ "EllipsisString": base.EllipsisString,
+ "AppName": func() string {
+ return setting.AppName
+ },
+ "AppSlogan": func() string {
+ return setting.AppSlogan
+ },
+ "AppDisplayName": func() string {
+ return setting.AppDisplayName
+ },
+ "AppDomain": func() string { // documented in mail-templates.md
+ return setting.Domain
+ },
+ }
+}
+
+func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template, name string, content []byte) error {
+ // Split template into subject and body
+ var subjectContent []byte
+ bodyContent := content
+ loc := mailSubjectSplit.FindIndex(content)
+ if loc != nil {
+ subjectContent = content[0:loc[0]]
+ bodyContent = content[loc[1]:]
+ }
+ if _, err := stpl.New(name).Parse(string(subjectContent)); err != nil {
+ return fmt.Errorf("failed to parse template [%s/subject]: %w", name, err)
+ }
+ if _, err := btpl.New(name).Parse(string(bodyContent)); err != nil {
+ return fmt.Errorf("failed to parse template [%s/body]: %w", name, err)
+ }
+ return nil
+}
+
+// Mailer provides the templates required for sending notification mails.
+func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
+ subjectTemplates := texttmpl.New("")
+ bodyTemplates := template.New("")
+
+ subjectTemplates.Funcs(mailSubjectTextFuncMap())
+ bodyTemplates.Funcs(NewFuncMap())
+
+ assetFS := AssetFS()
+ refreshTemplates := func(firstRun bool) {
+ if !firstRun {
+ log.Trace("Reloading mail templates")
+ }
+ assetPaths, err := ListMailTemplateAssetNames(assetFS)
+ if err != nil {
+ log.Error("Failed to list mail templates: %v", err)
+ return
+ }
+
+ for _, assetPath := range assetPaths {
+ content, layerName, err := assetFS.ReadLayeredFile(assetPath)
+ if err != nil {
+ log.Warn("Failed to read mail template %s by %s: %v", assetPath, layerName, err)
+ continue
+ }
+ tmplName := strings.TrimPrefix(strings.TrimSuffix(assetPath, ".tmpl"), "mail/")
+ if firstRun {
+ log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
+ }
+ if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
+ if firstRun {
+ log.Fatal("Failed to parse mail template, err: %v", err)
+ }
+ log.Error("Failed to parse mail template, err: %v", err)
+ }
+ }
+ }
+
+ refreshTemplates(true)
+
+ if !setting.IsProd {
+ // Now subjectTemplates and bodyTemplates are both synchronized
+ // thus it is safe to call refresh from a different goroutine
+ go assetFS.WatchLocalChanges(ctx, func() {
+ refreshTemplates(false)
+ })
+ }
+
+ return subjectTemplates, bodyTemplates
+}