diff --git a/.drone1.yml b/.drone1.yml index 4d401e705b3..a4921cb8be2 100644 --- a/.drone1.yml +++ b/.drone1.yml @@ -82,7 +82,6 @@ steps: GOPROXY: 'https://goproxy.kolaente.de' depends_on: [ build ] commands: - - ./mage-static build:generate - ./mage-static check:got-swag - wget -O - -q https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.31.0 - ./mage-static check:golangci @@ -158,7 +157,6 @@ steps: environment: GOPROXY: 'https://goproxy.kolaente.de' commands: - - ./mage-static build:generate - ./mage-static test:unit depends_on: [ fetch-tags, mage ] when: @@ -172,7 +170,6 @@ steps: VIKUNJA_TESTS_USE_CONFIG: 1 VIKUNJA_DATABASE_TYPE: sqlite commands: - - ./mage-static build:generate - ./mage-static test:unit depends_on: [ fetch-tags, mage ] when: @@ -190,7 +187,6 @@ steps: VIKUNJA_DATABASE_PASSWORD: vikunjatest VIKUNJA_DATABASE_DATABASE: vikunjatest commands: - - ./mage-static build:generate - ./mage-static test:unit depends_on: [ fetch-tags, mage ] when: @@ -209,7 +205,6 @@ steps: VIKUNJA_DATABASE_DATABASE: vikunjatest VIKUNJA_DATABASE_SSLMODE: disable commands: - - ./mage-static build:generate - ./mage-static test:unit depends_on: [ fetch-tags, mage ] when: @@ -221,7 +216,6 @@ steps: environment: GOPROXY: 'https://goproxy.kolaente.de' commands: - - ./mage-static build:generate - ./mage-static test:integration depends_on: [ fetch-tags, mage ] when: @@ -235,7 +229,6 @@ steps: VIKUNJA_TESTS_USE_CONFIG: 1 VIKUNJA_DATABASE_TYPE: sqlite commands: - - ./mage-static build:generate - ./mage-static test:integration depends_on: [ fetch-tags, mage ] when: @@ -253,7 +246,6 @@ steps: VIKUNJA_DATABASE_PASSWORD: vikunjatest VIKUNJA_DATABASE_DATABASE: vikunjatest commands: - - ./mage-static build:generate - ./mage-static test:integration depends_on: [ fetch-tags, mage ] when: @@ -272,7 +264,6 @@ steps: VIKUNJA_DATABASE_DATABASE: vikunjatest VIKUNJA_DATABASE_SSLMODE: disable commands: - - ./mage-static build:generate - ./mage-static test:integration depends_on: [ fetch-tags, mage ] when: @@ -323,7 +314,6 @@ steps: commands: - export PATH=$PATH:$GOPATH/bin - go install github.com/magefile/mage - - ./mage-static build:generate - ./mage-static release:dirs depends_on: [ fetch-tags, mage ] @@ -496,24 +486,24 @@ steps: - tag depends_on: [ build-os-packages ] -### Broken, disabled until we figure out how to fix it -# - name: deb-structure -# image: kolaente/reprepro -# pull: true -# environment: -# GPG_PRIVATE_KEY: -# from_secret: gpg_privatekey -# commands: -# - export GPG_TTY=$(tty) -# - gpg -qk -# - echo "use-agent" >> ~/.gnupg/gpg.conf -# - gpgconf --kill gpg-agent -# - echo $GPG_PRIVATE_KEY > ~/frederik.gpg -# - gpg --import ~/frederik.gpg -# - mkdir debian/conf -p -# - cp build/reprepro-dist-conf debian/conf/distributions -# - ./mage-static release:reprepro -# depends_on: [ build-os-packages ] + ### Broken, disabled until we figure out how to fix it + # - name: deb-structure + # image: kolaente/reprepro + # pull: true + # environment: + # GPG_PRIVATE_KEY: + # from_secret: gpg_privatekey + # commands: + # - export GPG_TTY=$(tty) + # - gpg -qk + # - echo "use-agent" >> ~/.gnupg/gpg.conf + # - gpgconf --kill gpg-agent + # - echo $GPG_PRIVATE_KEY > ~/frederik.gpg + # - gpg --import ~/frederik.gpg + # - mkdir debian/conf -p + # - cp build/reprepro-dist-conf debian/conf/distributions + # - ./mage-static release:reprepro + # depends_on: [ build-os-packages ] # Push the releases to our pseudo-s3-bucket - name: release-deb diff --git a/Dockerfile b/Dockerfile index d0947011110..2c4b0596ae8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ WORKDIR ${GOPATH}/src/code.vikunja.io/api # Checkout version if set RUN if [ -n "${VIKUNJA_VERSION}" ]; then git checkout "${VIKUNJA_VERSION}"; fi \ && go install github.com/magefile/mage \ - && mage build:clean build:build + && mage build:clean build ################### # The actual image diff --git a/docs/content/doc/development/notifications.md b/docs/content/doc/development/notifications.md new file mode 100644 index 00000000000..d93f52072fb --- /dev/null +++ b/docs/content/doc/development/notifications.md @@ -0,0 +1,110 @@ +--- +date: 2021-02-07T19:26:34+02:00 +title: "Notifications" +toc: true +draft: false +menu: + sidebar: + parent: "development" +--- + +# Notifications + +Vikunjs provides a simple abstraction to send notifications per mail and in the database. + +{{< table_of_contents >}} + +## Definition + +Each notification has to implement this interface: + +```golang +type Notification interface { + ToMail() *Mail + ToDB() interface{} +} +``` + +Both functions return the formatted messages for mail and database. + +A notification will only be sent or recorded for those of the two methods which don't return `nil`. +For example, if your notification should not be recorded in the database but only sent out per mail, it is enough to let the `ToDB` function return `nil`. + +### Mail notifications + +A list of chainable functions is available to compose a mail: + +```golang +mail := NewMail(). + // The optional sender of the mail message. + From("test@example.com"). + // The optional receipient of the mail message. Uses the mail address of the notifiable if omitted. + To("test@otherdomain.com"). + // The subject of the mail to send. + Subject("Testmail"). + // The greeting, or "intro" line of the mail. + Greeting("Hi there,"). + // A line of text + Line("This is a line of text"). + // An action can contain a title and a url. It gets rendered as a big button in the mail. + // Note that you can have only one action per mail. + // All lines added before an action will appearr in the mail before the button, all lines + // added afterwards will appear after it. + Action("The Action", "https://example.com"). + // Another line of text. + Line("This should be an outro line"). +``` + +If not provided, the `from` field of the mail contains the value configured in [`mailer.fromemail`](https://vikunja.io/docs/config-options/#fromemail). + +### Database notifications + +All data returned from the `ToDB()` method is serialized to json and saved into the database, along with the id of the notifiable and a time stamp. + +## Creating a new notification + +The easiest way to generate a mail is by using the `mage dev:make-notification` command. + +It takes the name of the notification and the package where the notification will be created. + +## Notifiables + +Notifiables can receive a notification. +A notifiable is defined with this interface: + +```golang +type Notifiable interface { + // Should return the email address this notifiable has. + RouteForMail() string + // Should return the id of the notifiable entity + RouteForDB() int64 +} +``` + +The `User` type from the `user` package implements this interface. + +## Sending a notification + +Sending a notification is done with the `Notify` method from the `notifications` package. +It takes a notifiable and a notification as input. + +For example, the email confirm notification when a new user registers is sent like this: + +```golang +n := &EmailConfirmNotification{ + User: update.User, + IsNew: false, +} + +err = notifications.Notify(update.User, n) +return +``` + +## Testing + +The `mail` package provides a `Fake()` method which you should call in the `MainTest` functions of your package. +If it was called, no mails are being sent and you can instead assert they have been sent with the `AssertSent` method. + +## Example + +Take a look at the [pkg/user/notifications.go](https://code.vikunja.io/api/src/branch/master/pkg/user/notifications.go) file for a good example. diff --git a/go.sum b/go.sum index f36da376fba..0f92e0e13f4 100644 --- a/go.sum +++ b/go.sum @@ -608,6 +608,7 @@ github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.2 h1:8mVmC9kjFFmA8H4pKMUhcblgifdkOIXPvbhN1T36q1M= github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -619,6 +620,7 @@ github.com/onsi/gomega v1.10.3 h1:gph6h/qe9GSUw1NhH1gp+qb+h8rXD8Cy60Z32Qw3ELA= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.10.4 h1:NiTx7EEvBzu9sFOD1zORteLSt3o8gnlvZZwSE9TnY9U= github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= diff --git a/magefile.go b/magefile.go index 31176ad2a59..3b62a535b8b 100644 --- a/magefile.go +++ b/magefile.go @@ -61,15 +61,16 @@ var ( // Aliases are mage aliases of targets Aliases = map[string]interface{}{ - "build": Build.Build, - "do-the-swag": DoTheSwag, - "check:got-swag": Check.GotSwag, - "release:os-package": Release.OsPackage, - "dev:make-migration": Dev.MakeMigration, - "dev:make-event": Dev.MakeEvent, - "dev:make-listener": Dev.MakeListener, - "generate-docs": GenerateDocs, - "check:golangci-fix": Check.GolangciFix, + "build": Build.Build, + "do-the-swag": DoTheSwag, + "check:got-swag": Check.GotSwag, + "release:os-package": Release.OsPackage, + "dev:make-migration": Dev.MakeMigration, + "dev:make-event": Dev.MakeEvent, + "dev:make-listener": Dev.MakeListener, + "dev:make-notification": Dev.MakeNotification, + "generate-docs": GenerateDocs, + "check:golangci-fix": Check.GolangciFix, } ) @@ -434,16 +435,9 @@ func (Build) Clean() error { return nil } -// Generates static content into the final binary -func (Build) Generate() { - mg.Deps(initVars) - runAndStreamOutput("go", "generate", PACKAGE+"/pkg/static") -} - // Builds a vikunja binary, ready to run func (Build) Build() { mg.Deps(initVars) - mg.Deps(Build.Generate) runAndStreamOutput("go", "build", Goflags[0], "-tags", Tags, "-ldflags", "-s -w "+Ldflags, "-o", Executable) } @@ -452,7 +446,7 @@ type Release mg.Namespace // Runs all steps in the right order to create release packages for various platforms func (Release) Release(ctx context.Context) error { mg.Deps(initVars) - mg.Deps(Build.Generate, Release.Dirs) + mg.Deps(Release.Dirs) mg.Deps(Release.Windows, Release.Linux, Release.Darwin) // Run compiling in parallel to speed it up @@ -894,6 +888,44 @@ func (s *` + name + `) Handle(payload message.Payload) (err error) { return nil } +// Create a new notification. Takes the name of the notification as the first argument and the module where the notification should be created as the second argument. Notifications will be appended to the pkg//notifications.go file. +func (Dev) MakeNotification(name, module string) error { + + name = strcase.ToCamel(name) + + if !strings.HasSuffix(name, "Notification") { + name += "Notification" + } + + newNotificationCode := ` +// ` + name + ` represents a ` + name + ` notification +type ` + name + ` struct { +} + +// ToMail returns the mail notification for ` + name + ` +func (n *` + name + `) ToMail() *notifications.Mail { + return notifications.NewMail(). + Subject(""). + Greeting("Hi "). + Line(""). + Action("", "") +} + +// ToDB returns the ` + name + ` notification in a format which can be saved in the db +func (n *` + name + `) ToDB() interface{} { + return nil +} +` + filename := "./pkg/" + module + "/notifications.go" + if err := appendToFile(filename, newNotificationCode); err != nil { + return err + } + + printSuccess("The new notification has been created successfully! Head over to %s and adjust its content.", filename) + + return nil +} + type configOption struct { key string description string diff --git a/pkg/cmd/testmail.go b/pkg/cmd/testmail.go index de7d791ef08..e8dac3441d8 100644 --- a/pkg/cmd/testmail.go +++ b/pkg/cmd/testmail.go @@ -17,9 +17,11 @@ package cmd import ( + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/initialize" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/mail" + "code.vikunja.io/api/pkg/notifications" "github.com/spf13/cobra" ) @@ -39,8 +41,20 @@ var testmailCmd = &cobra.Command{ }, Run: func(cmd *cobra.Command, args []string) { log.Info("Sending testmail...") - email := args[0] - if err := mail.SendTestMail(email); err != nil { + message := notifications.NewMail(). + From(config.MailerFromEmail.GetString()). + To(args[0]). + Subject("Test from Vikunja"). + Line("This is a test mail!"). + Line("If you received this, Vikunja is correctly set up to send emails."). + Action("Go to your instance", config.ServiceFrontendurl.GetString()) + + opts, err := notifications.RenderMail(message) + if err != nil { + log.Errorf("Error sending test mail: %s", err.Error()) + return + } + if err := mail.SendTestMail(opts); err != nil { log.Errorf("Error sending test mail: %s", err.Error()) return } diff --git a/pkg/mail/send_mail.go b/pkg/mail/send_mail.go index 51b4c541e0a..b962c3a0f57 100644 --- a/pkg/mail/send_mail.go +++ b/pkg/mail/send_mail.go @@ -17,19 +17,14 @@ package mail import ( - "bytes" - "html/template" - "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" - "code.vikunja.io/api/pkg/static" - "code.vikunja.io/api/pkg/utils" - "github.com/shurcooL/httpfs/html/vfstemplate" "gopkg.in/gomail.v2" ) // Opts holds infos for a mail type Opts struct { + From string To string Subject string Message string @@ -56,7 +51,7 @@ type header struct { // SendTestMail sends a test mail to a receipient. // It works without a queue. -func SendTestMail(to string) error { +func SendTestMail(opts *Opts) error { if config.MailerHost.GetString() == "" { log.Warning("Mailer seems to be not configured! Please see the config docs for more details.") return nil @@ -69,19 +64,17 @@ func SendTestMail(to string) error { } defer s.Close() - m := gomail.NewMessage() - m.SetHeader("From", config.MailerFromEmail.GetString()) - m.SetHeader("To", to) - m.SetHeader("Subject", "Test from Vikunja") - m.SetBody("text/plain", "This is a test mail! If you got this, Vikunja is correctly set up to send emails.") + m := sendMail(opts) return gomail.Send(s, m) } -// SendMail puts a mail in the queue -func SendMail(opts *Opts) { +func sendMail(opts *Opts) *gomail.Message { m := gomail.NewMessage() - m.SetHeader("From", config.MailerFromEmail.GetString()) + if opts.From == "" { + opts.From = config.MailerFromEmail.GetString() + } + m.SetHeader("From", opts.From) m.SetHeader("To", opts.To) m.SetHeader("Subject", opts.Subject) for _, h := range opts.Headers { @@ -97,49 +90,16 @@ func SendMail(opts *Opts) { m.SetBody("text/plain", opts.Message) m.AddAlternative("text/html", opts.HTMLMessage) } + return m +} +// SendMail puts a mail in the queue +func SendMail(opts *Opts) { + if isUnderTest { + sentMails = append(sentMails, opts) + return + } + + m := sendMail(opts) Queue <- m } - -// Template holds a pointer about a template -type Template struct { - Templates *template.Template -} - -// SendMailWithTemplate parses a template and sends it via mail -func SendMailWithTemplate(to, subject, tpl string, data map[string]interface{}) { - var htmlContent bytes.Buffer - var plainContent bytes.Buffer - - t, err := vfstemplate.ParseGlob(static.Templates, nil, "*.tmpl") - if err != nil { - log.Errorf("SendMailWithTemplate: ParseGlob: %v", err) - return - } - - boundary := "np" + utils.MakeRandomString(13) - - data["Boundary"] = boundary - data["FrontendURL"] = config.ServiceFrontendurl.GetString() - - if err := t.ExecuteTemplate(&htmlContent, tpl+".html.tmpl", data); err != nil { - log.Errorf("ExecuteTemplate: %v", err) - return - } - - if err := t.ExecuteTemplate(&plainContent, tpl+".plain.tmpl", data); err != nil { - log.Errorf("ExecuteTemplate: %v", err) - return - } - - opts := &Opts{ - To: to, - Subject: subject, - Message: plainContent.String(), - HTMLMessage: htmlContent.String(), - ContentType: ContentTypeMultipart, - Boundary: boundary, - } - - SendMail(opts) -} diff --git a/pkg/static/templates_generate.go b/pkg/mail/testing.go similarity index 59% rename from pkg/static/templates_generate.go rename to pkg/mail/testing.go index 3cd21e66264..737150b2e0a 100644 --- a/pkg/static/templates_generate.go +++ b/pkg/mail/testing.go @@ -14,24 +14,35 @@ // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . -// +build ignore - -package main +package mail import ( - "log" - "net/http" + "reflect" + "testing" - "github.com/shurcooL/vfsgen" + "github.com/stretchr/testify/assert" ) -func main() { - err := vfsgen.Generate(http.Dir(`../../templates/mail`), vfsgen.Options{ - PackageName: "static", - BuildTags: "!dev", - VariableName: "Templates", - }) - if err != nil { - log.Fatalln(err) - } +var ( + isUnderTest bool + sentMails []*Opts +) + +// Fake stops any mails from being sent and instead allows for recording and querying them. +func Fake() { + isUnderTest = true + sentMails = nil +} + +// AssertSent asserts if a mail has been sent +func AssertSent(t *testing.T, opts *Opts) { + var found bool + for _, testMail := range sentMails { + if reflect.DeepEqual(testMail, opts) { + found = true + break + } + } + + assert.True(t, found, "Failed to assert mail '%v' has been sent.", opts) } diff --git a/pkg/static/static.go b/pkg/migration/20210207192805.go similarity index 50% rename from pkg/static/static.go rename to pkg/migration/20210207192805.go index 1f36c8dfdcf..8acd3ff821f 100644 --- a/pkg/static/static.go +++ b/pkg/migration/20210207192805.go @@ -14,8 +14,35 @@ // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . -//go:generate go run -tags=dev templates_generate.go +package migration -package static +import ( + "time" -// The single purpose of this file is to invoke the generation of static files + "src.techknowlogick.com/xormigrate" + "xorm.io/xorm" +) + +type notifications20210207192805 struct { + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` + NotifiableID int64 `xorm:"bigint not null" json:"-"` + Notification interface{} `xorm:"json not null" json:"notification"` + Created time.Time `xorm:"created not null" json:"created"` +} + +func (notifications20210207192805) TableName() string { + return "notifications" +} + +func init() { + migrations = append(migrations, &xormigrate.Migration{ + ID: "20210207192805", + Description: "Add notifications table", + Migrate: func(tx *xorm.Engine) error { + return tx.Sync2(notifications20210207192805{}) + }, + Rollback: func(tx *xorm.Engine) error { + return nil + }, + }) +} diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go new file mode 100644 index 00000000000..af655aaaeb4 --- /dev/null +++ b/pkg/models/notifications.go @@ -0,0 +1,47 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package models + +import ( + "strconv" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/notifications" + "code.vikunja.io/api/pkg/user" +) + +// ReminderDueNotification represents a ReminderDueNotification notification +type ReminderDueNotification struct { + User *user.User + Task *Task +} + +// ToMail returns the mail notification for ReminderDueNotification +func (n *ReminderDueNotification) ToMail() *notifications.Mail { + return notifications.NewMail(). + To(n.User.Email). + Subject(`Reminder for "`+n.Task.Title+`"`). + Greeting("Hi "+n.User.GetName()+","). + Line(`This is a friendly reminder of the task "`+n.Task.Title+`".`). + Action("Open Task", config.ServiceFrontendurl.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)). + Line("Have a nice day!") +} + +// ToDB returns the ReminderDueNotification notification in a format which can be saved in the db +func (n *ReminderDueNotification) ToDB() interface{} { + return nil +} diff --git a/pkg/models/task_reminder.go b/pkg/models/task_reminder.go index 78979721f1c..4746fc797cd 100644 --- a/pkg/models/task_reminder.go +++ b/pkg/models/task_reminder.go @@ -19,13 +19,14 @@ package models import ( "time" + "code.vikunja.io/api/pkg/notifications" + "code.vikunja.io/api/pkg/db" "xorm.io/xorm" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/cron" "code.vikunja.io/api/pkg/log" - "code.vikunja.io/api/pkg/mail" "code.vikunja.io/api/pkg/user" ) @@ -61,11 +62,6 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs return } - assignees, err := getRawTaskAssigneesForTasks(s, taskIDs) - if err != nil { - return - } - taskMap := make(map[int64]*Task, len(taskIDs)) err = s.In("id", taskIDs).Find(&taskMap) if err != nil { @@ -73,12 +69,22 @@ func getTaskUsersForTasks(s *xorm.Session, taskIDs []int64) (taskUsers []*taskUs } for _, taskID := range taskIDs { + u, exists := creators[taskMap[taskID].CreatedByID] + if !exists { + continue + } + taskUsers = append(taskUsers, &taskUser{ Task: taskMap[taskID], - User: creators[taskMap[taskID].CreatedByID], + User: u, }) } + assignees, err := getRawTaskAssigneesForTasks(s, taskIDs) + if err != nil { + return + } + for _, assignee := range assignees { if !assignee.EmailRemindersEnabled { // Can't filter that through a query directly since we're using another function continue @@ -168,12 +174,17 @@ func RegisterReminderCron() { log.Debugf("[Task Reminder Cron] Sending reminders to %d users", len(users)) for _, u := range users { - data := map[string]interface{}{ - "User": u.User, - "Task": u.Task, + n := &ReminderDueNotification{ + User: u.User, + Task: u.Task, + } + + err = notifications.Notify(u.User, n) + if err != nil { + log.Errorf("[Task Reminder Cron] Could not notify user %d: %s", u.User.ID, err) + return } - mail.SendMailWithTemplate(u.User.Email, `Reminder for "`+u.Task.Title+`"`, "reminder-email", data) log.Debugf("[Task Reminder Cron] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID) } }) diff --git a/pkg/notifications/mail.go b/pkg/notifications/mail.go new file mode 100644 index 00000000000..b7080ebcb93 --- /dev/null +++ b/pkg/notifications/mail.go @@ -0,0 +1,93 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import "code.vikunja.io/api/pkg/mail" + +// Mail is a mail message +type Mail struct { + from string + to string + subject string + actionText string + actionURL string + greeting string + introLines []string + outroLines []string +} + +// NewMail creates a new mail object with a default greeting +func NewMail() *Mail { + return &Mail{ + greeting: "Hi,", + } +} + +// From sets the from name and email address +func (m *Mail) From(from string) *Mail { + m.from = from + return m +} + +// To sets the recipient of the mail message +func (m *Mail) To(to string) *Mail { + m.to = to + return m +} + +// Subject sets the subject of the mail message +func (m *Mail) Subject(subject string) *Mail { + m.subject = subject + return m +} + +// Greeting sets the greeting of the mail message +func (m *Mail) Greeting(greeting string) *Mail { + m.greeting = greeting + return m +} + +// Action sets any action a mail might have +func (m *Mail) Action(text, url string) *Mail { + m.actionText = text + m.actionURL = url + return m +} + +// Line adds a line of text to the mail +func (m *Mail) Line(line string) *Mail { + if m.actionURL == "" { + m.introLines = append(m.introLines, line) + return m + } + + m.outroLines = append(m.outroLines, line) + + return m +} + +// SendMail passes the notification to the mailing queue for sending +func SendMail(m *Mail) error { + opts, err := RenderMail(m) + if err != nil { + return err + } + + mail.SendMail(opts) + + return nil +} diff --git a/pkg/notifications/mail_render.go b/pkg/notifications/mail_render.go new file mode 100644 index 00000000000..b45f7b51088 --- /dev/null +++ b/pkg/notifications/mail_render.go @@ -0,0 +1,137 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import ( + "bytes" + templatehtml "html/template" + templatetext "text/template" + + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/mail" + "code.vikunja.io/api/pkg/utils" +) + +const mailTemplatePlain = ` +{{ .Greeting }} +{{ range $line := .IntroLines}} +{{ $line }} +{{ end }} +{{ if .ActionURL }}{{ .ActionText }}: +{{ .ActionURL }}{{end}} +{{ range $line := .OutroLines}} +{{ $line }} +{{ end }}` + +const mailTemplateHTML = ` + + + + + + +
+
+

