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

Compare commits

..

No commits in common. "6ffdd4dad79c1ae834fdfd672c5d46afe0cbcc3a" and "5c87d109a329346e6839b7d3c8025d230f58313e" have entirely different histories.

26 changed files with 118 additions and 615 deletions

253
api.go
View File

@ -126,7 +126,7 @@ func (app *appContext) checkInvites() {
wait.Add(1)
go func(addr string) {
defer wait.Done()
msg, err := app.email.constructExpiry(code, data, app, false)
msg, err := app.email.constructExpiry(code, data, app)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %s", code, err)
} else if err := app.email.send(msg, addr); err != nil {
@ -163,7 +163,7 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
for address, settings := range notify {
if settings["notify-expiry"] {
go func() {
msg, err := app.email.constructExpiry(code, inv, app, false)
msg, err := app.email.constructExpiry(code, inv, app)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification: %s", code, err)
} else if err := app.email.send(msg, address); err != nil {
@ -295,7 +295,7 @@ func (app *appContext) NewUserAdmin(gc *gin.Context) {
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
msg, err := app.email.constructWelcome(req.Username, app, false)
msg, err := app.email.constructWelcome(req.Username, app)
if err != nil {
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
respondUser(500, true, false, err.Error(), gc)
@ -351,7 +351,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
f = func(gc *gin.Context) {
app.debug.Printf("%s: Email confirmation required", req.Code)
respond(401, "confirmEmail", gc)
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app, false)
msg, err := app.email.constructConfirmation(req.Code, req.Username, key, app)
if err != nil {
app.err.Printf("%s: Failed to construct confirmation email: %s", req.Code, err)
} else if err := app.email.send(msg, req.Email); err != nil {
@ -380,7 +380,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
for address, settings := range invite.Notify {
if settings["notify-creation"] {
go func() {
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app, false)
msg, err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app)
if err != nil {
app.err.Printf("%s: Failed to construct user creation notification: %s", req.Code, err)
} else if err := app.email.send(msg, address); err != nil {
@ -436,7 +436,7 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc
}
if emailEnabled && app.config.Section("welcome_email").Key("enabled").MustBool(false) && req.Email != "" {
app.debug.Printf("%s: Sending welcome email to %s", req.Username, req.Email)
msg, err := app.email.constructWelcome(req.Username, app, false)
msg, err := app.email.constructWelcome(req.Username, app)
if err != nil {
app.err.Printf("%s: Failed to construct welcome email: %s", req.Username, err)
} else if err := app.email.send(msg, req.Email); err != nil {
@ -516,7 +516,7 @@ func (app *appContext) Announce(gc *gin.Context) {
}
addresses = append(addresses, addr.(string))
}
msg, err := app.email.constructTemplate(req.Subject, req.Message, app)
msg, err := app.email.constructAnnouncement(req.Subject, req.Message, app)
if err != nil {
app.err.Printf("Failed to construct announcement emails: %s", err)
respondBool(500, false, gc)
@ -576,7 +576,7 @@ func (app *appContext) DeleteUsers(gc *gin.Context) {
}
if len(addresses) != 0 {
go func(reason string, addresses []string) {
msg, err := app.email.constructDeleted(reason, app, false)
msg, err := app.email.constructDeleted(reason, app)
if err != nil {
app.err.Printf("Failed to construct account deletion emails: %s", err)
} else if err := app.email.send(msg, addresses...); err != nil {
@ -638,7 +638,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if emailEnabled && req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", inviteCode)
invite.Email = req.Email
msg, err := app.email.constructInvite(inviteCode, invite, app, false)
msg, err := app.email.constructInvite(inviteCode, invite, app)
if err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: Failed to construct invite email: %s", inviteCode, err)
@ -1269,239 +1269,6 @@ 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.
// @Produce json
// @Success 200 {object} boolResponse
@ -1524,7 +1291,7 @@ func (app *appContext) Logout(gc *gin.Context) {
// @Produce json
// @Success 200 {object} langDTO
// @Failure 500 {object} stringResponse
// @Router /lang/{page} [get]
// @Router /lang [get]
// @tags Other
func (app *appContext) GetLanguages(gc *gin.Context) {
page := gc.Param("page")

View File

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

View File

@ -853,14 +853,6 @@
"type": "text",
"value": "",
"description": "The path to a directory which following the same form as the internal 'lang/' directory. See GitHub for more info."
},
"custom_emails": {
"name": "Custom email content",
"required": false,
"requires_restart": false,
"type": "text",
"value": "",
"description": "JSON file generated by program in settings, different from email_html/email_text. See wiki for more info."
}
}
}

277
email.go
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -174,9 +174,3 @@ type settings struct {
}
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
}
address = addr.(string)
msg, err := app.email.constructReset(pwr, app, false)
msg, err := app.email.constructReset(pwr, app)
if err != nil {
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
app.debug.Printf("%s: Error: %s", pwr.Username, err)

View File

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

View File

@ -15,33 +15,16 @@ import (
type Storage struct {
timePattern string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path, customEmails_path string
invite_path, emails_path, policy_path, configuration_path, displayprefs_path, ombi_path, profiles_path string
invites Invites
profiles map[string]Profile
defaultProfile string
emails, displayprefs, ombi_template map[string]interface{}
customEmails customEmails
policy mediabrowser.Policy
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
type Profile struct {
@ -411,14 +394,6 @@ func (st *Storage) storeEmails() error {
return storeJSON(st.emails_path, st.emails)
}
func (st *Storage) loadCustomEmails() error {
return loadJSON(st.customEmails_path, &st.customEmails)
}
func (st *Storage) storeCustomEmails() error {
return storeJSON(st.customEmails_path, st.customEmails)
}
func (st *Storage) loadPolicy() error {
return loadJSON(st.policy_path, &st.policy)
}

View File

@ -1,53 +0,0 @@
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>")
}