From fbbb03a47d2bab62703174313d7d1da1c3cfb37b Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 28 Jul 2024 16:02:47 +0100 Subject: [PATCH] email: add new "expiry adjusted email" just the email at this point. Also wrote up a little guide on adding new emails on wiki.jfa-go.com. --- api-messages.go | 63 +++++++++--------------------- config.go | 3 ++ config/config-base.json | 31 ++++++++++++++- email.go | 64 +++++++++++++++++++++++++++++++ lang.go | 25 ++++++------ lang/email/en-us.json | 9 ++++- mail/expiry-adjusted.mjml | 81 +++++++++++++++++++++++++++++++++++++++ mail/expiry-adjusted.txt | 10 +++++ migrations.go | 50 ++++++++++++++++++++++++ storage.go | 23 ++++++----- 10 files changed, 290 insertions(+), 69 deletions(-) create mode 100644 mail/expiry-adjusted.mjml create mode 100644 mail/expiry-adjusted.txt diff --git a/api-messages.go b/api-messages.go index 452dc57..499c1ff 100644 --- a/api-messages.go +++ b/api-messages.go @@ -26,18 +26,19 @@ func (app *appContext) GetCustomContent(gc *gin.Context) { adminLang = app.storage.lang.chosenAdminLang } list := emailListDTO{ - "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled}, - "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled}, - "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled}, - "UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled}, - "UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled}, - "UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled}, - "InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled}, - "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled}, - "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled}, - "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled}, - "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled}, - "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled}, + "UserCreated": {Name: app.storage.lang.Email[lang].UserCreated["name"], Enabled: app.storage.MustGetCustomContentKey("UserCreated").Enabled}, + "InviteExpiry": {Name: app.storage.lang.Email[lang].InviteExpiry["name"], Enabled: app.storage.MustGetCustomContentKey("InviteExpiry").Enabled}, + "PasswordReset": {Name: app.storage.lang.Email[lang].PasswordReset["name"], Enabled: app.storage.MustGetCustomContentKey("PasswordReset").Enabled}, + "UserDeleted": {Name: app.storage.lang.Email[lang].UserDeleted["name"], Enabled: app.storage.MustGetCustomContentKey("UserDeleted").Enabled}, + "UserDisabled": {Name: app.storage.lang.Email[lang].UserDisabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserDisabled").Enabled}, + "UserEnabled": {Name: app.storage.lang.Email[lang].UserEnabled["name"], Enabled: app.storage.MustGetCustomContentKey("UserEnabled").Enabled}, + "UserExpiryAdjusted": {Name: app.storage.lang.Email[lang].UserExpiryAdjusted["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpiryAdjusted").Enabled}, + "InviteEmail": {Name: app.storage.lang.Email[lang].InviteEmail["name"], Enabled: app.storage.MustGetCustomContentKey("InviteEmail").Enabled}, + "WelcomeEmail": {Name: app.storage.lang.Email[lang].WelcomeEmail["name"], Enabled: app.storage.MustGetCustomContentKey("WelcomeEmail").Enabled}, + "EmailConfirmation": {Name: app.storage.lang.Email[lang].EmailConfirmation["name"], Enabled: app.storage.MustGetCustomContentKey("EmailConfirmation").Enabled}, + "UserExpired": {Name: app.storage.lang.Email[lang].UserExpired["name"], Enabled: app.storage.MustGetCustomContentKey("UserExpired").Enabled}, + "UserLogin": {Name: app.storage.lang.Admin[adminLang].Strings["userPageLogin"], Enabled: app.storage.MustGetCustomContentKey("UserLogin").Enabled}, + "UserPage": {Name: app.storage.lang.Admin[adminLang].Strings["userPagePage"], Enabled: app.storage.MustGetCustomContentKey("UserPage").Enabled}, } filter := gc.Query("filter") @@ -51,39 +52,6 @@ func (app *appContext) GetCustomContent(gc *gin.Context) { gc.JSON(200, list) } -// No longer needed, these are stored by string keys in the database now. -/* func (app *appContext) getCustomMessage(id string) *CustomContent { - switch id { - case "Announcement": - return &CustomContent{} - case "UserCreated": - return &app.storage.customEmails.UserCreated - case "InviteExpiry": - return &app.storage.customEmails.InviteExpiry - case "PasswordReset": - return &app.storage.customEmails.PasswordReset - case "UserDeleted": - return &app.storage.customEmails.UserDeleted - case "UserDisabled": - return &app.storage.customEmails.UserDisabled - case "UserEnabled": - return &app.storage.customEmails.UserEnabled - case "InviteEmail": - return &app.storage.customEmails.InviteEmail - case "WelcomeEmail": - return &app.storage.customEmails.WelcomeEmail - case "EmailConfirmation": - return &app.storage.customEmails.EmailConfirmation - case "UserExpired": - return &app.storage.customEmails.UserExpired - case "UserLogin": - return &app.storage.userPage.Login - case "UserPage": - return &app.storage.userPage.Page - } - return nil -} */ - // @Summary Sets the corresponding custom content. // @Produce json // @Param CustomContent body CustomContent true "Content = email (in markdown)." @@ -217,6 +185,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { msg, err = app.email.constructEnabled("", app, true) } values = app.email.deletedValues(app.storage.lang.Email[lang].Strings.get("reason"), app, false) + case "UserExpiryAdjusted": + if noContent { + msg, err = app.email.constructExpiryAdjusted(time.Time{}, "", app, true) + } + values = app.email.expiryAdjustedValues(time.Now(), app.storage.lang.Email[lang].Strings.get("reason"), app, false, true) case "InviteEmail": if noContent { msg, err = app.email.constructInvite("", Invite{}, app, true) diff --git a/config.go b/config.go index ed3dd53..8426bab 100644 --- a/config.go +++ b/config.go @@ -105,6 +105,9 @@ func (app *appContext) loadConfig() error { app.MustSetValue("user_expiry", "email_html", "jfa-go:"+"user-expired.html") app.MustSetValue("user_expiry", "email_text", "jfa-go:"+"user-expired.txt") + app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html") + app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt") + app.MustSetValue("matrix", "topic", "Jellyfin notifications") app.MustSetValue("matrix", "show_on_reg", "true") diff --git a/config/config-base.json b/config/config-base.json index f770dc2..dfd5114 100644 --- a/config/config-base.json +++ b/config/config-base.json @@ -1713,7 +1713,7 @@ "order": [], "meta": { "name": "User Expiry", - "description": "When set on an invite, users will be deleted or disabled a specified amount of time after they create their account." + "description": "When set on an invite, users will be deleted or disabled a specified amount of time after they create their account. Expiries can also be set and extended for invididual users, optionally with a message why." }, "settings": { "behaviour": { @@ -1765,6 +1765,35 @@ "type": "text", "value": "", "description": "Path to custom email in plain text" + }, + "adjustment_subject": { + "name": "Adjustment: email subject", + "required": false, + "requires_restart": false, + "depends_true": "messages|enabled", + "type": "text", + "value": "", + "description": "Subject of adjustment emails, sent optionally when setting/extending an expiry." + }, + "adjustment_email_html": { + "name": "Adjustment: Custom email (HTML)", + "required": false, + "requires_restart": false, + "advanced": true, + "depends_true": "messages|enabled", + "type": "text", + "value": "", + "description": "Path to custom email html" + }, + "adjustment_email_text": { + "name": "Adjustment: Custom email (plaintext)", + "required": false, + "requires_restart": false, + "advanced": true, + "depends_true": "messages|enabled", + "type": "text", + "value": "", + "description": "Path to custom email in plain text" } } }, diff --git a/email.go b/email.go index 156e55e..505b71d 100644 --- a/email.go +++ b/email.go @@ -741,6 +741,70 @@ func (emailer *Emailer) constructEnabled(reason string, app *appContext, noSub b return email, nil } +func (emailer *Emailer) expiryAdjustedValues(expiry time.Time, reason string, app *appContext, noSub bool, custom bool) map[string]interface{} { + template := map[string]interface{}{ + "yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"), + "ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"), + "reasonString": emailer.lang.Strings.get("reason"), + "newExpiry": "", + "message": "", + } + if noSub { + empty := []string{"reason", "newExpiry"} + for _, v := range empty { + template[v] = "{" + v + "}" + } + } else { + template["reason"] = reason + template["message"] = app.config.Section("messages").Key("message").String() + exp := app.formatDatetime(expiry) + if !expiry.IsZero() { + if custom { + template["newExpiry"] = exp + } else if !expiry.IsZero() { + template["newExpiry"] = emailer.lang.UserExpiryAdjusted.template("newExpiry", tmpl{ + "date": exp, + }) + } + } + } + return template +} + +func (emailer *Emailer) constructExpiryAdjusted(expiry time.Time, reason string, app *appContext, noSub bool) (*Message, error) { + email := &Message{ + Subject: app.config.Section("user_expiry").Key("adjustment_subject").MustString(emailer.lang.UserExpiryAdjusted.get("title")), + } + var err error + var template map[string]interface{} + message := app.storage.MustGetCustomContentKey("UserExpiryAdjusted") + if message.Enabled { + template = emailer.expiryAdjustedValues(expiry, reason, app, noSub, true) + } else { + template = emailer.expiryAdjustedValues(expiry, reason, app, noSub, false) + } + if noSub { + template["newExpiry"] = emailer.lang.UserExpiryAdjusted.template("newExpiry", tmpl{ + "date": "{newExpiry}", + }) + } + if message.Enabled { + content := templateEmail( + message.Content, + message.Variables, + nil, + template, + ) + email, err = emailer.constructTemplate(email.Subject, content, app) + } else { + email.HTML, email.Text, email.Markdown, err = emailer.construct(app, "user_expiry", "adjustment_email_", template) + } + if err != nil { + return nil, err + } + return email, nil +} + func (emailer *Emailer) welcomeValues(username string, expiry time.Time, app *appContext, noSub bool, custom bool) map[string]interface{} { template := map[string]interface{}{ "welcome": emailer.lang.WelcomeEmail.get("welcome"), diff --git a/lang.go b/lang.go index 8dbdc46..dc2e8c0 100644 --- a/lang.go +++ b/lang.go @@ -93,18 +93,19 @@ func (ls *emailLangs) getOptions() [][2]string { } type emailLang struct { - Meta langMeta `json:"meta"` - Strings langSection `json:"strings"` - UserCreated langSection `json:"userCreated"` - InviteExpiry langSection `json:"inviteExpiry"` - PasswordReset langSection `json:"passwordReset"` - UserDeleted langSection `json:"userDeleted"` - UserDisabled langSection `json:"userDisabled"` - UserEnabled langSection `json:"userEnabled"` - InviteEmail langSection `json:"inviteEmail"` - WelcomeEmail langSection `json:"welcomeEmail"` - EmailConfirmation langSection `json:"emailConfirmation"` - UserExpired langSection `json:"userExpired"` + Meta langMeta `json:"meta"` + Strings langSection `json:"strings"` + UserCreated langSection `json:"userCreated"` + InviteExpiry langSection `json:"inviteExpiry"` + PasswordReset langSection `json:"passwordReset"` + UserDeleted langSection `json:"userDeleted"` + UserDisabled langSection `json:"userDisabled"` + UserEnabled langSection `json:"userEnabled"` + UserExpiryAdjusted langSection `json:"userExpiryAdjusted"` + InviteEmail langSection `json:"inviteEmail"` + WelcomeEmail langSection `json:"welcomeEmail"` + EmailConfirmation langSection `json:"emailConfirmation"` + UserExpired langSection `json:"userExpired"` } type setupLangs map[string]setupLang diff --git a/lang/email/en-us.json b/lang/email/en-us.json index 0f00b2b..1614434 100644 --- a/lang/email/en-us.json +++ b/lang/email/en-us.json @@ -45,6 +45,13 @@ "title": "Your account has been re-enabled - Jellyfin", "yourAccountWasEnabled": "Your account was re-enabled." }, + "userExpiryAdjusted": { + "name": "Expiry adjusted", + "title": "Account expiry adjusted - Jellyfin", + "yourExpiryWasAdjusted": "Your account's expiry date has been adjusted.", + "ifPreviouslyDisabled": "If your account was previously disabled, it has been re-enabled.", + "newExpiry": "Your account will now expire on {date}." + }, "inviteEmail": { "name": "Invite email", "title": "Invite - Jellyfin", @@ -74,4 +81,4 @@ "yourAccountHasExpired": "Your account has expired.", "contactTheAdmin": "Contact the administrator for more info." } -} \ No newline at end of file +} diff --git a/mail/expiry-adjusted.mjml b/mail/expiry-adjusted.mjml new file mode 100644 index 0000000..ef30478 --- /dev/null +++ b/mail/expiry-adjusted.mjml @@ -0,0 +1,81 @@ + + + + + + + + :root { + Color-scheme: light dark; + supported-color-schemes: light dark; + } + @media (prefers-color-scheme: light) { + Color-scheme: dark; + .body { + background: #242424 !important; + background-color: #242424 !important; + } + [data-ogsc] .body { + background: #242424 !important; + background-color: #242424 !important; + } + [data-ogsb] .body { + background: #242424 !important; + background-color: #242424 !important; + } + } + @media (prefers-color-scheme: dark) { + Color-scheme: dark; + .body { + background: #242424 !important; + background-color: #242424 !important; + } + [data-ogsc] .body { + background: #242424 !important; + background-color: #242424 !important; + } + [data-ogsb] .body { + background: #242424 !important; + background-color: #242424 !important; + } + } + + + + + + + + + + + + + + + + {{ .jellyfin }} + + + + + +

