diff --git a/app/notification_email.go b/app/notification_email.go index 914b729a01..965dd9a292 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -201,6 +201,18 @@ func truncateUserNames(name string, i int) string { return name } +type FieldRow struct { + Cells []*model.SlackAttachmentField +} + +type EmailMessageAttachment struct { + model.SlackAttachment + + Pretext template.HTML + Text template.HTML + FieldRows []FieldRow +} + type postData struct { SenderName string ChannelName string @@ -211,6 +223,7 @@ type postData struct { Time string ShowChannelIcon bool OtherChannelMembersCount int + MessageAttachments []*EmailMessageAttachment } /** @@ -245,6 +258,7 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos } pData.Message = template.HTML(normalizedPostMessage) pData.Time = translateFunc("app.notification.body.dm.time", messageTime) + pData.MessageAttachments = a.processMessageAttachments(post) } data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale) @@ -306,6 +320,81 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos return a.Srv().TemplatesContainer().RenderToString("messages_notification", data) } +func (a *App) processMessageAttachments(post *model.Post) []*EmailMessageAttachment { + emailMessageAttachments := []*EmailMessageAttachment{} + + for _, messageAttachment := range post.Attachments() { + emailMessageAttachment := &EmailMessageAttachment{ + SlackAttachment: *messageAttachment, + Pretext: a.prepareTextForEmail(messageAttachment.Pretext), + Text: a.prepareTextForEmail(messageAttachment.Text), + } + + stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title) + if err != nil { + mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err)) + stripedTitle = "" + } + + emailMessageAttachment.Title = stripedTitle + + shortFieldRow := FieldRow{} + + for i := range messageAttachment.Fields { + // Create a new instance to avoid altering the original pointer reference + // We update field value to parse markdown. + // If we do that on the original pointer, the rendered text in mattermost + // becomes invalid as its no longer a markdown string, but rather an HTML string. + field := &model.SlackAttachmentField{ + Title: messageAttachment.Fields[i].Title, + Value: messageAttachment.Fields[i].Value, + Short: messageAttachment.Fields[i].Short, + } + + if stringValue, ok := field.Value.(string); ok { + field.Value = a.prepareTextForEmail(stringValue) + } + + if !field.Short { + if len(shortFieldRow.Cells) > 0 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}}) + } else { + shortFieldRow.Cells = append(shortFieldRow.Cells, field) + + if len(shortFieldRow.Cells) == 2 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + } + } + + // collect any leftover short fields + if len(shortFieldRow.Cells) > 0 { + emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow) + shortFieldRow = FieldRow{} + } + + emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment) + } + + return emailMessageAttachments +} + +func (a *App) prepareTextForEmail(text string) template.HTML { + escapedText := html.EscapeString(text) + markdownText, err := utils.MarkdownToHTML(escapedText) + if err != nil { + mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err)) + return template.HTML(text) + } + + return template.HTML(markdownText) +} + type formattedPostTime struct { Time time.Time Year string diff --git a/app/notification_email_test.go b/app/notification_email_test.go index 8b48b50035..4d0f77f433 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -339,6 +339,103 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body)) } +func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing.T) { + th := SetupWithStoreMock(t) + defer th.TearDown() + + recipient := &model.User{} + post := &model.Post{ + Message: "This is the message", + } + + messageAttachments := []*model.SlackAttachment{ + { + Color: "#FF0000", + Pretext: "message attachment 1 pretext", + AuthorName: "author name", + AuthorLink: "https://example.com/slack_attachment_1/author_link", + AuthorIcon: "https://example.com/slack_attachment_1/author_icon", + Title: "message attachment 1 title", + TitleLink: "https://example.com/slack_attachment_1/title_link", + Text: "message attachment 1 text", + ImageURL: "https://example.com/slack_attachment_1/image", + ThumbURL: "https://example.com/slack_attachment_1/thumb", + Fields: []*model.SlackAttachmentField{ + { + Short: true, + Title: "message attachment 1 field 1 title", + Value: "message attachment 1 field 1 value", + }, + { + Short: false, + Title: "message attachment 1 field 2 title", + Value: "message attachment 1 field 2 value", + }, + { + Short: true, + Title: "message attachment 1 field 3 title", + Value: "message attachment 1 field 3 value", + }, + { + Short: true, + Title: "message attachment 1 field 4 title", + Value: "message attachment 1 field 4 value", + }, + }, + }, + { + Color: "#FF0000", + Pretext: "message attachment 2 pretext", + AuthorName: "author name 2", + Text: "message attachment 2 text", + }, + } + + model.ParseSlackAttachment(post, messageAttachments) + + channel := &model.Channel{ + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + + channelName := "ChannelName" + senderName := "sender" + teamName := "testteam" + teamURL := "http://localhost:8065/testteam" + emailNotificationContentsType := model.EmailNotificationContentsFull + translateFunc := i18n.GetUserTranslations("en") + + 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) + + body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") + require.NoError(t, err) + require.Contains(t, body, "#FF0000") + require.Contains(t, body, "message attachment 1 pretext") + require.Contains(t, body, "author name") + require.Contains(t, body, "https://example.com/slack_attachment_1/author_link") + require.Contains(t, body, "https://example.com/slack_attachment_1/author_icon") + require.Contains(t, body, "message attachment 1 title") + require.Contains(t, body, "https://example.com/slack_attachment_1/title_link") + require.Contains(t, body, "message attachment 1 text") + require.Contains(t, body, "https://example.com/slack_attachment_1/image") + require.Contains(t, body, "https://example.com/slack_attachment_1/thumb") + require.Contains(t, body, "message attachment 1 field 1 title") + require.Contains(t, body, "message attachment 1 field 1 value") + require.Contains(t, body, "message attachment 1 field 2 title") + require.Contains(t, body, "message attachment 1 field 2 value") + require.Contains(t, body, "message attachment 1 field 3 title") + require.Contains(t, body, "message attachment 1 field 3 value") + require.Contains(t, body, "message attachment 1 field 4 title") + require.Contains(t, body, "message attachment 1 field 4 value") + require.Contains(t, body, "https://example.com/slack_attachment_1/thumb") + require.Contains(t, body, "message attachment 2 pretext") + require.Contains(t, body, "author name 2") + require.Contains(t, body, "message attachment 2 text") +} + // from here func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) { th := SetupWithStoreMock(t) diff --git a/templates/invite_body.mjml b/templates/invite_body.mjml index 869a359b2c..d16c20af68 100644 --- a/templates/invite_body.mjml +++ b/templates/invite_body.mjml @@ -8,7 +8,13 @@ {{if .Props.Message}} {{range .Props.Posts}}
- + + + + + + +
{{end}}
{{else}} diff --git a/templates/messages_notification.html b/templates/messages_notification.html index 9c40227203..7b47478cfa 100644 --- a/templates/messages_notification.html +++ b/templates/messages_notification.html @@ -356,7 +356,160 @@ max-width: 100% !important; } } + + .messageAttachments * { + font-family: Open Sans, sans-serif !important; + } + + .messageAttachmentContent { + padding: 0px; + border: 1px solid rgba(63, 67, 80, 0.16); + margin-bottom: 20px; + border-radius: 0 4px 4px 0; + } + + .messageAttachmentContent>table, + .attachment__body { + font-family: Open Sans, sans-serif; + text-align: left; + font-size: 14px; + line-height: 20px; + color: #3F4350; + } + + .messageAttachmentContent .attachment__author-icon { + width: 14px; + height: 14px; + margin-right: 5px; + border-radius: 50px; + vertical-align: middle; + } + + .attachment__author-name { + opacity: 0.6; + } + + .messageAttachmentContent p { + margin: 0; + } + + .attachment__title { + padding: 0; + margin: 5px 0; + font-size: 14px; + font-weight: 600; + line-height: 18px; + } + + .attachment__image { + max-height: 300px; + border: 1px solid transparent; + margin-bottom: 1em; + } + + .attachment__thumb-image { + max-width: 100%; + max-height: 75px; + } + + .attachment__thumb-container { + max-width: 80px; + width: max-content; + } + + .attachment__wrapper { + width: 100%; + display: flex; + flex-direction: row; + gap: 12px; + } + + .attachment__body { + flex: 1; + } + + .messageAttachmentContent>table.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; + } + + .attachment__footer-icon { + width: 16px; + height: 16px; + } + + .attachment__footer-container { + color: #a3a3a3; + font-size: 12px; + } + + .messageAttachment_title { + padding-top: 1em; + font-weight: 600; + margin-bottom: 4px; + } + + .pretext h1 { + font-size: 28px; + line-height: 32px; + } + + .pretext h2 { + font-size: 25px; + line-height: 30px; + } + + .pretext h3 { + font-size: 22px; + line-height: 25px; + } + + .pretext h4 { + font-size: 19px; + line-height: 24px; + } + + .pretext h5 { + font-size: 15px; + line-height: 20px; + } + + .pretext h6 { + font-size: 1em; + line-height: 1.4em; + } + + .pretext>* { + margin-bottom: 16px; + } + + .messageAttachments { + margin-top: 22px; + } + + .messageAttachments h1, + h2, + h3, + h4, + h5, + h6 { + font-weight: 500; + } + + code { + padding: 2px 4px; + font-size: 90%; + background-color: rgba(63, 67, 80, 0.1); + border-radius: 4px; + } + + .messageAttachments a { + background-color: unset !important; + border: none !important; + padding: unset !important; + } + @@ -507,7 +660,96 @@ - + + {{if .MessageAttachments}} +
+ {{range .MessageAttachments}} +
+
+ {{.Pretext}} +
+
+ + + {{if or .AuthorIcon .AuthorName}} + + + + {{end}} + {{if .Title}} + + + + {{end}} + +
+ {{if .AuthorLink}}{{end}} + {{if .AuthorIcon}}attachment author icon{{end}} + {{if .AuthorName}}{{.AuthorName}}{{end}} + {{if .AuthorLink}}{{end}} +
+

+ {{if .TitleLink}}{{end}} + {{.Title}} + {{if .Title}}{{end}} +

+
+
+
+ + + + + + {{if .ImageURL}} + + + + {{end}} + +
+
{{.Text}}
+
+ +
+ + {{range .FieldRows}} + + {{ $length := len .Cells }} + {{range .Cells}} + + {{end}} + + {{end}} +
+
{{.Title}}
+
{{.Value}}
+
+
+ {{if .ThumbURL}} +
+ +
+ {{end}} +
+ {{if .Footer}} + + + + + + + + {{end}} +
+
+ {{end}} +
+ {{end}} +
diff --git a/templates/messages_notification.mjml b/templates/messages_notification.mjml index ad840ff420..a2b53b6b16 100644 --- a/templates/messages_notification.mjml +++ b/templates/messages_notification.mjml @@ -1,13 +1,21 @@ + {{range .Props.Posts}}
- + + + + + + + +
{{end}}
diff --git a/templates/partials/card.mjml b/templates/partials/card.mjml deleted file mode 100644 index f1c835e04c..0000000000 --- a/templates/partials/card.mjml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - -
- - - - - {{.Message}} - - - - {{if .MessageURL}} - {{$.Props.MessageButton}} - {{end}} - - - diff --git a/templates/partials/message_attachment.css b/templates/partials/message_attachment.css new file mode 100644 index 0000000000..ee65c7f8a3 --- /dev/null +++ b/templates/partials/message_attachment.css @@ -0,0 +1,146 @@ +.messageAttachments * { + font-family: Open Sans, sans-serif !important; +} + +.messageAttachmentContent { + padding: 0px; + border: 1px solid rgba(63, 67, 80, 0.16); + margin-bottom: 20px; + border-radius: 0 4px 4px 0; +} + +.messageAttachmentContent > table, .attachment__body { + font-family: Open Sans, sans-serif; + text-align: left; + font-size: 14px; + line-height: 20px; + color: #3F4350; +} + +.messageAttachmentContent .attachment__author-icon { + width: 14px; + height: 14px; + margin-right: 5px; + border-radius: 50px; + vertical-align: middle; +} + +.attachment__author-name { + opacity: 0.6; +} + +.messageAttachmentContent p { + margin: 0; +} + +.attachment__title { + padding: 0; + margin: 5px 0; + font-size: 14px; + font-weight: 600; + line-height: 18px; +} + +.attachment__image { + max-height: 300px; + border: 1px solid transparent; + margin-bottom: 1em; +} + + +.attachment__thumb-image { + max-width: 100%; + max-height: 75px; +} + +.attachment__thumb-container { + max-width: 80px; + width: max-content; +} + +.attachment__wrapper { + width: 100%; + display: flex; + flex-direction: row; + gap: 12px; +} + +.attachment__body { + flex: 1; +} + +.messageAttachmentContent > table.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; +} + +.attachment__footer-icon { + width: 16px; + height: 16px; +} + +.attachment__footer-container { + color: #a3a3a3; + font-size: 12px; +} + +.messageAttachment_title { + padding-top: 1em; + font-weight: 600; + margin-bottom: 4px; +} + +.pretext h1 { + font-size: 28px; + line-height: 32px; +} + +.pretext h2 { + font-size: 25px; + line-height: 30px; +} + +.pretext h3 { + font-size: 22px; + line-height: 25px; +} + +.pretext h4 { + font-size: 19px; + line-height: 24px; +} + +.pretext h5 { + font-size: 15px; + line-height: 20px; +} + +.pretext h6 { + font-size: 1em; + line-height: 1.4em; +} + +.pretext > * { + margin-bottom: 16px; +} + +.messageAttachments { + margin-top: 22px; +} + +.messageAttachments h1, h2, h3, h4, h5, h6 { + font-weight: 500; +} + +code { + padding: 2px 4px; + font-size: 90%; + background-color: rgba(63, 67, 80, 0.1); + border-radius: 4px; +} + +.messageAttachments a { + background-color: unset !important; + border: none !important; + padding: unset !important; +} diff --git a/templates/partials/message_attachment.html b/templates/partials/message_attachment.html new file mode 100644 index 0000000000..68dd5fe21b --- /dev/null +++ b/templates/partials/message_attachment.html @@ -0,0 +1,95 @@ +{{if .MessageAttachments}} +
+ {{range .MessageAttachments}} +
+
+ {{.Pretext}} +
+
+
-
-
{{.SenderName}}
- {{if .Time}} -
{{.Time}}
- {{end}} - {{if .ChannelName}} -
- {{if .ShowChannelIcon}} - - {{end}} -
- {{if .OtherChannelMembersCount}} - {{.OtherChannelMembersCount}} - {{end}} - {{.ChannelName}} -
-
- {{end}} -
-
+ + {{if or .AuthorIcon .AuthorName}} + + + + {{end}} + {{if .Title}} + + + + {{end}} + +
+ {{if .AuthorLink}}{{end}} + {{if .AuthorIcon}}attachment author icon{{end}} + {{if .AuthorName}}{{.AuthorName}}{{end}} + {{if .AuthorLink}}{{end}} +
+

+ {{if .TitleLink}}{{end}} + {{.Title}} + {{if .Title}}{{end}} +

+
+ +
+
+ + + + + + {{if .ImageURL}} + + + + {{end}} +
+
{{.Text}}
+
+ +
+ + + {{range .FieldRows}} + + {{ $length := len .Cells }} + {{range .Cells}} + + {{end}} + + {{end}} +
+
{{.Title}}
+
{{.Value}}
+
+
+ + {{if .ThumbURL}} +
+ +
+ {{end}} +
+ + {{if .Footer}} + + + + + + + + {{end}} + +
+ + {{end}} + +{{end}} diff --git a/templates/partials/message_attachment_styles.mjml b/templates/partials/message_attachment_styles.mjml new file mode 100644 index 0000000000..0e0de4dd3a --- /dev/null +++ b/templates/partials/message_attachment_styles.mjml @@ -0,0 +1,5 @@ + + + + + diff --git a/templates/partials/message_avatar_col.mjml b/templates/partials/message_avatar_col.mjml new file mode 100644 index 0000000000..dc4686896d --- /dev/null +++ b/templates/partials/message_avatar_col.mjml @@ -0,0 +1,3 @@ + + + diff --git a/templates/partials/message_button.mjml b/templates/partials/message_button.mjml new file mode 100644 index 0000000000..997edac1bc --- /dev/null +++ b/templates/partials/message_button.mjml @@ -0,0 +1,5 @@ + + {{if .MessageURL}} + {{$.Props.MessageButton}} + {{end}} + diff --git a/templates/partials/sender_info_col.mjml b/templates/partials/sender_info_col.mjml new file mode 100644 index 0000000000..69b37d1853 --- /dev/null +++ b/templates/partials/sender_info_col.mjml @@ -0,0 +1,30 @@ + + + + +
+
{{.SenderName}}
+ {{if .Time}} +
{{.Time}}
+ {{end}} + {{if .ChannelName}} +
+ {{if .ShowChannelIcon}} + + {{end}} +
+ {{if .OtherChannelMembersCount}} + {{.OtherChannelMembersCount}} + {{end}} + {{.ChannelName}} +
+
+ {{end}} +
+ + +
+ + {{.Message}} + +