mirror of
https://github.com/muety/wakapi.git
synced 2023-08-10 21:12:56 +03:00
refactor: mail service abstraction layer
This commit is contained in:
parent
29c04c3ac5
commit
a4e7158db2
@ -169,6 +169,7 @@ You can specify configuration options either via a config file (default: `config
|
||||
| `db.ssl` | `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
|
||||
| `db.automgirate_fail_silently` | `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
|
||||
| `mail.enabled` | `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
|
||||
| `mail.sender` | `WAKAPI_MAIL_SENDER` | `noreply@wakapi.dev` | Default sender address for outgoing mails (ignored for MailWhale) |
|
||||
| `mail.provider` | `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
|
||||
| `mail.smtp.*` | `WAKAPI_MAIL_SMTP_*` | `-` | Various options to configure SMTP. See [default config](config.default.yaml) for details |
|
||||
| `mail.mailwhale.*` | `WAKAPI_MAIL_MAILWHALE_*` | `-` | Various options to configure [MailWhale](https://mailwhale.dev) sending service. See [default config](config.default.yaml) for details |
|
||||
|
@ -46,14 +46,18 @@ sentry:
|
||||
mail:
|
||||
enabled: true # whether to enable mails (used for password resets, reports, etc.)
|
||||
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
|
||||
smtp: # smtp settings when sending mails via smtp
|
||||
sender: Wakapi <noreply@wakapi.dev> # ignored for mailwhale
|
||||
|
||||
# smtp settings when sending mails via smtp
|
||||
smtp:
|
||||
host:
|
||||
port:
|
||||
username:
|
||||
password:
|
||||
tls:
|
||||
sender: Wakapi <noreply@wakapi.dev>
|
||||
mailwhale: # mailwhale.dev settings when using mailwhale as sending service
|
||||
|
||||
# mailwhale.dev settings when using mailwhale as sending service
|
||||
mailwhale:
|
||||
url:
|
||||
client_id:
|
||||
client_secret:
|
||||
|
@ -116,6 +116,7 @@ type mailConfig struct {
|
||||
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
|
||||
MailWhale MailwhaleMailConfig `yaml:"mailwhale"`
|
||||
Smtp SMTPMailConfig `yaml:"smtp"`
|
||||
Sender string `env:"WAKAPI_MAIL_SENDER" yaml:"sender"`
|
||||
}
|
||||
|
||||
type MailwhaleMailConfig struct {
|
||||
@ -130,7 +131,6 @@ type SMTPMailConfig struct {
|
||||
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
|
||||
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
|
||||
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
|
||||
Sender string `env:"WAKAPI_MAIL_SMTP_SENDER"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -5,6 +5,9 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const HtmlType = "text/html; charset=UTF-8"
|
||||
const PlainType = "text/html; charset=UTF-8"
|
||||
|
||||
type Mail struct {
|
||||
From MailAddress
|
||||
To MailAddresses
|
||||
@ -15,13 +18,13 @@ type Mail struct {
|
||||
|
||||
func (m *Mail) WithText(text string) *Mail {
|
||||
m.Body = text
|
||||
m.Type = "text/plain; charset=UTF-8"
|
||||
m.Type = PlainType
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Mail) WithHTML(html string) *Mail {
|
||||
m.Body = html
|
||||
m.Type = "text/html; charset=UTF-8"
|
||||
m.Type = HtmlType
|
||||
return m
|
||||
}
|
||||
|
||||
|
@ -5,11 +5,12 @@ import (
|
||||
"fmt"
|
||||
"github.com/muety/wakapi/models"
|
||||
"github.com/muety/wakapi/routes"
|
||||
"github.com/muety/wakapi/services"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/services"
|
||||
"github.com/muety/wakapi/views"
|
||||
)
|
||||
|
||||
@ -22,31 +23,76 @@ const (
|
||||
subjectReport = "Wakapi – Your Latest Report"
|
||||
)
|
||||
|
||||
type PasswordResetTplData struct {
|
||||
ResetLink string
|
||||
type SendingService interface {
|
||||
Send(*models.Mail) error
|
||||
}
|
||||
|
||||
type ImportNotificationTplData struct {
|
||||
PublicUrl string
|
||||
Duration string
|
||||
NumHeartbeats int
|
||||
type MailService struct {
|
||||
config *conf.Config
|
||||
sendingService SendingService
|
||||
}
|
||||
|
||||
type ReportTplData struct {
|
||||
Report *models.Report
|
||||
}
|
||||
|
||||
// Factory
|
||||
func NewMailService() services.IMailService {
|
||||
config := conf.Get()
|
||||
|
||||
var sendingService SendingService
|
||||
sendingService = &NoopSendingService{}
|
||||
|
||||
if config.Mail.Enabled {
|
||||
if config.Mail.Provider == conf.MailProviderMailWhale {
|
||||
return NewMailWhaleService(config.Mail.MailWhale, config.Server.PublicUrl)
|
||||
sendingService = NewMailWhaleSendingService(config.Mail.MailWhale)
|
||||
} else if config.Mail.Provider == conf.MailProviderSmtp {
|
||||
return NewSMTPMailService(config.Mail.Smtp, config.Server.PublicUrl)
|
||||
sendingService = NewSMTPSendingService(config.Mail.Smtp)
|
||||
}
|
||||
}
|
||||
return &NoopMailService{}
|
||||
|
||||
return &MailService{sendingService: sendingService, config: config}
|
||||
}
|
||||
|
||||
func (m *MailService) SendPasswordReset(recipient *models.User, resetLink string) error {
|
||||
tpl, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mail := &models.Mail{
|
||||
From: models.MailAddress(m.config.Mail.Sender),
|
||||
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||
Subject: subjectPasswordReset,
|
||||
}
|
||||
mail.WithHTML(tpl.String())
|
||||
return m.sendingService.Send(mail)
|
||||
}
|
||||
|
||||
func (m *MailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
|
||||
tpl, err := getImportNotificationTemplate(ImportNotificationTplData{
|
||||
PublicUrl: m.config.Server.PublicUrl,
|
||||
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
|
||||
NumHeartbeats: numHeartbeats,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mail := &models.Mail{
|
||||
From: models.MailAddress(m.config.Mail.Sender),
|
||||
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||
Subject: subjectImportNotification,
|
||||
}
|
||||
mail.WithHTML(tpl.String())
|
||||
return m.sendingService.Send(mail)
|
||||
}
|
||||
|
||||
func (m *MailService) SendReport(recipient *models.User, report *models.Report) error {
|
||||
tpl, err := getReportTemplate(ReportTplData{report})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mail := &models.Mail{
|
||||
From: models.MailAddress(m.config.Mail.Sender),
|
||||
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||
Subject: subjectReport,
|
||||
}
|
||||
mail.WithHTML(tpl.String())
|
||||
return m.sendingService.Send(mail)
|
||||
}
|
||||
|
||||
func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
|
||||
|
@ -11,8 +11,7 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type MailWhaleMailService struct {
|
||||
publicUrl string
|
||||
type MailWhaleSendingService struct {
|
||||
config conf.MailwhaleMailConfig
|
||||
httpClient *http.Client
|
||||
}
|
||||
@ -26,9 +25,8 @@ type MailWhaleSendRequest struct {
|
||||
TemplateVars map[string]string `json:"template_vars"`
|
||||
}
|
||||
|
||||
func NewMailWhaleService(config conf.MailwhaleMailConfig, publicUrl string) *MailWhaleMailService {
|
||||
return &MailWhaleMailService{
|
||||
publicUrl: publicUrl,
|
||||
func NewMailWhaleSendingService(config conf.MailwhaleMailConfig) *MailWhaleSendingService {
|
||||
return &MailWhaleSendingService{
|
||||
config: config,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
@ -36,47 +34,19 @@ func NewMailWhaleService(config conf.MailwhaleMailConfig, publicUrl string) *Mai
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MailWhaleMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
|
||||
template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.send(recipient.Email, subjectPasswordReset, template.String(), true)
|
||||
}
|
||||
|
||||
func (s *MailWhaleMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
|
||||
template, err := getImportNotificationTemplate(ImportNotificationTplData{
|
||||
PublicUrl: s.publicUrl,
|
||||
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
|
||||
NumHeartbeats: numHeartbeats,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.send(recipient.Email, subjectImportNotification, template.String(), true)
|
||||
}
|
||||
|
||||
func (s *MailWhaleMailService) SendReport(recipient *models.User, report *models.Report) error {
|
||||
template, err := getReportTemplate(ReportTplData{report})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.send(recipient.Email, subjectReport, template.String(), true)
|
||||
}
|
||||
|
||||
func (s *MailWhaleMailService) send(to, subject, body string, isHtml bool) error {
|
||||
if to == "" {
|
||||
func (s *MailWhaleSendingService) Send(mail *models.Mail) error {
|
||||
if len(mail.To) == 0 {
|
||||
return errors.New("not sending mail as recipient mail address seems to be invalid")
|
||||
}
|
||||
|
||||
sendRequest := &MailWhaleSendRequest{
|
||||
To: []string{to},
|
||||
Subject: subject,
|
||||
To: mail.To.Strings(),
|
||||
Subject: mail.Subject,
|
||||
}
|
||||
if isHtml {
|
||||
sendRequest.Html = body
|
||||
if mail.Type == models.HtmlType {
|
||||
sendRequest.Html = mail.Body
|
||||
} else {
|
||||
sendRequest.Text = body
|
||||
sendRequest.Text = mail.Body
|
||||
}
|
||||
payload, _ := json.Marshal(sendRequest)
|
||||
|
||||
|
@ -3,24 +3,11 @@ package mail
|
||||
import (
|
||||
"github.com/emvi/logbuch"
|
||||
"github.com/muety/wakapi/models"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NoopMailService struct{}
|
||||
type NoopSendingService struct{}
|
||||
|
||||
const notImplemented = "noop mail service doing nothing instead of sending password reset mail to %s"
|
||||
|
||||
func (n *NoopMailService) SendReport(recipient *models.User, report *models.Report) error {
|
||||
logbuch.Info(notImplemented, recipient.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
|
||||
logbuch.Info(notImplemented, recipient.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *NoopMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
|
||||
logbuch.Info(notImplemented, recipient.ID)
|
||||
func (n *NoopSendingService) Send(mail *models.Mail) error {
|
||||
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to [%v]", mail.To.Strings())
|
||||
return nil
|
||||
}
|
||||
|
@ -2,28 +2,20 @@ package mail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/emersion/go-sasl"
|
||||
"github.com/emersion/go-smtp"
|
||||
conf "github.com/muety/wakapi/config"
|
||||
"github.com/muety/wakapi/models"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SMTPMailService struct {
|
||||
publicUrl string
|
||||
type SMTPSendingService struct {
|
||||
config conf.SMTPMailConfig
|
||||
auth sasl.Client
|
||||
}
|
||||
|
||||
func (s *SMTPMailService) SendReport(recipient *models.User, report *models.Report) error {
|
||||
panic("implement me") // TODO
|
||||
}
|
||||
|
||||
func NewSMTPMailService(config conf.SMTPMailConfig, publicUrl string) *SMTPMailService {
|
||||
return &SMTPMailService{
|
||||
publicUrl: publicUrl,
|
||||
func NewSMTPSendingService(config conf.SMTPMailConfig) *SMTPSendingService {
|
||||
return &SMTPSendingService{
|
||||
config: config,
|
||||
auth: sasl.NewPlainClient(
|
||||
"",
|
||||
@ -33,51 +25,15 @@ func NewSMTPMailService(config conf.SMTPMailConfig, publicUrl string) *SMTPMailS
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SMTPMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
|
||||
template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mail := &models.Mail{
|
||||
From: models.MailAddress(s.config.Sender),
|
||||
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||
Subject: subjectPasswordReset,
|
||||
}
|
||||
mail.WithHTML(template.String())
|
||||
|
||||
return s.send(s.config.ConnStr(), s.config.TLS, s.auth, mail.From.Raw(), mail.To.RawStrings(), mail.Reader())
|
||||
}
|
||||
|
||||
func (s *SMTPMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
|
||||
template, err := getImportNotificationTemplate(ImportNotificationTplData{
|
||||
PublicUrl: s.publicUrl,
|
||||
Duration: fmt.Sprintf("%.0f seconds", duration.Seconds()),
|
||||
NumHeartbeats: numHeartbeats,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mail := &models.Mail{
|
||||
From: models.MailAddress(s.config.Sender),
|
||||
To: models.MailAddresses([]models.MailAddress{models.MailAddress(recipient.Email)}),
|
||||
Subject: subjectImportNotification,
|
||||
}
|
||||
mail.WithHTML(template.String())
|
||||
|
||||
return s.send(s.config.ConnStr(), s.config.TLS, s.auth, mail.From.Raw(), mail.To.RawStrings(), mail.Reader())
|
||||
}
|
||||
|
||||
func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string, to []string, r io.Reader) error {
|
||||
func (s *SMTPSendingService) Send(mail *models.Mail) error {
|
||||
dial := smtp.Dial
|
||||
if tls {
|
||||
if s.config.TLS {
|
||||
dial = func(addr string) (*smtp.Client, error) {
|
||||
return smtp.DialTLS(addr, nil)
|
||||
}
|
||||
}
|
||||
|
||||
c, err := dial(addr)
|
||||
c, err := dial(s.config.ConnStr())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -89,18 +45,18 @@ func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string
|
||||
return err
|
||||
}
|
||||
}
|
||||
if a != nil {
|
||||
if s.auth != nil {
|
||||
if ok, _ := c.Extension("AUTH"); !ok {
|
||||
return errors.New("smtp: server doesn't support AUTH")
|
||||
}
|
||||
if err = c.Auth(a); err != nil {
|
||||
if err = c.Auth(s.auth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = c.Mail(from, nil); err != nil {
|
||||
if err = c.Mail(mail.From.Raw(), nil); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, addr := range to {
|
||||
for _, addr := range mail.To.RawStrings() {
|
||||
if err = c.Rcpt(addr); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -109,7 +65,7 @@ func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(w, r)
|
||||
_, err = io.Copy(w, mail.Reader())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
17
services/mail/types.go
Normal file
17
services/mail/types.go
Normal file
@ -0,0 +1,17 @@
|
||||
package mail
|
||||
|
||||
import "github.com/muety/wakapi/models"
|
||||
|
||||
type PasswordResetTplData struct {
|
||||
ResetLink string
|
||||
}
|
||||
|
||||
type ImportNotificationTplData struct {
|
||||
PublicUrl string
|
||||
Duration string
|
||||
NumHeartbeats int
|
||||
}
|
||||
|
||||
type ReportTplData struct {
|
||||
Report *models.Report
|
||||
}
|
@ -111,7 +111,7 @@
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
|
||||
<p style="font-family: sans-serif; font-size: 18px; font-weight: 500; margin: 0; Margin-bottom: 15px;">Your Stats from {{ .Report.From | simpledate }} to {{ .Report.To | simpledate }}</p>
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have coded a total of <strong>{{ .Report.Summary.TotalTime | duration }}</strong> between {{ .Report.From | simpledate }} and {{ .Report.To | simpledate }}</p>
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">You have coded a total of <strong>{{ .Report.Summary.TotalTime | duration }}</strong> between {{ .Report.From | simpledate }} and {{ .Report.To | simpledate }}.</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 16px; font-weight: 500; margin: 0; Margin-bottom: 15px; Margin-top: 30px;">Projects</p>
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="btn btn-primary" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; box-sizing: border-box;">
|
||||
|
Loading…
Reference in New Issue
Block a user