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.
This commit is contained in:
Harvey Tindall
2025-08-31 17:12:50 +01:00
parent 60dbfa2d1e
commit e67f1bf1a9
12 changed files with 135 additions and 125 deletions

View File

@@ -124,7 +124,7 @@ func (app *appContext) deleteExpiredInvite(data Invite) {
func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup { func (app *appContext) sendAdminExpiryNotification(data Invite) *sync.WaitGroup {
notify := data.Notify 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 return nil
} }
var wait sync.WaitGroup 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). // @Summary Get the number of invites stored in the database that have been used (but are still valid).
// @Produce json // @Produce json
// @Success 200 {object} PageCountDTO // @Success 200 {object} PageCountDTO
// @Router /invites/count [get] // @Router /invites/count/used [get]
// @Security Bearer // @Security Bearer
// @tags Invites // @tags Invites
func (app *appContext) GetInviteUsedCount(gc *gin.Context) { func (app *appContext) GetInviteUsedCount(gc *gin.Context) {

View File

@@ -148,11 +148,11 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "PasswordReset": case "PasswordReset":
msg, err = app.email.constructReset(PasswordReset{}, true) msg, err = app.email.constructReset(PasswordReset{}, true)
case "UserDeleted": case "UserDeleted":
msg, err = app.email.constructDeleted("", true) msg, err = app.email.constructDeleted("", "", true)
case "UserDisabled": case "UserDisabled":
msg, err = app.email.constructDisabled("", true) msg, err = app.email.constructDisabled("", "", true)
case "UserEnabled": case "UserEnabled":
msg, err = app.email.constructEnabled("", true) msg, err = app.email.constructEnabled("", "", true)
case "UserExpiryAdjusted": case "UserExpiryAdjusted":
msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", true) msg, err = app.email.constructExpiryAdjusted("", time.Time{}, "", true)
case "ExpiryReminder": case "ExpiryReminder":
@@ -164,7 +164,7 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
case "EmailConfirmation": case "EmailConfirmation":
msg, err = app.email.constructConfirmation("", "", "", true) msg, err = app.email.constructConfirmation("", "", "", true)
case "UserExpired": case "UserExpired":
msg, err = app.email.constructUserExpired(true) msg, err = app.email.constructUserExpired("", true)
case "Announcement": case "Announcement":
case "UserPage": case "UserPage":
case "UserLogin": case "UserLogin":
@@ -181,14 +181,13 @@ func (app *appContext) GetCustomMessageTemplate(gc *gin.Context) {
} }
} }
var mail *Message var mail *Message = nil
if contentInfo.ContentType == CustomMessage { if contentInfo.ContentType == CustomMessage {
mail = &Message{} mail, err = app.email.construct(EmptyCustomContent, CustomContent{
err = app.email.construct(EmptyCustomContent, CustomContent{
Name: EmptyCustomContent.Name, Name: EmptyCustomContent.Name,
Enabled: true, Enabled: true,
Content: "<div class=\"preview-content\"></div>", Content: "<div class=\"preview-content\"></div>",
}, map[string]any{}, mail) }, map[string]any{})
if err != nil { if err != nil {
respondBool(500, false, gc) respondBool(500, false, gc)
return return

View File

@@ -380,19 +380,6 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
"SetPolicy": map[string]string{}, "SetPolicy": map[string]string{},
} }
sendMail := messagesEnabled 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 activityType := ActivityDisabled
if req.Enabled { if req.Enabled {
activityType = ActivityEnabled activityType = ActivityEnabled
@@ -404,6 +391,18 @@ func (app *appContext) EnableDisableUsers(gc *gin.Context) {
app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err) app.err.Printf(lm.FailedGetUser, user.ID, lm.Jellyfin, err)
continue 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) err, _, _ = app.SetUserDisabled(user, !req.Enabled)
if err != nil { if err != nil {
errors["SetPolicy"][user.ID] = err.Error() errors["SetPolicy"][user.ID] = err.Error()
@@ -449,15 +448,6 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
gc.BindJSON(&req) gc.BindJSON(&req)
errors := map[string]string{} errors := map[string]string{}
sendMail := messagesEnabled 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 { for _, userID := range req.Users {
user, err := app.jf.UserByID(userID, false) user, err := app.jf.UserByID(userID, false)
if err != nil { if err != nil {
@@ -465,6 +455,15 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
errors[userID] = err.Error() 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 deleted := false
err, deleted = app.DeleteUser(user) err, deleted = app.DeleteUser(user)
if err != nil { if err != nil {
@@ -677,11 +676,10 @@ func (app *appContext) Announce(gc *gin.Context) {
app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err) app.err.Printf(lm.FailedGetUser, userID, lm.Jellyfin, err)
continue continue
} }
msg := &Message{} msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
err = app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
Enabled: true, Enabled: true,
Content: req.Message, Content: req.Message,
}, map[string]any{"username": user.Name}, msg) }, map[string]any{"username": user.Name})
if err != nil { if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err) app.err.Printf(lm.FailedConstructAnnouncementMessage, userID, err)
respondBool(500, false, gc) respondBool(500, false, gc)
@@ -694,11 +692,10 @@ func (app *appContext) Announce(gc *gin.Context) {
} }
// app.info.Printf(lm.SentAnnouncementMessage, "*", "?") // app.info.Printf(lm.SentAnnouncementMessage, "*", "?")
} else { } else {
msg := &Message{} msg, err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
err := app.email.construct(AnnouncementCustomContent(req.Subject), CustomContent{
Enabled: true, Enabled: true,
Content: req.Message, Content: req.Message,
}, map[string]any{"username": ""}, msg) }, map[string]any{"username": ""})
if err != nil { if err != nil {
app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err) app.err.Printf(lm.FailedConstructAnnouncementMessage, "*", err)
respondBool(500, false, gc) respondBool(500, false, gc)

View File

@@ -31,6 +31,7 @@ func (app *appContext) loadArgs(firstCall bool) {
SWAGGER = flag.Bool("swagger", false, "Enable swagger at /swagger/index.html") 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.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() flag.Parse()
if *help { if *help {
@@ -52,11 +53,14 @@ func (app *appContext) loadArgs(firstCall bool) {
if NO_API_AUTH_DO_NOT_USE && *DEBUG { if NO_API_AUTH_DO_NOT_USE && *DEBUG {
NO_API_AUTH_DO_NOT_USE = false NO_API_AUTH_DO_NOT_USE = false
forceJfID := NO_API_AUTH_FORCE_JFID
NO_API_AUTH_FORCE_JFID = ""
buf := bufio.NewReader(os.Stdin) buf := bufio.NewReader(os.Stdin)
app.err.Print(lm.NoAPIAuthPrompt) app.err.Print(lm.NoAPIAuthPrompt)
sentence, err := buf.ReadBytes('\n') sentence, err := buf.ReadBytes('\n')
if err == nil && strings.ContainsRune(string(sentence), 'y') { if err == nil && strings.ContainsRune(string(sentence), 'y') {
NO_API_AUTH_DO_NOT_USE = true NO_API_AUTH_DO_NOT_USE = true
NO_API_AUTH_FORCE_JFID = forceJfID
} }
} }
} }

13
auth.go
View File

@@ -40,7 +40,11 @@ func (app *appContext) logIpErr(gc *gin.Context, user bool, out string) {
} }
func (app *appContext) webAuth() gin.HandlerFunc { 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) } 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() 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) { func checkToken(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"])

View File

@@ -245,7 +245,7 @@ var customContent = map[string]CustomContentInfo{
"reason", "reason",
), ),
Placeholders: defaultVals(map[string]any{ Placeholders: defaultVals(map[string]any{
"newExpiry": "", "newExpiry": "01/01/01 00:00",
"reason": "Reason", "reason": "Reason",
}), }),
SourceFile: ContentSourceFileInfo{ SourceFile: ContentSourceFileInfo{

View File

@@ -246,14 +246,21 @@ type templ interface {
Execute(wr io.Writer, data interface{}) error 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 { if cc.Enabled {
// Use template email, rather than the built-in's email file. // Use template email, rather than the built-in's email file.
contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile contentInfo.SourceFile = customContent["TemplateEmail"].SourceFile
content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data) content, err := templateEmail(cc.Content, contentInfo.Variables, contentInfo.Conditionals, data)
if err != nil { if err != nil {
emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err) emailer.err.Printf(lm.FailedConstructCustomContent, msg.Subject, err)
return err return msg, err
} }
html := markdown.ToHTML([]byte(content), nil, markdownRenderer) html := markdown.ToHTML([]byte(content), nil, markdownRenderer)
text := stripMarkdown(content) text := stripMarkdown(content)
@@ -268,10 +275,6 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
data = templateData data = templateData
} }
var err error = nil 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 var tpl templ
msg.Text = "" msg.Text = ""
@@ -313,7 +316,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
tpl, err = textTemplate.ParseFS(filesystem, fpath) tpl, err = textTemplate.ParseFS(filesystem, fpath)
} }
if err != nil { 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". // For constructTemplate, if "md" is found in data it's used in stead of "text".
foundMarkdown := false foundMarkdown := false
@@ -326,7 +329,7 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
var tplData bytes.Buffer var tplData bytes.Buffer
err = tpl.Execute(&tplData, data) err = tpl.Execute(&tplData, data)
if err != nil { if err != nil {
return err return msg, err
} }
if foundMarkdown { if foundMarkdown {
data["plaintext"], data["md"] = data["md"], data["plaintext"] data["plaintext"], data["md"] = data["md"], data["plaintext"]
@@ -339,10 +342,10 @@ func (emailer *Emailer) construct(contentInfo CustomContentInfo, cc CustomConten
msg.Markdown = tplData.String() 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] contentInfo := customContent[name]
template := map[string]any{ template := map[string]any{
"username": username, "username": username,
@@ -355,17 +358,14 @@ func (emailer *Emailer) baseValues(name string, username string, placeholders bo
template[v] = "{" + v + "}" template[v] = "{" + v + "}"
} }
} }
email := &Message{ return contentInfo, template
Subject: contentInfo.Subject(emailer.config, &emailer.lang),
}
return contentInfo, template, email
} }
func (emailer *Emailer) constructConfirmation(code, username, key string, placeholders bool) (*Message, error) { func (emailer *Emailer) constructConfirmation(code, username, key string, placeholders bool) (*Message, error) {
if placeholders { if placeholders {
username = "{username}" 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}), "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"), "clickBelow": emailer.lang.EmailConfirmation.get("clickBelow"),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
@@ -381,15 +381,14 @@ func (emailer *Emailer) constructConfirmation(code, username, key string, placeh
template["confirmationURL"] = inviteLink template["confirmationURL"] = inviteLink
} }
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) { func (emailer *Emailer) constructInvite(invite Invite, placeholders bool) (*Message, error) {
expiry := invite.ValidTill expiry := invite.ValidTill
d, t, expiresIn := emailer.formatExpiry(expiry, false) d, t, expiresIn := emailer.formatExpiry(expiry, false)
inviteLink := fmt.Sprintf("%s%s/%s", ExternalURI(nil), PAGES.Form, invite.Code) 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"), "hello": emailer.lang.InviteEmail.get("hello"),
"youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"), "youHaveBeenInvited": emailer.lang.InviteEmail.get("youHaveBeenInvited"),
"toJoin": emailer.lang.InviteEmail.get("toJoin"), "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) template["inviteExpiry"] = emailer.lang.InviteEmail.template("inviteExpiry", template)
} }
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Message, error) { func (emailer *Emailer) constructExpiry(invite Invite, placeholders bool) (*Message, error) {
expiry := formatDatetime(invite.ValidTill) 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"), "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
"expiredAt": emailer.lang.InviteExpiry.get("expiredAt"), "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) template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", template)
} }
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructCreated(username, address string, when time.Time, invite Invite, placeholders bool) (*Message, error) { 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. // NOTE: This was previously invite.Created, not sure why.
created := formatDatetime(when) 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"), "aUserWasCreated": emailer.lang.UserCreated.get("aUserWasCreated"),
"nameString": emailer.lang.Strings.get("name"), "nameString": emailer.lang.Strings.get("name"),
"addressString": emailer.lang.Strings.get("emailAddress"), "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) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructReset(pwr PasswordReset, placeholders bool) (*Message, error) { 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) d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true)
linkResetEnabled := emailer.config.Section("password_resets").Key("link_reset").MustBool(false) 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}), "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": pwr.Username}),
"someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"), "someoneHasRequestedReset": emailer.lang.PasswordReset.get("someoneHasRequestedReset"),
"ifItWasYou": emailer.lang.PasswordReset.get("ifItWasYou"), "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) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructDeleted(reason string, placeholders bool) (*Message, error) { func (emailer *Emailer) constructDeleted(username, reason string, placeholders bool) (*Message, error) {
if placeholders { if placeholders {
username = "{username}"
reason = "{reason}" 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"), "yourAccountWas": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reasonString": emailer.lang.Strings.get("reason"), "reasonString": emailer.lang.Strings.get("reason"),
"reason": reason, "reason": reason,
}) })
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructDisabled(reason string, placeholders bool) (*Message, error) { func (emailer *Emailer) constructDisabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders { if placeholders {
username = "{username}"
reason = "{reason}" 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"), "yourAccountWas": emailer.lang.UserDisabled.get("yourAccountWasDisabled"),
"reasonString": emailer.lang.Strings.get("reason"), "reasonString": emailer.lang.Strings.get("reason"),
"reason": reason, "reason": reason,
}) })
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructEnabled(reason string, placeholders bool) (*Message, error) { func (emailer *Emailer) constructEnabled(username, reason string, placeholders bool) (*Message, error) {
if placeholders { if placeholders {
username = "{username}"
reason = "{reason}" 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"), "yourAccountWas": emailer.lang.UserEnabled.get("yourAccountWasEnabled"),
"reasonString": emailer.lang.Strings.get("reason"), "reasonString": emailer.lang.Strings.get("reason"),
"reason": reason, "reason": reason,
}) })
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructExpiryAdjusted(username string, expiry time.Time, reason string, placeholders bool) (*Message, error) { 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}" username = "{username}"
} }
exp := formatDatetime(expiry) 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}), "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"), "yourExpiryWasAdjusted": emailer.lang.UserExpiryAdjusted.get("yourExpiryWasAdjusted"),
"ifPreviouslyDisabled": emailer.lang.UserExpiryAdjusted.get("ifPreviouslyDisabled"), "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 emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Time, placeholders bool) (*Message, error) { 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}" username = "{username}"
} }
d, t, expiresIn := emailer.formatExpiry(expiry, false) 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}), "helloUser": emailer.lang.Strings.template("helloUser", tmpl{"username": username}),
"yourAccountIsDueToExpire": emailer.lang.ExpiryReminder.get("yourAccountIsDueToExpire"), "yourAccountIsDueToExpire": emailer.lang.ExpiryReminder.get("yourAccountIsDueToExpire"),
"expiresIn": expiresIn, "expiresIn": expiresIn,
@@ -576,8 +570,7 @@ func (emailer *Emailer) constructExpiryReminder(username string, expiry time.Tim
template["yourAccountIsDueToExpire"] = emailer.lang.ExpiryReminder.template("yourAccountIsDueToExpire", template) template["yourAccountIsDueToExpire"] = emailer.lang.ExpiryReminder.template("yourAccountIsDueToExpire", template)
} }
} }
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructWelcome(username string, expiry time.Time, placeholders bool) (*Message, error) { 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}" username = "{username}"
exp = "{yourAccountWillExpire}" 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"), "welcome": emailer.lang.WelcomeEmail.get("welcome"),
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), "youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"),
"jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"), "jellyfinURLString": emailer.lang.WelcomeEmail.get("jellyfinURL"),
@@ -604,18 +597,16 @@ func (emailer *Emailer) constructWelcome(username string, expiry time.Time, plac
template["yourAccountWillExpire"] = exp template["yourAccountWillExpire"] = exp
} }
} }
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
func (emailer *Emailer) constructUserExpired(placeholders bool) (*Message, error) { func (emailer *Emailer) constructUserExpired(username string, placeholders bool) (*Message, error) {
contentInfo, template, msg := emailer.baseValues("UserExpired", "", placeholders, map[string]any{ contentInfo, template := emailer.baseValues("UserExpired", username, placeholders, map[string]any{
"yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"), "yourAccountHasExpired": emailer.lang.UserExpired.get("yourAccountHasExpired"),
"contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"), "contactTheAdmin": emailer.lang.UserExpired.get("contactTheAdmin"),
}) })
cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name) cc := emailer.storage.MustGetCustomContentKey(contentInfo.Name)
err := emailer.construct(contentInfo, cc, template, msg) return emailer.construct(contentInfo, cc, template)
return msg, err
} }
// calls the send method in the underlying emailClient. // calls the send method in the underlying emailClient.

