From 51839b59428153cca230f73c694e53b40d249630 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 13 Sep 2020 21:07:15 +0100 Subject: [PATCH] Restructure email sending smtp and mailgun now implement an emailClient interface, which the Emailer can use. --- api.go | 28 +++++---- config.go | 2 + email.go | 162 +++++++++++++++++++++++++++++++---------------------- main.go | 6 +- pwreset.go | 6 +- 5 files changed, 120 insertions(+), 84 deletions(-) diff --git a/api.go b/api.go index b693bdc..bdd4c1b 100644 --- a/api.go +++ b/api.go @@ -92,10 +92,12 @@ func (app *appContext) checkInvites() { for address, settings := range notify { if settings["notify-expiry"] { go func() { - if app.email.constructExpiry(code, data, app) != nil { + if err := app.email.constructExpiry(code, data, app); 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); 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 +128,12 @@ 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 { + if err := app.email.constructExpiry(code, inv, app); 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); 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 +220,10 @@ 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 { + if err := app.email.constructCreated(req.Code, req.Username, req.Email, invite, app); 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); err != nil { app.err.Printf("%s: Failed to send user creation notification", req.Code) app.debug.Printf("%s: Error: %s", req.Code, err) } else { @@ -304,7 +308,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) { 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); 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 +357,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 +561,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 +569,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) diff --git a/config.go b/config.go index 38f34cd..e3b8688 100644 --- a/config.go +++ b/config.go @@ -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 } diff --git a/email.go b/email.go index 5baa6a8..7a741b7 100644 --- a/email.go +++ b/email.go @@ -15,16 +15,65 @@ 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 Email struct { +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 { + content email + fromAddr, fromName string + sender emailClient +} + +type email struct { subject string html, text string } @@ -50,22 +99,44 @@ 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, } } @@ -100,7 +171,6 @@ func (email *Emailer) constructInvite(code string, invite Invite, app *appContex email.content.text = tplData.String() } } - email.sendType = "invite" return nil } @@ -127,7 +197,6 @@ func (email *Emailer) constructExpiry(code string, invite Invite, app *appContex email.content.text = tplData.String() } } - email.sendType = "expiry" return nil } @@ -162,7 +231,6 @@ func (email *Emailer) constructCreated(code, username, address string, invite In email.content.text = tplData.String() } } - email.sendType = "created" return nil } @@ -194,47 +262,9 @@ func (email *Emailer) constructReset(pwr Pwr, app *appContext) error { email.content.text = tplData.String() } } - email.sendType = "reset" return 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) error { + return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, emailer.content) } diff --git a/main.go b/main.go index 67ebd35..c9aead2 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/pwreset.go b/pwreset.go index 5455f08..93abecb 100644 --- a/pwreset.go +++ b/pwreset.go @@ -71,10 +71,12 @@ 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 { + if err := app.email.constructReset(pwr, app); 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); 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) }