diff --git a/api.go b/api.go index a5bd698..cff0894 100644 --- a/api.go +++ b/api.go @@ -126,7 +126,7 @@ func (app *appContext) checkInvites() { wait.Add(1) go func(addr string) { defer wait.Done() - msg, err := app.email.constructExpiry(code, data, app) + msg, err := app.email.constructExpiry(code, data, app, false) if err != nil { app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) } else if err := app.email.send(msg, addr); err != nil { @@ -163,7 +163,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool for address, settings := range notify { if settings["notify-expiry"] { go func() { - msg, err := app.email.constructExpiry(code, inv, app) + msg, err := app.email.constructExpiry(code, inv, app, false) if err != nil { app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) } else if err := app.email.send(msg, address); err != nil { @@ -295,7 +295,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) { } if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) - msg, err := app.email.constructWelcome(req.Username, app) + msg, err := app.email.constructWelcome(req.Username, app, false) if err != nil { app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err) respondUser(500, true, false, err.Error(), gc) @@ -351,7 +351,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc f = func(gc *gin.Context) { app.debug.Printf("%s: Email confirmation required", req.Code) respond(401, "confirmEmail", gc) - msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app) + msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false) if err != nil { app.err.Printf("%s: Failed to construct confirmation email: %s", req.Code, err) } else if err := app.email.send(msg, req.Email); err != nil { @@ -380,7 +380,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc for address, settings := range invite.Notify { if settings["notify-creation"] { go func() { - msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) + msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false) if err != nil { app.err.Printf("%s: Failed to construct user creation notification: %s", req.Code, err) } else if err := app.email.send(msg, address); err != nil { @@ -436,7 +436,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc } if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" { app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email) - msg, err := app.email.constructWelcome(req.Username, app) + msg, err := app.email.constructWelcome(req.Username, app, false) if err != nil { app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err) } else if err := app.email.send(msg, req.Email); err != nil { @@ -576,7 +576,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { } if len(addresses) != 0 { go func(reason string, addresses []string) { - msg, err := app.email.constructDeleted(reason, app) + msg, err := app.email.constructDeleted(reason, app, false) if err != nil { app.err.Printf("Failed to construct account deletion emails: %s", err) } else if err := app.email.send(msg, addresses...); err != nil { @@ -638,7 +638,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) { app.debug.Printf("%s: Sending invite email", inviteCode) invite.Email = req.Email - msg, err := app.email.constructInvite(inviteCode, invite, app) + msg, err := app.email.constructInvite(inviteCode, invite, app, false) if err != nil { invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) app.err.Printf("%s: Failed to construct invite email: %s", inviteCode, err) @@ -1269,6 +1269,108 @@ func (app *appContext) ModifyConfig(gc *gin.Context) { } } +// @Summary Get a list of email names and IDs. +// @Produce json +// @Success 200 {object} emailListDTO +// @Router /config/emails [get] +// @tags Configuration +func (app *appContext) GetEmails(gc *gin.Context) { + gc.JSON(200, emailListDTO{ + "UserCreated": app.storage.lang.Email["en-us"].UserCreated["name"], + "InviteExpiry": app.storage.lang.Email["en-us"].InviteExpiry["name"], + "PasswordReset": app.storage.lang.Email["en-us"].PasswordReset["name"], + "UserDeleted": app.storage.lang.Email["en-us"].UserDeleted["name"], + "InviteEmail": app.storage.lang.Email["en-us"].InviteEmail["name"], + "WelcomeEmail": app.storage.lang.Email["en-us"].WelcomeEmail["name"], + "EmailConfirmation": app.storage.lang.Email["en-us"].EmailConfirmation["name"], + }) +} + +// @Summary Returns the boilerplate email and list of used variables in it. +// @Produce json +// @Success 200 {object} emailDTO +// @Failure 500 {object} boolResponse +// @Router /config/emails/{id} [get] +// @tags Configuration +func (app *appContext) GetEmail(gc *gin.Context) { + id := gc.Param("id") + var content string + var err error + var msg *Email + if id == "UserCreated" { + content = app.storage.customEmails.UserCreated + if content == "" { + msg, err = app.email.constructCreated("", "", "", Invite{}, app, true) + content = msg.text + } + // app.storage.customEmails.UserCreated = content + } else if id == "InviteExpiry" { + content = app.storage.customEmails.InviteExpiry + if content == "" { + msg, err = app.email.constructExpiry("", Invite{}, app, true) + content = msg.text + } + // app.storage.customEmails.InviteExpiry = content + } else if id == "PasswordReset" { + content = app.storage.customEmails.PasswordReset + if content == "" { + msg, err = app.email.constructReset(PasswordReset{}, app, true) + content = msg.text + } + // app.storage.customEmails.PasswordReset = content + } else if id == "UserDeleted" { + content = app.storage.customEmails.UserDeleted + if content == "" { + msg, err = app.email.constructDeleted("", app, true) + content = msg.text + } + // app.storage.customEmails.UserDeleted = content + } else if id == "InviteEmail" { + content = app.storage.customEmails.InviteEmail + if content == "" { + msg, err = app.email.constructInvite("", Invite{}, app, true) + content = msg.text + } + // app.storage.customEmails.InviteEmail = content + } else if id == "WelcomeEmail" { + content = app.storage.customEmails.WelcomeEmail + if content == "" { + msg, err = app.email.constructWelcome("", app, true) + content = msg.text + } + // app.storage.customEmails.WelcomeEmail = content + } else if id == "EmailConfirmation" { + content = app.storage.customEmails.EmailConfirmation + if content == "" { + msg, err = app.email.constructConfirmation("", "", "", app, true) + content = msg.text + } + // app.storage.customEmails.EmailConfirmation = content + } + 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++ + } + } + gc.JSON(200, emailDTO{Content: content, Variables: variables}) +} + // @Summary Logout by deleting refresh token from cookies. // @Produce json // @Success 200 {object} boolResponse @@ -1291,7 +1393,7 @@ func (app *appContext) Logout(gc *gin.Context) { // @Produce json // @Success 200 {object} langDTO // @Failure 500 {object} stringResponse -// @Router /lang [get] +// @Router /lang/{page} [get] // @tags Other func (app *appContext) GetLanguages(gc *gin.Context) { page := gc.Param("page") diff --git a/config.go b/config.go index b1d7200..ec52de4 100644 --- a/config.go +++ b/config.go @@ -32,7 +32,7 @@ func (app *appContext) loadConfig() error { app.config.Section("jellyfin").Key("public_server").SetValue(app.config.Section("jellyfin").Key("public_server").MustString(app.config.Section("jellyfin").Key("server").String())) for _, key := range app.config.Section("files").Keys() { - if key.Name() != "html_templates" { + if name := key.Name(); name != "html_templates" && name != "lang_files" { key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) } } @@ -63,8 +63,8 @@ func (app *appContext) loadConfig() error { app.config.Section("welcome_email").Key("email_html").SetValue(app.config.Section("welcome_email").Key("email_html").MustString("jfa-go:" + "welcome.html")) app.config.Section("welcome_email").Key("email_text").SetValue(app.config.Section("welcome_email").Key("email_text").MustString("jfa-go:" + "welcome.txt")) - app.config.Section("announcement_email").Key("email_html").SetValue(app.config.Section("announcement_email").Key("email_html").MustString("jfa-go:" + "announcement.html")) - app.config.Section("announcement_email").Key("email_text").SetValue(app.config.Section("announcement_email").Key("email_text").MustString("jfa-go:" + "announcement.txt")) + app.config.Section("template_email").Key("email_html").SetValue(app.config.Section("template_email").Key("email_html").MustString("jfa-go:" + "template.html")) + app.config.Section("template_email").Key("email_text").SetValue(app.config.Section("template_email").Key("email_text").MustString("jfa-go:" + "template.txt")) app.config.Section("jellyfin").Key("version").SetValue(VERSION) app.config.Section("jellyfin").Key("device").SetValue("jfa-go") @@ -76,6 +76,9 @@ func (app *appContext) loadConfig() error { emailEnabled = true } + app.storage.customEmails_path = app.config.Section("files").Key("custom_emails").String() + app.storage.loadCustomEmails() + substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("") oldFormLang := app.config.Section("ui").Key("language").MustString("") diff --git a/config/config-base.json b/config/config-base.json index 9119fc1..a681639 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -853,6 +853,14 @@ "type": "text", "value": "", "description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info." + }, + "custom_emails": { + "name": "Custom email content", + "required": false, + "requires_restart": false, + "type": "text", + "value": "", + "description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info." } } } diff --git a/email.go b/email.go index ad98c27..a53d619 100644 --- a/email.go +++ b/email.go @@ -212,22 +212,32 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string, return } -func (emailer *Emailer) constructConfirmation(code, username, key string, app *appContext) (*Email, error) { +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")), } - message := app.config.Section("email").Key("message").String() - inviteLink := app.config.Section("invite_emails").Key("url_base").String() - inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) var err error - email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{ - "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}), + template := map[string]interface{}{ "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), - "urlVal": inviteLink, "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), - "message": message, - }) + "message": "", + } + if noSub { + template["helloUser"] = emailer.lang.Strings.get("helloUser") + empty := []string{"confirmationURL"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + message := app.config.Section("email").Key("message").String() + inviteLink := app.config.Section("invite_emails").Key("url_base").String() + inviteLink = fmt.Sprintf("%s/%s?key=%s", inviteLink, code, key) + template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": username}) + template["confirmationURL"] = inviteLink + template["message"] = message + } + email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template) if err != nil { return nil, err } @@ -241,7 +251,7 @@ func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContex text := strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(md), "

