diff --git a/go.mod b/go.mod index 04d644ef9a5..9c0368b9706 100644 --- a/go.mod +++ b/go.mod @@ -69,6 +69,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/swaggo/swag v1.7.0 github.com/ulule/limiter/v3 v3.8.0 + github.com/yuin/goldmark v1.3.5 golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 @@ -99,4 +100,4 @@ replace ( gopkg.in/fsnotify.v1 => github.com/kolaente/fsnotify v1.4.10-0.20200411160148-1bc3c8ff4048 // See https://github.com/fsnotify/fsnotify/issues/328 and https://github.com/golang/go/issues/26904 ) -go 1.13 +go 1.15 diff --git a/go.sum b/go.sum index aaaf99fcf02..ccf477cb403 100644 --- a/go.sum +++ b/go.sum @@ -743,6 +743,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5 h1:dPmz1Snjq0kmkz159iL7S6WzdahUTHnHB5M56WFVifs= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index 1e291a56e62..77cb8fb61ab 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -214,3 +214,37 @@ func (n *UndoneTaskOverdueNotification) ToDB() interface{} { func (n *UndoneTaskOverdueNotification) Name() string { return "task.undone.overdue" } + +// UndoneTasksOverdueNotification represents a UndoneTasksOverdueNotification notification +type UndoneTasksOverdueNotification struct { + User *user.User + Tasks []*Task +} + +// ToMail returns the mail notification for UndoneTasksOverdueNotification +func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail { + + overdueLine := "" + for _, task := range n.Tasks { + until := time.Until(task.DueDate).Round(1*time.Hour) * -1 + overdueLine += `* [` + task.Title + `](` + config.ServiceFrontendurl.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `), overdue since ` + utils.HumanizeDuration(until) + "\n" + } + + return notifications.NewMail(). + Subject(`Your overdue tasks`). + Greeting("Hi "+n.User.GetName()+","). + Line("You have the following overdue tasks:"). + Line(overdueLine). + Action("Open Vikunja", config.ServiceFrontendurl.GetString()). + Line("Have a nice day!") +} + +// ToDB returns the UndoneTasksOverdueNotification notification in a format which can be saved in the db +func (n *UndoneTasksOverdueNotification) ToDB() interface{} { + return nil +} + +// Name returns the name of the notification +func (n *UndoneTasksOverdueNotification) Name() string { + return "task.undone.overdue" +} diff --git a/pkg/models/task_overdue_reminder.go b/pkg/models/task_overdue_reminder.go index 8ab72460c65..4faa79930ae 100644 --- a/pkg/models/task_overdue_reminder.go +++ b/pkg/models/task_overdue_reminder.go @@ -19,6 +19,8 @@ package models import ( "time" + "code.vikunja.io/api/pkg/user" + "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/cron" "code.vikunja.io/api/pkg/db" @@ -48,6 +50,11 @@ func getUndoneOverdueTasks(s *xorm.Session, now time.Time) (taskIDs []int64, err return } +type userWithTasks struct { + user *user.User + tasks []*Task +} + // RegisterOverdueReminderCron registers a function which checks once a day for tasks that are overdue and not done. func RegisterOverdueReminderCron() { if !config.ServiceEnableEmailReminders.GetBool() { @@ -76,21 +83,41 @@ func RegisterOverdueReminderCron() { return } + uts := make(map[int64]*userWithTasks) + for _, t := range users { + _, exists := uts[t.User.ID] + if !exists { + uts[t.User.ID] = &userWithTasks{ + user: t.User, + tasks: []*Task{}, + } + } + uts[t.User.ID].tasks = append(uts[t.User.ID].tasks, t.Task) + } + log.Debugf("[Undone Overdue Tasks Reminder] Sending reminders to %d users", len(users)) - for _, u := range users { - n := &UndoneTaskOverdueNotification{ - User: u.User, - Task: u.Task, + for _, ut := range uts { + var n notifications.Notification = &UndoneTasksOverdueNotification{ + User: ut.user, + Tasks: ut.tasks, } - err = notifications.Notify(u.User, n) + if len(ut.tasks) == 1 { + n = &UndoneTaskOverdueNotification{ + User: ut.user, + Task: ut.tasks[0], + } + } + + err = notifications.Notify(ut.user, n) if err != nil { - log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", u.User.ID, err) + log.Errorf("[Undone Overdue Tasks Reminder] Could not notify user %d: %s", ut.user.ID, err) return } - log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for task %d to user %d", u.Task.ID, u.User.ID) + log.Debugf("[Undone Overdue Tasks Reminder] Sent reminder email for %d tasks to user %d", len(ut.tasks), ut.user.ID) + continue } }) if err != nil { diff --git a/pkg/notifications/mail_render.go b/pkg/notifications/mail_render.go index b45f7b51088..0ab72442b37 100644 --- a/pkg/notifications/mail_render.go +++ b/pkg/notifications/mail_render.go @@ -24,6 +24,8 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/mail" "code.vikunja.io/api/pkg/utils" + + "github.com/yuin/goldmark" ) const mailTemplatePlain = ` @@ -54,10 +56,8 @@ const mailTemplateHTML = ` {{ .Greeting }}

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

- {{ $line }} -

+{{ range $line := .IntroLinesHTML}} + {{ $line }} {{ end }} {{ if .ActionURL }} @@ -67,10 +67,8 @@ const mailTemplateHTML = ` {{end}} -{{ range $line := .OutroLines}} -

- {{ $line }} -

+{{ range $line := .OutroLinesHTML}} + {{ $line }} {{ end }} {{ if .ActionURL }} @@ -114,6 +112,32 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) { data["Boundary"] = boundary data["FrontendURL"] = config.ServiceFrontendurl.GetString() + var introLinesHTML []templatehtml.HTML + for _, line := range m.introLines { + md := []byte(templatehtml.HTMLEscapeString(line)) + var buf bytes.Buffer + err = goldmark.Convert(md, &buf) + if err != nil { + return nil, err + } + //#nosec - the html is escaped few lines before + introLinesHTML = append(introLinesHTML, templatehtml.HTML(buf.String())) + } + data["IntroLinesHTML"] = introLinesHTML + + var outroLinesHTML []templatehtml.HTML + for _, line := range m.outroLines { + md := []byte(templatehtml.HTMLEscapeString(line)) + var buf bytes.Buffer + err = goldmark.Convert(md, &buf) + if err != nil { + return nil, err + } + //#nosec - the html is escaped few lines before + outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(buf.String())) + } + data["OutroLinesHTML"] = outroLinesHTML + err = plain.Execute(&plainContent, data) if err != nil { return nil, err diff --git a/pkg/notifications/mail_test.go b/pkg/notifications/mail_test.go index 57f6e649115..a33201d737a 100644 --- a/pkg/notifications/mail_test.go +++ b/pkg/notifications/mail_test.go @@ -90,6 +90,7 @@ func TestRenderMail(t *testing.T) { Subject("Testmail"). Greeting("Hi there,"). Line("This is a line"). + Line("This **line** contains [a link](https://vikunja.io)"). Line("And another one"). Action("The action", "https://example.com"). Line("This should be an outro line"). @@ -105,6 +106,8 @@ Hi there, This is a line +This **line** contains [a link](https://vikunja.io) + And another one The action: @@ -132,13 +135,14 @@ And one more, because why not?

-

- This is a line -

+

This is a line

+ + +

This line contains a link

+ + +

And another one

-

- And another one -

@@ -149,13 +153,11 @@ And one more, because why not? -

- This should be an outro line -

+

This should be an outro line

+ + +

And one more, because why not?

-

- And one more, because why not? -