From 058cac2e7b2fae66b894a8e926b2ed14d1415198 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sat, 20 Feb 2021 22:49:59 +0000 Subject: [PATCH] implement email editor w/ live(?) preview not accessible in the ui currently, but the object is available as window.ee for testing. --- api.go | 92 +++++++++++++++++++++++++++--- css/base.css | 4 ++ email.go | 63 ++++++++++----------- html/admin.html | 24 ++++++++ lang/admin/en-us.json | 4 ++ models.go | 4 ++ router.go | 3 +- stripmd.go | 4 +- ts/admin.ts | 2 + ts/modules/settings.ts | 124 +++++++++++++++++++++++++++++++++++++++++ ts/typings/d.ts | 1 + 11 files changed, 285 insertions(+), 40 deletions(-) diff --git a/api.go b/api.go index d3b52a8..dbc42f5 100644 --- a/api.go +++ b/api.go @@ -1375,6 +1375,84 @@ func (app *appContext) SetEmailState(gc *gin.Context) { respondBool(200, true, gc) } +// @Summary Render and return an email for testing purposes. +// @Produce json +// @Param customEmail body customEmail true "Content = email (in markdown)." +// @Success 200 {object} Email +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /config/emails/{id}/test [post] +// @tags Configuration +func (app *appContext) GetTestEmail(gc *gin.Context) { + var req customEmail + gc.BindJSON(&req) + if req.Content == "" { + app.debug.Println("Test failed: Content was empty") + respondBool(400, false, gc) + return + } + id := gc.Param("id") + var msg *Email + var err error + var cache customEmail + var restore func(cache customEmail) + username := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("username") + emailAddress := app.storage.lang.Email[app.storage.lang.chosenEmailLang].Strings.get("emailAddress") + if id == "UserCreated" { + cache = app.storage.customEmails.UserCreated + restore = func(cache customEmail) { app.storage.customEmails.UserCreated = cache } + app.storage.customEmails.UserCreated.Content = req.Content + app.storage.customEmails.UserCreated.Enabled = true + msg, err = app.email.constructCreated("xxxxxx", username, emailAddress, Invite{}, app, false) + } else if id == "InviteExpiry" { + cache = app.storage.customEmails.InviteExpiry + restore = func(cache customEmail) { app.storage.customEmails.InviteExpiry = cache } + app.storage.customEmails.InviteExpiry.Content = req.Content + app.storage.customEmails.InviteExpiry.Enabled = true + msg, err = app.email.constructExpiry("xxxxxx", Invite{}, app, false) + } else if id == "PasswordReset" { + cache = app.storage.customEmails.PasswordReset + restore = func(cache customEmail) { app.storage.customEmails.PasswordReset = cache } + app.storage.customEmails.PasswordReset.Content = req.Content + app.storage.customEmails.PasswordReset.Enabled = true + msg, err = app.email.constructReset(PasswordReset{Pin: "12-34-56", Username: username}, app, false) + } else if id == "UserDeleted" { + cache = app.storage.customEmails.UserDeleted + restore = func(cache customEmail) { app.storage.customEmails.UserDeleted = cache } + app.storage.customEmails.UserDeleted.Content = req.Content + app.storage.customEmails.UserDeleted.Enabled = true + msg, err = app.email.constructDeleted(app.storage.lang.Email[app.storage.lang.chosenEmailLang].UserDeleted.get("reason"), app, false) + } else if id == "InviteEmail" { + cache = app.storage.customEmails.InviteEmail + restore = func(cache customEmail) { app.storage.customEmails.InviteEmail = cache } + app.storage.customEmails.InviteEmail.Content = req.Content + app.storage.customEmails.InviteEmail.Enabled = true + msg, err = app.email.constructInvite("xxxxxx", Invite{}, app, false) + } else if id == "WelcomeEmail" { + cache = app.storage.customEmails.WelcomeEmail + restore = func(cache customEmail) { app.storage.customEmails.WelcomeEmail = cache } + app.storage.customEmails.WelcomeEmail.Content = req.Content + app.storage.customEmails.WelcomeEmail.Enabled = true + msg, err = app.email.constructWelcome(username, app, false) + } else if id == "EmailConfirmation" { + cache = app.storage.customEmails.EmailConfirmation + restore = func(cache customEmail) { app.storage.customEmails.EmailConfirmation = cache } + app.storage.customEmails.EmailConfirmation.Content = req.Content + app.storage.customEmails.EmailConfirmation.Enabled = true + msg, err = app.email.constructConfirmation("xxxxxx", username, "xxxxxx", app, false) + } else { + respondBool(400, false, gc) + return + } + restore(cache) + if err != nil { + respondBool(500, false, gc) + app.err.Printf("Failed to construct test email: %s", err) + return + } + gc.JSON(200, msg) +} + // @Summary Returns the boilerplate email and list of used variables in it. // @Produce json // @Success 200 {object} customEmail @@ -1395,7 +1473,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructCreated("", "", "", Invite{}, app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.UserCreated.Variables } @@ -1406,7 +1484,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructExpiry("", Invite{}, app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.InviteExpiry.Variables } @@ -1417,7 +1495,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructReset(PasswordReset{}, app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.PasswordReset.Variables } @@ -1428,7 +1506,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructDeleted("", app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.UserDeleted.Variables } @@ -1439,7 +1517,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructInvite("", Invite{}, app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.InviteEmail.Variables } @@ -1450,7 +1528,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructWelcome("", app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.WelcomeEmail.Variables } @@ -1461,7 +1539,7 @@ func (app *appContext) GetEmail(gc *gin.Context) { if content == "" { newEmail = true msg, err = app.email.constructConfirmation("", "", "", app, true) - content = msg.text + content = msg.Text } else { variables = app.storage.customEmails.EmailConfirmation.Variables } diff --git a/css/base.css b/css/base.css index 2e6f656..f10293b 100644 --- a/css/base.css +++ b/css/base.css @@ -88,6 +88,10 @@ div.card:contains(section.banner.footer) { margin-left: 0.5rem; } +.mr-half { + margin-right: 0.5rem; +} + .mr-1 { margin-right: 1rem; } diff --git a/email.go b/email.go index 6fd1873..3ad3772 100644 --- a/email.go +++ b/email.go @@ -33,14 +33,14 @@ type Mailgun struct { func (mg *Mailgun) send(fromName, fromAddr string, email *Email, address ...string) error { message := mg.client.NewMessage( fmt.Sprintf("%s <%s>", fromName, fromAddr), - email.subject, - email.text, + email.Subject, + email.Text, ) for _, a := range address { // Adding variable tells mailgun to do a batch send, so users don't see other recipients. message.AddRecipientAndVariables(a, map[string]interface{}{"unique_id": a}) } - message.SetHtml(email.html) + message.SetHtml(email.HTML) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() _, _, err := mg.client.Send(ctx, message) @@ -69,10 +69,10 @@ func (sm *SMTP) send(fromName, fromAddr string, email *Email, address ...string) go func(addr string) { defer wg.Done() e := jEmail.NewEmail() - e.Subject = email.subject + e.Subject = email.Subject e.From = from - e.Text = []byte(email.text) - e.HTML = []byte(email.html) + e.Text = []byte(email.Text) + e.HTML = []byte(email.HTML) e.To = []string{addr} if sm.sslTLS { err = e.SendWithTLS(server, sm.auth, tlsConfig) @@ -94,8 +94,9 @@ type Emailer struct { // Email stores content. type Email struct { - subject string - html, text string + Subject string `json:"subject"` + HTML string `json:"html"` + Text string `json:"text"` } func (emailer *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern, timePattern string) (d, t, expiresIn string) { @@ -213,7 +214,7 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string, func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), + Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), } var err error template := map[string]interface{}{ @@ -245,9 +246,9 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template) + email.HTML, email.Text, err = emailer.construct(app, "email_confirmation", "email_", template) } if err != nil { return nil, err @@ -256,13 +257,13 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a } func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) { - email := &Email{subject: subject} + email := &Email{Subject: subject} renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) html := markdown.ToHTML([]byte(md), nil, renderer) text := stripMarkdown(md) message := app.config.Section("email").Key("message").String() var err error - email.html, email.text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{ + email.HTML, email.Text, err = emailer.construct(app, "template_email", "email_", map[string]interface{}{ "text": template.HTML(html), "plaintext": text, "message": message, @@ -275,7 +276,7 @@ func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) ( func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")), + Subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")), } expiry := invite.ValidTill d, t, expiresIn := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern) @@ -312,9 +313,9 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template) + email.HTML, email.Text, err = emailer.construct(app, "invite_emails", "email_", template) } if err != nil { return nil, err @@ -324,7 +325,7 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: emailer.lang.InviteExpiry.get("title"), + Subject: emailer.lang.InviteExpiry.get("title"), } expiry := app.formatDatetime(invite.ValidTill) template := map[string]interface{}{ @@ -347,9 +348,9 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template) + email.HTML, email.Text, err = emailer.construct(app, "notifications", "expiry_", template) } if err != nil { return nil, err @@ -359,7 +360,7 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: emailer.lang.UserCreated.get("title"), + Subject: emailer.lang.UserCreated.get("title"), } template := map[string]interface{}{ "nameString": emailer.lang.Strings.get("name"), @@ -397,9 +398,9 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "notifications", "created_", template) + email.HTML, email.Text, err = emailer.construct(app, "notifications", "created_", template) } if err != nil { return nil, err @@ -409,7 +410,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")), + Subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")), } d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("email").Key("message").String() @@ -446,9 +447,9 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template) + email.HTML, email.Text, err = emailer.construct(app, "password_resets", "email_", template) } if err != nil { return nil, err @@ -458,7 +459,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), + Subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), } template := map[string]interface{}{ "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), @@ -483,9 +484,9 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "deletion", "email_", template) + email.HTML, email.Text, err = emailer.construct(app, "deletion", "email_", template) } if err != nil { return nil, err @@ -495,7 +496,7 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub bool) (*Email, error) { email := &Email{ - subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), + Subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), } template := map[string]interface{}{ "welcome": emailer.lang.WelcomeEmail.get("welcome"), @@ -523,9 +524,9 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub content = strings.ReplaceAll(content, v, replaceWith.(string)) } } - email, err = emailer.constructTemplate(email.subject, content, app) + email, err = emailer.constructTemplate(email.Subject, content, app) } else { - email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template) + email.HTML, email.Text, err = emailer.construct(app, "welcome_email", "email_", template) } if err != nil { return nil, err diff --git a/html/admin.html b/html/admin.html index b74e9cd..6f397db 100644 --- a/html/admin.html +++ b/html/admin.html @@ -109,6 +109,30 @@ +