From e67f1bf1a988f58b48eb28e0db3a8b79fc795488 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 31 Aug 2025 17:12:50 +0100 Subject: [PATCH] emails: fix and confirm function of all emails both custom and standard emails tested, quite a few fixes made, including to an old bug with admin notifs. --- api-invites.go | 4 +- api-messages.go | 15 +++---- api-users.go | 53 +++++++++++----------- args.go | 4 ++ auth.go | 13 +++++- customcontent.go | 2 +- email.go | 99 +++++++++++++++++++----------------------- generic-d.go | 8 ++++ main.go | 37 ++++++++-------- router.go | 9 ++-- ts/modules/settings.ts | 14 +++--- user-d.go | 2 +- 12 files changed, 135 insertions(+), 125 deletions(-) diff --git a/api-invites.go b/api-invites.go index 1ecfd47..4a47d28 100644 --- a/api-invites.go +++ b/api-invites.go @@ -124,7 +124,7 @@ func (app *appContext) deleteExpiredInvite(data Invite) { func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup { notify := data.Notify - if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) != 0 { + if !emailEnabled || !app.config.Section("notifications").Key("enabled").MustBool(false) || len(notify) == 0 { return nil } var wait sync.WaitGroup @@ -283,7 +283,7 @@ func (app *appContext) GetInviteCount(gc *gin.Context) { // @Summary Get the number of invites stored in the database that have been used (but are still valid). // @Produce json // @Success 200 {object} PageCountDTO -// @Router /invites/count [get] +// @Router /invites/count/used [get] // @Security Bearer // @tags Invites func (app *appContext) GetInviteUsedCount(gc *gin.Context) { diff --git a/api-messages.go b/api-messages.go index 82dbd8d..c3378d1 100644 --- a/api-messages.go +++ b/api-messages.go @@ -148,11 +148,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { case "PasswordReset": msg, err = app.email.constructReset(PasswordReset{}, true) case "UserDeleted": - msg, err = app.email.constructDeleted("", true) + msg, err = app.email.constructDeleted("", "", true) case "UserDisabled": - msg, err = app.email.constructDisabled("", true) + msg, err = app.email.constructDisabled("", "", true) case "UserEnabled": - msg, err = app.email.constructEnabled("", true) + msg, err = app.email.constructEnabled("", "", true) case "UserExpiryAdjusted": msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", true) case "ExpiryReminder": @@ -164,7 +164,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { case "EmailConfirmation": msg, err = app.email.constructConfirmation("", "", "", true) case "UserExpired": - msg, err = app.email.constructUserExpired(true) + msg, err = app.email.constructUserExpired("", true) case "Announcement": case "UserPage": case "UserLogin": @@ -181,14 +181,13 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) { } } - var mail *Message + var mail *Message = nil if contentInfo.ContentType == CustomMessage { - mail = &Message{} - err = app.email.construct(EmptyCustomContent, CustomContent{ + mail, err = app.email.construct(EmptyCustomContent, CustomContent{ Name: EmptyCustomContent.Name, Enabled: true, Content: "
", - }, map[string]any{}, mail) + }, map[string]any{}) if err != nil { respondBool(500, false, gc) return diff --git a/api-users.go b/api-users.go index 3b272c4..dc89f23 100644 --- a/api-users.go +++ b/api-users.go @@ -380,19 +380,6 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { "SetPolicy": map[string]string{}, } sendMail := messagesEnabled - var msg *Message - var err error - if sendMail { - if req.Enabled { - msg, err = app.email.constructEnabled(req.Reason, false) - } else { - msg, err = app.email.constructDisabled(req.Reason, false) - } - if err != nil { - app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err) - sendMail = false - } - } activityType := ActivityDisabled if req.Enabled { activityType = ActivityEnabled @@ -404,6 +391,18 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) { app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err) continue } + var msg *Message + if sendMail { + if req.Enabled { + msg, err = app.email.constructEnabled(user.Name, req.Reason, false) + } else { + msg, err = app.email.constructDisabled(user.Name, req.Reason, false) + } + if err != nil { + app.err.Printf(lm.FailedConstructEnableDisableMessage, "?", err) + sendMail = false + } + } err, _, _ = app.SetUserDisabled(user, !req.Enabled) if err != nil { errors["SetPolicy"][user.ID] = err.Error() @@ -449,15 +448,6 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { gc.BindJSON(&req) errors := map[string]string{} sendMail := messagesEnabled - var msg *Message - var err error - if sendMail { - msg, err = app.email.constructDeleted(req.Reason, false) - if err != nil { - app.err.Printf(lm.FailedConstructDeletionMessage, "?", err) - sendMail = false - } - } for _, userID := range req.Users { user, err := app.jf.UserByID(userID, false) if err != nil { @@ -465,6 +455,15 @@ func (app *appContext) DeleteUsers(gc *gin.Context) { errors[userID] = err.Error() } + var msg *Message = nil + if sendMail { + msg, err = app.email.constructDeleted(user.Name, req.Reason, false) + if err != nil { + app.err.Printf(lm.FailedConstructDeletionMessage, "?", err) + sendMail = false + } + } + deleted := false err, deleted = app.DeleteUser(user) if err != nil { @@ -677,11 +676,10 @@ func (app *appContext) Announce(gc *gin.Context) { app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err) continue } - msg := &Message{} - err = app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{ + msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{ Enabled: true, Content: req.Message, - }, map[string]any{"username": user.Name}, msg) + }, map[string]any{"username": user.Name}) if err != nil { app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err) respondBool(500, false, gc) @@ -694,11 +692,10 @@ func (app *appContext) Announce(gc *gin.Context) { } // app.info.Printf(lm.SentAnnouncementMessage, "*", "?") } else { - msg := &Message{} - err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{ + msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{ Enabled: true, Content: req.Message, - }, map[string]any{"username": ""}, msg) + }, map[string]any{"username": ""}) if err != nil { app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err) respondBool(500, false, gc) diff --git a/args.go b/args.go index 3f2db7d..bf58876 100644 --- a/args.go +++ b/args.go @@ -31,6 +31,7 @@ func (app *appContext) loadArgs(firstCall bool) { SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html") flag.BoolVar(&NO_API_AUTH_DO_NOT_USE, "disable-api-auth-do-not-use", false, "Disables API authentication. DO NOT USE!") + flag.StringVar(&NO_API_AUTH_FORCE_JFID, "disable-api-auth-force-jf-id", "", "Assume given JFID when API auth is disabled.") flag.Parse() if *help { @@ -52,11 +53,14 @@ func (app *appContext) loadArgs(firstCall bool) { if NO_API_AUTH_DO_NOT_USE && *DEBUG { NO_API_AUTH_DO_NOT_USE = false + forceJfID := NO_API_AUTH_FORCE_JFID + NO_API_AUTH_FORCE_JFID = "" buf := bufio.NewReader(os.Stdin) app.err.Print(lm.NoAPIAuthPrompt) sentence, err := buf.ReadBytes('\n') if err == nil && strings.ContainsRune(string(sentence), 'y') { NO_API_AUTH_DO_NOT_USE = true + NO_API_AUTH_FORCE_JFID = forceJfID } } } diff --git a/auth.go b/auth.go index 7a0a939..b1e8e68 100644 --- a/auth.go +++ b/auth.go @@ -40,7 +40,11 @@ func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) { } func (app *appContext) webAuth() gin.HandlerFunc { - return app.authenticate + if NO_API_AUTH_DO_NOT_USE { + return app.bogusAuthenticate + } else { + return app.authenticate + } } func (app *appContext) authLog(v any) { app.debug.PrintfCustomLevel(4, lm.FailedAuthRequest, v) } @@ -138,6 +142,13 @@ func (app *appContext) authenticate(gc *gin.Context) { gc.Next() } +// bogusAuthenticate is for use with NO_API_AUTH_DO_NOT_USE, it sets the jfId/userId value from NO_API_AUTH_FORCE_JF_ID. +func (app *appContext) bogusAuthenticate(gc *gin.Context) { + gc.Set("jfId", NO_API_AUTH_FORCE_JFID) + gc.Set("userId", NO_API_AUTH_FORCE_JFID) + gc.Next() +} + func checkToken(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) diff --git a/customcontent.go b/customcontent.go index 09f37fd..21c623c 100644 --- a/customcontent.go +++ b/customcontent.go @@ -245,7 +245,7 @@ var customContent = map[string]CustomContentInfo{ "reason", ), Placeholders: defaultVals(map[string]any{ - "newExpiry": "", + "newExpiry": "01/01/01 00:00", "reason": "Reason", }), SourceFile: ContentSourceFileInfo{ diff --git a/email.go b/email.go index d5a8fdc..78e94a1 100644 --- a/email.go +++ b/email.go @@ -246,14 +246,21 @@ type templ interface { Execute(wr io.Writer, data interface{}) error } -func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomContent, data map[string]any, msg *Message) error { +func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomContent, data map[string]any) (*Message, error) { + msg := &Message{ + Subject: contentInfo.Subject(emailer.config, &emailer.lang), + } + // Template the subject for bonus points + if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil { + msg.Subject = subject + } if cc.Enabled { // Use template email, rather than the built-in's email file. contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data) if err != nil { emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err) - return err + return msg, err } html := markdown.ToHTML([]byte(content), nil, markdownRenderer) text := stripMarkdown(content) @@ -268,10 +275,6 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten data = templateData } var err error = nil - // Template the subject for bonus points - if subject, err := templateEmail(msg.Subject, contentInfo.Variables, contentInfo.Conditionals, data); err == nil { - msg.Subject = subject - } var tpl templ msg.Text = "" @@ -313,7 +316,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten tpl, err = textTemplate.ParseFS(filesystem, fpath) } if err != nil { - return fmt.Errorf("error reading from fs path \"%s\": %v", fpath, err) + return msg, fmt.Errorf("error reading from fs path \"%s\": %v", fpath, err) } // For constructTemplate, if "md" is found in data it's used in stead of "text". foundMarkdown := false @@ -326,7 +329,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten var tplData bytes.Buffer err = tpl.Execute(&tplData, data) if err != nil { - return err + return msg, err } if foundMarkdown { data["plaintext"], data["md"] = data["md"], data["plaintext"] @@ -339,10 +342,10 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten msg.Markdown = tplData.String() } } - return nil + return msg, nil } -func (emailer *Emailer) baseValues(name string, username string, placeholders bool, values map[string]any) (CustomContentInfo, map[string]any, *Message) { +func (emailer *Emailer) baseValues(name string, username string, placeholders bool, values map[string]any) (CustomContentInfo, map[string]any) { contentInfo := customContent[name] template := map[string]any{ "username": username, @@ -355,17 +358,14 @@ func (emailer *Emailer) baseValues(name string, username string, placeholders bo template[v] = "{" + v + "}" } } - email := &Message{ - Subject: contentInfo.Subject(emailer.config, &emailer.lang), - } - return contentInfo, template, email + return contentInfo, template } func (emailer *Emailer) constructConfirmation(code, username, key string, placeholders bool) (*Message, error) { if placeholders { username = "{username}" } - contentInfo, template, msg := emailer.baseValues("EmailConfirmation", username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("EmailConfirmation", username, placeholders, map[string]any{ "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}), "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), @@ -381,15 +381,14 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, placeh template["confirmationURL"] = inviteLink } cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) { expiry := invite.ValidTill d, t, expiresIn := emailer.formatExpiry(expiry, false) inviteLink := fmt.Sprintf("%s%s/%s", ExternalURI(nil), PAGES.Form, invite.Code) - contentInfo, template, msg := emailer.baseValues("InviteEmail", "", placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("InviteEmail", "", placeholders, map[string]any{ "hello": emailer.lang.InviteEmail.get("hello"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), "toJoin": emailer.lang.InviteEmail.get("toJoin"), @@ -404,13 +403,12 @@ func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Mess template["inviteExpiry"] = emailer.lang.InviteEmail.template("inviteExpiry", template) } cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Message, error) { expiry := formatDatetime(invite.ValidTill) - contentInfo, template, msg := emailer.baseValues("InviteExpiry", "", placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("InviteExpiry", "", placeholders, map[string]any{ "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), "expiredAt": emailer.lang.InviteExpiry.get("expiredAt"), @@ -421,14 +419,13 @@ func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Mess template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", template) } cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructCreated(username, address string, when time.Time, invite Invite, placeholders bool) (*Message, error) { // NOTE: This was previously invite.Created, not sure why. created := formatDatetime(when) - contentInfo, template, msg := emailer.baseValues("UserCreated", username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("UserCreated", username, placeholders, map[string]any{ "aUserWasCreated": emailer.lang.UserCreated.get("aUserWasCreated"), "nameString": emailer.lang.Strings.get("name"), "addressString": emailer.lang.Strings.get("emailAddress"), @@ -446,8 +443,7 @@ func (emailer *Emailer) constructCreated(username, address string, when time.Tim } } cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*Message, error) { @@ -456,7 +452,7 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*M } d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true) linkResetEnabled := emailer.config.Section("password_resets").Key("link_reset").MustBool(false) - contentInfo, template, msg := emailer.baseValues("PasswordReset", pwr.Username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("PasswordReset", pwr.Username, placeholders, map[string]any{ "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}), "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), "ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), @@ -487,50 +483,49 @@ func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*M } } cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } -func (emailer *Emailer) constructDeleted(reason string, placeholders bool) (*Message, error) { +func (emailer *Emailer) constructDeleted(username, reason string, placeholders bool) (*Message, error) { if placeholders { + username = "{username}" reason = "{reason}" } - contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("UserDeleted", username, placeholders, map[string]any{ "yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), "reasonString": emailer.lang.Strings.get("reason"), "reason": reason, }) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } -func (emailer *Emailer) constructDisabled(reason string, placeholders bool) (*Message, error) { +func (emailer *Emailer) constructDisabled(username, reason string, placeholders bool) (*Message, error) { if placeholders { + username = "{username}" reason = "{reason}" } - contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("UserDisabled", username, placeholders, map[string]any{ "yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"), "reasonString": emailer.lang.Strings.get("reason"), "reason": reason, }) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } -func (emailer *Emailer) constructEnabled(reason string, placeholders bool) (*Message, error) { +func (emailer *Emailer) constructEnabled(username, reason string, placeholders bool) (*Message, error) { if placeholders { + username = "{username}" reason = "{reason}" } - contentInfo, template, msg := emailer.baseValues("UserDeleted", "", placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("UserEnabled", username, placeholders, map[string]any{ "yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"), "reasonString": emailer.lang.Strings.get("reason"), "reason": reason, }) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Time, reason string, placeholders bool) (*Message, error) { @@ -538,7 +533,7 @@ func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Tim username = "{username}" } exp := formatDatetime(expiry) - contentInfo, template, msg := emailer.baseValues("UserExpiryAdjusted", username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("UserExpiryAdjusted", username, placeholders, map[string]any{ "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}), "yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"), "ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"), @@ -554,8 +549,7 @@ func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Tim }) } } - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Time, placeholders bool) (*Message, error) { @@ -563,7 +557,7 @@ func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Tim username = "{username}" } d, t, expiresIn := emailer.formatExpiry(expiry, false) - contentInfo, template, msg := emailer.baseValues("ExpiryReminder", username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("ExpiryReminder", username, placeholders, map[string]any{ "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}), "yourAccountIsDueToExpire": emailer.lang.ExpiryReminder.get("yourAccountIsDueToExpire"), "expiresIn": expiresIn, @@ -576,8 +570,7 @@ func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Tim template["yourAccountIsDueToExpire"] = emailer.lang.ExpiryReminder.template("yourAccountIsDueToExpire", template) } } - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } func (emailer *Emailer) constructWelcome(username string, expiry time.Time, placeholders bool) (*Message, error) { @@ -586,7 +579,7 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, plac username = "{username}" exp = "{yourAccountWillExpire}" } - contentInfo, template, msg := emailer.baseValues("WelcomeEmail", username, placeholders, map[string]any{ + contentInfo, template := emailer.baseValues("WelcomeEmail", username, placeholders, map[string]any{ "welcome": emailer.lang.WelcomeEmail.get("welcome"), "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), "jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"), @@ -604,18 +597,16 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, plac template["yourAccountWillExpire"] = exp } } - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } -func (emailer *Emailer) constructUserExpired(placeholders bool) (*Message, error) { - contentInfo, template, msg := emailer.baseValues("UserExpired", "", placeholders, map[string]any{ +func (emailer *Emailer) constructUserExpired(username string, placeholders bool) (*Message, error) { + contentInfo, template := emailer.baseValues("UserExpired", username, placeholders, map[string]any{ "yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"), "contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"), }) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) - err := emailer.construct(contentInfo, cc, template, msg) - return msg, err + return emailer.construct(contentInfo, cc, template) } // calls the send method in the underlying emailClient. diff --git a/generic-d.go b/generic-d.go index 775ac0c..e10e9d1 100644 --- a/generic-d.go +++ b/generic-d.go @@ -11,6 +11,7 @@ import ( type GenericDaemon struct { Stopped bool ShutdownChannel chan string + TriggerChannel chan bool Interval time.Duration period time.Duration jobs []func(app *appContext) @@ -27,6 +28,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app d := GenericDaemon{ Stopped: false, ShutdownChannel: make(chan string), + TriggerChannel: make(chan bool), Interval: interval, period: interval, app: app, @@ -46,6 +48,8 @@ func (d *GenericDaemon) run() { case <-d.ShutdownChannel: d.ShutdownChannel <- "Down" return + case <-d.TriggerChannel: + break case <-time.After(d.period): break } @@ -61,6 +65,10 @@ func (d *GenericDaemon) run() { } } +func (d *GenericDaemon) Trigger() { + d.TriggerChannel <- true +} + func (d *GenericDaemon) Shutdown() { d.Stopped = true d.ShutdownChannel <- "Down" diff --git a/main.go b/main.go index 8c6c712..24d392f 100644 --- a/main.go +++ b/main.go @@ -112,18 +112,19 @@ type appContext struct { adminUsers []User invalidTokens []string // Keeping jf name because I can't think of a better one - jf *mediabrowser.MediaBrowser - authJf *mediabrowser.MediaBrowser - ombi *OmbiWrapper - js *JellyseerrWrapper - thirdPartyServices []ThirdPartyService - storage *Storage - validator Validator - email *Emailer - telegram *TelegramDaemon - discord *DiscordDaemon - matrix *MatrixDaemon - contactMethods []ContactMethodLinker + jf *mediabrowser.MediaBrowser + authJf *mediabrowser.MediaBrowser + ombi *OmbiWrapper + js *JellyseerrWrapper + thirdPartyServices []ThirdPartyService + storage *Storage + validator Validator + email *Emailer + telegram *TelegramDaemon + discord *DiscordDaemon + matrix *MatrixDaemon + housekeepingDaemon, userDaemon *GenericDaemon + contactMethods []ContactMethodLinker LoggerSet host string port int @@ -505,13 +506,13 @@ func start(asDaemon, firstCall bool) { os.Exit(0) } - invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app) - go invDaemon.run() - defer invDaemon.Shutdown() + app.housekeepingDaemon = newHousekeepingDaemon(time.Duration(60*time.Second), app) + go app.housekeepingDaemon.run() + defer app.housekeepingDaemon.Shutdown() - userDaemon := newUserDaemon(time.Duration(60*time.Second), app) - go userDaemon.run() - defer userDaemon.Shutdown() + app.userDaemon = newUserDaemon(time.Duration(60*time.Second), app) + go app.userDaemon.run() + defer app.userDaemon.Shutdown() var jellyseerrDaemon *GenericDaemon if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) { diff --git a/router.go b/router.go index d69fb79..e086ae6 100644 --- a/router.go +++ b/router.go @@ -18,6 +18,7 @@ import ( var ( // Disables authentication for the API. Do not use! NO_API_AUTH_DO_NOT_USE = false + NO_API_AUTH_FORCE_JFID = "" ) // loads HTML templates. If [files]/html_templates is set, alternative files inside the directory are loaded in place of the internal templates. @@ -188,11 +189,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) { } var api *gin.RouterGroup - if NO_API_AUTH_DO_NOT_USE && *DEBUG { - api = router.Group("/") - } else { - api = router.Group("/", app.webAuth()) - } + api = router.Group("/", app.webAuth()) for _, p := range routePrefixes { var user *gin.RouterGroup @@ -244,6 +241,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) { api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/restart", app.restart) api.GET(p+"/logs", app.GetLog) + api.POST(p+"/tasks/housekeeping", func(gc *gin.Context) { app.housekeepingDaemon.Trigger(); gc.Status(http.StatusNoContent) }) + api.POST(p+"/tasks/users", func(gc *gin.Context) { app.userDaemon.Trigger(); gc.Status(http.StatusNoContent) }) api.POST(p+"/backups", app.CreateBackup) api.GET(p+"/backups/:fname", app.GetBackup) api.GET(p+"/backups", app.GetBackups) diff --git a/ts/modules/settings.ts b/ts/modules/settings.ts index e927252..19a74a6 100644 --- a/ts/modules/settings.ts +++ b/ts/modules/settings.ts @@ -1126,9 +1126,9 @@ class MessageEditor { this._variables.innerHTML = innerHTML let buttons = this._variables.querySelectorAll("span.button") as NodeListOf; for (let i = 0; i < this._templ.variables.length; i++) { - buttons[i].innerHTML = `` + this._templ.variables[i] + ``; + buttons[i].innerHTML = `` + "{" + this._templ.variables[i] + "}" + ``; buttons[i].onclick = () => { - insertText(this._textArea, this._templ.variables[i]); + insertText(this._textArea, "{" + this._templ.variables[i] + "}"); this.loadPreview(); // this._timeout = setTimeout(this.loadPreview, this._finishInterval); } @@ -1146,9 +1146,9 @@ class MessageEditor { this._conditionals.innerHTML = innerHTML buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf; for (let i = 0; i < this._templ.conditionals.length; i++) { - buttons[i].innerHTML = `{if ` + this._templ.conditionals[i].slice(1) + ``; + buttons[i].innerHTML = `{if ` + this._templ.conditionals[i] + "}" + ``; buttons[i].onclick = () => { - insertText(this._textArea, "{if " + this._templ.conditionals[i].slice(1) + "{endif}"); + insertText(this._textArea, "{if " + this._templ.conditionals[i] + "}" + "{endif}"); this.loadPreview(); // this._timeout = setTimeout(this.loadPreview, this._finishInterval); } @@ -1162,9 +1162,9 @@ class MessageEditor { let content = this._textArea.value; if (this._templ.variables) { for (let variable of this._templ.variables) { - let value = this._templ.values[variable.slice(1, -1)]; - if (value === undefined) { value = variable; } - content = content.replace(new RegExp(variable, "g"), value); + let value = this._templ.values[variable]; + if (value === undefined) { value = "{" + variable + "}"; } + content = content.replace(new RegExp("{" + variable + "}", "g"), value); } } if (this._templ.html == "") { diff --git a/user-d.go b/user-d.go index b605d97..d4aca9c 100644 --- a/user-d.go +++ b/user-d.go @@ -173,7 +173,7 @@ func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) { if name == "" { continue } - msg, err := app.email.constructUserExpired(false) + msg, err := app.email.constructUserExpired(user.Name, false) if err != nil { app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err) } else if err := app.sendByID(msg, user.ID); err != nil {