+ Vikunja +

+
+

+ {{ .Greeting }} +

+ +{{ range $line := .IntroLines}} +

+ {{ $line }} +

+{{ end }} + +{{ if .ActionURL }} + + {{ .ActionText }} + +{{end}} + +{{ range $line := .OutroLines}} +

+ {{ $line }} +

+{{ end }} + +{{ if .ActionURL }} +

+ If the button above doesn't work, copy the url below and paste it in your browsers address bar:
+ {{ .ActionURL }} +

+{{ end }} +
+
+
+ + +` + +// RenderMail takes a precomposed mail message and renders it into a ready to send mail.Opts object +func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) { + + var htmlContent bytes.Buffer + var plainContent bytes.Buffer + + plain, err := templatetext.New("mail-plain").Parse(mailTemplatePlain) + if err != nil { + return nil, err + } + + html, err := templatehtml.New("mail-plain").Parse(mailTemplateHTML) + if err != nil { + return nil, err + } + + boundary := "np" + utils.MakeRandomString(13) + + data := make(map[string]interface{}) + + data["Greeting"] = m.greeting + data["IntroLines"] = m.introLines + data["OutroLines"] = m.outroLines + data["ActionText"] = m.actionText + data["ActionURL"] = m.actionURL + data["Boundary"] = boundary + data["FrontendURL"] = config.ServiceFrontendurl.GetString() + + err = plain.Execute(&plainContent, data) + if err != nil { + return nil, err + } + err = html.Execute(&htmlContent, data) + if err != nil { + return nil, err + } + + mailOpts = &mail.Opts{ + From: m.from, + To: m.to, + Subject: m.subject, + ContentType: mail.ContentTypeMultipart, + Message: plainContent.String(), + HTMLMessage: htmlContent.String(), + Boundary: boundary, + } + + return mailOpts, nil +} diff --git a/pkg/notifications/mail_test.go b/pkg/notifications/mail_test.go new file mode 100644 index 00000000000..fddeb306db0 --- /dev/null +++ b/pkg/notifications/mail_test.go @@ -0,0 +1,173 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewMail(t *testing.T) { + t.Run("Full mail", func(t *testing.T) { + mail := NewMail(). + From("test@example.com"). + To("test@otherdomain.com"). + Subject("Testmail"). + Greeting("Hi there,"). + Line("This is a line"). + Line("And another one"). + Action("the actiopn", "https://example.com"). + Line("This should be an outro line"). + Line("And one more, because why not?") + + assert.Equal(t, "test@example.com", mail.from) + assert.Equal(t, "test@otherdomain.com", mail.to) + assert.Equal(t, "Testmail", mail.subject) + assert.Equal(t, "Hi there,", mail.greeting) + assert.Len(t, mail.introLines, 2) + assert.Equal(t, "This is a line", mail.introLines[0]) + assert.Equal(t, "And another one", mail.introLines[1]) + assert.Len(t, mail.outroLines, 2) + assert.Equal(t, "This should be an outro line", mail.outroLines[0]) + assert.Equal(t, "And one more, because why not?", mail.outroLines[1]) + }) + t.Run("No greeting", func(t *testing.T) { + mail := NewMail(). + From("test@example.com"). + To("test@otherdomain.com"). + Subject("Testmail"). + Line("This is a line"). + Line("And another one") + + assert.Equal(t, "test@example.com", mail.from) + assert.Equal(t, "test@otherdomain.com", mail.to) + assert.Equal(t, "Testmail", mail.subject) + assert.Equal(t, "Hi,", mail.greeting) // Default greeting + assert.Len(t, mail.introLines, 2) + assert.Equal(t, "This is a line", mail.introLines[0]) + assert.Equal(t, "And another one", mail.introLines[1]) + }) + t.Run("No action", func(t *testing.T) { + mail := NewMail(). + From("test@example.com"). + To("test@otherdomain.com"). + Subject("Testmail"). + Line("This is a line"). + Line("And another one"). + Line("This should be an outro line"). + Line("And one more, because why not?") + + assert.Equal(t, "test@example.com", mail.from) + assert.Equal(t, "test@otherdomain.com", mail.to) + assert.Equal(t, "Testmail", mail.subject) + assert.Len(t, mail.introLines, 4) + assert.Equal(t, "This is a line", mail.introLines[0]) + assert.Equal(t, "And another one", mail.introLines[1]) + assert.Equal(t, "This should be an outro line", mail.introLines[2]) + assert.Equal(t, "And one more, because why not?", mail.introLines[3]) + }) +} + +func TestRenderMail(t *testing.T) { + mail := NewMail(). + From("test@example.com"). + To("test@otherdomain.com"). + Subject("Testmail"). + Greeting("Hi there,"). + Line("This is a line"). + Line("And another one"). + Action("The action", "https://example.com"). + Line("This should be an outro line"). + Line("And one more, because why not?") + + mailopts, err := RenderMail(mail) + assert.NoError(t, err) + assert.Equal(t, mail.from, mailopts.From) + assert.Equal(t, mail.to, mailopts.To) + + assert.Equal(t, ` +Hi there, + +This is a line + +And another one + +The action: +https://example.com + +This should be an outro line + +And one more, because why not? +`, mailopts.Message) + assert.Equal(t, ` + + + + + + +
+
+

