1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-06-27 20:07:46 +02:00

Restructure email sending

smtp and mailgun now implement an emailClient interface, which the
Emailer can use.
This commit is contained in:
Harvey Tindall 2020-09-13 21:07:15 +01:00
parent 831296a3e8
commit 51839b5942
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
5 changed files with 120 additions and 84 deletions

28
api.go
View File

@ -92,10 +92,12 @@ 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 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) 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.err.Printf("%s: Failed to send expiry notification", code)
app.debug.Printf("Error: %s", err)
} else { } else {
app.info.Printf("Sent expiry notification to %s", address) 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 { for address, settings := range notify {
if settings["notify-expiry"] { if settings["notify-expiry"] {
go func() { 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) 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.err.Printf("%s: Failed to send expiry notification", code)
app.debug.Printf("Error: %s", err)
} else { } else {
app.info.Printf("Sent expiry notification to %s", address) 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 { for address, settings := range invite.Notify {
if settings["notify-creation"] { if settings["notify-creation"] {
go func() { 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.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 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.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,7 +308,7 @@ func (app *appContext) GenerateInvite(gc *gin.Context) {
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, app); err != nil { } else if err := app.email.send(req.Email); 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)
@ -353,9 +357,9 @@ func (app *appContext) GetInvites(gc *gin.Context) {
address = app.config.Section("ui").Key("email").String() address = app.config.Section("ui").Key("email").String()
} }
if _, ok := inv.Notify[address]; ok { if _, ok := inv.Notify[address]; ok {
for _, notify_type := range []string{"notify-expiry", "notify-creation"} { for _, notifyType := range []string{"notify-expiry", "notify-creation"} {
if _, ok = inv.Notify[notify_type]; ok { if _, ok = inv.Notify[notifyType]; ok {
invite[notify_type] = inv.Notify[address][notify_type] invite[notifyType] = inv.Notify[address][notifyType]
} }
} }
} }
@ -557,7 +561,7 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
respond(500, "Couldn't get user", gc) respond(500, "Couldn't get user", gc)
return return
} }
userId := user["Id"].(string) userID := user["Id"].(string)
policy := user["Policy"].(map[string]interface{}) policy := user["Policy"].(map[string]interface{})
app.storage.policy = policy app.storage.policy = policy
app.storage.storePolicy() app.storage.storePolicy()
@ -565,7 +569,7 @@ func (app *appContext) SetDefaults(gc *gin.Context) {
if req.Homescreen { if req.Homescreen {
configuration := user["Configuration"].(map[string]interface{}) configuration := user["Configuration"].(map[string]interface{})
var displayprefs 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 { if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get DisplayPrefs: Code %d", status) app.err.Printf("Failed to get DisplayPrefs: Code %d", status)
app.debug.Printf("Error: %s", err) 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_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.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 return nil
} }

162
email.go
View File

@ -15,16 +15,65 @@ import (
"github.com/mailgun/mailgun-go/v4" "github.com/mailgun/mailgun-go/v4"
) )
type Emailer struct { // implements email sending, right now via smtp or mailgun.
smtpAuth smtp.Auth type emailClient interface {
sendType, sendMethod, fromAddr, fromName string send(address, fromName, fromAddr string, email email) error
content Email
mg *mailgun.MailgunImpl
mime string
host string
} }
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 subject string
html, text string html, text string
} }
@ -50,22 +99,44 @@ func (email *Emailer) formatExpiry(expiry time.Time, tzaware bool, datePattern,
return return
} }
func (email *Emailer) init(app *appContext) { func NewEmailer(app *appContext) *Emailer {
email.fromAddr = app.config.Section("email").Key("address").String() emailer := &Emailer{
email.fromName = app.config.Section("email").Key("from").String() fromAddr: app.config.Section("email").Key("address").String(),
email.sendMethod = app.config.Section("email").Key("method").String() fromName: app.config.Section("email").Key("from").String(),
if email.sendMethod == "mailgun" { }
email.mg = mailgun.NewMailgun(strings.Split(email.fromAddr, "@")[1], app.config.Section("mailgun").Key("api_key").String()) method := app.config.Section("email").Key("method").String()
api_url := app.config.Section("mailgun").Key("api_url").String() if method == "smtp" {
// Mailgun client takes the base url, so we need to trim off the end (e.g 'v3/messages' sslTls := false
if strings.Contains(api_url, "messages") { if app.config.Section("smtp").Key("encryption").String() == "ssl_tls" {
api_url = api_url[0:strings.LastIndex(api_url, "/")] sslTls = true
api_url = api_url[0:strings.LastIndex(api_url, "/")]
} }
email.mg.SetAPIBase(api_url) 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 email.sendMethod == "smtp" { } else if method == "mailgun" {
app.host = app.config.Section("smtp").Key("server").String() emailer.NewMailgun(app.config.Section("mailgun").Key("api_url").String(), app.config.Section("mailgun").Key("api_key").String())
email.smtpAuth = smtp.PlainAuth("", email.fromAddr, app.config.Section("smtp").Key("password").String(), app.host) }
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.content.text = tplData.String()
} }
} }
email.sendType = "invite"
return nil return nil
} }
@ -127,7 +197,6 @@ func (email *Emailer) constructExpiry(code string, invite Invite, app *appContex
email.content.text = tplData.String() email.content.text = tplData.String()
} }
} }
email.sendType = "expiry"
return nil return nil
} }
@ -162,7 +231,6 @@ func (email *Emailer) constructCreated(code, username, address string, invite In
email.content.text = tplData.String() email.content.text = tplData.String()
} }
} }
email.sendType = "created"
return nil return nil
} }
@ -194,47 +262,9 @@ func (email *Emailer) constructReset(pwr Pwr, app *appContext) error {
email.content.text = tplData.String() email.content.text = tplData.String()
} }
} }
email.sendType = "reset"
return nil return nil
} }
func (email *Emailer) send(address string, app *appContext) error { func (emailer *Emailer) send(address string) error {
if email.sendMethod == "mailgun" { return emailer.sender.send(address, emailer.fromName, emailer.fromAddr, emailer.content)
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
} }

View File

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

View File

@ -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) app.err.Printf("Couldn't find email for user \"%s\". Make sure it's set", pwr.Username)
return 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) 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.err.Printf("Failed to send password reset email to \"%s\"", address)
app.debug.Printf("%s: Error: %s", pwr.Username, err)
} else { } else {
app.info.Printf("Sent password reset email to \"%s\"", address) app.info.Printf("Sent password reset email to \"%s\"", address)
} }