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)