mirror of
https://github.com/go-gitea/gitea.git
synced 2026-01-29 10:49:39 +01:00
196 lines
4.9 KiB
Go
196 lines
4.9 KiB
Go
// Copyright 2022 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package templates
|
|
|
|
import (
|
|
"html/template"
|
|
"io"
|
|
"regexp"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
texttmpl "text/template"
|
|
|
|
"code.gitea.io/gitea/modules/graceful"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/util"
|
|
)
|
|
|
|
type MailRender struct {
|
|
TemplateNames []string
|
|
BodyTemplates struct {
|
|
HasTemplate func(name string) bool
|
|
ExecuteTemplate func(w io.Writer, name string, data any) error
|
|
}
|
|
|
|
// FIXME: MAIL-TEMPLATE-SUBJECT: only "issue" related messages support using subject from templates
|
|
// It is an incomplete implementation from "Use templates for issue e-mail subject and body" https://github.com/go-gitea/gitea/pull/8329
|
|
SubjectTemplates *texttmpl.Template
|
|
|
|
tmplRenderer *tmplRender
|
|
|
|
mockedBodyTemplates map[string]*template.Template
|
|
}
|
|
|
|
// 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": evalTokens,
|
|
|
|
"EllipsisString": util.EllipsisDisplayString,
|
|
"AppName": func() string {
|
|
return setting.AppName
|
|
},
|
|
"AppDomain": func() string { // documented in mail-templates.md
|
|
return setting.Domain
|
|
},
|
|
}
|
|
}
|
|
|
|
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
|
|
|
func newMailRenderer() (*MailRender, error) {
|
|
subjectTemplates := texttmpl.New("")
|
|
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
|
|
|
renderer := &MailRender{
|
|
SubjectTemplates: subjectTemplates,
|
|
}
|
|
|
|
assetFS := AssetFS()
|
|
|
|
renderer.tmplRenderer = &tmplRender{
|
|
collectTemplateNames: func() ([]string, error) {
|
|
names, err := assetFS.ListAllFiles(".", true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names = slices.DeleteFunc(names, func(file string) bool {
|
|
return !strings.HasPrefix(file, "mail/") || !strings.HasSuffix(file, ".tmpl")
|
|
})
|
|
for i, name := range names {
|
|
names[i] = strings.TrimSuffix(strings.TrimPrefix(name, "mail/"), ".tmpl")
|
|
}
|
|
renderer.TemplateNames = names
|
|
return names, nil
|
|
},
|
|
readTemplateContent: func(name string) ([]byte, error) {
|
|
content, err := assetFS.ReadFile("mail/" + name + ".tmpl")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var subjectContent []byte
|
|
bodyContent := content
|
|
loc := mailSubjectSplit.FindIndex(content)
|
|
if loc != nil {
|
|
subjectContent, bodyContent = content[0:loc[0]], content[loc[1]:]
|
|
}
|
|
_, err = renderer.SubjectTemplates.New(name).Parse(string(subjectContent))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bodyContent, nil
|
|
},
|
|
}
|
|
|
|
renderer.BodyTemplates.HasTemplate = func(name string) bool {
|
|
if renderer.mockedBodyTemplates[name] != nil {
|
|
return true
|
|
}
|
|
return renderer.tmplRenderer.Templates().HasTemplate(name)
|
|
}
|
|
|
|
staticFuncMap := NewFuncMap()
|
|
renderer.BodyTemplates.ExecuteTemplate = func(w io.Writer, name string, data any) error {
|
|
if t, ok := renderer.mockedBodyTemplates[name]; ok {
|
|
return t.Execute(w, data)
|
|
}
|
|
t, err := renderer.tmplRenderer.Templates().Executor(name, staticFuncMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return t.Execute(w, data)
|
|
}
|
|
|
|
err := renderer.tmplRenderer.recompileTemplates(staticFuncMap)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return renderer, nil
|
|
}
|
|
|
|
func (r *MailRender) MockTemplate(name, subject, body string) func() {
|
|
if r.mockedBodyTemplates == nil {
|
|
r.mockedBodyTemplates = make(map[string]*template.Template)
|
|
}
|
|
oldSubject := r.SubjectTemplates
|
|
r.SubjectTemplates, _ = r.SubjectTemplates.Clone()
|
|
texttmpl.Must(r.SubjectTemplates.New(name).Parse(subject))
|
|
|
|
oldBody, hasOldBody := r.mockedBodyTemplates[name]
|
|
mockFuncMap := NewFuncMap()
|
|
r.mockedBodyTemplates[name] = template.Must(template.New(name).Funcs(mockFuncMap).Parse(body))
|
|
return func() {
|
|
r.SubjectTemplates = oldSubject
|
|
if hasOldBody {
|
|
r.mockedBodyTemplates[name] = oldBody
|
|
} else {
|
|
delete(r.mockedBodyTemplates, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
var (
|
|
globalMailRenderer *MailRender
|
|
globalMailRendererMu sync.RWMutex
|
|
)
|
|
|
|
func MailRendererReload() error {
|
|
globalMailRendererMu.Lock()
|
|
defer globalMailRendererMu.Unlock()
|
|
r, err := newMailRenderer()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
globalMailRenderer = r
|
|
return nil
|
|
}
|
|
|
|
func MailRenderer() *MailRender {
|
|
globalMailRendererMu.RLock()
|
|
r := globalMailRenderer
|
|
globalMailRendererMu.RUnlock()
|
|
if r != nil {
|
|
return r
|
|
}
|
|
|
|
globalMailRendererMu.Lock()
|
|
defer globalMailRendererMu.Unlock()
|
|
if globalMailRenderer != nil {
|
|
return globalMailRenderer
|
|
}
|
|
|
|
var err error
|
|
globalMailRenderer, err = newMailRenderer()
|
|
if err != nil {
|
|
log.Fatal("Failed to initialize mail renderer: %v", err)
|
|
}
|
|
|
|
if !setting.IsProd {
|
|
go AssetFS().WatchLocalChanges(graceful.GetManager().ShutdownContext(), func() {
|
|
globalMailRendererMu.Lock()
|
|
defer globalMailRendererMu.Unlock()
|
|
r, err := newMailRenderer()
|
|
if err != nil {
|
|
log.Error("Mail template error: %v", err)
|
|
return
|
|
}
|
|
globalMailRenderer = r
|
|
})
|
|
}
|
|
return globalMailRenderer
|
|
}
|