mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-04-28 18:38:24 -05:00
feat(notifications): include link to settings in notifications
This commit is contained in:
@@ -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:").
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
<p style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
<div style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
<p>
|
||||
If the button above doesn't work, copy the url below and paste it in your browser's address bar:<br/>
|
||||
{{ .ActionURL }}
|
||||
</p>
|
||||
{{ range $line := .FooterLinesHTML}}
|
||||
{{ $line }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ if .FooterLinesHTML }}
|
||||
<div style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
{{ range $line := .FooterLinesHTML }}
|
||||
{{ $line }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, `
|
||||
<!doctype html>
|
||||
<html style="width: 100%; height: 100%; padding: 0; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width: display-width;">
|
||||
</head>
|
||||
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
|
||||
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
|
||||
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
|
||||
<h1 style="font-size: 30px; Text-align: center;">
|
||||
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
|
||||
</h1>
|
||||
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
|
||||
<p>
|
||||
Hi there,
|
||||
</p>
|
||||
|
||||
|
||||
<p>This is a line</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, 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, `
|
||||
<!doctype html>
|
||||
<html style="width: 100%; height: 100%; padding: 0; margin: 0;">
|
||||
<head>
|
||||
@@ -166,15 +225,186 @@ And one more, because why not?
|
||||
|
||||
|
||||
|
||||
<p style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
<div style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
<p>
|
||||
If the button above doesn't work, copy the url below and paste it in your browser's address bar:<br/>
|
||||
https://example.com
|
||||
</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, 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, `
|
||||
<!doctype html>
|
||||
<html style="width: 100%; height: 100%; padding: 0; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width: display-width;">
|
||||
</head>
|
||||
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
|
||||
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
|
||||
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
|
||||
<h1 style="font-size: 30px; Text-align: center;">
|
||||
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
|
||||
</h1>
|
||||
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
|
||||
<p>
|
||||
Hi there,
|
||||
</p>
|
||||
|
||||
|
||||
<p>This is a line</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
|
||||
<p>This is a footer line</p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, 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, `
|
||||
<!doctype html>
|
||||
<html style="width: 100%; height: 100%; padding: 0; margin: 0;">
|
||||
<head>
|
||||
<meta name="viewport" content="width: display-width;">
|
||||
</head>
|
||||
<body style="width: 100%; padding: 0; margin: 0; background: #f3f4f6">
|
||||
<div style="width: 100%; font-family: 'Open Sans', sans-serif; Text-rendering: optimizeLegibility">
|
||||
<div style="width: 600px; margin: 0 auto; Text-align: justify;">
|
||||
<h1 style="font-size: 30px; Text-align: center;">
|
||||
<img src="cid:logo.png" style="height: 75px;" alt="Vikunja"/>
|
||||
</h1>
|
||||
<div style="border: 1px solid #dbdbdb; -webkit-box-shadow: 0.3em 0.3em 0.8em #e6e6e6; box-shadow: 0.3em 0.3em 0.8em #e6e6e6; color: #4a4a4a; padding: 5px 25px; border-radius: 3px; background: #fff;">
|
||||
<p>
|
||||
Hi there,
|
||||
</p>
|
||||
|
||||
|
||||
<p>This is a line</p>
|
||||
|
||||
|
||||
<p>This <strong>line</strong> contains <a href="https://vikunja.io" rel="nofollow">a link</a></p>
|
||||
|
||||
|
||||
<p>And another one</p>
|
||||
|
||||
|
||||
|
||||
|
||||
<a href="https://example.com" title="The action"
|
||||
style="position: relative;Text-decoration:none;display: block;border-radius: 4px;cursor: pointer;padding-bottom: 8px;padding-left: 14px;padding-right: 14px;padding-top: 8px;width:280px;margin:10px auto;Text-align: center;white-space: nowrap;border: 0;Text-transform: uppercase;font-size: 14px;font-weight: 700;-webkit-box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);box-shadow: 0 3px 6px rgba(107,114,128,.12),0 2px 4px rgba(107,114,128,.1);background-color: #1973ff;border-color: transparent;color: #fff;">
|
||||
The action
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<p>This should be an outro line</p>
|
||||
|
||||
|
||||
<p>And one more, because why not?</p>
|
||||
|
||||
|
||||
|
||||
|
||||
<div style="color: #9CA3AF;font-size:12px;border-top: 1px solid #dbdbdb;margin-top:20px;padding-top:20px;">
|
||||
<p>
|
||||
If the button above doesn't work, copy the url below and paste it in your browser's address bar:<br/>
|
||||
https://example.com
|
||||
</p>
|
||||
|
||||
<p>This is a footer line</p>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`, mailopts.HTMLMessage)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user