diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go
index a9e887f3b..96442e59c 100644
--- a/pkg/models/notifications.go
+++ b/pkg/models/notifications.go
@@ -21,11 +21,10 @@ import (
"strconv"
"time"
- "code.vikunja.io/api/pkg/utils"
-
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/notifications"
"code.vikunja.io/api/pkg/user"
+ "code.vikunja.io/api/pkg/utils"
)
// ReminderDueNotification represents a ReminderDueNotification notification
@@ -38,6 +37,7 @@ type ReminderDueNotification struct {
// ToMail returns the mail notification for ReminderDueNotification
func (n *ReminderDueNotification) ToMail() *notifications.Mail {
return notifications.NewMail().
+ IncludeLinkToSettings().
To(n.User.Email).
Subject(`Reminder for "`+n.Task.Title+`" (`+n.Project.Title+`)`).
Greeting("Hi "+n.User.GetName()+",").
@@ -227,6 +227,7 @@ type UndoneTaskOverdueNotification struct {
func (n *UndoneTaskOverdueNotification) ToMail() *notifications.Mail {
until := time.Until(n.Task.DueDate).Round(1*time.Hour) * -1
return notifications.NewMail().
+ IncludeLinkToSettings().
Subject(`Task "`+n.Task.Title+`" (`+n.Project.Title+`) is overdue`).
Greeting("Hi "+n.User.GetName()+",").
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" (`+n.Project.Title+`) which is `+getOverdueSinceString(until)+` and not yet done.`).
@@ -270,6 +271,7 @@ func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
}
return notifications.NewMail().
+ IncludeLinkToSettings().
Subject(`Your overdue tasks`).
Greeting("Hi "+n.User.GetName()+",").
Line("You have the following overdue tasks:").
diff --git a/pkg/notifications/mail.go b/pkg/notifications/mail.go
index da7a9fd05..88f50a76d 100644
--- a/pkg/notifications/mail.go
+++ b/pkg/notifications/mail.go
@@ -16,18 +16,22 @@
package notifications
-import "code.vikunja.io/api/pkg/mail"
+import (
+ "code.vikunja.io/api/pkg/config"
+ "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 []*mailLine
- outroLines []*mailLine
+ from string
+ to string
+ subject string
+ actionText string
+ actionURL string
+ greeting string
+ introLines []*mailLine
+ outroLines []*mailLine
+ footerLines []*mailLine
}
type mailLine struct {
@@ -76,6 +80,18 @@ func (m *Mail) Line(line string) *Mail {
return m.appendLine(line, false)
}
+func (m *Mail) FooterLine(line string) *Mail {
+ m.footerLines = append(m.footerLines, &mailLine{
+ Text: line,
+ })
+ return m
+}
+
+func (m *Mail) IncludeLinkToSettings() *Mail {
+ m.FooterLine("You can change your notification settings [here](" + config.ServicePublicURL.GetString() + "user/settings/general).")
+ return m
+}
+
func (m *Mail) HTML(line string) *Mail {
return m.appendLine(line, true)
}
diff --git a/pkg/notifications/mail_render.go b/pkg/notifications/mail_render.go
index 062bd9236..5c8265af9 100644
--- a/pkg/notifications/mail_render.go
+++ b/pkg/notifications/mail_render.go
@@ -41,6 +41,9 @@ const mailTemplatePlain = `
{{ .ActionURL }}{{end}}
{{ range $line := .OutroLines}}
{{ $line.Text }}
+{{ end }}
+{{ range $line := .FooterLines}}
+{{ $line.Text }}
{{ end }}`
const mailTemplateHTML = `
@@ -76,10 +79,23 @@ const mailTemplateHTML = `
{{ end }}
{{ if .ActionURL }}
-
+
+
If the button above doesn't work, copy the url below and paste it in your browser's address bar:
{{ .ActionURL }}
+{{ range $line := .FooterLinesHTML}}
+ {{ $line }}
+ {{ end }}
+
+{{ else }}
+{{ if .FooterLinesHTML }}
+
+ {{ range $line := .FooterLinesHTML }}
+ {{ $line }}
+ {{ end }}
+
+{{ end }}
{{ end }}
@@ -91,6 +107,29 @@ const mailTemplateHTML = `
//go:embed logo.png
var logo embed.FS
+func convertLinesToHTML(lines []*mailLine) (linesHTML []templatehtml.HTML, err error) {
+ p := bluemonday.UGCPolicy()
+
+ for _, line := range lines {
+ if line.isHTML {
+ // #nosec G203 -- the html is sanitized
+ linesHTML = append(linesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
+ continue
+ }
+
+ md := []byte(templatehtml.HTMLEscapeString(line.Text))
+ var buf bytes.Buffer
+ err = goldmark.Convert(md, &buf)
+ if err != nil {
+ return nil, err
+ }
+ // #nosec G203 -- the html is sanitized
+ linesHTML = append(linesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
+ }
+
+ return
+}
+
// 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) {
@@ -114,50 +153,26 @@ func RenderMail(m *Mail) (mailOpts *mail.Opts, err error) {
data["Greeting"] = m.greeting
data["IntroLines"] = m.introLines
data["OutroLines"] = m.outroLines
+ data["FooterLines"] = m.footerLines
data["ActionText"] = m.actionText
data["ActionURL"] = m.actionURL
data["Boundary"] = boundary
data["FrontendURL"] = config.ServicePublicURL.GetString()
- p := bluemonday.UGCPolicy()
-
- var introLinesHTML []templatehtml.HTML
- for _, line := range m.introLines {
- if line.isHTML {
- // #nosec G203 -- the html is sanitized
- introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
- continue
- }
-
- md := []byte(templatehtml.HTMLEscapeString(line.Text))
- var buf bytes.Buffer
- err = goldmark.Convert(md, &buf)
- if err != nil {
- return nil, err
- }
- // #nosec G203 -- the html is sanitized
- introLinesHTML = append(introLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
+ data["IntroLinesHTML"], err = convertLinesToHTML(m.introLines)
+ if err != nil {
+ return nil, err
}
- data["IntroLinesHTML"] = introLinesHTML
- var outroLinesHTML []templatehtml.HTML
- for _, line := range m.outroLines {
- if line.isHTML {
- // #nosec G203 -- the html is sanitized
- outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(line.Text)))
- continue
- }
-
- md := []byte(templatehtml.HTMLEscapeString(line.Text))
- var buf bytes.Buffer
- err = goldmark.Convert(md, &buf)
- if err != nil {
- return nil, err
- }
- // #nosec G203 -- the html is sanitized
- outroLinesHTML = append(outroLinesHTML, templatehtml.HTML(p.Sanitize(buf.String())))
+ data["OutroLinesHTML"], err = convertLinesToHTML(m.outroLines)
+ if err != nil {
+ return nil, err
+ }
+
+ data["FooterLinesHTML"], err = convertLinesToHTML(m.footerLines)
+ if err != nil {
+ return nil, err
}
- data["OutroLinesHTML"] = outroLinesHTML
err = plain.Execute(&plainContent, data)
if err != nil {
diff --git a/pkg/notifications/mail_test.go b/pkg/notifications/mail_test.go
index c04d3be45..022be4a02 100644
--- a/pkg/notifications/mail_test.go
+++ b/pkg/notifications/mail_test.go
@@ -89,24 +89,82 @@ func TestNewMail(t *testing.T) {
}
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("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").
- Line("And one more, because why not?")
+ t.Run("simple", func(t *testing.T) {
+ mail := NewMail().
+ From("test@example.com").
+ To("test@otherdomain.com").
+ Subject("Testmail").
+ Greeting("Hi there,").
+ Line("This is a line")
- mailopts, err := RenderMail(mail)
- require.NoError(t, err)
- assert.Equal(t, mail.from, mailopts.From)
- assert.Equal(t, mail.to, mailopts.To)
+ mailopts, err := RenderMail(mail)
+ require.NoError(t, err)
+ assert.Equal(t, mail.from, mailopts.From)
+ assert.Equal(t, mail.to, mailopts.To)
- assert.Equal(t, `
+ assert.Equal(t, `
+Hi there,
+
+This is a line
+
+
+
+`, mailopts.Message)
+ assert.Equal(t, `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi there,
+
+
+
+
This is a line
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`, mailopts.HTMLMessage)
+ })
+ t.Run("with action", 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("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").
+ Line("And one more, because why not?")
+
+ mailopts, err := RenderMail(mail)
+ require.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
@@ -121,8 +179,9 @@ https://example.com
This should be an outro line
And one more, because why not?
+
`, mailopts.Message)
- assert.Equal(t, `
+ assert.Equal(t, `
@@ -166,15 +225,186 @@ And one more, because why not?
-
+
+
If the button above doesn't work, copy the url below and paste it in your browser's address bar:
https://example.com
+
+
`, mailopts.HTMLMessage)
+ })
+ t.Run("with footer", func(t *testing.T) {
+ mail := NewMail().
+ From("test@example.com").
+ To("test@otherdomain.com").
+ Subject("Testmail").
+ Greeting("Hi there,").
+ Line("This is a line").
+ FooterLine("This is a footer line")
+
+ mailopts, err := RenderMail(mail)
+ require.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
+
+
+
+
+This is a footer line
+`, mailopts.Message)
+ assert.Equal(t, `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi there,
+
+
+
+
This is a line
+
+
+
+
+
+
+
+
+
+
+
+
This is a footer line
+
+
+
+
+
+
+
+
+
+
+`, mailopts.HTMLMessage)
+ })
+ t.Run("with footer and action", 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("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").
+ Line("And one more, because why not?").
+ FooterLine("This is a footer line")
+
+ mailopts, err := RenderMail(mail)
+ require.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
+
+This **line** contains [a link](https://vikunja.io)
+
+And another one
+
+The action:
+https://example.com
+
+This should be an outro line
+
+And one more, because why not?
+
+
+This is a footer line
+`, mailopts.Message)
+ assert.Equal(t, `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hi there,
+
+
+
+
This is a line
+
+
+
This line contains a link
+
+
+
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 browser's address bar:
+ https://example.com
+
+
+
This is a footer line
+
+
+
+
+
+
+
+
+
+`, mailopts.HTMLMessage)
+ })
}