View File

@@ -11,6 +11,7 @@ import (
type GenericDaemon struct { type GenericDaemon struct {
Stopped bool Stopped bool
ShutdownChannel chan string ShutdownChannel chan string
TriggerChannel chan bool
Interval time.Duration Interval time.Duration
period time.Duration period time.Duration
jobs []func(app *appContext) jobs []func(app *appContext)
@@ -27,6 +28,7 @@ func NewGenericDaemon(interval time.Duration, app *appContext, jobs ...func(app
d := GenericDaemon{ d := GenericDaemon{
Stopped: false, Stopped: false,
ShutdownChannel: make(chan string), ShutdownChannel: make(chan string),
TriggerChannel: make(chan bool),
Interval: interval, Interval: interval,
period: interval, period: interval,
app: app, app: app,
@@ -46,6 +48,8 @@ func (d *GenericDaemon) run() {
case <-d.ShutdownChannel: case <-d.ShutdownChannel:
d.ShutdownChannel <- "Down" d.ShutdownChannel <- "Down"
return return
case <-d.TriggerChannel:
break
case <-time.After(d.period): case <-time.After(d.period):
break break
} }
@@ -61,6 +65,10 @@ func (d *GenericDaemon) run() {
} }
} }
func (d *GenericDaemon) Trigger() {
d.TriggerChannel <- true
}
func (d *GenericDaemon) Shutdown() { func (d *GenericDaemon) Shutdown() {
d.Stopped = true d.Stopped = true
d.ShutdownChannel <- "Down" d.ShutdownChannel <- "Down"

37
main.go
View File

@@ -112,18 +112,19 @@ type appContext struct {
adminUsers []User adminUsers []User
invalidTokens []string invalidTokens []string
// Keeping jf name because I can't think of a better one // Keeping jf name because I can't think of a better one
jf *mediabrowser.MediaBrowser jf *mediabrowser.MediaBrowser
authJf *mediabrowser.MediaBrowser authJf *mediabrowser.MediaBrowser
ombi *OmbiWrapper ombi *OmbiWrapper
js *JellyseerrWrapper js *JellyseerrWrapper
thirdPartyServices []ThirdPartyService thirdPartyServices []ThirdPartyService
storage *Storage storage *Storage
validator Validator validator Validator
email *Emailer email *Emailer
telegram *TelegramDaemon telegram *TelegramDaemon
discord *DiscordDaemon discord *DiscordDaemon
matrix *MatrixDaemon matrix *MatrixDaemon
contactMethods []ContactMethodLinker housekeepingDaemon, userDaemon *GenericDaemon
contactMethods []ContactMethodLinker
LoggerSet LoggerSet
host string host string
port int port int
@@ -505,13 +506,13 @@ func start(asDaemon, firstCall bool) {
os.Exit(0) os.Exit(0)
} }
invDaemon := newHousekeepingDaemon(time.Duration(60*time.Second), app) app.housekeepingDaemon = newHousekeepingDaemon(time.Duration(60*time.Second), app)
go invDaemon.run() go app.housekeepingDaemon.run()
defer invDaemon.Shutdown() defer app.housekeepingDaemon.Shutdown()
userDaemon := newUserDaemon(time.Duration(60*time.Second), app) app.userDaemon = newUserDaemon(time.Duration(60*time.Second), app)
go userDaemon.run() go app.userDaemon.run()
defer userDaemon.Shutdown() defer app.userDaemon.Shutdown()
var jellyseerrDaemon *GenericDaemon var jellyseerrDaemon *GenericDaemon
if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) { if app.config.Section("jellyseerr").Key("enabled").MustBool(false) && app.config.Section("jellyseerr").Key("import_existing").MustBool(false) {

View File

@@ -18,6 +18,7 @@ import (
var ( var (
// Disables authentication for the API. Do not use! // Disables authentication for the API. Do not use!
NO_API_AUTH_DO_NOT_USE = false 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. // 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 var api *gin.RouterGroup
if NO_API_AUTH_DO_NOT_USE && *DEBUG { api = router.Group("/", app.webAuth())
api = router.Group("/")
} else {
api = router.Group("/", app.webAuth())
}
for _, p := range routePrefixes { for _, p := range routePrefixes {
var user *gin.RouterGroup var user *gin.RouterGroup
@@ -244,6 +241,8 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)
api.GET(p+"/logs", app.GetLog) 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.POST(p+"/backups", app.CreateBackup)
api.GET(p+"/backups/:fname", app.GetBackup) api.GET(p+"/backups/:fname", app.GetBackup)
api.GET(p+"/backups", app.GetBackups) api.GET(p+"/backups", app.GetBackups)

View File

@@ -1126,9 +1126,9 @@ class MessageEditor {
this._variables.innerHTML = innerHTML this._variables.innerHTML = innerHTML
let buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>; let buttons = this._variables.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
for (let i = 0; i < this._templ.variables.length; i++) { for (let i = 0; i < this._templ.variables.length; i++) {
buttons[i].innerHTML = `<span class="font-mono bg-inherit">` + this._templ.variables[i] + `</span>`; buttons[i].innerHTML = `<span class="font-mono bg-inherit">` + "{" + this._templ.variables[i] + "}" + `</span>`;
buttons[i].onclick = () => { buttons[i].onclick = () => {
insertText(this._textArea, this._templ.variables[i]); insertText(this._textArea, "{" + this._templ.variables[i] + "}");
this.loadPreview(); this.loadPreview();
// this._timeout = setTimeout(this.loadPreview, this._finishInterval); // this._timeout = setTimeout(this.loadPreview, this._finishInterval);
} }
@@ -1146,9 +1146,9 @@ class MessageEditor {
this._conditionals.innerHTML = innerHTML this._conditionals.innerHTML = innerHTML
buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>; buttons = this._conditionals.querySelectorAll("span.button") as NodeListOf<HTMLSpanElement>;
for (let i = 0; i < this._templ.conditionals.length; i++) { for (let i = 0; i < this._templ.conditionals.length; i++) {
buttons[i].innerHTML = `<span class="font-mono bg-inherit">{if ` + this._templ.conditionals[i].slice(1) + `</span>`; buttons[i].innerHTML = `<span class="font-mono bg-inherit">{if ` + this._templ.conditionals[i] + "}" + `</span>`;
buttons[i].onclick = () => { 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.loadPreview();
// this._timeout = setTimeout(this.loadPreview, this._finishInterval); // this._timeout = setTimeout(this.loadPreview, this._finishInterval);
} }
@@ -1162,9 +1162,9 @@ class MessageEditor {
let content = this._textArea.value; let content = this._textArea.value;
if (this._templ.variables) { if (this._templ.variables) {
for (let variable of this._templ.variables) { for (let variable of this._templ.variables) {
let value = this._templ.values[variable.slice(1, -1)]; let value = this._templ.values[variable];
if (value === undefined) { value = variable; } if (value === undefined) { value = "{" + variable + "}"; }
content = content.replace(new RegExp(variable, "g"), value); content = content.replace(new RegExp("{" + variable + "}", "g"), value);
} }
} }
if (this._templ.html == "") { if (this._templ.html == "") {

View File

@@ -173,7 +173,7 @@ func (app *appContext) checkUsers(remindBeforeExpiry *DayTimerSet) {
if name == "" { if name == "" {
continue continue
} }
msg, err := app.email.constructUserExpired(false) msg, err := app.email.constructUserExpired(user.Name, false)
if err != nil { if err != nil {
app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err) app.err.Printf(lm.FailedConstructExpiryMessage, user.ID, err)
} else if err := app.sendByID(msg, user.ID); err != nil { } else if err := app.sendByID(msg, user.ID); err != nil {