1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-28 03:50:10 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
b8dfb5d6a3
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.
2020-09-13 21:18:47 +01:00
51839b5942
Restructure email sending
smtp and mailgun now implement an emailClient interface, which the
Emailer can use.
2020-09-13 21:07:15 +01:00
5 changed files with 163 additions and 114 deletions

34
api.go
View File

@ -92,10 +92,13 @@ func (app *appContext) checkInvites() {
for address, settings := range notify {
if settings["notify-expiry"] {
go func() {
if app.email.constructExpiry(code, data, app) != nil {
msg, err := app.email.constructExpiry(code, data, app)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification", code)
} else if app.email.send(address, app) != nil {
app.debug.Printf("Error: %s", err)
} else if err := app.email.send(address, msg); err != nil {
app.err.Printf("%s: Failed to send expiry notification", code)
app.debug.Printf("Error: %s", err)
} else {
app.info.Printf("Sent expiry notification to %s", address)
}
@ -126,10 +129,13 @@ func (app *appContext) checkInvite(code string, used bool, username string) bool
for address, settings := range notify {
if settings["notify-expiry"] {
go func() {
if app.email.constructExpiry(code, inv, app) != nil {
msg, err := app.email.constructExpiry(code, inv, app)
if err != nil {
app.err.Printf("%s: Failed to construct expiry notification", code)
} else if app.email.send(address, app) != nil {
app.debug.Printf("Error: %s", err)
} else if err := app.email.send(address, msg); err != nil {
app.err.Printf("%s: Failed to send expiry notification", code)
app.debug.Printf("Error: %s", err)
} else {
app.info.Printf("Sent expiry notification to %s", address)
}
@ -216,10 +222,11 @@ func (app *appContext) NewUser(gc *gin.Context) {
for address, settings := range invite.Notify {
if settings["notify-creation"] {
go func() {
if app.email.constructCreated(req.Code, req.Username, req.Email, invite, app) != 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.debug.Printf("%s: Error: %s", req.Code, err)
} else if app.email.send(address, app) != nil {
} else if err := app.email.send(address, msg); err != nil {
app.err.Printf("%s: Failed to send user creation notification", req.Code)
app.debug.Printf("%s: Error: %s", req.Code, err)
} else {
@ -300,11 +307,12 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
if req.Email != "" && app.config.Section("invite_emails").Key("enabled").MustBool(false) {
app.debug.Printf("%s: Sending invite email", invite_code)
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)
app.err.Printf("%s: Failed to construct invite email", invite_code)
app.debug.Printf("%s: Error: %s", invite_code, err)
} else if err := app.email.send(req.Email, app); err != nil {
} else if err := app.email.send(req.Email, msg); err != nil {
invite.Email = fmt.Sprintf("Failed to send to %s", req.Email)
app.err.Printf("%s: %s", invite_code, invite.Email)
app.debug.Printf("%s: Error: %s", invite_code, err)
@ -353,9 +361,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
address = app.config.Section("ui").Key("email").String()
}
if _, ok := inv.Notify[address]; ok {
for _, notify_type := range []string{"notify-expiry", "notify-creation"} {
if _, ok = inv.Notify[notify_type]; ok {
invite[notify_type] = inv.Notify[address][notify_type]
for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok = inv.Notify[notifyType]; ok {
invite[notifyType] = inv.Notify[address][notifyType]
}
}
}
@ -557,7 +565,7 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
respond(500, "Couldn't get user", gc)
return
}
userId := user["Id"].(string)
userID := user["Id"].(string)
policy := user["Policy"].(map[string]interface{})
app.storage.policy = policy
app.storage.storePolicy()
@ -565,7 +573,7 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
if req.Homescreen {
configuration := user["Configuration"].(map[string]interface{})
var displayprefs map[string]interface{}
displayprefs, status, err = app.jf.getDisplayPreferences(userId)
displayprefs, status, err = app.jf.getDisplayPreferences(userID)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
app.debug.Printf("Error: %s", err)

View File

@ -69,5 +69,7 @@ func (app *appContext) loadConfig() error {
app.config.Section("notifications").Key("created_html").SetValue(app.config.Section("notifications").Key("created_html").MustString(filepath.Join(app.local_path, "created.html")))
app.config.Section("notifications").Key("created_text").SetValue(app.config.Section("notifications").Key("created_text").MustString(filepath.Join(app.local_path, "created.txt")))
app.email = NewEmailer(app)
return nil
}

228
email.go
View File

@ -15,15 +15,64 @@ import (
"github.com/mailgun/mailgun-go/v4"
)
type Emailer struct {
smtpAuth smtp.Auth
sendType, sendMethod, fromAddr, fromName string
content Email
mg *mailgun.MailgunImpl
mime string
host string
// implements email sending, right now via smtp or mailgun.
type emailClient interface {
send(address, fromName, fromAddr string, email *Email) error
}
type Mailgun struct {
client *mailgun.MailgunImpl
}
func (mg *Mailgun) send(address, fromName, fromAddr string, email *Email) error {
message := mg.client.NewMessage(
fmt.Sprintf("%s <%s>", fromName, fromAddr),
email.subject,
email.text,
address,
)
message.SetHtml(email.html)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
_, _, err := mg.client.Send(ctx, message)
return err
}
type Smtp struct {
sslTls bool
host, server string
port int
auth smtp.Auth
}
func (sm *Smtp) send(address, fromName, fromAddr string, email *Email) error {
e := jEmail.NewEmail()
e.Subject = email.subject
e.From = fmt.Sprintf("%s <%s>", fromName, fromAddr)
e.To = []string{address}
e.Text = []byte(email.text)
e.HTML = []byte(email.html)
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: sm.host,
}
server := fmt.Sprintf("%s:%d", sm.server, sm.port)
var err error
if sm.sslTls {
err = e.SendWithTLS(server, sm.auth, tlsConfig)
} else {
err = e.SendWithStartTLS(server, sm.auth, tlsConfig)
}
return err
}
// Emailer contains the email sender, email content, and methods to construct message content.
type Emailer struct {
fromAddr, fromName string
sender emailClient
}
// Email stores content.
type Email struct {
subject string
html, text string
@ -50,29 +99,53 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern,
return
}
func (email *Emailer) init(app *appContext) {
email.fromAddr = app.config.Section("email").Key("address").String()
email.fromName = app.config.Section("email").Key("from").String()
email.sendMethod = app.config.Section("email").Key("method").String()
if email.sendMethod == "mailgun" {
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").String())
api_url := app.config.Section("mailgun").Key("api_url").String()
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages'
if strings.Contains(api_url, "messages") {
api_url = api_url[0:strings.LastIndex(api_url, "/")]
api_url = api_url[0:strings.LastIndex(api_url, "/")]
func NewEmailer(app *appContext) *Emailer {
emailer := &Emailer{
fromAddr: app.config.Section("email").Key("address").String(),
fromName: app.config.Section("email").Key("from").String(),
}
method := app.config.Section("email").Key("method").String()
if method == "smtp" {
sslTls := false
if app.config.Section("smtp").Key("encryption").String() == "ssl_tls" {
sslTls = true
}
email.mg.SetAPIBase(api_url)
} else if email.sendMethod == "smtp" {
app.host = app.config.Section("smtp").Key("server").String()
email.smtpAuth = smtp.PlainAuth("", email.fromAddr, app.config.Section("smtp").Key("password").String(), app.host)
emailer.NewSMTP(app.config.Section("smtp").Key("server").String(), app.config.Section("smtp").Key("port").MustInt(465), app.config.Section("smtp").Key("password").String(), app.host, sslTls)
} else if method == "mailgun" {
emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
}
return emailer
}
func (emailer *Emailer) NewMailgun(url, key string) {
sender := &Mailgun{
client: mailgun.NewMailgun(strings.Split(emailer.fromAddr, "@")[1], key),
}
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages'
if strings.Contains(url, "messages") {
url = url[0:strings.LastIndex(url, "/")]
url = url[0:strings.LastIndex(url, "/")]
}
sender.client.SetAPIBase(url)
emailer.sender = sender
}
func (emailer *Emailer) NewSMTP(server string, port int, password, host string, sslTls bool) {
emailer.sender = &Smtp{
auth: smtp.PlainAuth("", emailer.fromAddr, password, host),
server: server,
host: host,
port: port,
sslTls: sslTls,
}
}
func (email *Emailer) constructInvite(code string, invite Invite, app *appContext) error {
email.content.subject = app.config.Section("invite_emails").Key("subject").String()
func (emailer *Emailer) constructInvite(code string, invite Invite, app *appContext) (*Email, error) {
email := &Email{
subject: app.config.Section("invite_emails").Key("subject").String(),
}
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()
invite_link := app.config.Section("invite_emails").Key("url_base").String()
invite_link = fmt.Sprintf("%s/%s", invite_link, code)
@ -81,7 +154,7 @@ func (email *Emailer) constructInvite(code string, invite Invite, app *appContex
fpath := app.config.Section("invite_emails").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return err
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
@ -92,26 +165,27 @@ func (email *Emailer) constructInvite(code string, invite Invite, app *appContex
"message": message,
})
if err != nil {
return err
return nil, err
}
if key == "html" {
email.content.html = tplData.String()
email.html = tplData.String()
} else {
email.content.text = tplData.String()
email.text = tplData.String()
}
}
email.sendType = "invite"
return nil
return email, nil
}
func (email *Emailer) constructExpiry(code string, invite Invite, app *appContext) error {
email.content.subject = "Notice: Invite expired"
func (emailer *Emailer) constructExpiry(code string, invite Invite, app *appContext) (*Email, error) {
email := &Email{
subject: "Notice: Invite expired",
}
expiry := app.formatDatetime(invite.ValidTill)
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("notifications").Key("expiry_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return err
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
@ -119,20 +193,21 @@ func (email *Emailer) constructExpiry(code string, invite Invite, app *appContex
"expiry": expiry,
})
if err != nil {
return err
return nil, err
}
if key == "html" {
email.content.html = tplData.String()
email.html = tplData.String()
} else {
email.content.text = tplData.String()
email.text = tplData.String()
}
}
email.sendType = "expiry"
return nil
return email, nil
}
func (email *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) error {
email.content.subject = "Notice: User created"
func (emailer *Emailer) constructCreated(code, username, address string, invite Invite, app *appContext) (*Email, error) {
email := &Email{
subject: "Notice: User created",
}
created := app.formatDatetime(invite.Created)
var tplAddress string
if app.config.Section("email").Key("no_username").MustBool(false) {
@ -144,7 +219,7 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
fpath := app.config.Section("notifications").Key("created_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return err
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
@ -154,27 +229,28 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
"time": created,
})
if err != nil {
return err
return nil, err
}
if key == "html" {
email.content.html = tplData.String()
email.html = tplData.String()
} else {
email.content.text = tplData.String()
email.text = tplData.String()
}
}
email.sendType = "created"
return nil
return email, nil
}
func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
email.content.subject = app.config.Section("password_resets").Key("subject").MustString("Password reset - Jellyfin")
d, t, expires_in := email.formatExpiry(pwr.Expiry, true, app.datePattern, app.timePattern)
func (emailer *Emailer) constructReset(pwr Pwr, app *appContext) (*Email, error) {
email := &Email{
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()
for _, key := range []string{"html", "text"} {
fpath := app.config.Section("password_resets").Key("email_" + key).String()
tpl, err := template.ParseFiles(fpath)
if err != nil {
return err
return nil, err
}
var tplData bytes.Buffer
err = tpl.Execute(&tplData, map[string]string{
@ -186,55 +262,17 @@ func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
"message": message,
})
if err != nil {
return err
return nil, err
}
if key == "html" {
email.content.html = tplData.String()
email.html = tplData.String()
} else {
email.content.text = tplData.String()
email.text = tplData.String()
}
}
email.sendType = "reset"
return nil
return email, nil
}
func (email *Emailer) send(address string, app *appContext) error {
if email.sendMethod == "mailgun" {
message := email.mg.NewMessage(
fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr),
email.content.subject,
email.content.text,
address)
message.SetHtml(email.content.html)
mgapp, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
_, _, err := email.mg.Send(mgapp, message)
if err != nil {
return err
}
} else if email.sendMethod == "smtp" {
e := jEmail.NewEmail()
e.Subject = email.content.subject
e.From = fmt.Sprintf("%s <%s>", email.fromName, email.fromAddr)
e.To = []string{address}
e.Text = []byte(email.content.text)
e.HTML = []byte(email.content.html)
smtpType := app.config.Section("smtp").Key("encryption").String()
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: app.host,
}
var err error
if smtpType == "ssl_tls" {
port := app.config.Section("smtp").Key("port").MustInt(465)
server := fmt.Sprintf("%s:%d", app.host, port)
err = e.SendWithTLS(server, email.smtpAuth, tlsConfig)
} else if smtpType == "starttls" {
port := app.config.Section("smtp").Key("port").MustInt(587)
server := fmt.Sprintf("%s:%d", app.host, port)
e.SendWithStartTLS(server, email.smtpAuth, tlsConfig)
}
return err
}
return nil
func (emailer *Emailer) send(address string, email *Email) error {
return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, email)
}

View File

@ -53,7 +53,7 @@ type appContext struct {
timePattern string
storage Storage
validator Validator
email Emailer
email *Emailer
info, debug, err *log.Logger
host string
port int
@ -361,8 +361,6 @@ func start(asDaemon, firstCall bool) {
}
app.validator.init(validatorConf)
app.email.init(app)
inviteDaemon := NewRepeater(time.Duration(60*time.Second), app)
go inviteDaemon.Run()
@ -476,7 +474,7 @@ func main() {
folder = os.Getenv("TEMP")
}
SOCK = filepath.Join(folder, SOCK)
fmt.Println(SOCK)
fmt.Println("Socket:", SOCK)
if flagPassed("start") {
args := []string{}
for i, f := range os.Args {

View File

@ -71,10 +71,13 @@ func pwrMonitor(app *appContext, watcher *fsnotify.Watcher) {
app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
return
}
if app.email.constructReset(pwr, app) != nil {
msg, err := app.email.constructReset(pwr, app)
if err != nil {
app.err.Printf("Failed to construct password reset email for %s", pwr.Username)
} else if app.email.send(address, app) != nil {
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else if err := app.email.send(address, msg); err != nil {
app.err.Printf("Failed to send password reset email to \"%s\"", address)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else {
app.info.Printf("Sent password reset email to \"%s\"", address)
}