diff --git a/main.go b/main.go
index dfb478e..19a5371 100644
--- a/main.go
+++ b/main.go
@@ -160,7 +160,7 @@ func main() {
// MVC Handlers
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
- settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService)
+ settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, keyValueService, mailService)
homeHandler := routes.NewHomeHandler(keyValueService)
loginHandler := routes.NewLoginHandler(userService, mailService)
imprintHandler := routes.NewImprintHandler(keyValueService)
diff --git a/models/mail_address.go b/models/mail_address.go
index 726a680..324f63e 100644
--- a/models/mail_address.go
+++ b/models/mail_address.go
@@ -55,3 +55,12 @@ func (m MailAddresses) RawStrings() []string {
}
return out
}
+
+func (m MailAddresses) AllValid() bool {
+ for _, a := range m {
+ if !a.Valid() {
+ return false
+ }
+ }
+ return true
+}
diff --git a/routes/login.go b/routes/login.go
index 166f606..b5c2900 100644
--- a/routes/login.go
+++ b/routes/login.go
@@ -283,7 +283,7 @@ func (h *LoginHandler) PostResetPassword(w http.ResponseWriter, r *http.Request)
} else {
go func(user *models.User) {
link := fmt.Sprintf("%s/set-password?token=%s", h.config.Server.GetPublicUrl(), user.ResetToken)
- if err := h.mailSrvc.SendPasswordResetMail(user, link); err != nil {
+ if err := h.mailSrvc.SendPasswordReset(user, link); err != nil {
logbuch.Error("failed to send password reset mail to %s – %v", user.ID, err)
} else {
logbuch.Info("sent password reset mail to %s", user.ID)
diff --git a/routes/settings.go b/routes/settings.go
index 61eb61e..8297f65 100644
--- a/routes/settings.go
+++ b/routes/settings.go
@@ -27,6 +27,7 @@ type SettingsHandler struct {
aggregationSrvc services.IAggregationService
languageMappingSrvc services.ILanguageMappingService
keyValueSrvc services.IKeyValueService
+ mailSrvc services.IMailService
httpClient *http.Client
}
@@ -40,6 +41,7 @@ func NewSettingsHandler(
aggregationService services.IAggregationService,
languageMappingService services.ILanguageMappingService,
keyValueService services.IKeyValueService,
+ mailService services.IMailService,
) *SettingsHandler {
return &SettingsHandler{
config: conf.Get(),
@@ -50,6 +52,7 @@ func NewSettingsHandler(
userSrvc: userService,
heartbeatSrvc: heartbeatService,
keyValueSrvc: keyValueService,
+ mailSrvc: mailService,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
@@ -401,6 +404,7 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
}
go func(user *models.User) {
+ start := time.Now()
importer := imports.NewWakatimeHeartbeatImporter(user.WakatimeApiKey)
countBefore, err := h.heartbeatSrvc.CountByUser(user)
@@ -443,6 +447,14 @@ func (h *SettingsHandler) actionImportWaktime(w http.ResponseWriter, r *http.Req
logbuch.Info("downloaded %d heartbeats for user '%s' (%d actually imported)", count, user.ID, countAfter-countBefore)
h.regenerateSummaries(user)
+
+ if user.Email != "" {
+ if err := h.mailSrvc.SendImportNotification(user, time.Now().Sub(start), int(countAfter-countBefore)); err != nil {
+ logbuch.Error("failed to send import notification mail to %s – %v", user.ID, err)
+ } else {
+ logbuch.Info("sent import notification mail to %s", user.ID)
+ }
+ }
}(user)
h.keyValueSrvc.PutString(&models.KeyStringValue{
diff --git a/services/mail/mail.go b/services/mail/mail.go
index 5aeb07c..b94ce68 100644
--- a/services/mail/mail.go
+++ b/services/mail/mail.go
@@ -11,29 +11,37 @@ import (
)
const (
- tplPath = "/views/mail"
- tplNamePasswordReset = "reset_password"
- subjectPasswordReset = "Wakapi – Password Reset"
+ tplPath = "/views/mail"
+ tplNamePasswordReset = "reset_password"
+ tplNameImportNotification = "import_finished"
+ subjectPasswordReset = "Wakapi – Password Reset"
+ subjectImportNotification = "Wakapi – Data Import Finished"
)
-type passwordResetLinkTplData struct {
+type PasswordResetTplData struct {
ResetLink string
}
+type ImportNotificationTplData struct {
+ PublicUrl string
+ Duration string
+ NumHeartbeats int
+}
+
// Factory
func NewMailService() services.IMailService {
config := conf.Get()
if config.Mail.Enabled {
if config.Mail.Provider == conf.MailProviderMailWhale {
- return NewMailWhaleService(config.Mail.MailWhale)
+ return NewMailWhaleService(config.Mail.MailWhale, config.Server.PublicUrl)
} else if config.Mail.Provider == conf.MailProviderSmtp {
- return NewSMTPMailService(config.Mail.Smtp)
+ return NewSMTPMailService(config.Mail.Smtp, config.Server.PublicUrl)
}
}
return &NoopMailService{}
}
-func getPasswordResetTemplate(data passwordResetLinkTplData) (*bytes.Buffer, error) {
+func getPasswordResetTemplate(data PasswordResetTplData) (*bytes.Buffer, error) {
tpl, err := loadTemplate(tplNamePasswordReset)
if err != nil {
return nil, err
@@ -45,6 +53,18 @@ func getPasswordResetTemplate(data passwordResetLinkTplData) (*bytes.Buffer, err
return &rendered, nil
}
+func getImportNotificationTemplate(data ImportNotificationTplData) (*bytes.Buffer, error) {
+ tpl, err := loadTemplate(tplNameImportNotification)
+ if err != nil {
+ return nil, err
+ }
+ var rendered bytes.Buffer
+ if err := tpl.Execute(&rendered, data); err != nil {
+ return nil, err
+ }
+ return &rendered, nil
+}
+
func loadTemplate(tplName string) (*template.Template, error) {
tplFile, err := pkger.Open(fmt.Sprintf("%s/%s.tpl.html", tplPath, tplName))
if err != nil {
diff --git a/services/mail/mailwhale.go b/services/mail/mailwhale.go
index bae7986..d7cc164 100644
--- a/services/mail/mailwhale.go
+++ b/services/mail/mailwhale.go
@@ -12,6 +12,7 @@ import (
)
type MailWhaleMailService struct {
+ publicUrl string
config *conf.MailwhaleMailConfig
httpClient *http.Client
}
@@ -25,26 +26,39 @@ type MailWhaleSendRequest struct {
TemplateVars map[string]string `json:"template_vars"`
}
-func NewMailWhaleService(config *conf.MailwhaleMailConfig) *MailWhaleMailService {
+func NewMailWhaleService(config *conf.MailwhaleMailConfig, publicUrl string) *MailWhaleMailService {
return &MailWhaleMailService{
- config: config,
+ publicUrl: publicUrl,
+ config: config,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
-func (s *MailWhaleMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
- template, err := getPasswordResetTemplate(passwordResetLinkTplData{ResetLink: resetLink})
+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) send(to, subject, body string, isHtml bool) error {
if to == "" {
- return errors.New("no recipient mail address set, cannot send password reset link")
+ return errors.New("not sending mail as recipient mail address seems to be invalid")
}
sendRequest := &MailWhaleSendRequest{
diff --git a/services/mail/noop.go b/services/mail/noop.go
index a1ef569..c26ae9f 100644
--- a/services/mail/noop.go
+++ b/services/mail/noop.go
@@ -3,11 +3,17 @@ package mail
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/models"
+ "time"
)
type NoopMailService struct{}
-func (n *NoopMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
+func (n *NoopMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
logbuch.Info("noop mail service doing nothing instead of sending password reset mail to %s", recipient.ID)
return nil
}
+
+func (n *NoopMailService) SendImportNotification(recipient *models.User, duration time.Duration, numHeartbeats int) error {
+ logbuch.Info("noop mail service doing nothing instead of sending import notification mail to %s", recipient.ID)
+ return nil
+}
diff --git a/services/mail/smtp.go b/services/mail/smtp.go
index cf15b3e..d040f51 100644
--- a/services/mail/smtp.go
+++ b/services/mail/smtp.go
@@ -2,21 +2,25 @@ 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 {
- config *conf.SMTPMailConfig
- auth sasl.Client
+ publicUrl string
+ config *conf.SMTPMailConfig
+ auth sasl.Client
}
-func NewSMTPMailService(config *conf.SMTPMailConfig) *SMTPMailService {
+func NewSMTPMailService(config *conf.SMTPMailConfig, publicUrl string) *SMTPMailService {
return &SMTPMailService{
- config: config,
+ publicUrl: publicUrl,
+ config: config,
auth: sasl.NewPlainClient(
"",
config.Username,
@@ -25,8 +29,8 @@ func NewSMTPMailService(config *conf.SMTPMailConfig) *SMTPMailService {
}
}
-func (s *SMTPMailService) SendPasswordResetMail(recipient *models.User, resetLink string) error {
- template, err := getPasswordResetTemplate(passwordResetLinkTplData{ResetLink: resetLink})
+func (s *SMTPMailService) SendPasswordReset(recipient *models.User, resetLink string) error {
+ template, err := getPasswordResetTemplate(PasswordResetTplData{ResetLink: resetLink})
if err != nil {
return err
}
@@ -38,14 +42,27 @@ func (s *SMTPMailService) SendPasswordResetMail(recipient *models.User, resetLin
}
mail.WithHTML(template.String())
- return s.send(
- s.config.ConnStr(),
- s.config.TLS,
- s.auth,
- mail.From.Raw(),
- mail.To.RawStrings(),
- mail.Reader(),
- )
+ 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 {
diff --git a/services/services.go b/services/services.go
index 0242cd7..43c9d24 100644
--- a/services/services.go
+++ b/services/services.go
@@ -80,5 +80,6 @@ type IUserService interface {
}
type IMailService interface {
- SendPasswordResetMail(recipient *models.User, resetLink string) error
+ SendPasswordReset(*models.User, string) error
+ SendImportNotification(*models.User, time.Duration, int) error
}
diff --git a/views/mail/import_finished.tpl.html b/views/mail/import_finished.tpl.html
new file mode 100644
index 0000000..6e68e3d
--- /dev/null
+++ b/views/mail/import_finished.tpl.html
@@ -0,0 +1,152 @@
+
+
+
+
+
+ Wakapi – Data Import Finished
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+ Data import finished
+ You have requested to import data from WakaTime to Wakapi. The import has now finished after {{ .Duration }} ({{ .NumHeartbeats }} new heartbeats imported).
You should be able to see the newly imported coding statistics in Wakapi.
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
diff --git a/views/mail/reset_password.tpl.html b/views/mail/reset_password.tpl.html
index 982baa7..c01cf1a 100644
--- a/views/mail/reset_password.tpl.html
+++ b/views/mail/reset_password.tpl.html
@@ -3,7 +3,7 @@
- Simple Transactional Email
+ Wakapi – Reset Password
-