diff --git a/emails/Makefile b/emails/Makefile index e44bd04b9a1..c91b98df6e9 100644 --- a/emails/Makefile +++ b/emails/Makefile @@ -1,14 +1,18 @@ -build: build-html build-txt +build: clean build-mjml build-grunt -build-html: +clean: + rm -rf dist/ + mkdir dist/ + +build-mjml: npx mjml \ --config.beautify true \ --config.minify false \ --config.validationLevel=strict \ --config.keepComments=false \ - ./templates/*.mjml --output ../public/emails/ + ./templates/*.mjml --output ./dist/ -build-txt: +build-grunt: npx grunt -.PHONY: build build-html build-txt +.PHONY: clean build build-mjml build-grunt diff --git a/emails/grunt/aliases.yaml b/emails/grunt/aliases.yaml index 550d7a5422a..090a082cdc0 100644 --- a/emails/grunt/aliases.yaml +++ b/emails/grunt/aliases.yaml @@ -1,5 +1,4 @@ default: - - 'clean' - 'assemble' - 'replace' - 'copy' diff --git a/emails/grunt/clean.js b/emails/grunt/clean.js deleted file mode 100644 index a829e293c12..00000000000 --- a/emails/grunt/clean.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function (config) { - return { - dist: ['dist'], - }; -}; diff --git a/emails/grunt/copy.js b/emails/grunt/copy.js index 4f794d85a3a..e1fc08f6453 100644 --- a/emails/grunt/copy.js +++ b/emails/grunt/copy.js @@ -7,5 +7,11 @@ module.exports = function () { src: ['**.txt'], dest: '../public/emails/', }, + html: { + expand: true, + cwd: 'dist', + src: ['**.html'], + dest: '../public/emails/', + }, }; }; diff --git a/emails/grunt/replace.js b/emails/grunt/replace.js index be4f18e34d4..92a0935f600 100644 --- a/emails/grunt/replace.js +++ b/emails/grunt/replace.js @@ -1,16 +1,45 @@ -module.exports = { - dist: { - overwrite: true, - src: ['dist/*.txt'], - replacements: [ - { - from: '[[', - to: '{{', - }, - { - from: ']]', - to: '}}', - }, - ], - }, +module.exports = function () { + 'use strict'; + + return { + txt, + comments, + }; +}; + +const txt = { + overwrite: true, + src: ['dist/*.txt'], + replacements: [ + { + from: '[[', + to: '{{', + }, + { + from: ']]', + to: '}}', + }, + ], +}; + +/** + * Replace all instances of HTML comments with {{ __dangerouslyInjectHTML "" }}. + * + * MJML will output comments which are specific to MS Outlook. + * + * Go's template/html package will strip all HTML comments and we need them to be preserved + * to work with MS Outlook on the Desktop. + */ +const HTML_SAFE_FUNC = '__dangerouslyInjectHTML'; +const commentBlock = /()/g; + +const comments = { + overwrite: true, + src: ['dist/*.html'], + replacements: [ + { + from: commentBlock, + to: `{{ ${HTML_SAFE_FUNC} \`$1\` }}`, + }, + ], }; diff --git a/emails/package.json b/emails/package.json index b6c5bc187e1..2beb155d0d2 100644 --- a/emails/package.json +++ b/emails/package.json @@ -7,7 +7,6 @@ "grunt": "1.5.3", "grunt-assemble": "0.6.3", "grunt-cli": "^1.4.3", - "grunt-contrib-clean": "2.0.0", "grunt-contrib-copy": "^1.0.0", "grunt-contrib-watch": "1.1.0", "grunt-text-replace": "0.4.0", diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index e81897ef5a0..11b43847035 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -54,8 +54,9 @@ func ProvideService(bus bus.Bus, cfg *setting.Cfg, mailer Mailer, store TempUser mailTemplates = template.New("name") mailTemplates.Funcs(template.FuncMap{ - "Subject": subjectTemplateFunc, - "HiddenSubject": hiddenSubjectTemplateFunc, + "Subject": subjectTemplateFunc, + "HiddenSubject": hiddenSubjectTemplateFunc, + "__dangerouslyInjectHTML": __dangerouslyInjectHTML, }) mailTemplates.Funcs(sprig.FuncMap()) @@ -174,6 +175,17 @@ func subjectTemplateFunc(obj map[string]interface{}, data map[string]interface{} return subj } +// __dangerouslyInjectHTML allows marking areas of am email template as HTML safe, this will _not_ sanitize the string and will allow HTML snippets to be rendered verbatim. +// Use with absolute care as this _could_ allow for XSS attacks when used in an insecure context. +// +// It's safe to ignore gosec warning G203 when calling this function in an HTML template because we assume anyone who has write access +// to the email templates folder is an administrator. +// +// nolint:gosec +func __dangerouslyInjectHTML(s string) template.HTML { + return template.HTML(s) +} + func (ns *NotificationService) SendEmailCommandHandlerSync(ctx context.Context, cmd *SendEmailCommandSync) error { message, err := ns.buildEmailMessage(&SendEmailCommand{ Data: cmd.Data, diff --git a/public/emails/invited_to_org.html b/public/emails/invited_to_org.html index ad3f5f57a6b..57f4b635068 100644 --- a/public/emails/invited_to_org.html +++ b/public/emails/invited_to_org.html @@ -5,9 +5,9 @@ {{ Subject .Subject .TemplateData "{{ .InvitedBy }} has added you to the {{ .OrgName }} organization" }} - + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }} - - ` }} + {{ __dangerouslyInjectHTML ` - + ` }} + {{ __dangerouslyInjectHTML `` }} - + {{ __dangerouslyInjectHTML `` }}