feat: e-mail templates (#1158)

* View definition added

* Get templates and texts from the database.

* Fill in texts in templates

* Fill in texts in templates

* Client API added

* Weekly backup

* Weekly backup

* Daily backup

* Weekly backup

* Tests added

* Corrections from merge branch

* Fixes from pull request review
This commit is contained in:
Michael Waeger
2021-01-18 14:17:22 +01:00
committed by GitHub
parent e7540e5e05
commit f2a32871a7
88 changed files with 5325 additions and 155 deletions

View File

@@ -10,6 +10,7 @@ import (
"github.com/caos/zitadel/internal/api/authz"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/models"
@@ -24,10 +25,19 @@ import (
)
const (
notificationTable = "notification.notifications"
NotifyUserID = "NOTIFICATION"
labelPolicyTableOrg = "management.label_policies"
labelPolicyTableDef = "adminapi.label_policies"
notificationTable = "notification.notifications"
NotifyUserID = "NOTIFICATION"
labelPolicyTableOrg = "management.label_policies"
labelPolicyTableDef = "adminapi.label_policies"
mailTemplateTableOrg = "management.mail_templates"
mailTemplateTableDef = "adminapi.mail_templates"
mailTextTableOrg = "management.mail_texts"
mailTextTableDef = "adminapi.mail_texts"
mailTextTypeDomainClaimed = "DomainClaimed"
mailTextTypeInitCode = "InitCode"
mailTextTypePasswordReset = "PasswordReset"
mailTextTypeVerifyEmail = "VerifyEmail"
mailTextTypeVerifyPhone = "VerifyPhone"
)
type Notification struct {
@@ -135,11 +145,22 @@ func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
return err
}
template, err := n.getMailTemplate(context.Background())
if err != nil {
return err
}
user, err := n.view.NotifyUserByID(event.AggregateID)
if err != nil {
return err
}
err = types.SendUserInitCode(n.statikDir, n.i18n, user, initCode, n.systemDefaults, n.AesCrypto, colors)
text, err := n.getMailText(context.Background(), mailTextTypeInitCode, user.PreferredLanguage[len(user.PreferredLanguage)-2:])
if err != nil {
return err
}
err = types.SendUserInitCode(string(template.Template), text, user, initCode, n.systemDefaults, n.AesCrypto, colors)
if err != nil {
return err
}
@@ -163,11 +184,21 @@ func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
return err
}
template, err := n.getMailTemplate(context.Background())
if err != nil {
return err
}
user, err := n.view.NotifyUserByID(event.AggregateID)
if err != nil {
return err
}
err = types.SendPasswordCode(n.statikDir, n.i18n, user, pwCode, n.systemDefaults, n.AesCrypto, colors)
text, err := n.getMailText(context.Background(), mailTextTypePasswordReset, user.PreferredLanguage[len(user.PreferredLanguage)-2:])
if err != nil {
return err
}
err = types.SendPasswordCode(string(template.Template), text, user, pwCode, n.systemDefaults, n.AesCrypto, colors)
if err != nil {
return err
}
@@ -191,11 +222,22 @@ func (n *Notification) handleEmailVerificationCode(event *models.Event) (err err
return err
}
template, err := n.getMailTemplate(context.Background())
if err != nil {
return err
}
user, err := n.view.NotifyUserByID(event.AggregateID)
if err != nil {
return err
}
err = types.SendEmailVerificationCode(n.statikDir, n.i18n, user, emailCode, n.systemDefaults, n.AesCrypto, colors)
text, err := n.getMailText(context.Background(), mailTextTypeVerifyEmail, user.PreferredLanguage[len(user.PreferredLanguage)-2:])
if err != nil {
return err
}
err = types.SendEmailVerificationCode(string(template.Template), text, user, emailCode, n.systemDefaults, n.AesCrypto, colors)
if err != nil {
return err
}
@@ -238,7 +280,21 @@ func (n *Notification) handleDomainClaimed(event *models.Event) (err error) {
if err != nil {
return err
}
err = types.SendDomainClaimed(n.statikDir, n.i18n, user, data["userName"], n.systemDefaults)
colors, err := n.getLabelPolicy(context.Background())
if err != nil {
return err
}
template, err := n.getMailTemplate(context.Background())
if err != nil {
return err
}
text, err := n.getMailText(context.Background(), mailTextTypeDomainClaimed, user.PreferredLanguage[len(user.PreferredLanguage)-2:])
if err != nil {
return err
}
err = types.SendDomainClaimed(string(template.Template), text, user, data["userName"], n.systemDefaults, colors)
if err != nil {
return err
}
@@ -306,3 +362,39 @@ func (n *Notification) getLabelPolicy(ctx context.Context) (*iam_model.LabelPoli
}
return iam_es_model.LabelPolicyViewToModel(policy), err
}
// Read organization specific template
func (n *Notification) getMailTemplate(ctx context.Context) (*iam_model.MailTemplateView, error) {
// read from Org
template, err := n.view.MailTemplateByAggregateID(authz.GetCtxData(ctx).OrgID, mailTemplateTableOrg)
if errors.IsNotFound(err) {
// read from default
template, err = n.view.MailTemplateByAggregateID(n.systemDefaults.IamID, mailTemplateTableDef)
if err != nil {
return nil, err
}
template.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.MailTemplateViewToModel(template), err
}
// Read organization specific texts
func (n *Notification) getMailText(ctx context.Context, textType string, language string) (*iam_model.MailTextView, error) {
// read from Org
mailText, err := n.view.MailTextByIDs(authz.GetCtxData(ctx).OrgID, textType, language, mailTextTableOrg)
if errors.IsNotFound(err) {
// read from default
mailText, err = n.view.MailTextByIDs(n.systemDefaults.IamID, textType, language, mailTextTableDef)
if err != nil {
return nil, err
}
mailText.Default = true
}
if err != nil {
return nil, err
}
return iam_es_model.MailTextViewToModel(mailText), err
}

View File

@@ -0,0 +1,10 @@
package view
import (
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
)
func (v *View) MailTemplateByAggregateID(aggregateID string, mailTemplateTableVar string) (*model.MailTemplateView, error) {
return view.GetMailTemplateByAggregateID(v.Db, mailTemplateTableVar, aggregateID)
}

View File

@@ -0,0 +1,10 @@
package view
import (
"github.com/caos/zitadel/internal/iam/repository/view"
"github.com/caos/zitadel/internal/iam/repository/view/model"
)
func (v *View) MailTextByIDs(aggregateID string, textType string, language string, mailTextTableVar string) (*model.MailTextView, error) {
return view.GetMailTextByIDs(v.Db, mailTextTableVar, aggregateID, textType, language)
}

View File

@@ -12,23 +12,21 @@ const (
templateFileName = "template.html"
)
func GetParsedTemplate(dir http.FileSystem, contentData interface{}) (string, error) {
template, err := ParseTemplateFile(dir, "", contentData)
func GetParsedTemplate(mailhtml string, contentData interface{}) (string, error) {
template, err := ParseTemplateFile(mailhtml, contentData)
if err != nil {
return "", err
}
return ParseTemplateText(template, contentData)
}
func ParseTemplateFile(dir http.FileSystem, fileName string, data interface{}) (string, error) {
if fileName == "" {
fileName = templateFileName
}
template, err := readFile(dir, fileName)
func ParseTemplateFile(mailhtml string, data interface{}) (string, error) {
tmpl, err := template.New("tmpl").Parse(mailhtml)
if err != nil {
return "", err
}
return parseTemplate(template, data)
return parseTemplate(tmpl, data)
}
func ParseTemplateText(text string, data interface{}) (string, error) {
@@ -63,3 +61,20 @@ func readFile(dir http.FileSystem, fileName string) (*template.Template, error)
}
return tmpl, nil
}
func readFileFromDatabase(dir http.FileSystem, fileName string) (*template.Template, error) {
f, err := dir.Open(templatesPath + "/" + fileName)
if err != nil {
return nil, err
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
tmpl, err := template.New(fileName).Parse(string(content))
if err != nil {
return nil, err
}
return tmpl, nil
}

View File

@@ -1,11 +1,11 @@
package types
import (
"net/http"
"html"
"strings"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
)
@@ -15,7 +15,7 @@ type DomainClaimedData struct {
URL string
}
func SendDomainClaimed(dir http.FileSystem, i18n *i18n.Translator, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults) error {
func SendDomainClaimed(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, username string, systemDefaults systemdefaults.SystemDefaults, colors *iam_model.LabelPolicyView) error {
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.DomainClaimed, &UrlData{UserID: user.ID})
if err != nil {
return err
@@ -27,11 +27,28 @@ func SendDomainClaimed(dir http.FileSystem, i18n *i18n.Translator, user *view_mo
"TempUsername": username,
"Domain": strings.Split(user.LastEmail, "@")[1],
}
systemDefaults.Notifications.TemplateData.DomainClaimed.Translate(i18n, args, user.PreferredLanguage)
data := &DomainClaimedData{TemplateData: systemDefaults.Notifications.TemplateData.DomainClaimed, URL: url}
template, err := templates.GetParsedTemplate(dir, data)
text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
text.Text, err = templates.ParseTemplateText(text.Text, args)
text.Text = html.UnescapeString(text.Text)
emailCodeData := &DomainClaimedData{
TemplateData: templates.TemplateData{
Title: text.Title,
PreHeader: text.PreHeader,
Subject: text.Subject,
Greeting: text.Greeting,
Text: html.UnescapeString(text.Text),
Href: url,
ButtonText: text.ButtonText,
PrimaryColor: colors.PrimaryColor,
SecondaryColor: colors.SecondaryColor,
},
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(user, systemDefaults.Notifications.TemplateData.DomainClaimed.Subject, template, systemDefaults.Notifications, true)
return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
}

View File

@@ -1,11 +1,10 @@
package types
import (
"net/http"
"html"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -17,7 +16,7 @@ type EmailVerificationCodeData struct {
URL string
}
func SendEmailVerificationCode(dir http.FileSystem, i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
func SendEmailVerificationCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -31,15 +30,29 @@ func SendEmailVerificationCode(dir http.FileSystem, i18n *i18n.Translator, user
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.VerifyEmail.Translate(i18n, args, user.PreferredLanguage)
emailCodeData := &EmailVerificationCodeData{TemplateData: systemDefaults.Notifications.TemplateData.VerifyEmail, URL: url}
// Set the color in initCodeData
emailCodeData.PrimaryColor = colors.PrimaryColor
emailCodeData.SecondaryColor = colors.SecondaryColor
template, err := templates.GetParsedTemplate(dir, emailCodeData)
text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
text.Text, err = templates.ParseTemplateText(text.Text, args)
text.Text = html.UnescapeString(text.Text)
emailCodeData := &EmailVerificationCodeData{
TemplateData: templates.TemplateData{
Title: text.Title,
PreHeader: text.PreHeader,
Subject: text.Subject,
Greeting: text.Greeting,
Text: html.UnescapeString(text.Text),
Href: url,
ButtonText: text.ButtonText,
PrimaryColor: colors.PrimaryColor,
SecondaryColor: colors.SecondaryColor,
},
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(user, systemDefaults.Notifications.TemplateData.VerifyEmail.Subject, template, systemDefaults.Notifications, true)
return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
}

View File

@@ -1,11 +1,10 @@
package types
import (
"net/http"
"html"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -23,7 +22,7 @@ type UrlData struct {
PasswordSet bool
}
func SendUserInitCode(dir http.FileSystem, i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
func SendUserInitCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -38,15 +37,28 @@ func SendUserInitCode(dir http.FileSystem, i18n *i18n.Translator, user *view_mod
"Code": codeString,
"PreferredLoginName": user.PreferredLoginName,
}
systemDefaults.Notifications.TemplateData.InitCode.Translate(i18n, args, user.PreferredLanguage)
initCodeData := &InitCodeEmailData{TemplateData: systemDefaults.Notifications.TemplateData.InitCode, URL: url}
// Set the color in initCodeData
initCodeData.PrimaryColor = colors.PrimaryColor
initCodeData.SecondaryColor = colors.SecondaryColor
template, err := templates.GetParsedTemplate(dir, initCodeData)
text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
text.Text, err = templates.ParseTemplateText(text.Text, args)
text.Text = html.UnescapeString(text.Text)
emailCodeData := &InitCodeEmailData{
TemplateData: templates.TemplateData{
Title: text.Title,
PreHeader: text.PreHeader,
Subject: text.Subject,
Greeting: text.Greeting,
Text: html.UnescapeString(text.Text),
Href: url,
ButtonText: text.ButtonText,
PrimaryColor: colors.PrimaryColor,
SecondaryColor: colors.SecondaryColor,
},
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(user, systemDefaults.Notifications.TemplateData.InitCode.Subject, template, systemDefaults.Notifications, true)
return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
}

View File

@@ -1,11 +1,10 @@
package types
import (
"net/http"
"html"
"github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/i18n"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/notification/templates"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
@@ -19,7 +18,7 @@ type PasswordCodeData struct {
URL string
}
func SendPasswordCode(dir http.FileSystem, i18n *i18n.Translator, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
func SendPasswordCode(mailhtml string, text *iam_model.MailTextView, user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm, colors *iam_model.LabelPolicyView) error {
codeString, err := crypto.DecryptString(code.Code, alg)
if err != nil {
return err
@@ -33,15 +32,30 @@ func SendPasswordCode(dir http.FileSystem, i18n *i18n.Translator, user *view_mod
"LastName": user.LastName,
"Code": codeString,
}
systemDefaults.Notifications.TemplateData.PasswordReset.Translate(i18n, args, user.PreferredLanguage)
passwordCodeData := &PasswordCodeData{TemplateData: systemDefaults.Notifications.TemplateData.PasswordReset, FirstName: user.FirstName, LastName: user.LastName, URL: url}
// Set the color in initCodeData
passwordCodeData.PrimaryColor = colors.PrimaryColor
passwordCodeData.SecondaryColor = colors.SecondaryColor
template, err := templates.GetParsedTemplate(dir, passwordCodeData)
text.Greeting, err = templates.ParseTemplateText(text.Greeting, args)
text.Text, err = templates.ParseTemplateText(text.Text, args)
text.Text = html.UnescapeString(text.Text)
emailCodeData := &PasswordCodeData{
TemplateData: templates.TemplateData{
Title: text.Title,
PreHeader: text.PreHeader,
Subject: text.Subject,
Greeting: text.Greeting,
Text: html.UnescapeString(text.Text),
Href: url,
ButtonText: text.ButtonText,
PrimaryColor: colors.PrimaryColor,
SecondaryColor: colors.SecondaryColor,
},
FirstName: user.FirstName,
LastName: user.LastName,
URL: url,
}
template, err := templates.GetParsedTemplate(mailhtml, emailCodeData)
if err != nil {
return err
}
return generateEmail(user, systemDefaults.Notifications.TemplateData.PasswordReset.Subject, template, systemDefaults.Notifications, false)
return generateEmail(user, text.Subject, template, systemDefaults.Notifications, true)
}