diff --git a/app.go b/app.go index 7630254..932fdca 100644 --- a/app.go +++ b/app.go @@ -428,15 +428,11 @@ func Initialize(apper Apper, debug bool) (*App, error) { initActivityPub(apper.App()) - if apper.App().cfg.Email.Domain != "" || apper.App().cfg.Email.MailgunPrivate != "" { - if apper.App().cfg.Email.Domain == "" { - log.Error("[FAILED] Starting publish jobs queue: no [letters]domain config value set.") - } else if apper.App().cfg.Email.MailgunPrivate == "" { - log.Error("[FAILED] Starting publish jobs queue: no [letters]mailgun_private config value set.") - } else { - log.Info("Starting publish jobs queue...") - go startPublishJobsQueue(apper.App()) - } + if apper.App().cfg.Email.Enabled() { + log.Info("Starting publish jobs queue...") + go startPublishJobsQueue(apper.App()) + } else { + log.Error("[FAILED] Starting publish jobs queue: no email provider is configured.") } // Handle local timeline, if enabled diff --git a/config/config.go b/config/config.go index 8a79224..7bee863 100644 --- a/config/config.go +++ b/config/config.go @@ -251,7 +251,8 @@ func (ac *AppCfg) LandingPath() string { } func (lc EmailCfg) Enabled() bool { - return lc.Domain != "" && lc.MailgunPrivate != "" + return (lc.Domain != "" && lc.MailgunPrivate != "") || + lc.Username != "" && lc.Password != "" && lc.Host != "" && lc.Port > 0 } func (ac AppCfg) SignupPath() string { diff --git a/email.go b/email.go index 7da60e4..eed3985 100644 --- a/email.go +++ b/email.go @@ -22,7 +22,6 @@ import ( "github.com/aymerick/douceur/inliner" "github.com/gorilla/mux" - "github.com/mailgun/mailgun-go" stripmd "github.com/writeas/go-strip-markdown/v2" "github.com/writeas/impart" "github.com/writeas/web-core/data" @@ -308,14 +307,14 @@ Originally published on ` + p.Collection.DisplayTitle() + ` (` + p.Collection.Ca Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/unsubscribe/%recipient.id%?t=%recipient.token%` - gun := mailgun.NewMailgun(app.cfg.Email.Domain, app.cfg.Email.MailgunPrivate) - - if app.cfg.Email.MailgunEurope { - gun.SetAPIBase("https://api.eu.mailgun.net/v3") + mlr, err := mailer.New(app.cfg.Email) + if err != nil { + return err + } + m, err := mlr.NewMessage(p.Collection.DisplayTitle()+" <"+p.Collection.Alias+"@"+app.cfg.Email.Domain+">", stripmd.Strip(p.DisplayTitle()), plainMsg) + if err != nil { + return err } - - - m := mailgun.NewMessage(p.Collection.DisplayTitle()+" <"+p.Collection.Alias+"@"+app.cfg.Email.Domain+">", stripmd.Strip(p.DisplayTitle()), plainMsg) replyTo := app.db.GetCollectionAttribute(collID, collAttrLetterReplyTo) if replyTo != "" { m.SetReplyTo(replyTo) @@ -412,13 +411,13 @@ Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/un return err } - m.SetHtml(html) + m.SetHTML(html) log.Info("[email] Adding %d recipient(s)", len(subs)) for _, s := range subs { e := s.FinalEmail(app.keys) log.Info("[email] Adding %s", e) - err = m.AddRecipientAndVariables(e, map[string]interface{}{ + err = m.AddRecipientAndVariables(e, map[string]string{ "id": s.ID, "to": e, "token": s.Token, @@ -428,8 +427,8 @@ Sent to %recipient.to%. Unsubscribe: ` + p.Collection.CanonicalURL() + `email/un } } - res, _, err := gun.Send(m) - log.Info("[email] Send result: %s", res) + err = mlr.Send(m) + log.Info("[email] Email sent") if err != nil { log.Error("Unable to send post email: %v", err) return err diff --git a/mailer/mailer.go b/mailer/mailer.go index a75b585..30892e6 100644 --- a/mailer/mailer.go +++ b/mailer/mailer.go @@ -13,8 +13,10 @@ package mailer import ( "fmt" "github.com/mailgun/mailgun-go" + "github.com/writeas/web-core/log" "github.com/writefreely/writefreely/config" mail "github.com/xhit/go-simple-mail/v2" + "strings" ) type ( @@ -27,7 +29,21 @@ type ( // Message holds the email contents and metadata for the preferred mailing provider. Message struct { mgMsg *mailgun.Message - smtpMsg *mail.Email + smtpMsg *SmtpMessage + } + + SmtpMessage struct { + from string + replyTo string + subject string + recipients []Recipient + html string + text string + } + + Recipient struct { + email string + vars map[string]string } ) @@ -48,6 +64,8 @@ func New(eCfg config.EmailCfg) (*Mailer, error) { if eCfg.EnableStartTLS { m.smtp.Encryption = mail.EncryptionSTARTTLS } + // To allow sending multiple email + m.smtp.KeepAlive = true } else { return nil, fmt.Errorf("no email provider is configured") } @@ -61,14 +79,16 @@ func (m *Mailer) NewMessage(from, subject, text string, to ...string) (*Message, if m.mailGun != nil { msg.mgMsg = m.mailGun.NewMessage(from, subject, text, to...) } else if m.smtp != nil { - msg.smtpMsg = mail.NewMSG() - msg.smtpMsg.SetFrom(from) - msg.smtpMsg.AddTo(to...) - msg.smtpMsg.SetSubject(subject) - msg.smtpMsg.AddAlternative(mail.TextPlain, text) - - if msg.smtpMsg.Error != nil { - return nil, msg.smtpMsg.Error + msg.smtpMsg = &SmtpMessage{ + from: from, + replyTo: "", + subject: subject, + recipients: make([]Recipient, len(to)), + html: "", + text: text, + } + for _, r := range to { + msg.smtpMsg.recipients = append(msg.smtpMsg.recipients, Recipient{r, make(map[string]string)}) } } return msg, nil @@ -77,12 +97,20 @@ func (m *Mailer) NewMessage(from, subject, text string, to ...string) (*Message, // SetHTML sets the body of the message. func (m *Message) SetHTML(html string) { if m.smtpMsg != nil { - m.smtpMsg.SetBody(mail.TextHTML, html) + m.smtpMsg.html = html } else if m.mgMsg != nil { m.mgMsg.SetHtml(html) } } +func (m *Message) SetReplyTo(replyTo string) { + if m.smtpMsg != nil { + m.smtpMsg.replyTo = replyTo + } else { + m.mgMsg.SetReplyTo(replyTo) + } +} + // AddTag attaches a tag to the Message for providers that support it. func (m *Message) AddTag(tag string) { if m.mgMsg != nil { @@ -90,6 +118,19 @@ func (m *Message) AddTag(tag string) { } } +func (m *Message) AddRecipientAndVariables(r string, vars map[string]string) error { + if m.smtpMsg != nil { + m.smtpMsg.recipients = append(m.smtpMsg.recipients, Recipient{r, vars}) + return nil + } else { + varsInterfaces := make(map[string]interface{}, len(vars)) + for k, v := range vars { + varsInterfaces[k] = v + } + return m.mgMsg.AddRecipientAndVariables(r, varsInterfaces) + } +} + // Send sends the given message via the preferred provider. func (m *Mailer) Send(msg *Message) error { if m.smtp != nil { @@ -97,7 +138,39 @@ func (m *Mailer) Send(msg *Message) error { if err != nil { return err } - return msg.smtpMsg.Send(client) + emailSent := false + for _, r := range msg.smtpMsg.recipients { + customMsg := mail.NewMSG() + customMsg.SetFrom(msg.smtpMsg.from) + if msg.smtpMsg.replyTo != "" { + customMsg.SetReplyTo(msg.smtpMsg.replyTo) + } + customMsg.SetSubject(msg.smtpMsg.subject) + customMsg.AddTo(r.email) + cText := msg.smtpMsg.text + cHtml := msg.smtpMsg.html + for v, value := range r.vars { + placeHolder := fmt.Sprintf("%%recipient.%s%%", v) + cText = strings.ReplaceAll(cText, placeHolder, value) + cHtml = strings.ReplaceAll(cHtml, placeHolder, value) + } + customMsg.SetBody(mail.TextHTML, cHtml) + customMsg.AddAlternative(mail.TextPlain, cText) + e := customMsg.Error + if e == nil { + e = customMsg.Send(client) + } + if e == nil { + emailSent = true + } else { + log.Error("Unable to send email to %s: %v", r.email, e) + err = e + } + } + if !emailSent { + // only send an error if no email could be sent (to avoid retry of successfully sent emails) + return err + } } else if m.mailGun != nil { _, _, err := m.mailGun.Send(msg.mgMsg) if err != nil { diff --git a/templates.go b/templates.go index 3bb7d13..030467c 100644 --- a/templates.go +++ b/templates.go @@ -14,8 +14,8 @@ import ( "errors" "html/template" "io" - "os" "net/http" + "os" "path/filepath" "strings"