diff --git a/cmd/campaigns.go b/cmd/campaigns.go index c97e5703b..d21d1caa5 100644 --- a/cmd/campaigns.go +++ b/cmd/campaigns.go @@ -663,7 +663,7 @@ func (a *App) sendTestMessage(sub models.Subscriber, camp *models.Campaign) erro func (a *App) validateCampaignFields(c campReq) (campReq, error) { if c.FromEmail == "" { c.FromEmail = a.cfg.FromEmail - } else if !reFromAddress.Match([]byte(c.FromEmail)) { + } else if !models.HasTplExpr(c.FromEmail) && !reFromAddress.Match([]byte(c.FromEmail)) { if _, err := a.importer.SanitizeEmail(c.FromEmail); err != nil { return c, errors.New(a.i18n.T("campaigns.fieldInvalidFromEmail")) } diff --git a/internal/manager/message.go b/internal/manager/message.go index e6cd7b5bf..c9ffda328 100644 --- a/internal/manager/message.go +++ b/internal/manager/message.go @@ -42,6 +42,15 @@ func (m *CampaignMessage) render() error { out.Reset() } + // Render the From header if it's a template. + if m.Campaign.FromEmailTpl != nil { + if err := m.Campaign.FromEmailTpl.ExecuteTemplate(&out, models.ContentTpl, m); err != nil { + return err + } + m.from = out.String() + out.Reset() + } + // Compile the main template. if err := m.Campaign.Tpl.ExecuteTemplate(&out, models.BaseTpl, m); err != nil { return err diff --git a/models/campaigns.go b/models/campaigns.go index 43e9b6a81..d8384bd34 100644 --- a/models/campaigns.go +++ b/models/campaigns.go @@ -65,6 +65,7 @@ type Campaign struct { ArchiveTemplateBody string `db:"archive_template_body" json:"-"` Tpl *template.Template `json:"-"` SubjectTpl *txttpl.Template `json:"-"` + FromEmailTpl *txttpl.Template `json:"-"` AltBodyTpl *template.Template `json:"-"` // HeaderTpls is holds optionally {{ templated }} campaign headers. @@ -139,19 +140,15 @@ func (camps Campaigns) LoadStats(stmt *sqlx.Stmt) error { // CompileTemplate compiles a campaign body template into its base // template and sets the resultant template to Campaign.Tpl. func (c *Campaign) CompileTemplate(f template.FuncMap) error { - // If the subject line has a template string, compile it. - if hasTplExpr(c.Subject) { - subj := c.Subject - for _, r := range regTplFuncs { - subj = r.regExp.ReplaceAllString(subj, r.replace) - } + var err error + c.SubjectTpl, err = compileTxtTpl("subject", c.Subject, f) + if err != nil { + return err + } - var txtFuncs map[string]any = f - subjTpl, err := txttpl.New(ContentTpl).Funcs(txtFuncs).Parse(subj) - if err != nil { - return fmt.Errorf("error compiling subject: %v", err) - } - c.SubjectTpl = subjTpl + c.FromEmailTpl, err = compileTxtTpl("from", c.FromEmail, f) + if err != nil { + return err } // Compile the base template. @@ -197,7 +194,7 @@ func (c *Campaign) CompileTemplate(f template.FuncMap) error { } c.Tpl = out - if hasTplExpr(c.AltBody.String) { + if HasTplExpr(c.AltBody.String) { b := c.AltBody.String for _, r := range regTplFuncs { b = r.regExp.ReplaceAllString(b, r.replace) @@ -209,42 +206,45 @@ func (c *Campaign) CompileTemplate(f template.FuncMap) error { c.AltBodyTpl = bTpl } - // Compile any header values that contain template expressions. - for _, set := range c.Headers { - for _, val := range set { - if hasTplExpr(val) { + for i, set := range c.Headers { + for hdr, val := range set { + if !HasTplExpr(val) { + continue + } + + tpl, err := compileTxtTpl(fmt.Sprintf("header %q", hdr), val, f) + if err != nil { + return err + } + + if c.HeaderTpls == nil { c.HeaderTpls = make([]map[string]*txttpl.Template, len(c.Headers)) - break } - } - if c.HeaderTpls != nil { - break - } - } - if c.HeaderTpls != nil { - var txtFuncs map[string]any = f - for i, set := range c.Headers { - c.HeaderTpls[i] = make(map[string]*txttpl.Template, len(set)) - for hdr, val := range set { - if !hasTplExpr(val) { - continue - } - tpl, err := txttpl.New(ContentTpl).Funcs(txtFuncs).Parse(val) - if err != nil { - return fmt.Errorf("error compiling header %q: %v", hdr, err) - } - c.HeaderTpls[i][hdr] = tpl + if c.HeaderTpls[i] == nil { + c.HeaderTpls[i] = make(map[string]*txttpl.Template) } + c.HeaderTpls[i][hdr] = tpl } } return nil } -// hasTplExpr checks whether a given string has a Go template expression with {{ and }}. -func hasTplExpr(s string) bool { - _, after, ok := strings.Cut(s, "{{") - return ok && strings.Contains(after, "}}") +func compileTxtTpl(label, val string, f template.FuncMap) (*txttpl.Template, error) { + if !HasTplExpr(val) { + return nil, nil + } + + for _, r := range regTplFuncs { + val = r.regExp.ReplaceAllString(val, r.replace) + } + + tpl, err := txttpl.New(ContentTpl).Funcs(f).Parse(val) + if err != nil { + return nil, fmt.Errorf("error compiling %s: %v", label, err) + } + + return tpl, nil } // ConvertContent converts a campaign's body from one format to another, @@ -270,3 +270,9 @@ func (c *Campaign) ConvertContent(from, to string) (string, error) { return out, nil } + +// HasTplExpr checks whether a given string has a Go template expression with {{ and }}. +func HasTplExpr(s string) bool { + _, after, ok := strings.Cut(s, "{{") + return ok && strings.Contains(after, "}}") +} diff --git a/models/messages.go b/models/messages.go index 7914dde53..9cb6c3ca1 100644 --- a/models/messages.go +++ b/models/messages.go @@ -86,7 +86,7 @@ func (m *TxMessage) Render(sub Subscriber, tpl *Template, funcs txttpl.FuncMap) b.Reset() // Render alt body if it has any templating strings. - if m.AltBody != "" && hasTplExpr(m.AltBody) { + if m.AltBody != "" && HasTplExpr(m.AltBody) { t, err := txttpl.New(BaseTpl).Funcs(funcs).Parse(m.AltBody) if err != nil { return fmt.Errorf("error compiling alt body: %v", err) @@ -104,7 +104,7 @@ func (m *TxMessage) Render(sub Subscriber, tpl *Template, funcs txttpl.FuncMap) subject = m.Subject ) if subject != "" { - if hasTplExpr(m.Subject) { + if HasTplExpr(m.Subject) { // If the subject has a template string, render that. s, err := txttpl.New(BaseTpl).Funcs(funcs).Parse(m.Subject) if err != nil { diff --git a/models/templates.go b/models/templates.go index 7d45f21eb..1605b06ac 100644 --- a/models/templates.go +++ b/models/templates.go @@ -44,10 +44,10 @@ func (t *Template) Compile(f template.FuncMap) error { t.Tpl = tpl // If the subject line has a template string, compile it. - if hasTplExpr(t.Subject) { + if HasTplExpr(t.Subject) { subj := t.Subject - subjTpl, err := txttpl.New(BaseTpl).Funcs(txttpl.FuncMap(f)).Parse(subj) + subjTpl, err := txttpl.New(BaseTpl).Funcs(f).Parse(subj) if err != nil { return fmt.Errorf("error compiling subject: %v", err) }