{{ .yourExpiryWasAdjusted }}

+ +

{{ .ifPreviouslyDisabled }}

+ +

{{ .newExpiry }}

+ +

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

+
+
+
+ + + + {{ .message }} + + + + +
diff --git a/mail/expiry-adjusted.txt b/mail/expiry-adjusted.txt new file mode 100644 index 0000000..b64b2b9 --- /dev/null +++ b/mail/expiry-adjusted.txt @@ -0,0 +1,10 @@ +{{ .yourExpiryWasAdjusted }} + +{{ .ifPreviouslyDisabled }} + +{{ .newExpiry }} + +{{ .reasonString }}: {{ .reason }} + +{{ .message }} + diff --git a/migrations.go b/migrations.go index 2efb884..209612c 100644 --- a/migrations.go +++ b/migrations.go @@ -17,6 +17,7 @@ func runMigrations(app *appContext) { linkExistingOmbiDiscordTelegram(app) // migrateHyphens(app) migrateToBadger(app) + intialiseCustomContent(app) } // Migrate pre-0.2.0 user templates to profiles @@ -329,6 +330,8 @@ func migrateToBadger(app *appContext) { app.storage.SetCustomContentKey("UserPage", app.storage.deprecatedUserPageContent.Page) } + // Custom content not present here was added post-badger. + err := app.storage.db.Upsert("migrated_to_db", MigrationStatus{true}) if err != nil { app.err.Fatalf("Failed to migrate to DB: %v\n", err) @@ -336,6 +339,53 @@ func migrateToBadger(app *appContext) { app.info.Println("All data migrated to database. JSON files in the config folder can be deleted if you are sure all data is correct in the app. Create an issue if you have problems.") } +// Simply creates an emply CC template if not imn the DB already. +// Add new CC types here! +func intialiseCustomContent(app *appContext) { + emptyCC := CustomContent{ + Enabled: false, + } + if _, ok := app.storage.GetCustomContentKey("UserCreated"); !ok { + app.storage.SetCustomContentKey("UserCreated", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("InviteExpiry"); !ok { + app.storage.SetCustomContentKey("InviteExpiry", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("PasswordReset"); !ok { + app.storage.SetCustomContentKey("PasswordReset", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserDeleted"); !ok { + app.storage.SetCustomContentKey("UserDeleted", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserDisabled"); !ok { + app.storage.SetCustomContentKey("UserDisabled", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserEnabled"); !ok { + app.storage.SetCustomContentKey("UserEnabled", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("InviteEmail"); !ok { + app.storage.SetCustomContentKey("InviteEmail", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("WelcomeEmail"); !ok { + app.storage.SetCustomContentKey("WelcomeEmail", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("EmailConfirmation"); !ok { + app.storage.SetCustomContentKey("EmailConfirmation", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserExpired"); !ok { + app.storage.SetCustomContentKey("UserExpired", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserLogin"); !ok { + app.storage.SetCustomContentKey("UserLogin", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserPage"); !ok { + app.storage.SetCustomContentKey("UserPage", emptyCC) + } + if _, ok := app.storage.GetCustomContentKey("UserExpiryAdjusted"); !ok { + app.storage.SetCustomContentKey("UserExpiryAdjusted", emptyCC) + } +} + // Migrate between hyphenated & non-hyphenated user IDs. Doesn't seem to happen anymore, so disabled. // func migrateHyphens(app *appContext) { // checkVersion := func(version string) int { diff --git a/storage.go b/storage.go index 1bc6b01..ff75989 100644 --- a/storage.go +++ b/storage.go @@ -610,16 +610,17 @@ type EmailAddress struct { } type customEmails struct { - UserCreated CustomContent `json:"userCreated"` - InviteExpiry CustomContent `json:"inviteExpiry"` - PasswordReset CustomContent `json:"passwordReset"` - UserDeleted CustomContent `json:"userDeleted"` - UserDisabled CustomContent `json:"userDisabled"` - UserEnabled CustomContent `json:"userEnabled"` - InviteEmail CustomContent `json:"inviteEmail"` - WelcomeEmail CustomContent `json:"welcomeEmail"` - EmailConfirmation CustomContent `json:"emailConfirmation"` - UserExpired CustomContent `json:"userExpired"` + UserCreated CustomContent `json:"userCreated"` + InviteExpiry CustomContent `json:"inviteExpiry"` + PasswordReset CustomContent `json:"passwordReset"` + UserDeleted CustomContent `json:"userDeleted"` + UserDisabled CustomContent `json:"userDisabled"` + UserEnabled CustomContent `json:"userEnabled"` + UserExpiryAdjusted CustomContent `json:"userExpiryAdjusted"` + InviteEmail CustomContent `json:"inviteEmail"` + WelcomeEmail CustomContent `json:"welcomeEmail"` + EmailConfirmation CustomContent `json:"emailConfirmation"` + UserExpired CustomContent `json:"userExpired"` } // CustomContent stores customized versions of jfa-go content, including emails and user messages. @@ -1225,6 +1226,7 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error { patchLang(&lang.UserDeleted, &fallback.UserDeleted, &english.UserDeleted) patchLang(&lang.UserDisabled, &fallback.UserDisabled, &english.UserDisabled) patchLang(&lang.UserEnabled, &fallback.UserEnabled, &english.UserEnabled) + patchLang(&lang.UserExpiryAdjusted, &fallback.UserExpiryAdjusted, &english.UserExpiryAdjusted) patchLang(&lang.InviteEmail, &fallback.InviteEmail, &english.InviteEmail) patchLang(&lang.WelcomeEmail, &fallback.WelcomeEmail, &english.WelcomeEmail) patchLang(&lang.EmailConfirmation, &fallback.EmailConfirmation, &english.EmailConfirmation) @@ -1239,6 +1241,7 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error { patchLang(&lang.UserDeleted, &english.UserDeleted) patchLang(&lang.UserDisabled, &english.UserDisabled) patchLang(&lang.UserEnabled, &english.UserEnabled) + patchLang(&lang.UserExpiryAdjusted, &english.UserExpiryAdjusted) patchLang(&lang.InviteEmail, &english.InviteEmail) patchLang(&lang.WelcomeEmail, &english.WelcomeEmail) patchLang(&lang.EmailConfirmation, &english.EmailConfirmation)