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.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 |
|
| `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.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.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.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 |
|
| `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:
|
mail:
|
||||||
enabled: true # whether to enable mails (used for password resets, reports, etc.)
|
enabled: true # whether to enable mails (used for password resets, reports, etc.)
|
||||||
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
|
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:
|
host:
|
||||||
port:
|
port:
|
||||||
username:
|
username:
|
||||||
password:
|
password:
|
||||||
tls:
|
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:
|
url:
|
||||||
client_id:
|
client_id:
|
||||||
client_secret:
|
client_secret:
|
||||||
|
@ -116,6 +116,7 @@ type mailConfig struct {
|
|||||||
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
|
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
|
||||||
MailWhale MailwhaleMailConfig `yaml:"mailwhale"`
|
MailWhale MailwhaleMailConfig `yaml:"mailwhale"`
|
||||||
Smtp SMTPMailConfig `yaml:"smtp"`
|
Smtp SMTPMailConfig `yaml:"smtp"`
|
||||||
|
Sender string `env:"WAKAPI_MAIL_SENDER" yaml:"sender"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MailwhaleMailConfig struct {
|
type MailwhaleMailConfig struct {
|
||||||
@ -130,7 +131,6 @@ type SMTPMailConfig struct {
|
|||||||
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
|
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
|
||||||
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
|
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
|
||||||
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
|
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
|
||||||
Sender string `env:"WAKAPI_MAIL_SMTP_SENDER"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
@ -5,6 +5,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const HtmlType = "text/html; charset=UTF-8"
|
||||||
|
const PlainType = "text/html; charset=UTF-8"
|
||||||
|
|
||||||
type Mail struct {
|
type Mail struct {
|
||||||
From MailAddress
|
From MailAddress
|
||||||
To MailAddresses
|
To MailAddresses
|
||||||
@ -15,13 +18,13 @@ type Mail struct {
|
|||||||
|
|
||||||
func (m *Mail) WithText(text string) *Mail {
|
func (m *Mail) WithText(text string) *Mail {
|
||||||
m.Body = text
|
m.Body = text
|
||||||
m.Type = "text/plain; charset=UTF-8"
|
m.Type = PlainType
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Mail) WithHTML(html string) *Mail {
|
func (m *Mail) WithHTML(html string) *Mail {
|
||||||
m.Body = html
|
m.Body = html
|
||||||
m.Type = "text/html; charset=UTF-8"
|
m.Type = HtmlType
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,11 +5,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"github.com/muety/wakapi/routes"
|
"github.com/muety/wakapi/routes"
|
||||||
|
"github.com/muety/wakapi/services"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/services"
|
|
||||||
"github.com/muety/wakapi/views"
|
"github.com/muety/wakapi/views"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -22,31 +23,76 @@ const (
|
|||||||
subjectReport = "Wakapi – Your Latest Report"
|
subjectReport = "Wakapi – Your Latest Report"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PasswordResetTplData struct {
|
type SendingService interface {
|
||||||
ResetLink string
|
Send(*models.Mail) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportNotificationTplData struct {
|
type MailService struct {
|
||||||
PublicUrl string
|
config *conf.Config
|
||||||
Duration string
|
sendingService SendingService
|
||||||
NumHeartbeats int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportTplData struct {
|
|
||||||
Report *models.Report
|
|
||||||
}
|
|
||||||
|
|
||||||
// Factory
|
|
||||||
func NewMailService() services.IMailService {
|
func NewMailService() services.IMailService {
|
||||||
config := conf.Get()
|
config := conf.Get()
|
||||||
|
|
||||||
|
var sendingService SendingService
|
||||||
|
sendingService = &NoopSendingService{}
|
||||||
|
|
||||||
if config.Mail.Enabled {
|
if config.Mail.Enabled {
|
||||||
if config.Mail.Provider == conf.MailProviderMailWhale {
|
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 {
|
} 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) {
|
func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
|
||||||
|
@ -11,8 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MailWhaleMailService struct {
|
type MailWhaleSendingService struct {
|
||||||
publicUrl string
|
|
||||||
config conf.MailwhaleMailConfig
|
config conf.MailwhaleMailConfig
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
@ -26,9 +25,8 @@ type MailWhaleSendRequest struct {
|
|||||||
TemplateVars map[string]string `json:"template_vars"`
|
TemplateVars map[string]string `json:"template_vars"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMailWhaleService(config conf.MailwhaleMailConfig, publicUrl string) *MailWhaleMailService {
|
func NewMailWhaleSendingService(config conf.MailwhaleMailConfig) *MailWhaleSendingService {
|
||||||
return &MailWhaleMailService{
|
return &MailWhaleSendingService{
|
||||||
publicUrl: publicUrl,
|
|
||||||
config: config,
|
config: config,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 10 * time.Second,
|
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 {
|
func (s *MailWhaleSendingService) Send(mail *models.Mail) error {
|
||||||
template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
|
if len(mail.To) == 0 {
|
||||||
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 == "" {
|
|
||||||
return errors.New("not sending mail as recipient mail address seems to be invalid")
|
return errors.New("not sending mail as recipient mail address seems to be invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
sendRequest := &MailWhaleSendRequest{
|
sendRequest := &MailWhaleSendRequest{
|
||||||
To: []string{to},
|
To: mail.To.Strings(),
|
||||||
Subject: subject,
|
Subject: mail.Subject,
|
||||||
}
|
}
|
||||||
if isHtml {
|
if mail.Type == models.HtmlType {
|
||||||
sendRequest.Html = body
|
sendRequest.Html = mail.Body
|
||||||
} else {
|
} else {
|
||||||
sendRequest.Text = body
|
sendRequest.Text = mail.Body
|
||||||
}
|
}
|
||||||
payload, _ := json.Marshal(sendRequest)
|
payload, _ := json.Marshal(sendRequest)
|
||||||
|
|
||||||
|
@ -3,24 +3,11 @@ package mail
|
|||||||
import (
|
import (
|
||||||
"github.com/emvi/logbuch"
|
"github.com/emvi/logbuch"
|
||||||
"github.com/muety/wakapi/models"
|
"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 *NoopSendingService) Send(mail *models.Mail) error {
|
||||||
|
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to [%v]", mail.To.Strings())
|
||||||
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)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,28 +2,20 @@ package mail
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
conf "github.com/muety/wakapi/config"
|
conf "github.com/muety/wakapi/config"
|
||||||
"github.com/muety/wakapi/models"
|
"github.com/muety/wakapi/models"
|
||||||
"io"
|
"io"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SMTPMailService struct {
|
type SMTPSendingService struct {
|
||||||
publicUrl string
|
|
||||||
config conf.SMTPMailConfig
|
config conf.SMTPMailConfig
|
||||||
auth sasl.Client
|
auth sasl.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SMTPMailService) SendReport(recipient *models.User, report *models.Report) error {
|
func NewSMTPSendingService(config conf.SMTPMailConfig) *SMTPSendingService {
|
||||||
panic("implement me") // TODO
|
return &SMTPSendingService{
|
||||||
}
|
|
||||||
|
|
||||||
func NewSMTPMailService(config conf.SMTPMailConfig, publicUrl string) *SMTPMailService {
|
|
||||||
return &SMTPMailService{
|
|
||||||
publicUrl: publicUrl,
|
|
||||||
config: config,
|
config: config,
|
||||||
auth: sasl.NewPlainClient(
|
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 {
|
func (s *SMTPSendingService) Send(mail *models.Mail) 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 {
|
|
||||||
dial := smtp.Dial
|
dial := smtp.Dial
|
||||||
if tls {
|
if s.config.TLS {
|
||||||
dial = func(addr string) (*smtp.Client, error) {
|
dial = func(addr string) (*smtp.Client, error) {
|
||||||
return smtp.DialTLS(addr, nil)
|
return smtp.DialTLS(addr, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := dial(addr)
|
c, err := dial(s.config.ConnStr())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -89,18 +45,18 @@ func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if a != nil {
|
if s.auth != nil {
|
||||||
if ok, _ := c.Extension("AUTH"); !ok {
|
if ok, _ := c.Extension("AUTH"); !ok {
|
||||||
return errors.New("smtp: server doesn't support AUTH")
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = c.Mail(from, nil); err != nil {
|
if err = c.Mail(mail.From.Raw(), nil); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, addr := range to {
|
for _, addr := range mail.To.RawStrings() {
|
||||||
if err = c.Rcpt(addr); err != nil {
|
if err = c.Rcpt(addr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -109,7 +65,7 @@ func (s *SMTPMailService) send(addr string, tls bool, a sasl.Client, from string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = io.Copy(w, r)
|
_, err = io.Copy(w, mail.Reader())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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>
|
<tr>
|
||||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
|
<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: 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>
|
<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;">
|
<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