mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-23 01:20:11 +00:00
decouple email content from sender to ensure thread safety
If two emails fired off at once, they would previously replace each other's content and possibly send the wrong email to the wrong person. construct* methods now return the email content, which is sent separately.
This commit is contained in:
parent
51839b5942
commit
b8dfb5d6a3
20
api.go
20
api.go
@ -92,10 +92,11 @@ func (app *appContext) checkInvites() {
|
|||||||
for address, settings := range notify {
|
for address, settings := range notify {
|
||||||
if settings["notify-expiry"] {
|
if settings["notify-expiry"] {
|
||||||
go func() {
|
go func() {
|
||||||
if err := app.email.constructExpiry(code, data, app); err != nil {
|
msg, err := app.email.constructExpiry(code, data, app)
|
||||||
|
if err != nil {
|
||||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||||
app.debug.Printf("Error: %s", err)
|
app.debug.Printf("Error: %s", err)
|
||||||
} else if err := app.email.send(address); err != nil {
|
} else if err := app.email.send(address, msg); err != nil {
|
||||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||||
app.debug.Printf("Error: %s", err)
|
app.debug.Printf("Error: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@ -128,10 +129,11 @@ 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() {
|
||||||
if err := app.email.constructExpiry(code, inv, app); err != nil {
|
msg, err := app.email.constructExpiry(code, inv, app)
|
||||||
|
if err != nil {
|
||||||
app.err.Printf("%s: Failed to construct expiry notification", code)
|
app.err.Printf("%s: Failed to construct expiry notification", code)
|
||||||
app.debug.Printf("Error: %s", err)
|
app.debug.Printf("Error: %s", err)
|
||||||
} else if err := app.email.send(address); err != nil {
|
} else if err := app.email.send(address, msg); err != nil {
|
||||||
app.err.Printf("%s: Failed to send expiry notification", code)
|
app.err.Printf("%s: Failed to send expiry notification", code)
|
||||||
app.debug.Printf("Error: %s", err)
|
app.debug.Printf("Error: %s", err)
|
||||||
} else {
|
} else {
|
||||||
@ -220,10 +222,11 @@ func (app *appContext) NewUser(gc *gin.Context) {
|
|||||||
for address, settings := range invite.Notify {
|
for address, settings := range invite.Notify {
|
||||||
if settings["notify-creation"] {
|
if settings["notify-creation"] {
|
||||||
go func() {
|
go func() {
|
||||||
if err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app); err != nil {
|
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", req.Code)
|
app.err.Printf("%s: Failed to construct user creation notification", req.Code)
|
||||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||||
} else if err := app.email.send(address); err != nil {
|
} else if err := app.email.send(address, msg); err != nil {
|
||||||
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
app.err.Printf("%s: Failed to send user creation notification", req.Code)
|
||||||
app.debug.Printf("%s: Error: %s", req.Code, err)
|
app.debug.Printf("%s: Error: %s", req.Code, err)
|
||||||
} else {
|
} else {
|
||||||
@ -304,11 +307,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
|
|||||||
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
|
||||||
app.debug.Printf("%s: Sending invite email", invite_code)
|
app.debug.Printf("%s: Sending invite email", invite_code)
|
||||||
invite.Email = req.Email
|
invite.Email = req.Email
|
||||||
if err := app.email.constructInvite(invite_code, invite, app); err != nil {
|
msg, err := app.email.constructInvite(invite_code, invite, app)
|
||||||
|
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", invite_code)
|
app.err.Printf("%s: Failed to construct invite email", invite_code)
|
||||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||||
} else if err := app.email.send(req.Email); err != nil {
|
} else if err := app.email.send(req.Email, msg); 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: %s", invite_code, invite.Email)
|
app.err.Printf("%s: %s", invite_code, invite.Email)
|
||||||
app.debug.Printf("%s: Error: %s", invite_code, err)
|
app.debug.Printf("%s: Error: %s", invite_code, err)
|
||||||
|
82
email.go
82
email.go
@ -17,14 +17,14 @@ import (
|
|||||||
|
|
||||||
// implements email sending, right now via smtp or mailgun.
|
// implements email sending, right now via smtp or mailgun.
|
||||||
type emailClient interface {
|
type emailClient interface {
|
||||||
send(address, fromName, fromAddr string, email email) error
|
send(address, fromName, fromAddr string, email *Email) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mailgun struct {
|
type Mailgun struct {
|
||||||
client *mailgun.MailgunImpl
|
client *mailgun.MailgunImpl
|
||||||
}
|
}
|
||||||
|
|
||||||
func (mg *Mailgun) send(address, fromName, fromAddr string, email email) error {
|
func (mg *Mailgun) send(address, fromName, fromAddr string, email *Email) error {
|
||||||
message := mg.client.NewMessage(
|
message := mg.client.NewMessage(
|
||||||
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
fmt.Sprintf("%s <%s>", fromName, fromAddr),
|
||||||
email.subject,
|
email.subject,
|
||||||
@ -45,7 +45,7 @@ type Smtp struct {
|
|||||||
auth smtp.Auth
|
auth smtp.Auth
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *Smtp) send(address, fromName, fromAddr string, email email) error {
|
func (sm *Smtp) send(address, fromName, fromAddr string, email *Email) error {
|
||||||
e := jEmail.NewEmail()
|
e := jEmail.NewEmail()
|
||||||
e.Subject = email.subject
|
e.Subject = email.subject
|
||||||
e.From = fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
e.From = fmt.Sprintf("%s <%s>", fromName, fromAddr)
|
||||||
@ -68,12 +68,12 @@ func (sm *Smtp) send(address, fromName, fromAddr string, email email) error {
|
|||||||
|
|
||||||
// Emailer contains the email sender, email content, and methods to construct message content.
|
// Emailer contains the email sender, email content, and methods to construct message content.
|
||||||
type Emailer struct {
|
type Emailer struct {
|
||||||
content email
|
|
||||||
fromAddr, fromName string
|
fromAddr, fromName string
|
||||||
sender emailClient
|
sender emailClient
|
||||||
}
|
}
|
||||||
|
|
||||||
type email struct {
|
// Email stores content.
|
||||||
|
type Email struct {
|
||||||
subject string
|
subject string
|
||||||
html, text string
|
html, text string
|
||||||
}
|
}
|
||||||
@ -140,10 +140,12 @@ func (emailer *Emailer) NewSMTP(server string, port int, password, host string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error {
|
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) {
|
||||||
email.content.subject = app.config.Section("invite_emails").Key("subject").String()
|
email := &Email{
|
||||||
|
subject: app.config.Section("invite_emails").Key("subject").String(),
|
||||||
|
}
|
||||||
expiry := invite.ValidTill
|
expiry := invite.ValidTill
|
||||||
d, t, expires_in := email.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
d, t, expires_in := emailer.formatExpiry(expiry, false, app.datePattern, app.timePattern)
|
||||||
message := app.config.Section("email").Key("message").String()
|
message := app.config.Section("email").Key("message").String()
|
||||||
invite_link := app.config.Section("invite_emails").Key("url_base").String()
|
invite_link := app.config.Section("invite_emails").Key("url_base").String()
|
||||||
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
|
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
|
||||||
@ -152,7 +154,7 @@ func (email *Emailer) constructInvite(code string, invite Invite, app *appContex
|
|||||||
fpath := app.config.Section("invite_emails").Key("email_" + key).String()
|
fpath := app.config.Section("invite_emails").Key("email_" + key).String()
|
||||||
tpl, err := template.ParseFiles(fpath)
|
tpl, err := template.ParseFiles(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tplData bytes.Buffer
|
var tplData bytes.Buffer
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
err = tpl.Execute(&tplData, map[string]string{
|
||||||
@ -163,25 +165,27 @@ func (email *Emailer) constructInvite(code string, invite Invite, app *appContex
|
|||||||
"message": message,
|
"message": message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if key == "html" {
|
if key == "html" {
|
||||||
email.content.html = tplData.String()
|
email.html = tplData.String()
|
||||||
} else {
|
} else {
|
||||||
email.content.text = tplData.String()
|
email.text = tplData.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (email *Emailer) constructExpiry(code string, invite Invite, app *appContext) error {
|
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) {
|
||||||
email.content.subject = "Notice: Invite expired"
|
email := &Email{
|
||||||
|
subject: "Notice: Invite expired",
|
||||||
|
}
|
||||||
expiry := app.formatDatetime(invite.ValidTill)
|
expiry := app.formatDatetime(invite.ValidTill)
|
||||||
for _, key := range []string{"html", "text"} {
|
for _, key := range []string{"html", "text"} {
|
||||||
fpath := app.config.Section("notifications").Key("expiry_" + key).String()
|
fpath := app.config.Section("notifications").Key("expiry_" + key).String()
|
||||||
tpl, err := template.ParseFiles(fpath)
|
tpl, err := template.ParseFiles(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tplData bytes.Buffer
|
var tplData bytes.Buffer
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
err = tpl.Execute(&tplData, map[string]string{
|
||||||
@ -189,19 +193,21 @@ func (email *Emailer) constructExpiry(code string, invite Invite, app *appContex
|
|||||||
"expiry": expiry,
|
"expiry": expiry,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if key == "html" {
|
if key == "html" {
|
||||||
email.content.html = tplData.String()
|
email.html = tplData.String()
|
||||||
} else {
|
} else {
|
||||||
email.content.text = tplData.String()
|
email.text = tplData.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (email *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) error {
|
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) {
|
||||||
email.content.subject = "Notice: User created"
|
email := &Email{
|
||||||
|
subject: "Notice: User created",
|
||||||
|
}
|
||||||
created := app.formatDatetime(invite.Created)
|
created := app.formatDatetime(invite.Created)
|
||||||
var tplAddress string
|
var tplAddress string
|
||||||
if app.config.Section("email").Key("no_username").MustBool(false) {
|
if app.config.Section("email").Key("no_username").MustBool(false) {
|
||||||
@ -213,7 +219,7 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
|
|||||||
fpath := app.config.Section("notifications").Key("created_" + key).String()
|
fpath := app.config.Section("notifications").Key("created_" + key).String()
|
||||||
tpl, err := template.ParseFiles(fpath)
|
tpl, err := template.ParseFiles(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tplData bytes.Buffer
|
var tplData bytes.Buffer
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
err = tpl.Execute(&tplData, map[string]string{
|
||||||
@ -223,26 +229,28 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
|
|||||||
"time": created,
|
"time": created,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if key == "html" {
|
if key == "html" {
|
||||||
email.content.html = tplData.String()
|
email.html = tplData.String()
|
||||||
} else {
|
} else {
|
||||||
email.content.text = tplData.String()
|
email.text = tplData.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
|
func (emailer *Emailer) constructReset(pwr Pwr, app *appContext) (*Email, error) {
|
||||||
email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
|
email := &Email{
|
||||||
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
|
subject: app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin"),
|
||||||
|
}
|
||||||
|
d, t, expires_in := 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()
|
||||||
for _, key := range []string{"html", "text"} {
|
for _, key := range []string{"html", "text"} {
|
||||||
fpath := app.config.Section("password_resets").Key("email_" + key).String()
|
fpath := app.config.Section("password_resets").Key("email_" + key).String()
|
||||||
tpl, err := template.ParseFiles(fpath)
|
tpl, err := template.ParseFiles(fpath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tplData bytes.Buffer
|
var tplData bytes.Buffer
|
||||||
err = tpl.Execute(&tplData, map[string]string{
|
err = tpl.Execute(&tplData, map[string]string{
|
||||||
@ -254,17 +262,17 @@ func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
|
|||||||
"message": message,
|
"message": message,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if key == "html" {
|
if key == "html" {
|
||||||
email.content.html = tplData.String()
|
email.html = tplData.String()
|
||||||
} else {
|
} else {
|
||||||
email.content.text = tplData.String()
|
email.text = tplData.String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return email, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (emailer *Emailer) send(address string) error {
|
func (emailer *Emailer) send(address string, email *Email) error {
|
||||||
return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, emailer.content)
|
return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, email)
|
||||||
}
|
}
|
||||||
|
@ -71,10 +71,11 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
|
|||||||
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := app.email.constructReset(pwr, app); err != nil {
|
msg, err := app.email.constructReset(pwr, app)
|
||||||
|
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)
|
||||||
} else if err := app.email.send(address); err != nil {
|
} else if err := app.email.send(address, msg); err != nil {
|
||||||
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
app.err.Printf("Failed to send password reset email to \"%s\"", address)
|
||||||
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
app.debug.Printf("%s: Error: %s", pwr.Username, err)
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
Reference in New Issue
Block a user