diff --git a/api.go b/api.go index cff0894..d3b52a8 100644 --- a/api.go +++ b/api.go @@ -516,7 +516,7 @@ func (app *appContext) Announce(gc *gin.Context) { } addresses = append(addresses, addr.(string)) } - msg, err := app.email.constructAnnouncement(req.Subject, req.Message, app) + msg, err := app.email.constructTemplate(req.Subject, req.Message, app) if err != nil { app.err.Printf("Failed to construct announcement emails: %s", err) respondBool(500, false, gc) @@ -1286,9 +1286,99 @@ func (app *appContext) GetEmails(gc *gin.Context) { }) } +// @Summary Sets the corresponding custom email. +// @Produce json +// @Param customEmail body customEmail true "Content = email (in markdown)." +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /config/emails/{id} [post] +// @tags Configuration +func (app *appContext) SetEmail(gc *gin.Context) { + var req customEmail + gc.BindJSON(&req) + id := gc.Param("id") + if req.Content == "" { + respondBool(400, false, gc) + return + } + if id == "UserCreated" { + app.storage.customEmails.UserCreated.Content = req.Content + app.storage.customEmails.UserCreated.Enabled = true + } else if id == "InviteExpiry" { + app.storage.customEmails.InviteExpiry.Content = req.Content + app.storage.customEmails.InviteExpiry.Enabled = true + } else if id == "PasswordReset" { + app.storage.customEmails.PasswordReset.Content = req.Content + app.storage.customEmails.PasswordReset.Enabled = true + } else if id == "UserDeleted" { + app.storage.customEmails.UserDeleted.Content = req.Content + app.storage.customEmails.UserDeleted.Enabled = true + } else if id == "InviteEmail" { + app.storage.customEmails.InviteEmail.Content = req.Content + app.storage.customEmails.InviteEmail.Enabled = true + } else if id == "WelcomeEmail" { + app.storage.customEmails.WelcomeEmail.Content = req.Content + app.storage.customEmails.WelcomeEmail.Enabled = true + } else if id == "EmailConfirmation" { + app.storage.customEmails.EmailConfirmation.Content = req.Content + app.storage.customEmails.EmailConfirmation.Enabled = true + } else { + respondBool(400, false, gc) + return + } + if app.storage.storeCustomEmails() != nil { + respondBool(500, false, gc) + return + } + respondBool(200, true, gc) +} + +// @Summary Enable/Disable custom email. +// @Produce json +// @Success 200 {object} boolResponse +// @Failure 400 {object} boolResponse +// @Failure 500 {object} boolResponse +// @Router /config/emails/{id}/{enable/disable} [post] +// @tags Configuration +func (app *appContext) SetEmailState(gc *gin.Context) { + id := gc.Param("id") + s := gc.Param("state") + enabled := false + if s == "enable" { + enabled = true + } else if s != "disable" { + respondBool(400, false, gc) + } + if id == "UserCreated" { + app.storage.customEmails.UserCreated.Enabled = enabled + } else if id == "InviteExpiry" { + app.storage.customEmails.InviteExpiry.Enabled = enabled + } else if id == "PasswordReset" { + app.storage.customEmails.PasswordReset.Enabled = enabled + } else if id == "UserDeleted" { + app.storage.customEmails.UserDeleted.Enabled = enabled + } else if id == "InviteEmail" { + app.storage.customEmails.InviteEmail.Enabled = enabled + } else if id == "WelcomeEmail" { + app.storage.customEmails.WelcomeEmail.Enabled = enabled + } else if id == "EmailConfirmation" { + app.storage.customEmails.EmailConfirmation.Enabled = enabled + } else { + respondBool(400, false, gc) + return + } + if app.storage.storeCustomEmails() != nil { + respondBool(500, false, gc) + return + } + respondBool(200, true, gc) +} + // @Summary Returns the boilerplate email and list of used variables in it. // @Produce json -// @Success 200 {object} emailDTO +// @Success 200 {object} customEmail +// @Failure 400 {object} boolResponse // @Failure 500 {object} boolResponse // @Router /config/emails/{id} [get] // @tags Configuration @@ -1297,78 +1387,119 @@ func (app *appContext) GetEmail(gc *gin.Context) { var content string var err error var msg *Email + var variables []string + var writeVars func(variables []string) + newEmail := false if id == "UserCreated" { - content = app.storage.customEmails.UserCreated + content = app.storage.customEmails.UserCreated.Content if content == "" { + newEmail = true msg, err = app.email.constructCreated("", "", "", Invite{}, app, true) content = msg.text + } else { + variables = app.storage.customEmails.UserCreated.Variables } + writeVars = func(variables []string) { app.storage.customEmails.UserCreated.Variables = variables } // app.storage.customEmails.UserCreated = content } else if id == "InviteExpiry" { - content = app.storage.customEmails.InviteExpiry + content = app.storage.customEmails.InviteExpiry.Content if content == "" { + newEmail = true msg, err = app.email.constructExpiry("", Invite{}, app, true) content = msg.text + } else { + variables = app.storage.customEmails.InviteExpiry.Variables } + writeVars = func(variables []string) { app.storage.customEmails.InviteExpiry.Variables = variables } // app.storage.customEmails.InviteExpiry = content } else if id == "PasswordReset" { - content = app.storage.customEmails.PasswordReset + content = app.storage.customEmails.PasswordReset.Content if content == "" { + newEmail = true msg, err = app.email.constructReset(PasswordReset{}, app, true) content = msg.text + } else { + variables = app.storage.customEmails.PasswordReset.Variables } + writeVars = func(variables []string) { app.storage.customEmails.PasswordReset.Variables = variables } // app.storage.customEmails.PasswordReset = content } else if id == "UserDeleted" { - content = app.storage.customEmails.UserDeleted + content = app.storage.customEmails.UserDeleted.Content if content == "" { + newEmail = true msg, err = app.email.constructDeleted("", app, true) content = msg.text + } else { + variables = app.storage.customEmails.UserDeleted.Variables } + writeVars = func(variables []string) { app.storage.customEmails.UserDeleted.Variables = variables } // app.storage.customEmails.UserDeleted = content } else if id == "InviteEmail" { - content = app.storage.customEmails.InviteEmail + content = app.storage.customEmails.InviteEmail.Content if content == "" { + newEmail = true msg, err = app.email.constructInvite("", Invite{}, app, true) content = msg.text + } else { + variables = app.storage.customEmails.InviteEmail.Variables } + writeVars = func(variables []string) { app.storage.customEmails.InviteEmail.Variables = variables } // app.storage.customEmails.InviteEmail = content } else if id == "WelcomeEmail" { - content = app.storage.customEmails.WelcomeEmail + content = app.storage.customEmails.WelcomeEmail.Content if content == "" { + newEmail = true msg, err = app.email.constructWelcome("", app, true) content = msg.text + } else { + variables = app.storage.customEmails.WelcomeEmail.Variables } + writeVars = func(variables []string) { app.storage.customEmails.WelcomeEmail.Variables = variables } // app.storage.customEmails.WelcomeEmail = content } else if id == "EmailConfirmation" { - content = app.storage.customEmails.EmailConfirmation + content = app.storage.customEmails.EmailConfirmation.Content if content == "" { + newEmail = true msg, err = app.email.constructConfirmation("", "", "", app, true) content = msg.text + } else { + variables = app.storage.customEmails.EmailConfirmation.Variables } + writeVars = func(variables []string) { app.storage.customEmails.EmailConfirmation.Variables = variables } // app.storage.customEmails.EmailConfirmation = content + } else { + respondBool(400, false, gc) + return } if err != nil { respondBool(500, false, gc) return } - variables := make([]string, strings.Count(content, "{")) - i := 0 - found := false - buf := "" - for _, c := range content { - if !found && c != '{' && c != '}' { - continue - } - found = true - buf += string(c) - if c == '}' { - found = false - variables[i] = buf - buf = "" - i++ + if newEmail { + variables = make([]string, strings.Count(content, "{")) + i := 0 + found := false + buf := "" + for _, c := range content { + if !found && c != '{' && c != '}' { + continue + } + found = true + buf += string(c) + if c == '}' { + found = false + variables[i] = buf + buf = "" + i++ + } } + writeVars(variables) } - gc.JSON(200, emailDTO{Content: content, Variables: variables}) + if app.storage.storeCustomEmails() != nil { + respondBool(500, false, gc) + return + } + gc.JSON(200, customEmail{Content: content, Variables: variables}) } // @Summary Logout by deleting refresh token from cookies. diff --git a/config.go b/config.go index ec52de4..ca1f847 100644 --- a/config.go +++ b/config.go @@ -33,10 +33,11 @@ func (app *appContext) loadConfig() error { for _, key := range app.config.Section("files").Keys() { if name := key.Name(); name != "html_templates" && name != "lang_files" { + fmt.Println(name) key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } - for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template"} { + for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails"} { app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) } app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") diff --git a/email.go b/email.go index a53d619..90a8d21 100644 --- a/email.go +++ b/email.go @@ -222,6 +222,7 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), "message": "", + "username": username, } if noSub { template["helloUser"] = emailer.lang.Strings.get("helloUser") @@ -237,14 +238,25 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, app *a template["confirmationURL"] = inviteLink template["message"] = message } - email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template) + if app.storage.customEmails.EmailConfirmation.Enabled { + content := app.storage.customEmails.EmailConfirmation.Content + for _, v := range app.storage.customEmails.EmailConfirmation.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template) + } if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContext) (*Email, error) { +func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) { email := &Email{subject: subject} renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) html := markdown.ToHTML([]byte(md), nil, renderer) @@ -277,6 +289,9 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont "toJoin": emailer.lang.InviteEmail.get("toJoin"), "linkButton": emailer.lang.InviteEmail.get("linkButton"), "message": "", + "date": d, + "time": t, + "expiresInMinutes": expiresIn, } if noSub { template["inviteExpiry"] = emailer.lang.InviteEmail.get("inviteExpiry") @@ -290,7 +305,18 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont template["message"] = message } var err error - email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template) + if app.storage.customEmails.InviteEmail.Enabled { + content := app.storage.customEmails.InviteEmail.Content + for _, v := range app.storage.customEmails.InviteEmail.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template) + } if err != nil { return nil, err } @@ -305,14 +331,27 @@ func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appCont template := map[string]interface{}{ "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), + "code": "\"" + code + "\"", + "time": expiry, } if noSub { template["expiredAt"] = emailer.lang.InviteExpiry.get("expiredAt") } else { - template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": "\"" + code + "\"", "time": expiry}) + template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": template["code"].(string), "time": template["time"].(string)}) } var err error - email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template) + if app.storage.customEmails.InviteExpiry.Enabled { + content := app.storage.customEmails.InviteExpiry.Content + for _, v := range app.storage.customEmails.InviteExpiry.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template) + } if err != nil { return nil, err } @@ -328,6 +367,7 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite "addressString": emailer.lang.Strings.get("emailAddress"), "timeString": emailer.lang.UserCreated.get("time"), "notificationNotice": "", + "code": "\"" + code + "\"", } if noSub { template["aUserWasCreated"] = emailer.lang.UserCreated.get("aUserWasCreated") @@ -343,14 +383,25 @@ func (emailer *Emailer) constructCreated(code, username, address string, invite } else { tplAddress = address } - template["aUserWasCreated"] = emailer.lang.UserCreated.template("aUserWasCreated", tmpl{"code": "\"" + code + "\""}) + template["aUserWasCreated"] = emailer.lang.UserCreated.template("aUserWasCreated", tmpl{"code": template["code"].(string)}) template["name"] = username template["address"] = tplAddress template["time"] = created template["notificationNotice"] = emailer.lang.UserCreated.get("notificationNotice") } var err error - email.html, email.text, err = emailer.construct(app, "notifications", "created_", template) + if app.storage.customEmails.UserCreated.Enabled { + content := app.storage.customEmails.UserCreated.Content + for _, v := range app.storage.customEmails.UserCreated.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "notifications", "created_", template) + } if err != nil { return nil, err } @@ -369,6 +420,10 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "pinString": emailer.lang.PasswordReset.get("pin"), "message": "", + "username": pwr.Username, + "date": d, + "time": t, + "expiresInMinutes": expiresIn, } if noSub { template["helloUser"] = emailer.lang.Strings.get("helloUser") @@ -384,7 +439,18 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext, noSub template["message"] = message } var err error - email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template) + if app.storage.customEmails.PasswordReset.Enabled { + content := app.storage.customEmails.PasswordReset.Content + for _, v := range app.storage.customEmails.PasswordReset.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template) + } if err != nil { return nil, err } @@ -410,7 +476,18 @@ func (emailer *Emailer) constructDeleted(reason string, app *appContext, noSub b template["message"] = app.config.Section("email").Key("message").String() } var err error - email.html, email.text, err = emailer.construct(app, "deletion", "email_", template) + if app.storage.customEmails.UserDeleted.Enabled { + content := app.storage.customEmails.UserDeleted.Content + for _, v := range app.storage.customEmails.UserDeleted.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "deletion", "email_", template) + } if err != nil { return nil, err } @@ -439,7 +516,18 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub template["message"] = app.config.Section("email").Key("message").String() } var err error - email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template) + if app.storage.customEmails.WelcomeEmail.Enabled { + content := app.storage.customEmails.WelcomeEmail.Content + for _, v := range app.storage.customEmails.WelcomeEmail.Variables { + replaceWith, ok := template[v[1:len(v)-1]] + if ok { + content = strings.ReplaceAll(content, v, replaceWith.(string)) + } + } + email, err = emailer.constructTemplate(email.subject, content, app) + } else { + email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template) + } if err != nil { return nil, err } @@ -448,5 +536,6 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext, noSub // calls the send method in the underlying emailClient. func (emailer *Emailer) send(email *Email, address ...string) error { + fmt.Printf("%+v\n", email) return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...) } diff --git a/lang.go b/lang.go index 3606e70..c7aa5dd 100644 --- a/lang.go +++ b/lang.go @@ -120,14 +120,18 @@ func (ls *setupLangs) getOptions() [][2]string { type langSection map[string]string type tmpl map[string]string -func (el langSection) template(field string, vals tmpl) string { - text := el.get(field) +func templateString(text string, vals tmpl) string { for key, val := range vals { text = strings.ReplaceAll(text, "{"+key+"}", val) } return text } +func (el langSection) template(field string, vals tmpl) string { + text := el.get(field) + return templateString(text, vals) +} + func (el langSection) format(field string, vals ...string) string { text := el.get(field) for _, val := range vals { diff --git a/models.go b/models.go index 8f09016..12a94e1 100644 --- a/models.go +++ b/models.go @@ -176,10 +176,6 @@ type settings struct { type langDTO map[string]string type emailListDTO map[string]string -type emailDTO struct { - Content string `json:"content"` - Variables []string `json:"Variables"` -} type emailSetDTO struct { Content string `json:"content"` diff --git a/router.go b/router.go index a2384eb..78dff39 100644 --- a/router.go +++ b/router.go @@ -141,6 +141,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.POST(p+"/users/announce", app.Announce) api.GET(p+"/config/emails", app.GetEmails) api.GET(p+"/config/emails/:id", app.GetEmail) + api.POST(p+"/config/emails/:id", app.SetEmail) + api.POST(p+"/config/emails/:id/:state", app.SetEmailState) api.GET(p+"/config", app.GetConfig) api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/restart", app.restart) diff --git a/storage.go b/storage.go index fcca11a..05046c7 100644 --- a/storage.go +++ b/storage.go @@ -27,13 +27,19 @@ type Storage struct { } type customEmails struct { - UserCreated string `json:"userCreated"` - InviteExpiry string `json:"inviteExpiry"` - PasswordReset string `json:"passwordReset"` - UserDeleted string `json:"userDeleted"` - InviteEmail string `json:"inviteEmail"` - WelcomeEmail string `json:"welcomeEmail"` - EmailConfirmation string `json:"emailConfirmation"` + UserCreated customEmail `json:"userCreated"` + InviteExpiry customEmail `json:"inviteExpiry"` + PasswordReset customEmail `json:"passwordReset"` + UserDeleted customEmail `json:"userDeleted"` + InviteEmail customEmail `json:"inviteEmail"` + WelcomeEmail customEmail `json:"welcomeEmail"` + EmailConfirmation customEmail `json:"emailConfirmation"` +} + +type customEmail struct { + Enabled bool `json:"enabled,omitempty"` + Content string `json:"content"` + Variables []string `json:"variables,omitempty"` } // timePattern: %Y-%m-%dT%H:%M:%S.%f