feat: Updated email templates
This commit is contained in:
+95
-5
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
htmltmpl "html/template"
|
||||
"io/fs"
|
||||
"math"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
"time"
|
||||
@@ -36,11 +37,13 @@ func init() {
|
||||
continue
|
||||
}
|
||||
h, err := htmltmpl.New("layout.html.tmpl").Funcs(htmltmpl.FuncMap(funcs)).
|
||||
Funcs(htmltmpl.FuncMap(perRender("", "accent"))).
|
||||
ParseFS(files, "templates/layout.html.tmpl", e)
|
||||
if err != nil {
|
||||
panic("mail: parse " + e + ": " + err.Error())
|
||||
}
|
||||
t, err := texttmpl.New("layout.txt.tmpl").Funcs(texttmpl.FuncMap(funcs)).
|
||||
Funcs(texttmpl.FuncMap(perRender("", "accent"))).
|
||||
ParseFS(files, "templates/layout.txt.tmpl", "templates/"+name+".txt.tmpl")
|
||||
if err != nil {
|
||||
panic("mail: parse " + name + ".txt.tmpl: " + err.Error())
|
||||
@@ -49,25 +52,54 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
// render produces the subject and both bodies for one message.
|
||||
// perRender are the funcs whose value belongs to one message rather than to
|
||||
// the package: the sender's portal address for the footer links, and the
|
||||
// message's tone, which the layout needs before the body has run. They are
|
||||
// registered with placeholders at parse time and rebound on a clone per render.
|
||||
func perRender(hq, tone string) map[string]any {
|
||||
return map[string]any{
|
||||
"hq": func() string { return hq },
|
||||
"tone": func() string { return tone },
|
||||
}
|
||||
}
|
||||
|
||||
// render produces the subject and both bodies for one message. hq is the
|
||||
// sender's PublicURL; empty drops the footer links.
|
||||
//
|
||||
// The subject comes from the text set, not the HTML one: html/template would
|
||||
// escape an ampersand in an instance name into "&" and mail clients show
|
||||
// subjects verbatim.
|
||||
func render(name string, data any) (message, error) {
|
||||
func render(name string, data any, hq string) (message, error) {
|
||||
s, ok := sets[name]
|
||||
if !ok {
|
||||
return message{}, fmt.Errorf("no such template")
|
||||
}
|
||||
|
||||
var tone strings.Builder
|
||||
if err := s.text.ExecuteTemplate(&tone, "tone", data); err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
per := perRender(hq, strings.TrimSpace(tone.String()))
|
||||
|
||||
ht, err := s.html.Clone()
|
||||
if err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
ht.Funcs(htmltmpl.FuncMap(per))
|
||||
tt, err := s.text.Clone()
|
||||
if err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
tt.Funcs(texttmpl.FuncMap(per))
|
||||
|
||||
var subject, text, html strings.Builder
|
||||
if err := s.text.ExecuteTemplate(&subject, "subject", data); err != nil {
|
||||
if err := tt.ExecuteTemplate(&subject, "subject", data); err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
if err := s.text.Execute(&text, data); err != nil {
|
||||
if err := tt.Execute(&text, data); err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
if err := s.html.Execute(&html, data); err != nil {
|
||||
if err := ht.Execute(&html, data); err != nil {
|
||||
return message{}, err
|
||||
}
|
||||
|
||||
@@ -110,6 +142,24 @@ var funcs = map[string]any{
|
||||
return m, nil
|
||||
},
|
||||
"list": func(v ...any) []any { return v },
|
||||
"add": func(a, b int) int { return a + b },
|
||||
|
||||
// pick chooses one of five values by tone: up, down, pend, accent, and
|
||||
// anything else. It lets the layout keep every hex while the tone of a
|
||||
// message is decided by the message.
|
||||
"pick": func(tone any, up, down, pend, accent, other string) string {
|
||||
switch fmt.Sprint(tone) {
|
||||
case "up":
|
||||
return up
|
||||
case "down":
|
||||
return down
|
||||
case "pend":
|
||||
return pend
|
||||
case "accent":
|
||||
return accent
|
||||
}
|
||||
return other
|
||||
},
|
||||
|
||||
// date is the one long-date format used across every Vantage email.
|
||||
"date": func(t time.Time) string { return t.Format("2 January 2006") },
|
||||
@@ -118,4 +168,44 @@ var funcs = map[string]any{
|
||||
"stamp": func(t time.Time) string { return t.Format("2006-01-02 15:04:05 MST") },
|
||||
"hours": func(d time.Duration) int { return int(d.Hours()) },
|
||||
"upper": strings.ToUpper,
|
||||
"year": func() int { return time.Now().Year() },
|
||||
|
||||
// daysUntil counts whole days left, rounding up so "expires tomorrow
|
||||
// afternoon" reads as 1, never 0. A date in the past is 0.
|
||||
"daysUntil": func(t time.Time) int {
|
||||
d := time.Until(t)
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
return int(math.Ceil(d.Hours() / 24))
|
||||
},
|
||||
"plural": func(n int, one, many string) string {
|
||||
if n == 1 {
|
||||
return fmt.Sprintf("%d %s", n, one)
|
||||
}
|
||||
return fmt.Sprintf("%d %s", n, many)
|
||||
},
|
||||
|
||||
// sevTone maps a scanner severity onto a layout tone, so the vulnerability
|
||||
// digest colours a finding the same way the control plane does.
|
||||
"sevTone": func(sev string) string {
|
||||
switch strings.ToLower(sev) {
|
||||
case "critical", "high":
|
||||
return "down"
|
||||
case "medium":
|
||||
return "pend"
|
||||
}
|
||||
return "accent"
|
||||
},
|
||||
// advisoryURL links a finding to its public advisory when the ID says
|
||||
// where that is. Other IDs get no link rather than a guessed one.
|
||||
"advisoryURL": func(id string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(id, "CVE-"):
|
||||
return "https://nvd.nist.gov/vuln/detail/" + id
|
||||
case strings.HasPrefix(id, "GHSA-"):
|
||||
return "https://github.com/advisories/" + id
|
||||
}
|
||||
return ""
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user