1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-10-18 09:00:11 +00:00

Compare commits

...

5 Commits

Author SHA1 Message Date
6ffdd4dad7
fix mistype in german email 2021-02-20 01:31:34 +00:00
98d59ba4e0
don't strip text on images 2021-02-20 01:20:43 +00:00
938523c18b
fix urls in custom email/announcements
Uses a nasty algorithm found in stripmd.go to change all occurrences
of '[linktext](link)' to just 'link' before passing to a decent markdown
stripper.
2021-02-20 01:03:11 +00:00
cc4e12c405
finish backend of custom emails
biggest bodge i've ever done but it works i guess.
2021-02-20 00:22:40 +00:00
eb406ef951
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.
2021-02-19 21:38:20 +00:00
26 changed files with 614 additions and 117 deletions

253
api.go
View File

@ -126,7 +126,7 @@ func (app *appContext) checkInvites() {
wait.Add(1) wait.Add(1)
go func(addr string) { go func(addr string) {
defer wait.Done() defer wait.Done()
msg, err := app.email.constructExpiry(code, data, app) msg, err := app.email.constructExpiry(code, data, app, false)
if err != nil { if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) app.err.Printf("%s: Failed to construct expiry notification: %s", code, err)
} else if err := app.email.send(msg, addr); err != nil { } 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 { for address, settings := range notify {
if settings["notify-expiry"] { if settings["notify-expiry"] {
go func() { go func() {
msg, err := app.email.constructExpiry(code, inv, app) msg, err := app.email.constructExpiry(code, inv, app, false)
if err != nil { if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %s", code, err) app.err.Printf("%s: Failed to construct expiry notification: %s", code, err)
} else if err := app.email.send(msg, address); err != nil { } 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 != "" { 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) 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 { if err != nil {
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err) app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
respondUser(500, true, false, err.Error(), gc) 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) { f = func(gc *gin.Context) {
app.debug.Printf("%s: Email confirmation required", req.Code) app.debug.Printf("%s: Email confirmation required", req.Code)
respond(401, "confirmEmail", gc) 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 { if err != nil {
app.err.Printf("%s: Failed to construct confirmation email: %s", req.Code, err) app.err.Printf("%s: Failed to construct confirmation email: %s", req.Code, err)
} else if err := app.email.send(msg, req.Email); err != nil { } 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 { for address, settings := range invite.Notify {
if settings["notify-creation"] { if settings["notify-creation"] {
go func() { 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 { if err != nil {
app.err.Printf("%s: Failed to construct user creation notification: %s", req.Code, err) app.err.Printf("%s: Failed to construct user creation notification: %s", req.Code, err)
} else if err := app.email.send(msg, address); err != nil { } 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 != "" { 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) 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 { if err != nil {
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err) app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
} else if err := app.email.send(msg, req.Email); err != nil { } else if err := app.email.send(msg, req.Email); err != nil {
@ -516,7 +516,7 @@ func (app *appContext) Announce(gc *gin.Context) {
} }
addresses = append(addresses, addr.(string)) addresses = append(addresses, addr.(string))
} }
msg, err := app.email.constructAnnouncement(req.Subject, req.Message, app) msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
if err != nil { if err != nil {
app.err.Printf("Failed to construct announcement emails: %s", err) app.err.Printf("Failed to construct announcement emails: %s", err)
respondBool(500, false, gc) respondBool(500, false, gc)
@ -576,7 +576,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
} }
if len(addresses) != 0 { if len(addresses) != 0 {
go func(reason string, addresses []string) { go func(reason string, addresses []string) {
msg, err := app.email.constructDeleted(reason, app) msg, err := app.email.constructDeleted(reason, app, false)
if err != nil { if err != nil {
app.err.Printf("Failed to construct account deletion emails: %s", err) app.err.Printf("Failed to construct account deletion emails: %s", err)
} else if err := app.email.send(msg, addresses...); err != nil { } 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) { if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", inviteCode) app.debug.Printf("%s: Sending invite email", inviteCode)
invite.Email = req.Email invite.Email = req.Email
msg, err := app.email.constructInvite(inviteCode, invite, app) msg, err := app.email.constructInvite(inviteCode, invite, app, false)
if err != nil { if err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email) invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: Failed to construct invite email: %s", inviteCode, err) app.err.Printf("%s: Failed to construct invite email: %s", inviteCode, err)
@ -1269,6 +1269,239 @@ 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 Sets the corresponding custom email.
// @Produce json
// @Param customEmail body customEmail true "Content = email (in markdown)."
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /config/emails/{id} [post]
// @tags Configuration
func (app *appContext) SetEmail(gc *gin.Context) {
var req customEmail
gc.BindJSON(&req)
id := gc.Param("id")
if req.Content == "" {
respondBool(400, false, gc)
return
}
if id == "UserCreated" {
app.storage.customEmails.UserCreated.Content = req.Content
app.storage.customEmails.UserCreated.Enabled = true
} else if id == "InviteExpiry" {
app.storage.customEmails.InviteExpiry.Content = req.Content
app.storage.customEmails.InviteExpiry.Enabled = true
} else if id == "PasswordReset" {
app.storage.customEmails.PasswordReset.Content = req.Content
app.storage.customEmails.PasswordReset.Enabled = true
} else if id == "UserDeleted" {
app.storage.customEmails.UserDeleted.Content = req.Content
app.storage.customEmails.UserDeleted.Enabled = true
} else if id == "InviteEmail" {
app.storage.customEmails.InviteEmail.Content = req.Content
app.storage.customEmails.InviteEmail.Enabled = true
} else if id == "WelcomeEmail" {
app.storage.customEmails.WelcomeEmail.Content = req.Content
app.storage.customEmails.WelcomeEmail.Enabled = true
} else if id == "EmailConfirmation" {
app.storage.customEmails.EmailConfirmation.Content = req.Content
app.storage.customEmails.EmailConfirmation.Enabled = true
} else {
respondBool(400, false, gc)
return
}
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Enable/Disable custom email.
// @Produce json
// @Success 200 {object} boolResponse
// @Failure 400 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /config/emails/{id}/{enable/disable} [post]
// @tags Configuration
func (app *appContext) SetEmailState(gc *gin.Context) {
id := gc.Param("id")
s := gc.Param("state")
enabled := false
if s == "enable" {
enabled = true
} else if s != "disable" {
respondBool(400, false, gc)
}
if id == "UserCreated" {
app.storage.customEmails.UserCreated.Enabled = enabled
} else if id == "InviteExpiry" {
app.storage.customEmails.InviteExpiry.Enabled = enabled
} else if id == "PasswordReset" {
app.storage.customEmails.PasswordReset.Enabled = enabled
} else if id == "UserDeleted" {
app.storage.customEmails.UserDeleted.Enabled = enabled
} else if id == "InviteEmail" {
app.storage.customEmails.InviteEmail.Enabled = enabled
} else if id == "WelcomeEmail" {
app.storage.customEmails.WelcomeEmail.Enabled = enabled
} else if id == "EmailConfirmation" {
app.storage.customEmails.EmailConfirmation.Enabled = enabled
} else {
respondBool(400, false, gc)
return
}
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
respondBool(200, true, gc)
}
// @Summary Returns the boilerplate email and list of used variables in it.
// @Produce json
// @Success 200 {object} customEmail
// @Failure 400 {object} boolResponse
// @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
var variables []string
var writeVars func(variables []string)
newEmail := false
if id == "UserCreated" {
content = app.storage.customEmails.UserCreated.Content
if content == "" {
newEmail = true
msg, err = app.email.constructCreated("", "", "", Invite{}, app, true)
content = msg.text
} else {
variables = app.storage.customEmails.UserCreated.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserCreated.Variables = variables }
// app.storage.customEmails.UserCreated = content
} else if id == "InviteExpiry" {
content = app.storage.customEmails.InviteExpiry.Content
if content == "" {
newEmail = true
msg, err = app.email.constructExpiry("", Invite{}, app, true)
content = msg.text
} else {
variables = app.storage.customEmails.InviteExpiry.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.InviteExpiry.Variables = variables }
// app.storage.customEmails.InviteExpiry = content
} else if id == "PasswordReset" {
content = app.storage.customEmails.PasswordReset.Content
if content == "" {
newEmail = true
msg, err = app.email.constructReset(PasswordReset{}, app, true)
content = msg.text
} else {
variables = app.storage.customEmails.PasswordReset.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.PasswordReset.Variables = variables }
// app.storage.customEmails.PasswordReset = content
} else if id == "UserDeleted" {
content = app.storage.customEmails.UserDeleted.Content
if content == "" {
newEmail = true
msg, err = app.email.constructDeleted("", app, true)
content = msg.text
} else {
variables = app.storage.customEmails.UserDeleted.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.UserDeleted.Variables = variables }
// app.storage.customEmails.UserDeleted = content
} else if id == "InviteEmail" {
content = app.storage.customEmails.InviteEmail.Content
if content == "" {
newEmail = true
msg, err = app.email.constructInvite("", Invite{}, app, true)
content = msg.text
} else {
variables = app.storage.customEmails.InviteEmail.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.InviteEmail.Variables = variables }
// app.storage.customEmails.InviteEmail = content
} else if id == "WelcomeEmail" {
content = app.storage.customEmails.WelcomeEmail.Content
if content == "" {
newEmail = true
msg, err = app.email.constructWelcome("", app, true)
content = msg.text
} else {
variables = app.storage.customEmails.WelcomeEmail.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.WelcomeEmail.Variables = variables }
// app.storage.customEmails.WelcomeEmail = content
} else if id == "EmailConfirmation" {
content = app.storage.customEmails.EmailConfirmation.Content
if content == "" {
newEmail = true
msg, err = app.email.constructConfirmation("", "", "", app, true)
content = msg.text
} else {
variables = app.storage.customEmails.EmailConfirmation.Variables
}
writeVars = func(variables []string) { app.storage.customEmails.EmailConfirmation.Variables = variables }
// app.storage.customEmails.EmailConfirmation = content
} else {
respondBool(400, false, gc)
return
}
if err != nil {
respondBool(500, false, gc)
return
}
if newEmail {
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++
}
}
writeVars(variables)
}
if app.storage.storeCustomEmails() != nil {
respondBool(500, false, gc)
return
}
gc.JSON(200, customEmail{Content: content, Variables: variables})
}
// @Summary Logout by deleting refresh token from cookies. // @Summary Logout by deleting refresh token from cookies.
// @Produce json // @Produce json
// @Success 200 {object} boolResponse // @Success 200 {object} boolResponse
@ -1291,7 +1524,7 @@ func (app *appContext) Logout(gc *gin.Context) {
// @Produce json // @Produce json
// @Success 200 {object} langDTO // @Success 200 {object} langDTO
// @Failure 500 {object} stringResponse // @Failure 500 {object} stringResponse
// @Router /lang [get] // @Router /lang/{page} [get]
// @tags Other // @tags Other
func (app *appContext) GetLanguages(gc *gin.Context) { func (app *appContext) GetLanguages(gc *gin.Context) {
page := gc.Param("page") page := gc.Param("page")

View File

@ -32,11 +32,12 @@ 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())) 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() { for _, key := range app.config.Section("files").Keys() {
if key.Name() != "html_templates" { if name := key.Name(); name != "html_templates" && name != "lang_files" {
fmt.Println(name)
key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json")))) key.SetValue(key.MustString(filepath.Join(app.dataPath, (key.Name() + ".json"))))
} }
} }
for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template"} { for _, key := range []string{"user_configuration", "user_displayprefs", "user_profiles", "ombi_template", "invites", "emails", "user_template", "custom_emails"} {
app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json")))) app.config.Section("files").Key(key).SetValue(app.config.Section("files").Key(key).MustString(filepath.Join(app.dataPath, (key + ".json"))))
} }
app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/") app.URLBase = strings.TrimSuffix(app.config.Section("ui").Key("url_base").MustString(""), "/")
@ -63,8 +64,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_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("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("template_email").Key("email_html").SetValue(app.config.Section("template_email").Key("email_html").MustString("jfa-go:" + "template.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_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("version").SetValue(VERSION)
app.config.Section("jellyfin").Key("device").SetValue("jfa-go") app.config.Section("jellyfin").Key("device").SetValue("jfa-go")
@ -76,6 +77,9 @@ func (app *appContext) loadConfig() error {
emailEnabled = true 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("") substituteStrings = app.config.Section("jellyfin").Key("substitute_jellyfin_strings").MustString("")
oldFormLang := app.config.Section("ui").Key("language").MustString("") oldFormLang := app.config.Section("ui").Key("language").MustString("")

View File

@ -853,6 +853,14 @@
"type": "text", "type": "text",
"value": "", "value": "",
"description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info." "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."
} }
} }
} }

285
email.go
View File

@ -18,7 +18,6 @@ import (
jEmail "github.com/jordan-wright/email" jEmail "github.com/jordan-wright/email"
"github.com/knz/strtime" "github.com/knz/strtime"
"github.com/mailgun/mailgun-go/v4" "github.com/mailgun/mailgun-go/v4"
stripmd "github.com/writeas/go-strip-markdown"
) )
// implements email sending, right now via smtp or mailgun. // implements email sending, right now via smtp or mailgun.
@ -212,36 +211,58 @@ func (emailer *Emailer) construct(app *appContext, section, keyFragment string,
return 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{ email := &Email{
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.EmailConfirmation.get("title")), 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 var err error
email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", map[string]interface{}{ template := map[string]interface{}{
"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"),
"urlVal": inviteLink,
"confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"), "confirmEmail": emailer.lang.EmailConfirmation.get("confirmEmail"),
"message": message, "message": "",
}) "username": username,
}
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
}
if app.storage.customEmails.EmailConfirmation.Enabled {
content := app.storage.customEmails.EmailConfirmation.Content
for _, v := range app.storage.customEmails.EmailConfirmation.Variables {
replaceWith, ok := template[v[1:len(v)-1]]
if ok {
content = strings.ReplaceAll(content, v, replaceWith.(string))
}
}
email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "email_confirmation", "email_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil return email, nil
} }
func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContext) (*Email, error) { func (emailer *Emailer) constructTemplate(subject, md string, app *appContext) (*Email, error) {
email := &Email{subject: subject} email := &Email{subject: subject}
renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants}) renderer := html.NewRenderer(html.RendererOptions{Flags: html.Smartypants})
html := markdown.ToHTML([]byte(md), nil, renderer) html := markdown.ToHTML([]byte(md), nil, renderer)
text := strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(md), "</p>"), "<p>") text := stripMarkdown(md)
message := app.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
var err error 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), "text": template.HTML(html),
"plaintext": text, "plaintext": text,
"message": message, "message": message,
@ -252,7 +273,7 @@ func (emailer *Emailer) constructAnnouncement(subject, md string, app *appContex
return email, nil 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{ email := &Email{
subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")), subject: app.config.Section("email_confirmation").Key("subject").MustString(emailer.lang.InviteEmail.get("title")),
} }
@ -261,120 +282,251 @@ func (emailer *Emailer) constructInvite(code string, invite Invite, app *appCont
message := app.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
inviteLink := app.config.Section("invite_emails").Key("url_base").String() inviteLink := app.config.Section("invite_emails").Key("url_base").String()
inviteLink = fmt.Sprintf("%s/%s", inviteLink, code) inviteLink = fmt.Sprintf("%s/%s", inviteLink, code)
var err error template := map[string]interface{}{
email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", map[string]interface{}{
"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"),
"inviteExpiry": emailer.lang.InviteEmail.template("inviteExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}),
"linkButton": emailer.lang.InviteEmail.get("linkButton"), "linkButton": emailer.lang.InviteEmail.get("linkButton"),
"invite_link": inviteLink, "message": "",
"message": message, "date": d,
}) "time": t,
"expiresInMinutes": expiresIn,
}
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
if app.storage.customEmails.InviteEmail.Enabled {
content := app.storage.customEmails.InviteEmail.Content
for _, v := range app.storage.customEmails.InviteEmail.Variables {
replaceWith, ok := template[v[1:len(v)-1]]
if ok {
content = strings.ReplaceAll(content, v, replaceWith.(string))
}
}
email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "invite_emails", "email_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil 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{ email := &Email{
subject: emailer.lang.InviteExpiry.get("title"), subject: emailer.lang.InviteExpiry.get("title"),
} }
expiry := app.formatDatetime(invite.ValidTill) expiry := app.formatDatetime(invite.ValidTill)
var err error template := map[string]interface{}{
email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", map[string]interface{}{
"inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"), "inviteExpired": emailer.lang.InviteExpiry.get("inviteExpired"),
"expiredAt": emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": "\"" + code + "\"", "time": expiry}),
"notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"), "notificationNotice": emailer.lang.InviteExpiry.get("notificationNotice"),
}) "code": "\"" + code + "\"",
"time": expiry,
}
if noSub {
template["expiredAt"] = emailer.lang.InviteExpiry.get("expiredAt")
} else {
template["expiredAt"] = emailer.lang.InviteExpiry.template("expiredAt", tmpl{"code": template["code"].(string), "time": template["time"].(string)})
}
var err error
if app.storage.customEmails.InviteExpiry.Enabled {
content := app.storage.customEmails.InviteExpiry.Content
for _, v := range app.storage.customEmails.InviteExpiry.Variables {
replaceWith, ok := template[v[1:len(v)-1]]
if ok {
content = strings.ReplaceAll(content, v, replaceWith.(string))
}
}
email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "notifications", "expiry_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil 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{ email := &Email{
subject: emailer.lang.UserCreated.get("title"), subject: emailer.lang.UserCreated.get("title"),
} }
created := app.formatDatetime(invite.Created) template := map[string]interface{}{
var tplAddress string "nameString": emailer.lang.Strings.get("name"),
if app.config.Section("email").Key("no_username").MustBool(false) { "addressString": emailer.lang.Strings.get("emailAddress"),
tplAddress = "n/a" "timeString": emailer.lang.UserCreated.get("time"),
"notificationNotice": "",
"code": "\"" + code + "\"",
}
if noSub {
template["aUserWasCreated"] = emailer.lang.UserCreated.get("aUserWasCreated")
empty := []string{"name", "address", "time"}
for _, v := range empty {
template[v] = "{" + v + "}"
}
} else { } 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": template["code"].(string)})
template["name"] = username
template["address"] = tplAddress
template["time"] = created
template["notificationNotice"] = emailer.lang.UserCreated.get("notificationNotice")
} }
var err error var err error
email.html, email.text, err = emailer.construct(app, "notifications", "created_", map[string]interface{}{ if app.storage.customEmails.UserCreated.Enabled {
"aUserWasCreated": emailer.lang.UserCreated.template("aUserWasCreated", tmpl{"code": "\"" + code + "\""}), content := app.storage.customEmails.UserCreated.Content
"name": emailer.lang.Strings.get("name"), for _, v := range app.storage.customEmails.UserCreated.Variables {
"address": emailer.lang.Strings.get("emailAddress"), replaceWith, ok := template[v[1:len(v)-1]]
"time": emailer.lang.UserCreated.get("time"), if ok {
"nameVal": username, content = strings.ReplaceAll(content, v, replaceWith.(string))
"addressVal": tplAddress, }
"timeVal": created, }
"notificationNotice": emailer.lang.UserCreated.get("notificationNotice"), email, err = emailer.constructTemplate(email.subject, content, app)
}) } else {
email.html, email.text, err = emailer.construct(app, "notifications", "created_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil 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{ email := &Email{
subject: app.config.Section("password_resets").Key("subject").MustString(emailer.lang.PasswordReset.get("title")), 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) d, t, expiresIn := emailer.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
message := app.config.Section("email").Key("message").String() message := app.config.Section("email").Key("message").String()
var err error template := map[string]interface{}{
email.html, email.text, err = emailer.construct(app, "password_resets", "email_", map[string]interface{}{
"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"),
"codeExpiry": emailer.lang.PasswordReset.template("codeExpiry", tmpl{"date": d, "time": t, "expiresInMinutes": expiresIn}),
"ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"), "ifItWasNotYou": emailer.lang.Strings.get("ifItWasNotYou"),
"pin": emailer.lang.PasswordReset.get("pin"), "pinString": emailer.lang.PasswordReset.get("pin"),
"pinVal": pwr.Pin, "message": "",
"message": message, "username": pwr.Username,
}) "date": d,
"time": t,
"expiresInMinutes": expiresIn,
}
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
if app.storage.customEmails.PasswordReset.Enabled {
content := app.storage.customEmails.PasswordReset.Content
for _, v := range app.storage.customEmails.PasswordReset.Variables {
replaceWith, ok := template[v[1:len(v)-1]]
if ok {
content = strings.ReplaceAll(content, v, replaceWith.(string))
}
}
email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "password_resets", "email_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil 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{ email := &Email{
subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")), subject: app.config.Section("deletion").Key("subject").MustString(emailer.lang.UserDeleted.get("title")),
} }
var err error template := map[string]interface{}{
email.html, email.text, err = emailer.construct(app, "deletion", "email_", map[string]interface{}{
"yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"), "yourAccountWasDeleted": emailer.lang.UserDeleted.get("yourAccountWasDeleted"),
"reason": emailer.lang.UserDeleted.get("reason"), "reasonString": emailer.lang.UserDeleted.get("reason"),
"reasonVal": 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
if app.storage.customEmails.UserDeleted.Enabled {
content := app.storage.customEmails.UserDeleted.Content
for _, v := range app.storage.customEmails.UserDeleted.Variables {
replaceWith, ok := template[v[1:len(v)-1]]
if ok {
content = strings.ReplaceAll(content, v, replaceWith.(string))
}
}
email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "deletion", "email_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return email, nil 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{ email := &Email{
subject: app.config.Section("welcome_email").Key("subject").MustString(emailer.lang.WelcomeEmail.get("title")), 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 var err error
email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", map[string]interface{}{ if app.storage.customEmails.WelcomeEmail.Enabled {
"welcome": emailer.lang.WelcomeEmail.get("welcome"), content := app.storage.customEmails.WelcomeEmail.Content
"youCanLoginWith": emailer.lang.WelcomeEmail.get("youCanLoginWith"), for _, v := range app.storage.customEmails.WelcomeEmail.Variables {
"jellyfinURL": emailer.lang.WelcomeEmail.get("jellyfinURL"), replaceWith, ok := template[v[1:len(v)-1]]
"jellyfinURLVal": app.config.Section("jellyfin").Key("public_server").String(), if ok {
"username": emailer.lang.Strings.get("username"), content = strings.ReplaceAll(content, v, replaceWith.(string))
"usernameVal": username, }
"message": app.config.Section("email").Key("message").String(), }
}) email, err = emailer.constructTemplate(email.subject, content, app)
} else {
email.html, email.text, err = emailer.construct(app, "welcome_email", "email_", template)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -383,5 +535,6 @@ func (emailer *Emailer) constructWelcome(username string, app *appContext) (*Ema
// calls the send method in the underlying emailClient. // calls the send method in the underlying emailClient.
func (emailer *Emailer) send(email *Email, address ...string) error { func (emailer *Emailer) send(email *Email, address ...string) error {
fmt.Printf("%+v\n", email)
return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...) return emailer.sender.send(emailer.fromName, emailer.fromAddr, email, address...)
} }

View File

@ -120,14 +120,18 @@ func (ls *setupLangs) getOptions() [][2]string {
type langSection map[string]string type langSection map[string]string
type tmpl map[string]string type tmpl map[string]string
func (el langSection) template(field string, vals tmpl) string { func templateString(text string, vals tmpl) string {
text := el.get(field)
for key, val := range vals { for key, val := range vals {
text = strings.ReplaceAll(text, "{"+key+"}", val) text = strings.ReplaceAll(text, "{"+key+"}", val)
} }
return text return text
} }
func (el langSection) template(field string, vals tmpl) string {
text := el.get(field)
return templateString(text, vals)
}
func (el langSection) format(field string, vals ...string) string { func (el langSection) format(field string, vals ...string) string {
text := el.get(field) text := el.get(field)
for _, val := range vals { for _, val := range vals {

View File

@ -8,21 +8,21 @@
}, },
"userCreated": { "userCreated": {
"title": "Mitteilung: Benutzer erstellt", "title": "Mitteilung: Benutzer erstellt",
"aUserWasCreated": "Ein Benutzer wurde unter Verwendung des Codes {n] erstellt.", "aUserWasCreated": "Ein Benutzer wurde unter Verwendung des Codes {code} erstellt.",
"time": "Zeit", "time": "Zeit",
"notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden." "notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden."
}, },
"inviteExpiry": { "inviteExpiry": {
"title": "Mitteilung: Invite abgelaufen", "title": "Mitteilung: Invite abgelaufen",
"inviteExpired": "Invite abgelaufen.", "inviteExpired": "Invite abgelaufen.",
"expiredAt": "Code {code} lief um {code} ab.", "expiredAt": "Code {code} lief um {time} ab.",
"notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden." "notificationNotice": "Hinweis: Benachrichtigungs-E-Mails können auf dem Administrator-Dashboard umgeschalten werden."
}, },
"passwordReset": { "passwordReset": {
"title": "Passwortzurücksetzung angefordert - Jellyfin", "title": "Passwortzurücksetzung angefordert - Jellyfin",
"someoneHasRequestedReset": "Jemand hat vor kurzem eine Passwortzurücksetzung auf Jellyfin angefordert.", "someoneHasRequestedReset": "Jemand hat vor kurzem eine Passwortzurücksetzung auf Jellyfin angefordert.",
"ifItWasYou": "Wenn du das warst, gib die PIN unten in die Eingabeaufforderung ein.", "ifItWasYou": "Wenn du das warst, gib die PIN unten in die Eingabeaufforderung ein.",
"codeExpiry": "Der Code wird am {time}, um [n} UTC ablaufen, was in {date} ist.", "codeExpiry": "Der Code wird am {date}, um {time} UTC ablaufen, was in {expiresInMinutes} ist.",
"pin": "PIN" "pin": "PIN"
}, },
"userDeleted": { "userDeleted": {
@ -35,7 +35,7 @@
"hello": "Hallo", "hello": "Hallo",
"youHaveBeenInvited": "Du wurdest zu Jellyfin eingeladen.", "youHaveBeenInvited": "Du wurdest zu Jellyfin eingeladen.",
"toJoin": "Um beizutreten, folge dem untenstehenden Link.", "toJoin": "Um beizutreten, folge dem untenstehenden Link.",
"inviteExpiry": "Dieser Invite wird am {time}; um {expiresInMinutes} ablaufen, was in {date} ist, also handle schnell.", "inviteExpiry": "Dieser Invite wird am {date}; um {time} ablaufen, was in {expiresInMinutes} ist, also handle schnell.",
"linkButton": "Richte dein Konto ein" "linkButton": "Richte dein Konto ein"
}, },
"welcomeEmail": { "welcomeEmail": {

View File

@ -7,18 +7,21 @@
"helloUser": "Hi {username}," "helloUser": "Hi {username},"
}, },
"userCreated": { "userCreated": {
"name": "User creation",
"title": "Notice: User created", "title": "Notice: User created",
"aUserWasCreated": "A user was created using code {code}.", "aUserWasCreated": "A user was created using code {code}.",
"time": "Time", "time": "Time",
"notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard."
}, },
"inviteExpiry": { "inviteExpiry": {
"name": "Invite expiry",
"title": "Notice: Invite expired", "title": "Notice: Invite expired",
"inviteExpired": "Invite expired.", "inviteExpired": "Invite expired.",
"expiredAt": "Code {code} expired at {time}.", "expiredAt": "Code {code} expired at {time}.",
"notificationNotice": "Note: Notification emails can be toggled on the admin dashboard." "notificationNotice": "Note: Notification emails can be toggled on the admin dashboard."
}, },
"passwordReset": { "passwordReset": {
"name": "Password reset",
"title": "Password reset requested - Jellyfin", "title": "Password reset requested - Jellyfin",
"someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.", "someoneHasRequestedReset": "Someone has recently requested a password reset on Jellyfin.",
"ifItWasYou": "If this was you, enter the pin below into the prompt.", "ifItWasYou": "If this was you, enter the pin below into the prompt.",
@ -26,11 +29,13 @@
"pin": "PIN" "pin": "PIN"
}, },
"userDeleted": { "userDeleted": {
"name": "User deletion",
"title": "Your account was deleted - Jellyfin", "title": "Your account was deleted - Jellyfin",
"yourAccountWasDeleted": "Your Jellyfin account was deleted.", "yourAccountWasDeleted": "Your Jellyfin account was deleted.",
"reason": "Reason" "reason": "Reason"
}, },
"inviteEmail": { "inviteEmail": {
"name": "Invite email",
"title": "Invite - Jellyfin", "title": "Invite - Jellyfin",
"hello": "Hi", "hello": "Hi",
"youHaveBeenInvited": "You've been invited to Jellyfin.", "youHaveBeenInvited": "You've been invited to Jellyfin.",
@ -39,12 +44,14 @@
"linkButton": "Setup your account" "linkButton": "Setup your account"
}, },
"welcomeEmail": { "welcomeEmail": {
"name": "Welcome email",
"title": "Welcome to Jellyfin", "title": "Welcome to Jellyfin",
"welcome": "Welcome to Jellyfin!", "welcome": "Welcome to Jellyfin!",
"youCanLoginWith": "You can login with the details below", "youCanLoginWith": "You can login with the details below",
"jellyfinURL": "URL" "jellyfinURL": "URL"
}, },
"emailConfirmation": { "emailConfirmation": {
"name": "Confirmation email",
"title": "Confirm your email - Jellyfin", "title": "Confirm your email - Jellyfin",
"clickBelow": "Click the link below to confirm your email address and start using Jellyfin.", "clickBelow": "Click the link below to confirm your email address and start using Jellyfin.",
"confirmEmail": "Confirm Email" "confirmEmail": "Confirm Email"

View File

@ -64,7 +64,7 @@
<p>{{ .clickBelow }}</p> <p>{{ .clickBelow }}</p>
<p>{{ .ifItWasNotYou }}</p> <p>{{ .ifItWasNotYou }}</p>
</mj-text> </mj-text>
<mj-button mj-class="blue bold" href="{{ .urlVal }}">{{ .confirmEmail }}</mj-button> <mj-button mj-class="blue bold" href="{{ .confirmationURL }}">{{ .confirmEmail }}</mj-button>
</mj-column> </mj-column>
</mj-section> </mj-section>
<mj-section mj-class="bg2"> <mj-section mj-class="bg2">

View File

@ -3,6 +3,6 @@
{{ .clickBelow }} {{ .clickBelow }}
{{ .ifItWasNotYou }} {{ .ifItWasNotYou }}
{{ .urlVal }} {{ .confirmationURL }}
{{ .message }} {{ .message }}

View File

@ -64,14 +64,14 @@
</mj-text> </mj-text>
<mj-table mj-class="text" container-background-color="#242424"> <mj-table mj-class="text" container-background-color="#242424">
<tr style="text-align: left;"> <tr style="text-align: left;">
<th>{{ .name }}</th> <th>{{ .nameString }}</th>
<th>{{ .address }}</th> <th>{{ .addressString }}</th>
<th>{{ .time }}</th> <th>{{ .timeString }}</th>
</tr> </tr>
<tr style="font-style: italic; text-align: left; color: rgb(153,153,153);"> <tr style="font-style: italic; text-align: left; color: rgb(153,153,153);">
<th>{{ .nameVal }}</th> <th>{{ .name }}</th>
<th>{{ .addressVal }}</th> <th>{{ .address }}</th>
<th>{{ .timeVal }}</th> <th>{{ .time }}</th>
</mj-table> </mj-table>
</mj-column> </mj-column>
</mj-section> </mj-section>

View File

@ -1,7 +1,7 @@
{{ .aUserWasCreated }} {{ .aUserWasCreated }}
{{ .name }}: {{ .nameVal }} {{ .nameString }}: {{ .name }}
{{ .address }}: {{ .addressVal }} {{ .addressString }}: {{ .address }}
{{ .time }}: {{ .timeVal }} {{ .timeString }}: {{ .time }}
{{ .notificationNotice }} {{ .notificationNotice }}

View File

@ -61,7 +61,7 @@
<mj-column> <mj-column>
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif"> <mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<h3>{{ .yourAccountWasDeleted }}</h3> <h3>{{ .yourAccountWasDeleted }}</h3>
<p>{{ .reason }}: <i>{{ .reasonVal }}</i></p> <p>{{ .reasonString }}: <i>{{ .reason }}</i></p>
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>

View File

@ -1,4 +1,4 @@
{{ .yourAccountWasDeleted }} {{ .yourAccountWasDeleted }}
{{ .reason }}: {{ .reasonVal }} {{ .reasonString }}: {{ .reason }}
{{ .message }} {{ .message }}

View File

@ -66,7 +66,7 @@
<p>{{ .codeExpiry }}</p> <p>{{ .codeExpiry }}</p>
<p>{{ .ifItWasNotYou }}</p> <p>{{ .ifItWasNotYou }}</p>
</mj-text> </mj-text>
<mj-button mj-class="blue bold"><mj-raw>{{ .pinVal }}</mj-raw></mj-button> <mj-button mj-class="blue bold"><mj-raw>{{ .pin }}</mj-raw></mj-button>
</mj-column> </mj-column>
</mj-section> </mj-section>
<mj-section mj-class="bg2"> <mj-section mj-class="bg2">

View File

@ -5,6 +5,6 @@
{{ .codeExpiry }} {{ .codeExpiry }}
{{ .ifItWasNotYou }} {{ .ifItWasNotYou }}
{{ .pin }}: {{ .pinVal }} {{ .pinString }}: {{ .pin }}
{{ .message }} {{ .message }}

View File

@ -65,7 +65,7 @@
<p>{{ .toJoin }}</p> <p>{{ .toJoin }}</p>
<p>{{ .inviteExpiry }}</p> <p>{{ .inviteExpiry }}</p>
</mj-text> </mj-text>
<mj-button mj-class="blue bold" href="{{ .invite_link }}">{{ .linkButton }}</mj-button> <mj-button mj-class="blue bold" href="{{ .inviteURL }}">{{ .linkButton }}</mj-button>
</mj-column> </mj-column>
</mj-section> </mj-section>
<mj-section mj-class="bg2"> <mj-section mj-class="bg2">

View File

@ -3,6 +3,6 @@
{{ .toJoin }} {{ .toJoin }}
{{ .inviteExpiry }} {{ .inviteExpiry }}
{{ .invite_link }} {{ .inviteURL }}
{{ .message }} {{ .message }}

View File

@ -62,8 +62,8 @@
<mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif"> <mj-text mj-class="text" font-size="16px" font-family="Noto Sans, Helvetica, Arial, sans-serif">
<h3>{{ .welcome }}</h3> <h3>{{ .welcome }}</h3>
<p>{{ .youCanLoginWith }}:</p> <p>{{ .youCanLoginWith }}:</p>
{{ .jellyfinURL }}: <a href="{{ .jellyfinURLVal }}">{{ .jellyfinURLVal }}</a> {{ .jellyfinURLString }}: <a href="{{ .jellyfinURLVal }}">{{ .jellyfinURL }}</a>
<p>{{ .username }}: <i>{{ .usernameVal }}</i></p> <p>{{ .usernameString }}: <i>{{ .username }}</i></p>
</mj-text> </mj-text>
</mj-column> </mj-column>
</mj-section> </mj-section>

View File

@ -2,8 +2,8 @@
{{ .youCanLoginWith }}: {{ .youCanLoginWith }}:
{{ .jellyfinURL }}: {{ .jellyfinURLVal }} {{ .jellyfinURLString }}: {{ .jellyfinURL }}
{{ .username }}: {{ .usernameVal }} {{ .usernameString }}: {{ .username }}
{{ .message }} {{ .message }}

View File

@ -174,3 +174,9 @@ type settings struct {
} }
type langDTO map[string]string type langDTO map[string]string
type emailListDTO map[string]string
type emailSetDTO struct {
Content string `json:"content"`
}

View File

@ -81,7 +81,7 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
return return
} }
address = addr.(string) address = addr.(string)
msg, err := app.email.constructReset(pwr, app) msg, err := app.email.constructReset(pwr, app, false)
if err != nil { if err != nil {
app.err.Printf("Failed to construct password reset email for %s", pwr.Username) app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
app.debug.Printf("%s: Error: %s", pwr.Username, err) app.debug.Printf("%s: Error: %s", pwr.Username, err)

View File

@ -139,6 +139,10 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
// api.POST(p + "/setDefaults", app.SetDefaults) // api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings) api.POST(p+"/users/settings", app.ApplySettings)
api.POST(p+"/users/announce", app.Announce) api.POST(p+"/users/announce", app.Announce)
api.GET(p+"/config/emails", app.GetEmails)
api.GET(p+"/config/emails/:id", app.GetEmail)
api.POST(p+"/config/emails/:id", app.SetEmail)
api.POST(p+"/config/emails/:id/:state", app.SetEmailState)
api.GET(p+"/config", app.GetConfig) api.GET(p+"/config", app.GetConfig)
api.POST(p+"/config", app.ModifyConfig) api.POST(p+"/config", app.ModifyConfig)
api.POST(p+"/restart", app.restart) api.POST(p+"/restart", app.restart)

View File

@ -14,15 +14,32 @@ import (
) )
type Storage struct { type Storage struct {
timePattern string timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path string
invites Invites invites Invites
profiles map[string]Profile profiles map[string]Profile
defaultProfile string defaultProfile string
emails, displayprefs, ombi_template map[string]interface{} emails, displayprefs, ombi_template map[string]interface{}
policy mediabrowser.Policy customEmails customEmails
configuration mediabrowser.Configuration policy mediabrowser.Policy
lang Lang configuration mediabrowser.Configuration
lang Lang
}
type customEmails struct {
UserCreated customEmail `json:"userCreated"`
InviteExpiry customEmail `json:"inviteExpiry"`
PasswordReset customEmail `json:"passwordReset"`
UserDeleted customEmail `json:"userDeleted"`
InviteEmail customEmail `json:"inviteEmail"`
WelcomeEmail customEmail `json:"welcomeEmail"`
EmailConfirmation customEmail `json:"emailConfirmation"`
}
type customEmail struct {
Enabled bool `json:"enabled,omitempty"`
Content string `json:"content"`
Variables []string `json:"variables,omitempty"`
} }
// timePattern: %Y-%m-%dT%H:%M:%S.%f // timePattern: %Y-%m-%dT%H:%M:%S.%f
@ -394,6 +411,14 @@ func (st *Storage) storeEmails() error {
return storeJSON(st.emails_path, st.emails) 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 { func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy) return loadJSON(st.policy_path, &st.policy)
} }

53
stripmd.go Normal file
View File

@ -0,0 +1,53 @@
package main
import (
"strings"
stripmd "github.com/writeas/go-strip-markdown"
)
func stripMarkdown(md string) string {
// Search for markdown-formatted urls, and replace them with just the url, then use a library to strip any traces of markdown. You'll need some eyebleach after this.
foundOpenSquare := false
openSquare := -1
openBracket := -1
closeBracket := -1
openSquares := []int{}
closeBrackets := []int{}
links := []string{}
foundOpen := false
for i, c := range md {
if !foundOpenSquare && !foundOpen && c != '[' && c != ']' {
continue
}
if c == '[' && md[i-1] != '!' {
foundOpenSquare = true
openSquare = i
} else if c == ']' {
if md[i+1] == '(' {
foundOpenSquare = false
foundOpen = true
openBracket = i + 1
continue
}
} else if c == ')' {
closeBracket = i
openSquares = append(openSquares, openSquare)
closeBrackets = append(closeBrackets, closeBracket)
links = append(links, md[openBracket+1:closeBracket])
openBracket = -1
closeBracket = -1
openSquare = -1
foundOpenSquare = false
foundOpen = false
}
}
fullLinks := make([]string, len(openSquares))
for i := range openSquares {
fullLinks[i] = md[openSquares[i] : closeBrackets[i]+1]
}
for i, _ := range openSquares {
md = strings.Replace(md, fullLinks[i], links[i], 1)
}
return strings.TrimPrefix(strings.TrimSuffix(stripmd.Strip(md), "</p>"), "<p>")
}