+ Vikunja +

+
+

+ Hi there, +

+ + +

+ This is a line +

+ +

+ And another one +

+ + + + + The action + + + + +

+ This should be an outro line +

+ +

+ And one more, because why not? +

+ + + +

+ If the button above doesn't work, copy the url below and paste it in your browsers address bar:
+ https://example.com +

+ +
+
+
+ + +`, mailopts.HTMLMessage) +} diff --git a/pkg/static/templates.go b/pkg/notifications/main_test.go similarity index 50% rename from pkg/static/templates.go rename to pkg/notifications/main_test.go index bef4179cf0b..36050157dcf 100644 --- a/pkg/static/templates.go +++ b/pkg/notifications/main_test.go @@ -14,10 +14,42 @@ // You should have received a copy of the GNU Affero General Public Licensee // along with this program. If not, see . -// +build dev +package notifications -package static +import ( + "os" + "testing" -import "net/http" + "code.vikunja.io/api/pkg/db" + "code.vikunja.io/api/pkg/log" + "code.vikunja.io/api/pkg/mail" -var Templates http.FileSystem = http.Dir(`templates/mail`) + "code.vikunja.io/api/pkg/config" +) + +// SetupTests initializes all db tests +func SetupTests() { + var err error + x, err := db.CreateTestEngine() + if err != nil { + log.Fatal(err) + } + + err = x.Sync2(&DatabaseNotification{}) + if err != nil { + log.Fatal(err) + } +} + +// TestMain is the main test function used to bootstrap the test env +func TestMain(m *testing.M) { + // Set default config + config.InitDefaultConfig() + // We need to set the root path even if we're not using the config, otherwise fixtures are not loaded correctly + config.ServiceRootpath.Set(os.Getenv("VIKUNJA_SERVICE_ROOTPATH")) + + SetupTests() + + mail.Fake() + os.Exit(m.Run()) +} diff --git a/pkg/notifications/notification.go b/pkg/notifications/notification.go new file mode 100644 index 00000000000..da3898c0952 --- /dev/null +++ b/pkg/notifications/notification.go @@ -0,0 +1,106 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import ( + "encoding/json" + "time" + + "code.vikunja.io/api/pkg/db" +) + +// Notification is a notification which can be sent via mail or db. +type Notification interface { + ToMail() *Mail + ToDB() interface{} +} + +// Notifiable is an entity which can be notified. Usually a user. +type Notifiable interface { + // Should return the email address this notifiable has. + RouteForMail() string + // Should return the id of the notifiable entity + RouteForDB() int64 +} + +// DatabaseNotification represents a notification that was saved to the database +type DatabaseNotification struct { + // The unique, numeric id of this notification. + ID int64 `xorm:"bigint autoincr not null unique pk" json:"id"` + + // The ID of the notifiable this notification is associated with. + NotifiableID int64 `xorm:"bigint not null" json:"-"` + // The actual content of the notification. + Notification interface{} `xorm:"json not null" json:"notification"` + + // A timestamp when this notification was created. You cannot change this value. + Created time.Time `xorm:"created not null" json:"created"` +} + +// TableName resolves to a better table name for notifications +func (d *DatabaseNotification) TableName() string { + return "notifications" +} + +// Notify notifies a notifiable of a notification +func Notify(notifiable Notifiable, notification Notification) (err error) { + + err = notifyMail(notifiable, notification) + if err != nil { + return + } + + return notifyDB(notifiable, notification) +} + +func notifyMail(notifiable Notifiable, notification Notification) error { + mail := notification.ToMail() + if mail == nil { + return nil + } + + mail.To(notifiable.RouteForMail()) + + return SendMail(mail) +} + +func notifyDB(notifiable Notifiable, notification Notification) (err error) { + + dbContent := notification.ToDB() + if dbContent == nil { + return nil + } + + content, err := json.Marshal(dbContent) + if err != nil { + return err + } + + s := db.NewSession() + dbNotification := &DatabaseNotification{ + NotifiableID: notifiable.RouteForDB(), + Notification: content, + } + + _, err = s.Insert(dbNotification) + if err != nil { + _ = s.Rollback() + return err + } + + return s.Commit() +} diff --git a/pkg/notifications/notification_test.go b/pkg/notifications/notification_test.go new file mode 100644 index 00000000000..f0baa0e8ac0 --- /dev/null +++ b/pkg/notifications/notification_test.go @@ -0,0 +1,86 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package notifications + +import ( + "testing" + + "code.vikunja.io/api/pkg/db" + "github.com/stretchr/testify/assert" + "xorm.io/xorm/schemas" +) + +type testNotification struct { + Test string + OtherValue int64 +} + +// ToMail returns the mail notification for testNotification +func (n *testNotification) ToMail() *Mail { + return NewMail(). + Subject("Test Notification"). + Line(n.Test) +} + +// ToDB returns the testNotification notification in a format which can be saved in the db +func (n *testNotification) ToDB() interface{} { + data := make(map[string]interface{}, 2) + data["test"] = n.Test + data["other_value"] = n.OtherValue + return data +} + +type testNotifiable struct { +} + +// RouteForMail routes a test notification for mail +func (t *testNotifiable) RouteForMail() string { + return "some@email.com" +} + +// RouteForDB routes a test notification for db +func (t *testNotifiable) RouteForDB() int64 { + return 42 +} + +func TestNotify(t *testing.T) { + tn := &testNotification{ + Test: "somethingsomething", + OtherValue: 42, + } + tnf := &testNotifiable{} + + err := Notify(tnf, tn) + + assert.NoError(t, err) + vals := map[string]interface{}{ + "notifiable_id": 42, + "notification": "'{\"other_value\":42,\"test\":\"somethingsomething\"}'", + } + + if db.Type() == schemas.POSTGRES { + vals["notification::jsonb"] = vals["notification"].(string) + "::jsonb" + delete(vals, "notification") + } + + if db.Type() == schemas.SQLITE { + vals["CAST(notification AS BLOB)"] = "CAST(" + vals["notification"].(string) + " AS BLOB)" + delete(vals, "notification") + } + + db.AssertExists(t, "notifications", vals, true) +} diff --git a/pkg/user/notifications.go b/pkg/user/notifications.go new file mode 100644 index 00000000000..b3c2b4c46cc --- /dev/null +++ b/pkg/user/notifications.go @@ -0,0 +1,94 @@ +// Vikunja is a to-do list application to facilitate your life. +// Copyright 2018-2021 Vikunja and contributors. All rights reserved. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public Licensee as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public Licensee for more details. +// +// You should have received a copy of the GNU Affero General Public Licensee +// along with this program. If not, see . + +package user + +import ( + "code.vikunja.io/api/pkg/config" + "code.vikunja.io/api/pkg/notifications" +) + +// EmailConfirmNotification represents a EmailConfirmNotification notification +type EmailConfirmNotification struct { + User *User + IsNew bool +} + +// ToMail returns the mail notification for EmailConfirmNotification +func (n *EmailConfirmNotification) ToMail() *notifications.Mail { + + subject := n.User.GetName() + ", please confirm your email address at Vikunja" + if n.IsNew { + subject = n.User.GetName() + " + Vikunja = <3" + } + + nn := notifications.NewMail(). + Subject(subject). + Greeting("Hi " + n.User.GetName() + ",") + + if n.IsNew { + nn.Line("Welcome to Vikunja!") + } + + return nn. + Line("To confirm your email address, click the link below:"). + Action("Confirm your email address", config.ServiceFrontendurl.GetString()+"?userEmailConfirm="+n.User.EmailConfirmToken). + Line("Have a nice day!") +} + +// ToDB returns the EmailConfirmNotification notification in a format which can be saved in the db +func (n *EmailConfirmNotification) ToDB() interface{} { + return nil +} + +// PasswordChangedNotification represents a PasswordChangedNotification notification +type PasswordChangedNotification struct { + User *User +} + +// ToMail returns the mail notification for PasswordChangedNotification +func (n *PasswordChangedNotification) ToMail() *notifications.Mail { + return notifications.NewMail(). + Subject("Your Password on Vikunja was changed"). + Greeting("Hi " + n.User.GetName() + ","). + Line("Your account password was successfully changed."). + Line("If this wasn't you, it could mean someone compromised your account. In this case contact your server's administrator.") +} + +// ToDB returns the PasswordChangedNotification notification in a format which can be saved in the db +func (n *PasswordChangedNotification) ToDB() interface{} { + return nil +} + +// ResetPasswordNotification represents a ResetPasswordNotification notification +type ResetPasswordNotification struct { + User *User +} + +// ToMail returns the mail notification for ResetPasswordNotification +func (n *ResetPasswordNotification) ToMail() *notifications.Mail { + return notifications.NewMail(). + Subject("Reset your password on Vikunja"). + Greeting("Hi "+n.User.GetName()+","). + Line("To reset your password, click the link below:"). + Action("Reset your password", config.ServiceFrontendurl.GetString()+"?userPasswordReset="+n.User.PasswordResetToken). + Line("Have a nice day!") +} + +// ToDB returns the ResetPasswordNotification notification in a format which can be saved in the db +func (n *ResetPasswordNotification) ToDB() interface{} { + return nil +} diff --git a/pkg/user/update_email.go b/pkg/user/update_email.go index 54434259921..2e3b9ceef67 100644 --- a/pkg/user/update_email.go +++ b/pkg/user/update_email.go @@ -18,7 +18,7 @@ package user import ( "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/mail" + "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/utils" "xorm.io/xorm" ) @@ -69,12 +69,11 @@ func UpdateEmail(s *xorm.Session, update *EmailUpdate) (err error) { } // Send the user a mail with a link to confirm the mail - data := map[string]interface{}{ - "User": update.User, - "IsNew": false, + n := &EmailConfirmNotification{ + User: update.User, + IsNew: false, } - mail.SendMailWithTemplate(update.User.Email, update.User.Username+", please confirm your email address at Vikunja", "confirm-email", data) - + err = notifications.Notify(update.User, n) return } diff --git a/pkg/user/user.go b/pkg/user/user.go index 69b044247d2..d2077efe31b 100644 --- a/pkg/user/user.go +++ b/pkg/user/user.go @@ -74,6 +74,16 @@ type User struct { web.Auth `xorm:"-" json:"-"` } +// RouteForMail routes all notifications for a user to its email address +func (u *User) RouteForMail() string { + return u.Email +} + +// RouteForDB routes all notifications for a user to their id +func (u *User) RouteForDB() int64 { + return u.ID +} + // GetID implements the Auth interface func (u *User) GetID() int64 { return u.ID @@ -84,6 +94,15 @@ func (User) TableName() string { return "users" } +// GetName returns the name if the user has one and the username otherwise. +func (u *User) GetName() string { + if u.Name != "" { + return u.Name + } + + return u.Username +} + // GetFromAuth returns a user object from a web.Auth object and returns an error if the underlying type // is not a user object func GetFromAuth(a web.Auth) (*User, error) { diff --git a/pkg/user/user_create.go b/pkg/user/user_create.go index 7163feb0585..f3472b271d5 100644 --- a/pkg/user/user_create.go +++ b/pkg/user/user_create.go @@ -19,7 +19,7 @@ package user import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/events" - "code.vikunja.io/api/pkg/mail" + "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/utils" "golang.org/x/crypto/bcrypt" "xorm.io/xorm" @@ -83,8 +83,17 @@ func CreateUser(s *xorm.Session, user *User) (newUser *User, err error) { return nil, err } - sendConfirmEmail(user) + // Dont send a mail if no mailer is configured + if !config.MailerEnabled.GetBool() { + return newUserOut, err + } + n := &EmailConfirmNotification{ + User: user, + IsNew: false, + } + + err = notifications.Notify(user, n) return newUserOut, err } @@ -145,18 +154,3 @@ func checkIfUserExists(s *xorm.Session, user *User) (err error) { return nil } - -func sendConfirmEmail(user *User) { - // Dont send a mail if no mailer is configured - if !config.MailerEnabled.GetBool() { - return - } - - // Send the user a mail with a link to confirm the mail - data := map[string]interface{}{ - "User": user, - "IsNew": true, - } - - mail.SendMailWithTemplate(user.Email, user.Username+" + Vikunja = <3", "confirm-email", data) -} diff --git a/pkg/user/user_password_reset.go b/pkg/user/user_password_reset.go index 96b2cf4e19e..4860e549a38 100644 --- a/pkg/user/user_password_reset.go +++ b/pkg/user/user_password_reset.go @@ -18,7 +18,7 @@ package user import ( "code.vikunja.io/api/pkg/config" - "code.vikunja.io/api/pkg/mail" + "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/utils" "xorm.io/xorm" ) @@ -44,10 +44,10 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) { } // Check if we have a token - var user User + user := &User{} exists, err := s. Where("password_reset_token = ?", reset.Token). - Get(&user) + Get(user) if err != nil { return } @@ -67,7 +67,7 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) { _, err = s. Cols("password", "password_reset_token"). Where("id = ?", user.ID). - Update(&user) + Update(user) if err != nil { return } @@ -78,12 +78,11 @@ func ResetPassword(s *xorm.Session, reset *PasswordReset) (err error) { } // Send a mail to the user to notify it his password was changed. - data := map[string]interface{}{ - "User": user, + n := &PasswordChangedNotification{ + User: user, } - mail.SendMailWithTemplate(user.Email, "Your password on Vikunja was changed", "password-changed", data) - + err = notifications.Notify(user, n) return } @@ -125,11 +124,10 @@ func RequestUserPasswordResetToken(s *xorm.Session, user *User) (err error) { return } - data := map[string]interface{}{ - "User": user, + n := &ResetPasswordNotification{ + User: user, } - // Send the user a mail with the reset token - mail.SendMailWithTemplate(user.Email, "Reset your password on Vikunja", "reset-password", data) + err = notifications.Notify(user, n) return } diff --git a/templates/mail/confirm-email.html.tmpl b/templates/mail/confirm-email.html.tmpl deleted file mode 100644 index 8388085b185..00000000000 --- a/templates/mail/confirm-email.html.tmpl +++ /dev/null @@ -1,18 +0,0 @@ -{{template "mail-header.tmpl" .}} -

- Hi {{.User.Username}},
- {{if .IsNew}} -
- Welcome to Vikunja! - {{end}} -
- To confirm your email address, click the link below: -

- - Confirm your email address - -

- If the button above doesn't work, copy the url below and paste it in your browsers address bar:
- {{.FrontendURL}}?userEmailConfirm={{.User.EmailConfirmToken}} -

-{{template "mail-footer.tmpl"}} \ No newline at end of file diff --git a/templates/mail/confirm-email.plain.tmpl b/templates/mail/confirm-email.plain.tmpl deleted file mode 100644 index 9db1ee76674..00000000000 --- a/templates/mail/confirm-email.plain.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -Hi {{.User.Username}}, - -{{if .IsNew}} - Welcome to Vikunja! - -{{end}} -To confirm your email address, copy the link below and paste it in your browser: - -{{.FrontendURL}}?userEmailConfirm={{.User.EmailConfirmToken}} \ No newline at end of file diff --git a/templates/mail/mail-footer.tmpl b/templates/mail/mail-footer.tmpl deleted file mode 100644 index 51acb488ebe..00000000000 --- a/templates/mail/mail-footer.tmpl +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/templates/mail/mail-header.tmpl b/templates/mail/mail-header.tmpl deleted file mode 100644 index 97031a652af..00000000000 --- a/templates/mail/mail-header.tmpl +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - -
-
-

- Vikunja -

-
\ No newline at end of file diff --git a/templates/mail/password-changed.html.tmpl b/templates/mail/password-changed.html.tmpl deleted file mode 100644 index 50cf66fba4d..00000000000 --- a/templates/mail/password-changed.html.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -{{template "mail-header.tmpl" .}} -

- Hi {{.User.Username}},
-
- Your account password was successfully changed. -
- If this wasn't you, it could mean someone compromised your account. In this case contact your server's administrator. -

-{{template "mail-footer.tmpl"}} \ No newline at end of file diff --git a/templates/mail/password-changed.plain.tmpl b/templates/mail/password-changed.plain.tmpl deleted file mode 100644 index 0e82c336c6c..00000000000 --- a/templates/mail/password-changed.plain.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -Hi {{.User.Username}}, - -Your account password was successfully changed. - -If this wasn't you, it could mean someone compromised your account. In this case contact your server's administrator. \ No newline at end of file diff --git a/templates/mail/reminder-email.html.tmpl b/templates/mail/reminder-email.html.tmpl deleted file mode 100644 index 44bc57d7ebd..00000000000 --- a/templates/mail/reminder-email.html.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{template "mail-header.tmpl" .}} -

- Hi {{if .User.Name}}{{.User.Name}}{{else}}{{.User.Username}}{{end}},
-
- This is a friendly reminder of the task "{{.Task.Title}}".
-

- - Open Task - -

- If the button above doesn't work, copy the url below and paste it in your browsers address bar:
- {{.FrontendURL}}tasks/{{.Task.ID}} -

-

- Have a nice day! -

-{{template "mail-footer.tmpl"}} \ No newline at end of file diff --git a/templates/mail/reminder-email.plain.tmpl b/templates/mail/reminder-email.plain.tmpl deleted file mode 100644 index b4f66e561eb..00000000000 --- a/templates/mail/reminder-email.plain.tmpl +++ /dev/null @@ -1,9 +0,0 @@ -Hi {{if .User.Name}}{{.User.Name}}{{else}}{{.User.Username}}{{end}}, - -This is a friendly reminder of the task "{{.Task.Title}}". - -You can view the task at: - -{{.FrontendURL}}tasks/{{.Task.ID}} - -Have a nice day! diff --git a/templates/mail/reset-password.html.tmpl b/templates/mail/reset-password.html.tmpl deleted file mode 100644 index e0dea5a9536..00000000000 --- a/templates/mail/reset-password.html.tmpl +++ /dev/null @@ -1,14 +0,0 @@ -{{template "mail-header.tmpl" .}} -

- Hi {{.User.Username}},
-
- To reset your password, click the link below: -

- - Reset your password - -

- If the button above doesn't work, copy the url below and paste it in your browsers address bar:
- {{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}} -

-{{template "mail-footer.tmpl"}} \ No newline at end of file diff --git a/templates/mail/reset-password.plain.tmpl b/templates/mail/reset-password.plain.tmpl deleted file mode 100644 index 83a46c40aee..00000000000 --- a/templates/mail/reset-password.plain.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -Hi {{.User.Username}}, - -Use the following link to reset your password: {{.FrontendURL}}?userPasswordReset={{.User.PasswordResetToken}} \ No newline at end of file