Skip to content

Commit

Permalink
feat: advanced e-mail templating support (#1859)
Browse files Browse the repository at this point in the history
Closes #834
Closes #925

Co-authored-by: aeneasr <[email protected]>
  • Loading branch information
landerss1 and aeneasr authored Nov 4, 2021
1 parent 70e75e1 commit 54b97b4
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{ $l := cat "lang=" .lang }}
{{ nospace $l }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{{- if eq .lang "en_US" -}}
{{ template "email.body.html.en_US.gotmpl" . }}
{{- end -}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{{ $t1 := title .input }}
{{ $t1 := nospace $t1 }}
{{ $t2 := upper $t1 }}
{{ $t3 := cat $t1 "," $t2 }}
{{ nospace $t3 }}
93 changes: 80 additions & 13 deletions courier/template/load_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package template
import (
"bytes"
"embed"
htemplate "html/template"
"io"
"os"
"path"
"path/filepath"
"text/template"

"github.com/Masterminds/sprig/v3"
Expand All @@ -18,9 +20,13 @@ var templates embed.FS

var cache, _ = lru.New(16)

func loadTemplate(osdir, name string) (*template.Template, error) {
type Template interface {
Execute(wr io.Writer, data interface{}) error
}

func loadBuiltInTemplate(osdir, name string, html bool) (Template, error) {
if t, found := cache.Get(name); found {
return t.(*template.Template), nil
return t.(Template), nil
}

file, err := os.DirFS(osdir).Open(name)
Expand All @@ -41,23 +47,84 @@ func loadTemplate(osdir, name string) (*template.Template, error) {
return nil, errors.WithStack(err)
}

t, err := template.New(name).Funcs(sprig.TxtFuncMap()).Parse(b.String())
if err != nil {
return nil, errors.WithStack(err)
var tpl Template
if html {
t, err := htemplate.New(name).Funcs(sprig.HtmlFuncMap()).Parse(b.String())
if err != nil {
return nil, errors.WithStack(err)
}
tpl = t
} else {
t, err := template.New(name).Funcs(sprig.TxtFuncMap()).Parse(b.String())
if err != nil {
return nil, errors.WithStack(err)
}
tpl = t
}

_ = cache.Add(name, tpl)
return tpl, nil
}

func loadTemplate(osdir, name, pattern string, html bool) (Template, error) {
if t, found := cache.Get(name); found {
return t.(Template), nil
}

// make sure osdir and template name exists, otherwise fallback to built in templates
f, _ := filepath.Glob(path.Join(osdir, name))
if f == nil {
return loadBuiltInTemplate(osdir, name, html)
}

// if pattern is defined, use it for glob
var glob string = name
if pattern != "" {
m, _ := filepath.Glob(path.Join(osdir, pattern))
if m != nil {
glob = pattern
}
}

_ = cache.Add(name, t)
return t, nil
var tpl Template
if html {
t, err := htemplate.New(filepath.Base(name)).Funcs(sprig.HtmlFuncMap()).ParseGlob(path.Join(osdir, glob))
if err != nil {
return nil, errors.WithStack(err)
}
tpl = t
} else {
t, err := template.New(filepath.Base(name)).Funcs(sprig.TxtFuncMap()).ParseGlob(path.Join(osdir, glob))
if err != nil {
return nil, errors.WithStack(err)
}
tpl = t
}

_ = cache.Add(name, tpl)
return tpl, nil
}

func loadTextTemplate(osdir, name string, model interface{}) (string, error) {
t, err := loadTemplate(osdir, name)
func loadTextTemplate(osdir, name, pattern string, model interface{}) (string, error) {
t, err := loadTemplate(osdir, name, pattern, false)
if err != nil {
return "", err
}
var tb bytes.Buffer
if err := t.ExecuteTemplate(&tb, name, model); err != nil {
return "", errors.WithStack(err)
var b bytes.Buffer
if err := t.Execute(&b, model); err != nil {
return "", err
}
return b.String(), nil
}

func loadHTMLTemplate(osdir, name, pattern string, model interface{}) (string, error) {
t, err := loadTemplate(osdir, name, pattern, true)
if err != nil {
return "", err
}
var b bytes.Buffer
if err := t.Execute(&b, model); err != nil {
return "", err
}
return tb.String(), nil
return b.String(), nil
}
32 changes: 26 additions & 6 deletions courier/template/load_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,52 @@ import (
)

func TestLoadTextTemplate(t *testing.T) {
var executeTemplate = func(t *testing.T, dir, name string) string {
tp, err := loadTextTemplate(dir, name, nil)
var executeTextTemplate = func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string {
tp, err := loadTextTemplate(dir, name, pattern, model)
require.NoError(t, err)
return tp
}

var executeHTMLTemplate = func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string {
tp, err := loadHTMLTemplate(dir, name, pattern, model)
require.NoError(t, err)
return tp
}

t.Run("method=from bundled", func(t *testing.T) {
actual := executeTemplate(t, "courier/builtin/templates", "test_stub/email.body.gotmpl")
actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.gotmpl", "", nil)
assert.Contains(t, actual, "stub email")
})

t.Run("method=fallback to bundled", func(t *testing.T) {
cache, _ = lru.New(16) // prevent cache hit
actual := executeTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl")
actual := executeTextTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl", "", nil)
assert.Contains(t, actual, "stub email")
})

t.Run("method=with Sprig functions", func(t *testing.T) {
cache, _ = lru.New(16) // prevent cache hit
m := map[string]interface{}{"input": "hello world"} // create a simple model
actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.sprig.gotmpl", "", m)
assert.Contains(t, actual, "HelloWorld,HELLOWORLD")
})

t.Run("method=html with nested templates", func(t *testing.T) {
cache, _ = lru.New(16) // prevent cache hit
m := map[string]interface{}{"lang": "en_US"} // create a simple model
actual := executeHTMLTemplate(t, "courier/builtin/templates/test_stub", "email.body.html.gotmpl", "email.body.html*", m)
assert.Contains(t, actual, "lang=en_US")
})

t.Run("method=cache works", func(t *testing.T) {
dir := os.TempDir()
name := x.NewUUID().String() + ".body.gotmpl"
fp := filepath.Join(dir, name)

require.NoError(t, os.WriteFile(fp, []byte("cached stub body"), 0666))
assert.Contains(t, executeTemplate(t, dir, name), "cached stub body")
assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body")

require.NoError(t, os.RemoveAll(fp))
assert.Contains(t, executeTemplate(t, dir, name), "cached stub body")
assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body")
})
}
6 changes: 3 additions & 3 deletions courier/template/recovery_invalid.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ func (t *RecoveryInvalid) EmailRecipient() (string, error) {
}

func (t *RecoveryInvalid) EmailSubject() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.subject.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.subject.gotmpl", "recovery/invalid/email.subject*", t.m)
}

func (t *RecoveryInvalid) EmailBody() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.body.gotmpl", t.m)
return loadHTMLTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.body.gotmpl", "recovery/invalid/email.body*", t.m)
}

func (t *RecoveryInvalid) EmailBodyPlaintext() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.body.plaintext.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/invalid/email.body.plaintext.gotmpl", "recovery/invalid/email.body.plaintext*", t.m)
}

func (t *RecoveryInvalid) MarshalJSON() ([]byte, error) {
Expand Down
7 changes: 4 additions & 3 deletions courier/template/recovery_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type (
RecoveryValidModel struct {
To string
RecoveryURL string
Identity map[string]interface{}
}
)

Expand All @@ -26,15 +27,15 @@ func (t *RecoveryValid) EmailRecipient() (string, error) {
}

func (t *RecoveryValid) EmailSubject() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.subject.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.subject.gotmpl", "recovery/valid/email.subject*", t.m)
}

func (t *RecoveryValid) EmailBody() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.body.gotmpl", t.m)
return loadHTMLTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.body.gotmpl", "recovery/valid/email.body*", t.m)
}

func (t *RecoveryValid) EmailBodyPlaintext() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.body.plaintext.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "recovery/valid/email.body.plaintext.gotmpl", "recovery/valid/email.body.plaintext*", t.m)
}

func (t *RecoveryValid) MarshalJSON() ([]byte, error) {
Expand Down
6 changes: 3 additions & 3 deletions courier/template/stub.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ func (t *TestStub) EmailRecipient() (string, error) {
}

func (t *TestStub) EmailSubject() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.subject.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.subject.gotmpl", "test_stub/email.subject*", t.m)
}

func (t *TestStub) EmailBody() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.body.gotmpl", t.m)
return loadHTMLTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.body.gotmpl", "test_stub/email.body*", t.m)
}

func (t *TestStub) EmailBodyPlaintext() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.body.plaintext.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "test_stub/email.body.plaintext.gotmpl", "test_stub/email.body.plaintext*", t.m)
}

