diff --git a/server/channels/app/notification.go b/server/channels/app/notification.go index 0927820acdf..7c12f8082f3 100644 --- a/server/channels/app/notification.go +++ b/server/channels/app/notification.go @@ -417,19 +417,21 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea if err != nil { c.Logger().Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(err)) } - if err := a.sendNotificationEmail(c, notification, profileMap[id], team, senderProfileImage); err != nil { - a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeEmail, model.NotificationReasonEmailSendError, model.NotificationNoPlatform) - a.NotificationsLog().Error("Error sending email notification", - mlog.String("type", model.NotificationTypeEmail), - mlog.String("post_id", post.Id), - mlog.String("status", model.NotificationStatusError), - mlog.String("reason", model.NotificationReasonEmailSendError), - mlog.String("sender_id", sender.Id), - mlog.String("receiver_id", id), - mlog.Err(err), - ) - c.Logger().Warn("Unable to send notification email.", mlog.Err(err)) - } + a.Srv().Go(func() { + if _, err := a.sendNotificationEmail(c, notification, profileMap[id], team, senderProfileImage); err != nil { + a.CountNotificationReason(model.NotificationStatusError, model.NotificationTypeEmail, model.NotificationReasonEmailSendError, model.NotificationNoPlatform) + a.NotificationsLog().Error("Error sending email notification", + mlog.String("type", model.NotificationTypeEmail), + mlog.String("post_id", post.Id), + mlog.String("status", model.NotificationStatusError), + mlog.String("reason", model.NotificationReasonEmailSendError), + mlog.String("sender_id", sender.Id), + mlog.String("receiver_id", id), + mlog.Err(err), + ) + c.Logger().Warn("Unable to send notification email.", mlog.Err(err)) + } + }) } else { a.NotificationsLog().Debug("Email disallowed by user", mlog.String("type", model.NotificationTypeEmail), diff --git a/server/channels/app/notification_email.go b/server/channels/app/notification_email.go index 59d251d7244..abf296b1f4b 100644 --- a/server/channels/app/notification_email.go +++ b/server/channels/app/notification_email.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/public/shared/request" @@ -20,14 +21,134 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/utils" ) -func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotification, user *model.User, team *model.Team, senderProfileImage []byte) error { +func (a *App) buildEmailNotification( + ctx request.CTX, + notification *PostNotification, + user *model.User, + team *model.Team, +) *model.EmailNotification { + channel := notification.Channel + post := notification.Post + sender := notification.Sender + + translateFunc := i18n.GetUserTranslations(user.Locale) + nameFormat := a.GetNotificationNameFormat(user) + + var useMilitaryTime bool + if data, err := a.Srv().Store().Preference().Get( + user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime, + ); err != nil { + ctx.Logger().Debug("Failed to retrieve user military time preference, defaulting to false", + mlog.String("user_id", user.Id), mlog.Err(err)) + useMilitaryTime = false + } else { + useMilitaryTime = data.Value == "true" + } + + channelName := notification.GetChannelName(nameFormat, "") + senderName := notification.GetSenderName(nameFormat, + *a.Config().ServiceSettings.EnablePostUsernameOverride) + + emailNotificationContentsType := model.EmailNotificationContentsFull + if license := a.Srv().License(); license != nil && *license.Features.EmailNotificationContents { + emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType + } + + var subject string + if channel.Type == model.ChannelTypeDirect { + subject = getDirectMessageNotificationEmailSubject( + user, post, translateFunc, *a.Config().TeamSettings.SiteName, senderName, useMilitaryTime) + } else if channel.Type == model.ChannelTypeGroup { + subject = getGroupMessageNotificationEmailSubject( + user, post, translateFunc, *a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime) + } else if *a.Config().EmailSettings.UseChannelInEmailNotifications { + subject = getNotificationEmailSubject( + user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime) + } else { + subject = getNotificationEmailSubject( + user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime) + } + + var title, subtitle string + if channel.Type == model.ChannelTypeDirect { + title = translateFunc("app.notification.body.dm.title", map[string]any{"SenderName": senderName}) + subtitle = translateFunc("app.notification.body.dm.subTitle", map[string]any{"SenderName": senderName}) + } else if channel.Type == model.ChannelTypeGroup { + title = translateFunc("app.notification.body.group.title", map[string]any{"SenderName": senderName}) + subtitle = translateFunc("app.notification.body.group.subTitle", map[string]any{"SenderName": senderName}) + } else { + title = translateFunc("app.notification.body.mention.title", map[string]any{"SenderName": senderName}) + subtitle = translateFunc("app.notification.body.mention.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName}) + } + + if a.IsCRTEnabledForUser(ctx, user.Id) && post.RootId != "" { + title = translateFunc("app.notification.body.thread.title", map[string]any{"SenderName": senderName}) + if channel.Type == model.ChannelTypeDirect { + subtitle = translateFunc("app.notification.body.thread_dm.subTitle", map[string]any{"SenderName": senderName}) + } else if channel.Type == model.ChannelTypeGroup { + subtitle = translateFunc("app.notification.body.thread_gm.subTitle", map[string]any{"SenderName": senderName}) + } else if emailNotificationContentsType == model.EmailNotificationContentsFull { + subtitle = translateFunc("app.notification.body.thread_channel_full.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName}) + } else { + subtitle = translateFunc("app.notification.body.thread_channel.subTitle", map[string]any{"SenderName": senderName}) + } + } + + var messageHTML, messageText string + if emailNotificationContentsType == model.EmailNotificationContentsFull { + messageHTML = a.GetMessageForNotification(post, team.Name, a.GetSiteURL(), translateFunc) + messageText = post.Message + } + + landingURL := a.GetSiteURL() + "/landing#/" + team.Name + buttonURL := landingURL + if team.Name != "select_team" { + buttonURL = landingURL + "/pl/" + post.Id + } + + return &model.EmailNotification{ + // Core identifiers (immutable) + PostId: post.Id, + ChannelId: channel.Id, + TeamId: team.Id, + SenderId: sender.Id, + SenderDisplayName: senderName, + RecipientId: user.Id, + RootId: post.RootId, + + // Context for plugin decision-making (immutable) + ChannelType: string(channel.Type), + ChannelName: channelName, + TeamName: team.DisplayName, + SenderUsername: sender.Username, + IsDirectMessage: channel.Type == model.ChannelTypeDirect, + IsGroupMessage: channel.Type == model.ChannelTypeGroup, + IsThreadReply: post.RootId != "", + IsCRTEnabled: a.IsCRTEnabledForUser(ctx, user.Id), + UseMilitaryTime: useMilitaryTime, + + // Customizable content fields + EmailNotificationContent: model.EmailNotificationContent{ + Subject: subject, + Title: title, + SubTitle: subtitle, + MessageHTML: messageHTML, + MessageText: messageText, + ButtonText: translateFunc("api.templates.post_body.button"), + ButtonURL: buttonURL, + FooterText: translateFunc("app.notification.footer.title"), + }, + } +} + +func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotification, user *model.User, team *model.Team, senderProfileImage []byte) (*model.EmailNotification, error) { channel := notification.Channel post := notification.Post if channel.IsGroupOrDirect() { teams, err := a.Srv().Store().Team().GetTeamsByUserId(user.Id) if err != nil { - return errors.Wrap(err, "unable to get user teams") + return nil, errors.Wrap(err, "unable to get user teams") } // if the recipient isn't in the current user's team, just pick one @@ -48,6 +169,41 @@ func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotificatio } } + // Create EmailNotification object for plugin customization + emailNotification := a.buildEmailNotification(c, notification, user, team) + + // Call plugin hook to allow customization of emailNotification + rejectionReason := "" + a.ch.RunMultiHook(func(hooks plugin.Hooks, manifest *model.Manifest) bool { + var replacementContent *model.EmailNotificationContent + replacementContent, rejectionReason = hooks.EmailNotificationWillBeSent(emailNotification) + if rejectionReason != "" { + c.Logger().Info("Email notification cancelled by plugin.", + mlog.String("rejection_reason", rejectionReason), + mlog.String("plugin_id", manifest.Id), + mlog.String("plugin_name", manifest.Name)) + return false + } + if replacementContent != nil { + emailNotification.EmailNotificationContent = *replacementContent + } + return true + }, plugin.EmailNotificationWillBeSentID) + + if rejectionReason != "" { + // Email notification rejected by plugin + a.CountNotificationReason(model.NotificationStatusNotSent, model.NotificationTypeEmail, model.NotificationReasonRejectedByPlugin, model.NotificationNoPlatform) + a.NotificationsLog().Debug("Email notification rejected by plugin", + mlog.String("type", model.NotificationTypeEmail), + mlog.String("status", model.NotificationStatusNotSent), + mlog.String("reason", model.NotificationReasonRejectedByPlugin), + mlog.String("rejection_reason", rejectionReason), + mlog.String("user_id", user.Id), + mlog.String("post_id", post.Id), + ) + return nil, nil + } + if *a.Config().EmailSettings.EnableEmailBatching { var sendBatched bool if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval); err != nil { @@ -60,57 +216,27 @@ func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotificatio if sendBatched { if err := a.Srv().EmailService.AddNotificationEmailToBatch(user, post, team); err == nil { - return nil + return emailNotification, nil } } // fall back to sending a single email if we can't batch it for some reason } - translateFunc := i18n.GetUserTranslations(user.Locale) - - var useMilitaryTime bool - if data, err := a.Srv().Store().Preference().Get(user.Id, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime); err != nil { - useMilitaryTime = false - } else { - useMilitaryTime = data.Value == "true" - } - - nameFormat := a.GetNotificationNameFormat(user) - - channelName := notification.GetChannelName(nameFormat, "") - senderName := notification.GetSenderName(nameFormat, *a.Config().ServiceSettings.EnablePostUsernameOverride) - - emailNotificationContentsType := model.EmailNotificationContentsFull - if license := a.Srv().License(); license != nil && *license.Features.EmailNotificationContents { - emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType - } - - var subjectText string - if channel.Type == model.ChannelTypeDirect { - subjectText = getDirectMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, senderName, useMilitaryTime) - } else if channel.Type == model.ChannelTypeGroup { - subjectText = getGroupMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime) - } else if *a.Config().EmailSettings.UseChannelInEmailNotifications { - subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime) - } else { - subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime) - } - + // Handle sender photo senderPhoto := "" embeddedFiles := make(map[string]io.Reader) - if emailNotificationContentsType == model.EmailNotificationContentsFull && senderProfileImage != nil { + if emailNotification.MessageHTML != "" && senderProfileImage != nil { senderPhoto = "user-avatar.png" embeddedFiles = map[string]io.Reader{ senderPhoto: bytes.NewReader(senderProfileImage), } } - landingURL := a.GetSiteURL() + "/landing#/" + team.Name - - var bodyText, err = a.getNotificationEmailBody(c, user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc, senderPhoto) + // Build email body using EmailNotification data + var bodyText, err = a.getNotificationEmailBodyFromEmailNotification(c, user, emailNotification, post, senderPhoto) if err != nil { - return errors.Wrap(err, "unable to render the email notification template") + return nil, errors.Wrap(err, "unable to render the email notification template") } templateString := "<%s@" + utils.GetHostnameFromSiteURL(a.GetSiteURL()) + ">" @@ -118,18 +244,18 @@ func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotificatio inReplyTo := "" references := "" - if post.Id != "" { - messageID = fmt.Sprintf(templateString, post.Id) + if emailNotification.PostId != "" { + messageID = fmt.Sprintf(templateString, emailNotification.PostId) } - if post.RootId != "" { - referencesVal := fmt.Sprintf(templateString, post.RootId) + if emailNotification.RootId != "" { + referencesVal := fmt.Sprintf(templateString, emailNotification.RootId) inReplyTo = referencesVal references = referencesVal } a.Srv().Go(func() { - if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles, messageID, inReplyTo, references, "Notification"); nErr != nil { + if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(emailNotification.Subject), bodyText, embeddedFiles, messageID, inReplyTo, references, "Notification"); nErr != nil { c.Logger().Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr)) } }) @@ -138,7 +264,7 @@ func (a *App) sendNotificationEmail(c request.CTX, notification *PostNotificatio a.Metrics().IncrementPostSentEmail() } - return nil + return emailNotification, nil } /** @@ -214,80 +340,53 @@ type postData struct { MessageAttachments []*email.EmailMessageAttachment } -/** - * Computes the email body for notification messages - */ -func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc, senderPhoto string) (string, error) { +func (a *App) GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string { + return a.Srv().EmailService.GetMessageForNotification(post, teamName, siteUrl, translateFunc) +} + +func (a *App) getNotificationEmailBodyFromEmailNotification(c request.CTX, recipient *model.User, emailNotification *model.EmailNotification, post *model.Post, senderPhoto string) (string, error) { + translateFunc := i18n.GetUserTranslations(recipient.Locale) + pData := postData{ - SenderName: truncateUserNames(senderName, 22), + SenderName: truncateUserNames(emailNotification.SenderDisplayName, 22), SenderPhoto: senderPhoto, } - t := utils.GetFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) - messageTime := map[string]any{ - "Hour": t.Hour, - "Minute": t.Minute, - "TimeZone": t.TimeZone, - } + if emailNotification.MessageHTML != "" { + pData.Message = template.HTML(emailNotification.MessageHTML) - if emailNotificationContentsType == model.EmailNotificationContentsFull { - postMessage := a.GetMessageForNotification(post, teamName, a.GetSiteURL(), translateFunc) - pData.Message = template.HTML(postMessage) + // Get formatted time for message using the UseMilitaryTime field + t := utils.GetFormattedPostTime(recipient, post, emailNotification.UseMilitaryTime, translateFunc) + messageTime := map[string]any{ + "Hour": t.Hour, + "Minute": t.Minute, + "TimeZone": t.TimeZone, + } pData.Time = translateFunc("app.notification.body.dm.time", messageTime) + + // Process message attachments pData.MessageAttachments = email.ProcessMessageAttachments(post, a.GetSiteURL()) } data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale) data.Props["SiteURL"] = a.GetSiteURL() - if teamName != "select_team" { - data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id - } else { - data.Props["ButtonURL"] = landingURL - } - - data.Props["SenderName"] = senderName - data.Props["Button"] = translateFunc("api.templates.post_body.button") - data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title") + data.Props["ButtonURL"] = emailNotification.ButtonURL + data.Props["SenderName"] = emailNotification.SenderDisplayName + data.Props["Button"] = emailNotification.ButtonText + data.Props["NotificationFooterTitle"] = emailNotification.FooterText data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin") data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info") + data.Props["Title"] = emailNotification.Title + data.Props["SubTitle"] = emailNotification.SubTitle - if channel.Type == model.ChannelTypeDirect { - // Direct Messages - data.Props["Title"] = translateFunc("app.notification.body.dm.title", map[string]any{"SenderName": senderName}) - data.Props["SubTitle"] = translateFunc("app.notification.body.dm.subTitle", map[string]any{"SenderName": senderName}) - } else if channel.Type == model.ChannelTypeGroup { - // Group Messages - data.Props["Title"] = translateFunc("app.notification.body.group.title", map[string]any{"SenderName": senderName}) - data.Props["SubTitle"] = translateFunc("app.notification.body.group.subTitle", map[string]any{"SenderName": senderName}) + if emailNotification.IsDirectMessage || emailNotification.IsGroupMessage { + // No channel name for DM/GM } else { - // mentions - data.Props["Title"] = translateFunc("app.notification.body.mention.title", map[string]any{"SenderName": senderName}) - data.Props["SubTitle"] = translateFunc("app.notification.body.mention.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName}) - pData.ChannelName = channelName + pData.ChannelName = emailNotification.ChannelName } - // Override title and subtile for replies with CRT enabled - if a.IsCRTEnabledForUser(c, recipient.Id) && post.RootId != "" { - // Title is the same in all cases - data.Props["Title"] = translateFunc("app.notification.body.thread.title", map[string]any{"SenderName": senderName}) - - if channel.Type == model.ChannelTypeDirect { - // Direct Reply - data.Props["SubTitle"] = translateFunc("app.notification.body.thread_dm.subTitle", map[string]any{"SenderName": senderName}) - } else if channel.Type == model.ChannelTypeGroup { - // Group Reply - data.Props["SubTitle"] = translateFunc("app.notification.body.thread_gm.subTitle", map[string]any{"SenderName": senderName}) - } else if emailNotificationContentsType == model.EmailNotificationContentsFull { - // Channel Reply with full content - data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel_full.subTitle", map[string]any{"SenderName": senderName, "ChannelName": channelName}) - } else { - // Channel Reply with generic content - data.Props["SubTitle"] = translateFunc("app.notification.body.thread_channel.subTitle", map[string]any{"SenderName": senderName}) - } - } - - // only include posts in notification email if email notification contents type is set to full - if emailNotificationContentsType == model.EmailNotificationContentsFull { + // Only include posts in notification email if message content is available + if emailNotification.MessageHTML != "" { data.Props["Posts"] = []postData{pData} } else { data.Props["Posts"] = []postData{} @@ -295,7 +394,3 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos return a.Srv().TemplatesContainer().RenderToString("messages_notification", data) } - -func (a *App) GetMessageForNotification(post *model.Post, teamName, siteUrl string, translateFunc i18n.TranslateFunc) string { - return a.Srv().EmailService.GetMessageForNotification(post, teamName, siteUrl, translateFunc) -} diff --git a/server/channels/app/notification_email_test.go b/server/channels/app/notification_email_test.go index 0c0f0bf37d4..09d0f7ed649 100644 --- a/server/channels/app/notification_email_test.go +++ b/server/channels/app/notification_email_test.go @@ -18,9 +18,52 @@ import ( "github.com/mattermost/mattermost/server/public/shared/i18n" "github.com/mattermost/mattermost/server/public/shared/timezones" "github.com/mattermost/mattermost/server/v8/channels/store/storetest/mocks" + "github.com/mattermost/mattermost/server/v8/channels/testlib" "github.com/mattermost/mattermost/server/v8/channels/utils" ) +// Helper function to create PostNotification for testing +func buildTestPostNotification(post *model.Post, channel *model.Channel, sender *model.User) *PostNotification { + return &PostNotification{ + Channel: channel, + Post: post, + Sender: sender, + ProfileMap: make(map[string]*model.User), + } +} + +// Helper function to create test user +func buildTestUser(id, username, displayName string, useMilitaryTime bool) *model.User { + return &model.User{ + Id: id, + Username: username, + Nickname: displayName, + Locale: "en", + } +} + +// Helper function to create test team +func buildTestTeam(id, name, displayName string) *model.Team { + return &model.Team{ + Id: id, + Name: name, + DisplayName: displayName, + } +} + +// Helper function to set up preference mocks +func setupPreferenceMocks(th *TestHelper, userId string, useMilitaryTime bool) { + preferenceStoreMock := mocks.PreferenceStore{} + if useMilitaryTime { + preferenceStoreMock.On("Get", userId, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime).Return(&model.Preference{Value: "true"}, nil) + } else { + preferenceStoreMock.On("Get", userId, model.PreferenceCategoryDisplaySettings, model.PreferenceNameUseMilitaryTime).Return(&model.Preference{Value: "false"}, nil) + } + // Mock the name format preference as well + preferenceStoreMock.On("Get", userId, model.PreferenceCategoryDisplaySettings, model.PreferenceNameNameFormat).Return(&model.Preference{Value: model.ShowUsername}, nil) + th.App.Srv().Store().(*mocks.Store).On("Preference").Return(&preferenceStoreMock) +} + func TestGetDirectMessageNotificationEmailSubject(t *testing.T) { mainHelper.Parallel(t) expectedPrefix := "[http://localhost:8065] New Direct Message from @sender on" @@ -76,31 +119,34 @@ func TestGetNotificationEmailBodyFullNotificationPublicChannel(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyFullNotificationGroupChannel(t *testing.T) { @@ -108,31 +154,34 @@ func TestGetNotificationEmailBodyFullNotificationGroupChannel(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeGroup, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got %s", body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyFullNotificationPrivateChannel(t *testing.T) { @@ -140,31 +189,34 @@ func TestGetNotificationEmailBodyFullNotificationPrivateChannel(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypePrivate, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyFullNotificationDirectChannel(t *testing.T) { @@ -172,31 +224,34 @@ func TestGetNotificationEmailBodyFullNotificationDirectChannel(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got %s", body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyFullNotificationLocaleTimeWithTimezone(t *testing.T) { @@ -205,30 +260,37 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeWithTimezone(t *testi defer th.TearDown() recipient := &model.User{ + Id: "test-recipient-id", + Username: "recipient", + Nickname: "Recipient User", + Locale: "en", Timezone: timezones.DefaultUserTimezone(), } recipient.Timezone["automaticTimezone"] = "America/New_York" post := &model.Post{ + Id: "test-post-id", CreateAt: 1524663790000, Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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, false, translateFunc, "user-avatar.png") + setupPreferenceMocks(th, recipient.Id, false) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) r, _ := regexp.Compile("E([S|D]+)T") zone := r.FindString(body) @@ -241,28 +303,33 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing defer th.TearDown() recipient := &model.User{ + Id: "test-recipient-id", + Username: "recipient", + Nickname: "Recipient User", + Locale: "en", Timezone: timezones.DefaultUserTimezone(), } post := &model.Post{ + Id: "test-post-id", CreateAt: 1524681000000, Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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) + setupPreferenceMocks(th, recipient.Id, true) + tm := time.Unix(post.CreateAt/1000, 0) zone, _ := tm.Zone() @@ -278,7 +345,9 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing err = tmp.Execute(&text, fmt.Sprintf("%s:%s %s", formattedTime.Hour, formattedTime.Minute, formattedTime.TimeZone)) require.NoError(t, err) - body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) postTimeLine := text.String() require.Contains(t, body, postTimeLine, fmt.Sprintf("Expected email text '%s'. Got %s", postTimeLine, body)) @@ -290,30 +359,37 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime12Hour(t *testing.T) defer th.TearDown() recipient := &model.User{ + Id: "test-recipient-id", + Username: "recipient", + Nickname: "Recipient User", + Locale: "en", Timezone: timezones.DefaultUserTimezone(), } recipient.Timezone["automaticTimezone"] = "America/New_York" post := &model.Post{ + Id: "test-post-id", CreateAt: 1524681000000, // 1524681000 // 1524681000000 Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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, false, translateFunc, "user-avatar.png") + setupPreferenceMocks(th, recipient.Id, false) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "2:30 PM", fmt.Sprintf("Expected email text '2:30 PM'. Got %s", body)) } @@ -324,30 +400,37 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) defer th.TearDown() recipient := &model.User{ + Id: "test-recipient-id", + Username: "recipient", + Nickname: "Recipient User", + Locale: "en", Timezone: timezones.DefaultUserTimezone(), } recipient.Timezone["automaticTimezone"] = "America/New_York" post := &model.Post{ + Id: "test-post-id", CreateAt: 1524681000000, Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body)) } @@ -358,26 +441,29 @@ func TestGetNotificationEmailBodyWithUserPreference(t *testing.T) { defer th.TearDown() recipient := &model.User{ + Id: "test-recipient-id", + Username: "recipient", + Nickname: "Recipient User", + Locale: "en", Timezone: timezones.DefaultUserTimezone(), } recipient.Timezone["automaticTimezone"] = "America/New_York" post := &model.Post{ + Id: "test-post-id", CreateAt: 1524681000000, Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") storeMock := th.App.Srv().Store().(*mocks.Store) teamStoreMock := mocks.TeamStore{} @@ -392,7 +478,11 @@ func TestGetNotificationEmailBodyWithUserPreference(t *testing.T) { expectedTimeFormat = "14:30" } - body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, is24HourFormat, translateFunc, "user-avatar.png") + setupPreferenceMocks(th, recipient.Id, is24HourFormat) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, expectedTimeFormat, fmt.Sprintf("Expected email text '%s'. Got %s", expectedTimeFormat, body)) } @@ -402,8 +492,9 @@ func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } @@ -453,23 +544,25 @@ func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing model.ParseSlackAttachment(post, messageAttachments) channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "#FF0000") require.Contains(t, body, "message attachment 1 pretext") @@ -501,30 +594,33 @@ func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsGeneric - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyGenericNotificationGroupChannel(t *testing.T) { @@ -532,30 +628,33 @@ func TestGetNotificationEmailBodyGenericNotificationGroupChannel(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeGroup, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsGeneric - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got %s", body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyGenericNotificationPrivateChannel(t *testing.T) { @@ -563,30 +662,33 @@ func TestGetNotificationEmailBodyGenericNotificationPrivateChannel(t *testing.T) th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypePrivate, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsGeneric - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T) { @@ -594,30 +696,33 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T) th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeDirect, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsGeneric - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got %s", body)) - require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, team.Name, fmt.Sprintf("Expected email text '%s'. Got %s", team.Name, body)) } func TestGetNotificationEmailEscapingChars(t *testing.T) { @@ -625,31 +730,32 @@ func TestGetNotificationEmailEscapingChars(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - ch := &model.Channel{ - DisplayName: "ChannelName", - Type: model.ChannelTypeOpen, - } - channelName := "ChannelName" - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) message := "Bold Test" post := &model.Post{ + Id: "test-post-id", Message: message, } - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + ch := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", + DisplayName: "ChannelName", + Type: model.ChannelTypeOpen, + } + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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, ch, - channelName, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc, "user-avatar.png") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, ch, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) assert.NotContains(t, body, message) @@ -661,27 +767,29 @@ func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { defer th.TearDown() ch := &model.Channel{ + Id: "test-channel-id", Name: "channelname", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } id := model.NewId() recipient := &model.User{ + Id: "test-recipient-id", Email: "success+" + id + "@simulator.amazonses.com", Username: "un_" + id, Nickname: "nn_" + id, Password: "Password1", EmailVerified: true, + Locale: "en", } post := &model.Post{ + Id: "test-post-id", Message: "This is the message ~" + ch.Name, } - senderName := "user1" - teamName := "testteam" + sender := buildTestUser("test-sender-id", "user1", "user1", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") teamURL := th.App.GetSiteURL() + "/landing#" + "/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") storeMock := th.App.Srv().Store().(*mocks.Store) teamStoreMock := mocks.TeamStore{} @@ -692,11 +800,13 @@ func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { channelStoreMock.On("GetByNames", "test", []string{ch.Name}, true).Return([]*model.Channel{ch}, nil) storeMock.On("Channel").Return(&channelStoreMock) + setupPreferenceMocks(th, recipient.Id, true) + th.App.Srv().EmailService.SetStore(storeMock) - body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, ch, - ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc, "user-avatar.png") + notification := buildTestPostNotification(post, ch, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name @@ -734,23 +844,11 @@ func TestGetNotificationEmailBodyMultiPublicChannelMention(t *testing.T) { message := fmt.Sprintf("This is the message Channel1: %s; Channel2: %s;"+ " Channel3: %s", mention, mention2, mention3) - id := model.NewId() - recipient := &model.User{ - Email: "success+" + id + "@simulator.amazonses.com", - Username: "un_" + id, - Nickname: "nn_" + id, - Password: "Password1", - EmailVerified: true, - } post := &model.Post{ Message: message, } - senderName := "user1" - teamName := "testteam" teamURL := th.App.GetSiteURL() + "/landing#" + "/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") storeMock := th.App.Srv().Store().(*mocks.Store) teamStoreMock := mocks.TeamStore{} @@ -763,16 +861,22 @@ func TestGetNotificationEmailBodyMultiPublicChannelMention(t *testing.T) { th.App.Srv().EmailService.SetStore(storeMock) - body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, ch, - ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc, "user-avatar.png") - require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name channelURL2 := teamURL + "/channels/" + ch2.Name channelURL3 := teamURL + "/channels/" + ch3.Name expMessage := fmt.Sprintf("This is the message Channel1: %s;"+ " Channel2: %s; Channel3: %s", channelURL, mention, channelURL2, mention2, channelURL3, mention3) + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) + sender := buildTestUser("test-sender-id", "user1", "user1", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") + + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, ch, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") + require.NoError(t, err) assert.Contains(t, body, expMessage) } @@ -782,27 +886,29 @@ func TestGetNotificationEmailBodyPrivateChannelMention(t *testing.T) { defer th.TearDown() ch := &model.Channel{ + Id: "test-channel-id", Name: "channelname", DisplayName: "ChannelName", Type: model.ChannelTypePrivate, } id := model.NewId() recipient := &model.User{ + Id: "test-recipient-id", Email: "success+" + id + "@simulator.amazonses.com", Username: "un_" + id, Nickname: "nn_" + id, Password: "Password1", EmailVerified: true, + Locale: "en", } post := &model.Post{ + Id: "test-post-id", Message: "This is the message ~" + ch.Name, } - senderName := "user1" - teamName := "testteam" + sender := buildTestUser("test-sender-id", "user1", "user1", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") teamURL := "http://localhost:8065/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") storeMock := th.App.Srv().Store().(*mocks.Store) teamStoreMock := mocks.TeamStore{} @@ -813,11 +919,13 @@ func TestGetNotificationEmailBodyPrivateChannelMention(t *testing.T) { channelStoreMock.On("GetByNames", "test", []string{ch.Name}, true).Return([]*model.Channel{ch}, nil) storeMock.On("Channel").Return(&channelStoreMock) + setupPreferenceMocks(th, recipient.Id, true) + th.App.Srv().EmailService.SetStore(storeMock) - body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, ch, - ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc, "user-avatar.png") + notification := buildTestPostNotification(post, ch, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name @@ -945,63 +1053,81 @@ func TestGenerateHyperlinkForChannelsPrivate(t *testing.T) { func TestLandingLink(t *testing.T) { mainHelper.Parallel(t) - th := SetupWithStoreMock(t) + + // Create a minimal helper that sets the site URL + mockStore := testlib.GetMockStoreForSetupFunctions() + th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), false, false, + func(cfg *model.Config) { + cfg.ServiceSettings.SiteURL = model.NewPointer("http://localhost:8065") + }, nil, t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ + Id: "test-post-id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") teamURL := "http://localhost:8065/landing#/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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } func TestLandingLinkPermalink(t *testing.T) { mainHelper.Parallel(t) - th := SetupWithStoreMock(t) + + // Create a minimal helper that sets the site URL + mockStore := testlib.GetMockStoreForSetupFunctions() + th := setupTestHelper(mockStore, mainHelper.GetSQLStore(), mainHelper.GetSQLSettings(), mainHelper.GetSearchEngine(), false, false, + func(cfg *model.Config) { + cfg.ServiceSettings.SiteURL = model.NewPointer("http://localhost:8065") + }, nil, t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) post := &model.Post{ Id: "Test_id", Message: "This is the message", } channel := &model.Channel{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } - channelName := "ChannelName" - senderName := "sender" - teamName := "testteam" - teamURL := "http://localhost:8065/landing#/testteam" - emailNotificationContentsType := model.EmailNotificationContentsFull - translateFunc := i18n.GetUserTranslations("en") + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") 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") + setupPreferenceMocks(th, recipient.Id, true) + + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + body, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) + require.Contains(t, body, "/pl/"+post.Id, fmt.Sprintf("Expected email text to contain permalink '/pl/%s'. Got %s", post.Id, body)) } func TestMarkdownConversion(t *testing.T) { @@ -1091,15 +1217,21 @@ func TestMarkdownConversion(t *testing.T) { th := SetupWithStoreMock(t) defer th.TearDown() - recipient := &model.User{} + recipient := buildTestUser("test-recipient-id", "recipient", "Recipient User", true) 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{ + Id: "test-channel-id", + Name: "testchannel", DisplayName: "ChannelName", Type: model.ChannelTypeOpen, } + sender := buildTestUser("test-sender-id", "sender", "sender", true) + team := buildTestTeam("test-team-id", "testteam", "testteam") + + setupPreferenceMocks(th, recipient.Id, true) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -1107,7 +1239,9 @@ func TestMarkdownConversion(t *testing.T) { Id: "Test_id", Message: tt.args, } - got, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, "ChannelName", "sender", "testteam", "http://localhost:8065/landing#/testteam", model.EmailNotificationContentsFull, true, i18n.GetUserTranslations("en"), "user-avatar.png") + notification := buildTestPostNotification(post, channel, sender) + emailNotification := th.App.buildEmailNotification(th.Context, notification, recipient, team) + got, err := th.App.getNotificationEmailBodyFromEmailNotification(th.Context, recipient, emailNotification, post, "user-avatar.png") require.NoError(t, err) require.Contains(t, got, tt.want) }) diff --git a/server/channels/app/plugin_hooks_test.go b/server/channels/app/plugin_hooks_test.go index 6484018be2d..cb55c64a38e 100644 --- a/server/channels/app/plugin_hooks_test.go +++ b/server/channels/app/plugin_hooks_test.go @@ -1617,6 +1617,131 @@ func TestHookNotificationWillBePushed(t *testing.T) { } } +//go:embed test_templates/hook_email_notification_will_be_sent.tmpl +var hookEmailNotificationWillBeSentTmpl string + +func TestHookEmailNotificationWillBeSent(t *testing.T) { + mainHelper.Parallel(t) + + tests := []struct { + name string + testCode string + expectedNotificationSubject string + expectedNotificationTitle string + expectedButtonText string + expectedFooterText string + }{ + { + name: "successfully sent", + testCode: `return nil, ""`, + }, + { + name: "email notification rejected", + testCode: `return nil, "rejected"`, + }, + { + name: "email notification modified", + testCode: `content := &model.EmailNotificationContent{ + Subject: "Modified Subject by Plugin", + Title: "Modified Title by Plugin", + ButtonText: "Modified Button by Plugin", + FooterText: "Modified Footer by Plugin", + } + return content, ""`, + expectedNotificationSubject: "Modified Subject by Plugin", + expectedNotificationTitle: "Modified Title by Plugin", + expectedButtonText: "Modified Button by Plugin", + expectedFooterText: "Modified Footer by Plugin", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mainHelper.Parallel(t) + + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create a test user for email notifications + user := th.CreateUser() + th.LinkUserToTeam(user, th.BasicTeam) + th.AddUserToChannel(user, th.BasicChannel) + + // Set up email notification preferences to disable batching + appErr := th.App.UpdatePreferences(th.Context, user.Id, model.Preferences{ + { + UserId: user.Id, + Category: model.PreferenceCategoryNotifications, + Name: model.PreferenceNameEmailInterval, + Value: model.PreferenceEmailIntervalNoBatchingSeconds, + }, + }) + require.Nil(t, appErr) + + // Disable email batching in config + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.EmailSettings.EnableEmailBatching = false + }) + + // Create and set up plugin + templatedPlugin := fmt.Sprintf(hookEmailNotificationWillBeSentTmpl, tt.testCode) + tearDown, _, _ := SetAppEnvironmentWithPlugins(t, []string{templatedPlugin}, th.App, th.NewPluginAPI) + defer tearDown() + + // For the modification test, create a simple test that verifies the hook is called + // The detailed verification would require more complex mocking which is beyond this test's scope + + // Create a post that will trigger email notification + post := &model.Post{ + UserId: th.BasicUser.Id, + ChannelId: th.BasicChannel.Id, + Message: "@" + user.Username + " test message", + CreateAt: model.GetMillis(), + } + + // Create notification + notification := &PostNotification{ + Post: post, + Channel: th.BasicChannel, + ProfileMap: map[string]*model.User{ + user.Id: user, + }, + Sender: th.BasicUser, + } + + // Send email notification (this will trigger the hook) + // Use assert.Eventually to handle any potential race conditions with plugin activation/deactivation + assert.Eventually(t, func() bool { + modifiedNotification, err := th.App.sendNotificationEmail(th.Context, notification, user, th.BasicTeam, nil) + + // For the rejected test case, we expect the notification to be rejected + if tt.name == "email notification rejected" { + // When rejected, sendNotificationEmail returns nil for the notification + return modifiedNotification == nil && err == nil + } + if err != nil || modifiedNotification == nil { + return false + } + + // Verify the modified notification fields + if tt.expectedNotificationSubject != "" && modifiedNotification.Subject != tt.expectedNotificationSubject { + return false + } + if tt.expectedNotificationTitle != "" && modifiedNotification.Title != tt.expectedNotificationTitle { + return false + } + if tt.expectedButtonText != "" && modifiedNotification.ButtonText != tt.expectedButtonText { + return false + } + if tt.expectedFooterText != "" && modifiedNotification.FooterText != tt.expectedFooterText { + return false + } + return true + }, 2*time.Second, 100*time.Millisecond) + }) + } +} + func TestHookMessagesWillBeConsumed(t *testing.T) { mainHelper.Parallel(t) diff --git a/server/channels/app/test_templates/hook_email_notification_will_be_sent.tmpl b/server/channels/app/test_templates/hook_email_notification_will_be_sent.tmpl new file mode 100644 index 00000000000..e0129f5961e --- /dev/null +++ b/server/channels/app/test_templates/hook_email_notification_will_be_sent.tmpl @@ -0,0 +1,18 @@ +package main + +import ( + "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/plugin" +) + +type MyPlugin struct { + plugin.MattermostPlugin +} + +func (p *MyPlugin) EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) { + %s +} + +func main() { + plugin.ClientMain(&MyPlugin{}) +} \ No newline at end of file diff --git a/server/public/model/email_notification.go b/server/public/model/email_notification.go new file mode 100644 index 00000000000..e9fc2cb6ca1 --- /dev/null +++ b/server/public/model/email_notification.go @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package model + +type EmailNotificationContent struct { + Subject string `json:"subject,omitempty"` + Title string `json:"title,omitempty"` + SubTitle string `json:"subtitle,omitempty"` + MessageHTML string `json:"message_html,omitempty"` + MessageText string `json:"message_text,omitempty"` + ButtonText string `json:"button_text,omitempty"` + ButtonURL string `json:"button_url,omitempty"` + FooterText string `json:"footer_text,omitempty"` +} + +type EmailNotification struct { + PostId string `json:"post_id"` + ChannelId string `json:"channel_id"` + TeamId string `json:"team_id"` + SenderId string `json:"sender_id"` + SenderDisplayName string `json:"sender_display_name,omitempty"` + RecipientId string `json:"recipient_id"` + RootId string `json:"root_id,omitempty"` + + ChannelType string `json:"channel_type"` + ChannelName string `json:"channel_name"` + TeamName string `json:"team_name"` + SenderUsername string `json:"sender_username"` + IsDirectMessage bool `json:"is_direct_message"` + IsGroupMessage bool `json:"is_group_message"` + IsThreadReply bool `json:"is_thread_reply"` + IsCRTEnabled bool `json:"is_crt_enabled"` + UseMilitaryTime bool `json:"use_military_time"` + + EmailNotificationContent +} diff --git a/server/public/plugin/client_rpc_generated.go b/server/public/plugin/client_rpc_generated.go index 9d4c6ecc04f..0de27b60cda 100644 --- a/server/public/plugin/client_rpc_generated.go +++ b/server/public/plugin/client_rpc_generated.go @@ -878,6 +878,41 @@ func (s *hooksRPCServer) ConfigurationWillBeSaved(args *Z_ConfigurationWillBeSav return nil } +func init() { + hookNameToId["EmailNotificationWillBeSent"] = EmailNotificationWillBeSentID +} + +type Z_EmailNotificationWillBeSentArgs struct { + A *model.EmailNotification +} + +type Z_EmailNotificationWillBeSentReturns struct { + A *model.EmailNotificationContent + B string +} + +func (g *hooksRPCClient) EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) { + _args := &Z_EmailNotificationWillBeSentArgs{emailNotification} + _returns := &Z_EmailNotificationWillBeSentReturns{} + if g.implemented[EmailNotificationWillBeSentID] { + if err := g.client.Call("Plugin.EmailNotificationWillBeSent", _args, _returns); err != nil { + g.log.Error("RPC call EmailNotificationWillBeSent to plugin failed.", mlog.Err(err)) + } + } + return _returns.A, _returns.B +} + +func (s *hooksRPCServer) EmailNotificationWillBeSent(args *Z_EmailNotificationWillBeSentArgs, returns *Z_EmailNotificationWillBeSentReturns) error { + if hook, ok := s.impl.(interface { + EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) + }); ok { + returns.A, returns.B = hook.EmailNotificationWillBeSent(args.A) + } else { + return encodableError(fmt.Errorf("Hook EmailNotificationWillBeSent called but not implemented.")) + } + return nil +} + func init() { hookNameToId["NotificationWillBePushed"] = NotificationWillBePushedID } diff --git a/server/public/plugin/hooks.go b/server/public/plugin/hooks.go index 702cce0f27d..53e44dc4019 100644 --- a/server/public/plugin/hooks.go +++ b/server/public/plugin/hooks.go @@ -63,6 +63,7 @@ const ( OnSharedChannelsProfileImageSyncMsgID = 44 GenerateSupportDataID = 45 OnSAMLLoginID = 46 + EmailNotificationWillBeSentID = 47 TotalHooksID = iota ) @@ -314,6 +315,21 @@ type Hooks interface { // Minimum server version: 8.0 ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, error) + // EmailNotificationWillBeSent is invoked before an email notification is sent to a user. + // This allows plugins to customize the email notification content including subject, + // title, subtitle, message content, buttons, and other email properties. + // + // To reject an email notification, return an non-empty string describing why the notification was rejected. + // To modify the notification, return the replacement, non-nil *model.EmailNotificationContent and an empty string. + // To allow the notification without modification, return a nil *model.EmailNotificationContent and an empty string. + // + // Note that core identifiers (PostId, ChannelId, TeamId, SenderId, RecipientId, RootId) and + // context fields (ChannelType, IsDirectMessage, etc.) are immutable and changes to them will be ignored. + // Only customizable content fields can be modified. + // + // Minimum server version: 11.00 + EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) + // NotificationWillBePushed is invoked before a push notification is sent to the push // notification server. // diff --git a/server/public/plugin/hooks_timer_layer_generated.go b/server/public/plugin/hooks_timer_layer_generated.go index cdf3e62c980..ded4d9e30f2 100644 --- a/server/public/plugin/hooks_timer_layer_generated.go +++ b/server/public/plugin/hooks_timer_layer_generated.go @@ -233,6 +233,13 @@ func (hooks *hooksTimerLayer) ConfigurationWillBeSaved(newCfg *model.Config) (*m return _returnsA, _returnsB } +func (hooks *hooksTimerLayer) EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) { + startTime := timePkg.Now() + _returnsA, _returnsB := hooks.hooksImpl.EmailNotificationWillBeSent(emailNotification) + hooks.recordTime(startTime, "EmailNotificationWillBeSent", true) + return _returnsA, _returnsB +} + func (hooks *hooksTimerLayer) NotificationWillBePushed(pushNotification *model.PushNotification, userID string) (*model.PushNotification, string) { startTime := timePkg.Now() _returnsA, _returnsB := hooks.hooksImpl.NotificationWillBePushed(pushNotification, userID) diff --git a/server/public/plugin/plugintest/hooks.go b/server/public/plugin/plugintest/hooks.go index 0789e5b9ed3..97b42da8890 100644 --- a/server/public/plugin/plugintest/hooks.go +++ b/server/public/plugin/plugintest/hooks.go @@ -57,6 +57,36 @@ func (_m *Hooks) ConfigurationWillBeSaved(newCfg *model.Config) (*model.Config, return r0, r1 } +// EmailNotificationWillBeSent provides a mock function with given fields: emailNotification +func (_m *Hooks) EmailNotificationWillBeSent(emailNotification *model.EmailNotification) (*model.EmailNotificationContent, string) { + ret := _m.Called(emailNotification) + + if len(ret) == 0 { + panic("no return value specified for EmailNotificationWillBeSent") + } + + var r0 *model.EmailNotificationContent + var r1 string + if rf, ok := ret.Get(0).(func(*model.EmailNotification) (*model.EmailNotificationContent, string)); ok { + return rf(emailNotification) + } + if rf, ok := ret.Get(0).(func(*model.EmailNotification) *model.EmailNotificationContent); ok { + r0 = rf(emailNotification) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.EmailNotificationContent) + } + } + + if rf, ok := ret.Get(1).(func(*model.EmailNotification) string); ok { + r1 = rf(emailNotification) + } else { + r1 = ret.Get(1).(string) + } + + return r0, r1 +} + // ExecuteCommand provides a mock function with given fields: c, args func (_m *Hooks) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) { ret := _m.Called(c, args)