From eb406ef9513cfde67973ece846e6346a3305a748 Mon Sep 17 00:00:00 2001
From: Harvey Tindall
Date: Fri, 19 Feb 2021 21:38:20 +0000
Subject: [PATCH] Implement email template generation
Variables are surrounded by {}, and initial (default) templates are
generated on demand from the plaintext version of emails. The custom
emails are intended to only be used if the user actually changes them,
as they lose the features of the default ones, such as tables.
---
api.go | 120 +++++++++++++-
config.go | 9 +-
config/config-base.json | 8 +
email.go | 191 +++++++++++++++-------
lang/email/en-us.json | 7 +
mail/confirmation.mjml | 2 +-
mail/confirmation.txt | 2 +-
mail/created.mjml | 12 +-
mail/created.txt | 6 +-
mail/deleted.mjml | 2 +-
mail/deleted.txt | 2 +-
mail/email.mjml | 2 +-
mail/email.txt | 2 +-
mail/invite-email.mjml | 2 +-
mail/invite-email.txt | 2 +-
mail/{announcement.mjml => template.mjml} | 0
mail/{announcement.txt => template.txt} | 0
mail/welcome.mjml | 4 +-
mail/welcome.txt | 4 +-
models.go | 10 ++
pwreset.go | 2 +-
router.go | 2 +
storage.go | 37 ++++-
23 files changed, 322 insertions(+), 106 deletions(-)
rename mail/{announcement.mjml => template.mjml} (100%)
rename mail/{announcement.txt => template.txt} (100%)
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)
}