diff --git a/app/notification_email.go b/app/notification_email.go index b5ebf1f352..b32d0f669f 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -15,6 +15,7 @@ import ( "github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost-server/v6/shared/i18n" "github.com/mattermost/mattermost-server/v6/shared/mlog" + "github.com/mattermost/mattermost-server/v6/utils" "github.com/pkg/errors" ) @@ -213,10 +214,16 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, if emailNotificationContentsType == model.EmailNotificationContentsFull { postMessage := a.GetMessageForNotification(post, translateFunc) postMessage = html.EscapeString(postMessage) - normalizedPostMessage, err := a.generateHyperlinkForChannels(postMessage, teamName, landingURL) + mdPostMessage, mdErr := utils.MarkdownToHTML(postMessage) + if mdErr != nil { + mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(mdErr)) + mdPostMessage = postMessage + } + + normalizedPostMessage, err := a.generateHyperlinkForChannels(mdPostMessage, teamName, landingURL) if err != nil { mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err)) - normalizedPostMessage = postMessage + normalizedPostMessage = mdPostMessage } pData.Message = template.HTML(normalizedPostMessage) pData.Time = translateFunc("app.notification.body.dm.time", messageTime) diff --git a/app/notification_email_test.go b/app/notification_email_test.go index 6f08db581a..41b58e71d4 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -825,3 +825,112 @@ func TestLandingLinkPermalink(t *testing.T) { require.NoError(t, err) require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } + +func TestMarkdownConversion(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + { + name: "markdown: escape string test", + args: "not bold", + want: "<b>not bold</b>", + }, + { + name: "markdown: strong", + args: "This is **Mattermost**", + want: "This is Mattermost", + }, + { + name: "markdown: blockquote", + args: "Below is blockquote\n" + + "> This is Mattermost blockquote\n" + + "> on multiple lines!", + want: "
\n" + + "

This is Mattermost blockquote\n" + + "on multiple lines!

\n" + + "
", + }, + { + name: "markdown: emphasis", + args: "This is *Mattermost*", + want: "This is Mattermost", + }, + { + name: "markdown: links", + args: "This is [Mattermost](https://mattermost.com)", + want: "This is Mattermost", + }, + { + name: "markdown: strikethrough", + args: "This is ~~Mattermost~~", + want: "This is Mattermost", + }, + { + name: "markdown: table", + args: "| Tables | Are | Cool |\n" + + "| ------------- |:-------------:| -----:|\n" + + "| col 3 is | right-aligned | $1600 |\n" + + "| col 2 is | centered | $12 |\n" + + "| zebra stripes | are neat | $1 |\n", + want: "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "
TablesAreCool
col 3 isright-aligned$1600
col 2 iscentered$12
zebra stripesare neat$1
", + }, + { + name: "markdown: multiline with header and links", + args: "###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)", + want: "
H6 header
\n" + + "

link 1 - link 2

", + }, + } + + th := SetupWithStoreMock(t) + defer th.TearDown() + + recipient := &model.User{} + storeMock := th.App.Srv().Store.(*mocks.Store) + teamStoreMock := mocks.TeamStore{} + teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) + storeMock.On("Team").Return(&teamStoreMock) + channel := &model.Channel{ + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + post := &model.Post{ + Id: "Test_id", + Message: tt.args, + } + got, err := th.App.getNotificationEmailBody(recipient, post, channel, "ChannelName", "sender", "testteam", "http://localhost:8065/landing#/testteam", model.EmailNotificationContentsFull, true, i18n.GetUserTranslations("en"), "user-avatar.png") + require.NoError(t, err) + require.Contains(t, got, tt.want) + }) + } +} diff --git a/utils/markdown.go b/utils/markdown.go index 3b79589966..68319add57 100644 --- a/utils/markdown.go +++ b/utils/markdown.go @@ -4,6 +4,8 @@ package utils import ( + "html" + "regexp" "strings" "github.com/yuin/goldmark" @@ -33,6 +35,28 @@ func StripMarkdown(markdown string) (string, error) { return strings.TrimSpace(buf.String()), nil } +// MarkdownToHTML takes a string containing Markdown and returns a string with HTML tagged version +func MarkdownToHTML(markdown string) (string, error) { + // Unescape any blockquote text to be parsed by the markdown parser. + re := regexp.MustCompile(`^|\n(>)`) + markdownClean := re.ReplaceAllFunc([]byte(markdown), func(s []byte) []byte { + out := html.UnescapeString(string(s)) + return []byte(out) + }) + + md := goldmark.New( + goldmark.WithExtensions(extension.GFM), + ) + + var b strings.Builder + + err := md.Convert(markdownClean, &b) + if err != nil { + return "", err + } + return b.String(), nil +} + type notificationRenderer struct { }