147 lines
4.9 KiB
Go
147 lines
4.9 KiB
Go
package templates
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
html "html/template"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
text "text/template"
|
|
|
|
"git.sendico.io/tech/sxchnge/notifications/model"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
)
|
|
|
|
type Content struct {
|
|
Subject string `json:"subject"`
|
|
HTML string `json:"html"`
|
|
Text string `json:"text"`
|
|
}
|
|
type Definition struct {
|
|
ID string `json:"id"`
|
|
Version string `json:"version"`
|
|
DefaultLocale string `json:"default_locale"`
|
|
Parameters []string `json:"parameters"`
|
|
Locales map[string]Content `json:"locales"`
|
|
}
|
|
type compiled struct {
|
|
definition Definition
|
|
locales map[string]renderers
|
|
}
|
|
type renderers struct {
|
|
subject, text *text.Template
|
|
html *html.Template
|
|
}
|
|
type Catalog struct{ entries map[string]compiled }
|
|
|
|
func Load(path string) (*Catalog, error) {
|
|
f, err := os.Open(path)
|
|
if err != nil {
|
|
return nil, merrors.OpaqueWrap(merrors.ErrInvalidArg, err, "cannot open template catalog")
|
|
}
|
|
defer f.Close()
|
|
var definitions []Definition
|
|
dec := json.NewDecoder(io.LimitReader(f, 4<<20))
|
|
dec.DisallowUnknownFields()
|
|
if err = dec.Decode(&definitions); err != nil {
|
|
return nil, merrors.OpaqueWrap(merrors.ErrInvalidArg, err, "invalid template catalog")
|
|
}
|
|
if err = dec.Decode(new(any)); err != io.EOF {
|
|
return nil, merrors.InvalidArgument("unexpected template catalog content")
|
|
}
|
|
return New(definitions)
|
|
}
|
|
func New(definitions []Definition) (*Catalog, error) {
|
|
c := &Catalog{entries: map[string]compiled{}}
|
|
if len(definitions) == 0 {
|
|
return nil, merrors.InvalidArgument("empty template catalog")
|
|
}
|
|
for _, d := range definitions {
|
|
key := d.ID + "\x00" + d.Version
|
|
if d.ID == "" || d.Version == "" || strings.ContainsRune(d.ID+d.Version, 0) {
|
|
return nil, merrors.InvalidArgument("invalid template identity")
|
|
}
|
|
if _, ok := c.entries[key]; ok {
|
|
return nil, merrors.InvalidArgument("duplicate template revision")
|
|
}
|
|
if _, ok := d.Locales[d.DefaultLocale]; !ok || d.DefaultLocale == "" {
|
|
return nil, merrors.InvalidArgument("missing default locale")
|
|
}
|
|
params := map[string]string{}
|
|
for _, p := range d.Parameters {
|
|
if p == "" {
|
|
return nil, merrors.InvalidArgument("empty parameter")
|
|
}
|
|
if _, ok := params[p]; ok {
|
|
return nil, merrors.InvalidArgument("duplicate parameter")
|
|
}
|
|
params[p] = "sample"
|
|
}
|
|
entry := compiled{definition: d, locales: map[string]renderers{}}
|
|
for locale, content := range d.Locales {
|
|
s, err := text.New("subject").Option("missingkey=error").Parse(content.Subject)
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument("invalid subject template")
|
|
}
|
|
t, err := text.New("text").Option("missingkey=error").Parse(content.Text)
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument("invalid text template")
|
|
}
|
|
h, err := html.New("html").Option("missingkey=error").Parse(content.HTML)
|
|
if err != nil {
|
|
return nil, merrors.InvalidArgument("invalid HTML template")
|
|
}
|
|
entry.locales[locale] = renderers{s, t, h}
|
|
}
|
|
c.entries[key] = entry
|
|
for locale := range d.Locales {
|
|
if _, err := c.Render(model.Request{TemplateID: d.ID, TemplateVersion: d.Version, Locale: locale, Parameters: params}); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
type boundedBuffer struct{ bytes.Buffer }
|
|
|
|
func (b *boundedBuffer) Write(p []byte) (int, error) {
|
|
if b.Len()+len(p) > 256<<10 {
|
|
return 0, merrors.InvalidArgument("rendered content too large")
|
|
}
|
|
return b.Buffer.Write(p)
|
|
}
|
|
func (c *Catalog) Render(r model.Request) (model.Email, error) {
|
|
entry, ok := c.entries[r.TemplateID+"\x00"+r.TemplateVersion]
|
|
if !ok {
|
|
return model.Email{}, merrors.NoData("template revision not found")
|
|
}
|
|
if len(r.Parameters) != len(entry.definition.Parameters) {
|
|
return model.Email{}, merrors.InvalidArgument("template parameters do not match")
|
|
}
|
|
for _, p := range entry.definition.Parameters {
|
|
if _, ok := r.Parameters[p]; !ok {
|
|
return model.Email{}, merrors.InvalidArgument("missing template parameter")
|
|
}
|
|
}
|
|
locale := r.Locale
|
|
if locale == "" {
|
|
locale = entry.definition.DefaultLocale
|
|
}
|
|
render, ok := entry.locales[locale]
|
|
if !ok {
|
|
return model.Email{}, merrors.InvalidArgument("unsupported template locale")
|
|
}
|
|
var subject, body, plain boundedBuffer
|
|
for _, run := range []func() error{func() error { return render.subject.Execute(&subject, r.Parameters) }, func() error { return render.html.Execute(&body, r.Parameters) }, func() error { return render.text.Execute(&plain, r.Parameters) }} {
|
|
if err := run(); err != nil {
|
|
return model.Email{}, merrors.OpaqueWrap(merrors.ErrInvalidArg, err, "template rendering failed")
|
|
}
|
|
}
|
|
if subject.Len() == 0 || subject.Len() > 998 || strings.ContainsAny(subject.String(), "\r\n") || (body.Len() == 0 && plain.Len() == 0) {
|
|
return model.Email{}, merrors.InvalidArgument("invalid rendered email")
|
|
}
|
|
return model.Email{Address: r.Address, Subject: subject.String(), HTML: body.String(), Text: plain.String()}, nil
|
|
}
|