"), "

") message := app.config.Section("email").Key("message").String() var err error - email.html, email.text, err = emailer.construct(app, "announcement_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, @@ -252,7 +262,7 @@ func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContex return email, nil } -func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) { +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")), } @@ -261,120 +271,175 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont message := app.config.Section("email").Key("message").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) - var err error - email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{ + template := map[string]interface{}{ "hello": emailer.lang.InviteEmail.get("hello"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), "toJoin": emailer.lang.InviteEmail.get("toJoin"), - "inviteExpiry": emailer.lang.InviteEmail.template("inviteExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}), "linkButton": emailer.lang.InviteEmail.get("linkButton"), - "invite_link": inviteLink, - "message": message, - }) + "message": "", + } + if noSub { + template["inviteExpiry"] = emailer.lang.InviteEmail.get("inviteExpiry") + empty := []string{"inviteURL"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + template["inviteExpiry"] = emailer.lang.InviteEmail.template("inviteExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}) + template["inviteURL"] = inviteLink + template["message"] = message + } + var err error + email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template) if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) { +func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext, noSub bool) (*Email, error) { email := &Email{ subject: emailer.lang.InviteExpiry.get("title"), } expiry := app.formatDatetime(invite.ValidTill) - var err error - email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{ + template := map[string]interface{}{ "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), - "expiredAt": emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": "\"" + code + "\"", "time": expiry}), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), - }) + } + if noSub { + template["expiredAt"] = emailer.lang.InviteExpiry.get("expiredAt") + } else { + template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": "\"" + code + "\"", "time": expiry}) + } + var err error + email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template) if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) { +func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext, noSub bool) (*Email, error) { email := &Email{ subject: emailer.lang.UserCreated.get("title"), } - created := app.formatDatetime(invite.Created) - var tplAddress string - if app.config.Section("email").Key("no_username").MustBool(false) { - tplAddress = "n/a" + template := map[string]interface{}{ + "nameString": emailer.lang.Strings.get("name"), + "addressString": emailer.lang.Strings.get("emailAddress"), + "timeString": emailer.lang.UserCreated.get("time"), + "notificationNotice": "", + } + if noSub { + template["aUserWasCreated"] = emailer.lang.UserCreated.get("aUserWasCreated") + empty := []string{"name", "address", "time"} + for _, v := range empty { + template[v] = "{" + v + "}" + } } else { - tplAddress = address + created := app.formatDatetime(invite.Created) + var tplAddress string + if app.config.Section("email").Key("no_username").MustBool(false) { + tplAddress = "n/a" + } else { + tplAddress = address + } + template["aUserWasCreated"] = emailer.lang.UserCreated.template("aUserWasCreated", tmpl{"code": "\"" + code + "\""}) + 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_", map[string]interface{}{ - "aUserWasCreated": emailer.lang.UserCreated.template("aUserWasCreated", tmpl{"code": "\"" + code + "\""}), - "name": emailer.lang.Strings.get("name"), - "address": emailer.lang.Strings.get("emailAddress"), - "time": emailer.lang.UserCreated.get("time"), - "nameVal": username, - "addressVal": tplAddress, - "timeVal": created, - "notificationNotice": emailer.lang.UserCreated.get("notificationNotice"), - }) + email.html, email.text, err = emailer.construct(app, "notifications", "created_", template) if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructReset(pwr PasswordReset, app *appContext) (*Email, error) { +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")), } d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern) message := app.config.Section("email").Key("message").String() - var err error - email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{ - "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}), + template := map[string]interface{}{ "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), - "codeExpiry": emailer.lang.PasswordReset.template("codeExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), - "pin": emailer.lang.PasswordReset.get("pin"), - "pinVal": pwr.Pin, - "message": message, - }) + "pinString": emailer.lang.PasswordReset.get("pin"), + "message": "", + } + if noSub { + template["helloUser"] = emailer.lang.Strings.get("helloUser") + template["codeExpiry"] = emailer.lang.PasswordReset.get("codeExpiry") + empty := []string{"pin"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + template["helloUser"] = emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}) + template["codeExpiry"] = emailer.lang.PasswordReset.template("codeExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}) + template["pin"] = pwr.Pin + template["message"] = message + } + var err error + email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template) if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructDeleted(reason string, app *appContext) (*Email, error) { +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")), } - var err error - email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{ + template := map[string]interface{}{ "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), - "reason": emailer.lang.UserDeleted.get("reason"), - "reasonVal": reason, - }) + "reasonString": emailer.lang.UserDeleted.get("reason"), + "message": "", + } + if noSub { + empty := []string{"reason"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + template["reason"] = reason + template["message"] = app.config.Section("email").Key("message").String() + } + var err error + email.html, email.text, err = emailer.construct(app, "deletion", "email_", template) if err != nil { return nil, err } return email, nil } -func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Email, error) { +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")), } + template := map[string]interface{}{ + "welcome": emailer.lang.WelcomeEmail.get("welcome"), + "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), + "jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"), + "usernameString": emailer.lang.Strings.get("username"), + "message": "", + } + if noSub { + empty := []string{"jellyfinURL", "username"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + template["jellyfinURL"] = app.config.Section("jellyfin").Key("public_server").String() + template["username"] = username + template["message"] = app.config.Section("email").Key("message").String() + } var err error - email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{ - "welcome": emailer.lang.WelcomeEmail.get("welcome"), - "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), - "jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), - "jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), - "username": emailer.lang.Strings.get("username"), - "usernameVal": username, - "message": app.config.Section("email").Key("message").String(), - }) + email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template) if err != nil { return nil, err } diff --git a/lang/email/en-us.json b/lang/email/en-us.json index 0f5ae25..920235e 100644 --- a/lang/email/en-us.json +++ b/lang/email/en-us.json @@ -7,18 +7,21 @@ "helloUser": "Hi {username}," }, "userCreated": { + "name": "User creation", "title": "Notice: User created", "aUserWasCreated": "A user was created using code {code}.", "time": "Time", "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." }, "inviteExpiry": { + "name": "Invite expiry", "title": "Notice: Invite expired", "inviteExpired": "Invite expired.", "expiredAt": "Code {code} expired at {time}.", "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." }, "passwordReset": { + "name": "Password reset", "title": "Password reset requested - Jellyfin", "someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.", "ifItWasYou": "If this was you, enter the pin below into the prompt.", @@ -26,11 +29,13 @@ "pin": "PIN" }, "userDeleted": { + "name": "User deletion", "title": "Your account was deleted - Jellyfin", "yourAccountWasDeleted": "Your Jellyfin account was deleted.", "reason": "Reason" }, "inviteEmail": { + "name": "Invite email", "title": "Invite - Jellyfin", "hello": "Hi", "youHaveBeenInvited": "You've been invited to Jellyfin.", @@ -39,12 +44,14 @@ "linkButton": "Setup your account" }, "welcomeEmail": { + "name": "Welcome email", "title": "Welcome to Jellyfin", "welcome": "Welcome to Jellyfin!", "youCanLoginWith": "You can login with the details below", "jellyfinURL": "URL" }, "emailConfirmation": { + "name": "Confirmation email", "title": "Confirm your email - Jellyfin", "clickBelow": "Click the link below to confirm your email address and start using Jellyfin.", "confirmEmail": "Confirm Email" diff --git a/mail/confirmation.mjml b/mail/confirmation.mjml index 18fbc7a..a3f0023 100644 --- a/mail/confirmation.mjml +++ b/mail/confirmation.mjml @@ -64,7 +64,7 @@

{{ .clickBelow }}

{{ .ifItWasNotYou }}

- {{ .confirmEmail }} + {{ .confirmEmail }} diff --git a/mail/confirmation.txt b/mail/confirmation.txt index e6a3606..cccc813 100644 --- a/mail/confirmation.txt +++ b/mail/confirmation.txt @@ -3,6 +3,6 @@ {{ .clickBelow }} {{ .ifItWasNotYou }} -{{ .urlVal }} +{{ .confirmationURL }} {{ .message }} diff --git a/mail/created.mjml b/mail/created.mjml index ea615ab..40a1417 100644 --- a/mail/created.mjml +++ b/mail/created.mjml @@ -64,14 +64,14 @@ - {{ .name }} - {{ .address }} - {{ .time }} + {{ .nameString }} + {{ .addressString }} + {{ .timeString }} - {{ .nameVal }} - {{ .addressVal }} - {{ .timeVal }} + {{ .name }} + {{ .address }} + {{ .time }} diff --git a/mail/created.txt b/mail/created.txt index f48e6cf..028b235 100644 --- a/mail/created.txt +++ b/mail/created.txt @@ -1,7 +1,7 @@ {{ .aUserWasCreated }} -{{ .name }}: {{ .nameVal }} -{{ .address }}: {{ .addressVal }} -{{ .time }}: {{ .timeVal }} +{{ .nameString }}: {{ .name }} +{{ .addressString }}: {{ .address }} +{{ .timeString }}: {{ .time }} {{ .notificationNotice }} diff --git a/mail/deleted.mjml b/mail/deleted.mjml index 1effe28..e41c10c 100644 --- a/mail/deleted.mjml +++ b/mail/deleted.mjml @@ -61,7 +61,7 @@

{{ .yourAccountWasDeleted }}

-

{{ .reason }}: {{ .reasonVal }}

+

{{ .reasonString }}: {{ .reason }}

diff --git a/mail/deleted.txt b/mail/deleted.txt index ce6eb10..298bfb2 100644 --- a/mail/deleted.txt +++ b/mail/deleted.txt @@ -1,4 +1,4 @@ {{ .yourAccountWasDeleted }} -{{ .reason }}: {{ .reasonVal }} +{{ .reasonString }}: {{ .reason }} {{ .message }} diff --git a/mail/email.mjml b/mail/email.mjml index a51d52f..e4c1e78 100644 --- a/mail/email.mjml +++ b/mail/email.mjml @@ -66,7 +66,7 @@

{{ .codeExpiry }}

{{ .ifItWasNotYou }}

- {{ .pinVal }} + {{ .pin }} diff --git a/mail/email.txt b/mail/email.txt index fae1348..c998ac4 100644 --- a/mail/email.txt +++ b/mail/email.txt @@ -5,6 +5,6 @@ {{ .codeExpiry }} {{ .ifItWasNotYou }} -{{ .pin }}: {{ .pinVal }} +{{ .pinString }}: {{ .pin }} {{ .message }} diff --git a/mail/invite-email.mjml b/mail/invite-email.mjml index 50259ad..cd8f298 100644 --- a/mail/invite-email.mjml +++ b/mail/invite-email.mjml @@ -65,7 +65,7 @@

{{ .toJoin }}

{{ .inviteExpiry }}

- {{ .linkButton }} + {{ .linkButton }}
diff --git a/mail/invite-email.txt b/mail/invite-email.txt index 46cdd84..c39f4c3 100644 --- a/mail/invite-email.txt +++ b/mail/invite-email.txt @@ -3,6 +3,6 @@ {{ .toJoin }} {{ .inviteExpiry }} -{{ .invite_link }} +{{ .inviteURL }} {{ .message }} diff --git a/mail/announcement.mjml b/mail/template.mjml similarity index 100% rename from mail/announcement.mjml rename to mail/template.mjml diff --git a/mail/announcement.txt b/mail/template.txt similarity index 100% rename from mail/announcement.txt rename to mail/template.txt diff --git a/mail/welcome.mjml b/mail/welcome.mjml index 4006e0c..90d828a 100644 --- a/mail/welcome.mjml +++ b/mail/welcome.mjml @@ -62,8 +62,8 @@

{{ .welcome }}

{{ .youCanLoginWith }}:

- {{ .jellyfinURL }}: {{ .jellyfinURLVal }} -

{{ .username }}: {{ .usernameVal }}

+ {{ .jellyfinURLString }}: {{ .jellyfinURL }} +

{{ .usernameString }}: {{ .username }}

diff --git a/mail/welcome.txt b/mail/welcome.txt index f11fde4..e7e83c6 100644 --- a/mail/welcome.txt +++ b/mail/welcome.txt @@ -2,8 +2,8 @@ {{ .youCanLoginWith }}: -{{ .jellyfinURL }}: {{ .jellyfinURLVal }} -{{ .username }}: {{ .usernameVal }} +{{ .jellyfinURLString }}: {{ .jellyfinURL }} +{{ .usernameString }}: {{ .username }} {{ .message }} diff --git a/models.go b/models.go index dbeb1c1..8f09016 100644 --- a/models.go +++ b/models.go @@ -174,3 +174,13 @@ 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/pwreset.go b/pwreset.go index 61a0fac..98ed169 100644 --- a/pwreset.go +++ b/pwreset.go @@ -81,7 +81,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) { return } address = addr.(string) - msg, err := app.email.constructReset(pwr, app) + msg, err := app.email.constructReset(pwr, app, false) if err != nil { app.err.Printf("Failed to construct password reset email for %s", pwr.Username) app.debug.Printf("%s: Error: %s", pwr.Username, err) diff --git a/router.go b/router.go index 548d4b0..a2384eb 100644 --- a/router.go +++ b/router.go @@ -139,6 +139,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) { // api.POST(p + "/setDefaults", app.SetDefaults) api.POST(p+"/users/settings", app.ApplySettings) api.POST(p+"/users/announce", app.Announce) + api.GET(p+"/config/emails", app.GetEmails) + api.GET(p+"/config/emails/:id", app.GetEmail) 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 a40786c..fcca11a 100644 --- a/storage.go +++ b/storage.go @@ -14,15 +14,26 @@ import ( ) type Storage struct { - timePattern string - invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string - invites Invites - profiles map[string]Profile - defaultProfile string - emails, displayprefs, ombi_template map[string]interface{} - policy mediabrowser.Policy - configuration mediabrowser.Configuration - lang Lang + timePattern string + invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path string + invites Invites + profiles map[string]Profile + defaultProfile string + emails, displayprefs, ombi_template map[string]interface{} + customEmails customEmails + policy mediabrowser.Policy + configuration mediabrowser.Configuration + lang Lang +} + +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"` } // timePattern: %Y-%m-%dT%H:%M:%S.%f @@ -394,6 +405,14 @@ func (st *Storage) storeEmails() error { return storeJSON(st.emails_path, st.emails) } +func (st *Storage) loadCustomEmails() error { + return loadJSON(st.customEmails_path, &st.customEmails) +} + +func (st *Storage) storeCustomEmails() error { + return storeJSON(st.customEmails_path, st.customEmails) +} + func (st *Storage) loadPolicy() error { return loadJSON(st.policy_path, &st.policy) }