func (t *TestStub) MarshalJSON() ([]byte, error) {
Expand Down
6 changes: 3 additions & 3 deletions courier/template/verification_invalid.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,15 @@ func (t *VerificationInvalid) EmailRecipient() (string, error) {
}

func (t *VerificationInvalid) EmailSubject() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.subject.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.subject.gotmpl", "verification/invalid/email.subject*", t.m)
}

func (t *VerificationInvalid) EmailBody() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.body.gotmpl", t.m)
return loadHTMLTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.body.gotmpl", "verification/invalid/email.body*", t.m)
}

func (t *VerificationInvalid) EmailBodyPlaintext() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.body.plaintext.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/invalid/email.body.plaintext.gotmpl", "verification/invalid/email.body.plaintext*", t.m)
}

func (t *VerificationInvalid) MarshalJSON() ([]byte, error) {
Expand Down
7 changes: 4 additions & 3 deletions courier/template/verification_valid.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type (
VerificationValidModel struct {
To string
VerificationURL string
Identity map[string]interface{}
}
)

Expand All @@ -26,15 +27,15 @@ func (t *VerificationValid) EmailRecipient() (string, error) {
}

func (t *VerificationValid) EmailSubject() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.subject.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.subject.gotmpl", "verification/valid/email.subject*", t.m)
}

func (t *VerificationValid) EmailBody() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.body.gotmpl", t.m)
return loadHTMLTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.body.gotmpl", "verification/valid/email.body*", t.m)
}

func (t *VerificationValid) EmailBodyPlaintext() (string, error) {
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.body.plaintext.gotmpl", t.m)
return loadTextTemplate(t.c.CourierTemplatesRoot(), "verification/valid/email.body.plaintext.gotmpl", "verification/valid/email.body.plaintext*", t.m)
}

func (t *VerificationValid) MarshalJSON() ([]byte, error) {
Expand Down
Loading

0 comments on commit 54b97b4

Please sign in to comment.