[MM-24714] Render markdown in email notifications (#18043)

Automatic Merge
This commit is contained in:
Joe
2021-09-01 10:59:01 +01:00
committed by GitHub
parent 9534f9fb62
commit 0b79defcbb
3 changed files with 142 additions and 2 deletions

View File

@@ -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)

View File

@@ -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: "<b>not bold</b>",
want: "&lt;b&gt;not bold&lt;/b&gt;",
},
{
name: "markdown: strong",
args: "This is **Mattermost**",
want: "This is <strong>Mattermost</strong>",
},
{
name: "markdown: blockquote",
args: "Below is blockquote\n" +
"> This is Mattermost blockquote\n" +
"> on multiple lines!",
want: "<blockquote>\n" +
"<p>This is Mattermost blockquote\n" +
"on multiple lines!</p>\n" +
"</blockquote>",
},
{
name: "markdown: emphasis",
args: "This is *Mattermost*",
want: "This is <em>Mattermost</em>",
},
{
name: "markdown: links",
args: "This is [Mattermost](https://mattermost.com)",
want: "This is <a href=\"https://mattermost.com\">Mattermost</a>",
},
{
name: "markdown: strikethrough",
args: "This is ~~Mattermost~~",
want: "This is <del>Mattermost</del>",
},
{
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: "<table>\n" +
"<thead>\n" +
"<tr>\n" +
"<th>Tables</th>\n" +
"<th style=\"text-align:center\">Are</th>\n" +
"<th style=\"text-align:right\">Cool</th>\n" +
"</tr>\n" +
"</thead>\n" +
"<tbody>\n" +
"<tr>\n" +
"<td>col 3 is</td>\n" +
"<td style=\"text-align:center\">right-aligned</td>\n" +
"<td style=\"text-align:right\">$1600</td>\n" +
"</tr>\n" +
"<tr>\n" +
"<td>col 2 is</td>\n" +
"<td style=\"text-align:center\">centered</td>\n" +
"<td style=\"text-align:right\">$12</td>\n" +
"</tr>\n" +
"<tr>\n" +
"<td>zebra stripes</td>\n" +
"<td style=\"text-align:center\">are neat</td>\n" +
"<td style=\"text-align:right\">$1</td>\n" +
"</tr>\n" +
"</tbody>\n" +
"</table>",
},
{
name: "markdown: multiline with header and links",
args: "###### H6 header\n[link 1](https://mattermost.com) - [link 2](https://mattermost.com)",
want: "<h6>H6 header</h6>\n" +
"<p><a href=\"https://mattermost.com\">link 1</a> - <a href=\"https://mattermost.com\">link 2</a></p>",
},
}
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)
})
}
}

View File

@@ -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(&gt;)`)
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 {
}