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