package template import ( "bytes" "errors" "fmt" "os" "path/filepath" "sync" "text/template" "git.autistici.org/ai3/go-common/mail/mdtext" bf "github.com/russross/blackfriday/v2" ) var ( // TemplateDirectory points at the directory containing templates. TemplateDirectory = "/etc/ai/templates/mail" // DefaultLanguage is the fallback language. DefaultLanguage = "en" // Global, lazily-initialized, shared template registry. templates *template.Template templateLoadMx sync.Mutex // Line width of email plain text bodies. emailLineWidth = 75 ) func init() { if d := os.Getenv("MAIL_TEMPLATE_DIR"); d != "" { TemplateDirectory = d } } func loadTemplates() (err error) { templateLoadMx.Lock() defer templateLoadMx.Unlock() if templates != nil { return } templates, err = template.ParseGlob(filepath.Join(TemplateDirectory, "*.??.md")) return } // SetTemplateDirectory can be used to (re)set the TemplateDirectory // once the program has started, so it's mostly useful for tests. func SetTemplateDirectory(d string) { templateLoadMx.Lock() templates = nil TemplateDirectory = d templateLoadMx.Unlock() } func findTemplate(name, lang string) *template.Template { if lang == "" { lang = DefaultLanguage } tpl := templates.Lookup(fmt.Sprintf("%s.%s.md", name, lang)) if tpl == nil && lang != DefaultLanguage { return findTemplate(name, DefaultLanguage) } return tpl } // Template represents a templated message body. type Template struct { body []byte } // New loads a template with the specified name and language, // and renders it with the given values. // // Templates are Markdown files loaded from the TemplateDirectory // (which can be overridden at runtime by setting the environment // variable MAIL_TEMPLATE_DIR), and must follow the <name>.<lang>.md // naming pattern. Such templates can then be rendered to plain text // or HTML. // // If a template with the desired language does not exist, we fall // back to using DefaultLanguage. func New(name, lang string, values map[string]interface{}) (*Template, error) { if err := loadTemplates(); err != nil { return nil, err } tpl := findTemplate(name, lang) if tpl == nil { return nil, errors.New("template not found") } var buf bytes.Buffer if err := tpl.Execute(&buf, values); err != nil { return nil, err } return &Template{ body: buf.Bytes(), }, nil } // Text renders the template body to plain text. func (t *Template) Text() []byte { return bf.Run(t.body, bf.WithRenderer(mdtext.NewTextRenderer(emailLineWidth))) } // HTML renders the template body to HTML. func (t *Template) HTML() []byte { return bf.Run(t.body) }