github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/notification_email.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See LICENSE.txt for license information. 3 4 package app 5 6 import ( 7 "fmt" 8 "html" 9 "html/template" 10 "net/url" 11 "path/filepath" 12 "strings" 13 "time" 14 15 "github.com/mattermost/go-i18n/i18n" 16 "github.com/mattermost/mattermost-server/v5/mlog" 17 "github.com/mattermost/mattermost-server/v5/model" 18 "github.com/mattermost/mattermost-server/v5/utils" 19 ) 20 21 func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) *model.AppError { 22 channel := notification.Channel 23 post := notification.Post 24 25 if channel.IsGroupOrDirect() { 26 teams, err := a.Srv().Store.Team().GetTeamsByUserId(user.Id) 27 if err != nil { 28 return err 29 } 30 31 // if the recipient isn't in the current user's team, just pick one 32 found := false 33 34 for i := range teams { 35 if teams[i].Id == team.Id { 36 found = true 37 break 38 } 39 } 40 41 if !found && len(teams) > 0 { 42 team = teams[0] 43 } else { 44 // in case the user hasn't joined any teams we send them to the select_team page 45 team = &model.Team{Name: "select_team", DisplayName: *a.Config().TeamSettings.SiteName} 46 } 47 } 48 49 if *a.Config().EmailSettings.EnableEmailBatching { 50 var sendBatched bool 51 if data, err := a.Srv().Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); err != nil { 52 // if the call fails, assume that the interval has not been explicitly set and batch the notifications 53 sendBatched = true 54 } else { 55 // if the user has chosen to receive notifications immediately, don't batch them 56 sendBatched = data.Value != model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS 57 } 58 59 if sendBatched { 60 if err := a.Srv().EmailService.AddNotificationEmailToBatch(user, post, team); err == nil { 61 return nil 62 } 63 } 64 65 // fall back to sending a single email if we can't batch it for some reason 66 } 67 68 translateFunc := utils.GetUserTranslations(user.Locale) 69 70 var useMilitaryTime bool 71 if data, err := a.Srv().Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_USE_MILITARY_TIME); err != nil { 72 useMilitaryTime = true 73 } else { 74 useMilitaryTime = data.Value == "true" 75 } 76 77 nameFormat := a.GetNotificationNameFormat(user) 78 79 channelName := notification.GetChannelName(nameFormat, "") 80 senderName := notification.GetSenderName(nameFormat, *a.Config().ServiceSettings.EnablePostUsernameOverride) 81 82 emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL 83 if license := a.Srv().License(); license != nil && *license.Features.EmailNotificationContents { 84 emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType 85 } 86 87 var subjectText string 88 if channel.Type == model.CHANNEL_DIRECT { 89 subjectText = getDirectMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, senderName, useMilitaryTime) 90 } else if channel.Type == model.CHANNEL_GROUP { 91 subjectText = getGroupMessageNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime) 92 } else if *a.Config().EmailSettings.UseChannelInEmailNotifications { 93 subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime) 94 } else { 95 subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime) 96 } 97 98 landingURL := a.GetSiteURL() + "/landing#/" + team.Name 99 var bodyText = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc) 100 101 a.Srv().Go(func() { 102 if err := a.Srv().EmailService.sendNotificationMail(user.Email, html.UnescapeString(subjectText), bodyText); err != nil { 103 mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(err)) 104 } 105 }) 106 107 if a.Metrics() != nil { 108 a.Metrics().IncrementPostSentEmail() 109 } 110 111 return nil 112 } 113 114 /** 115 * Computes the subject line for direct notification email messages 116 */ 117 func getDirectMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string, useMilitaryTime bool) string { 118 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 119 var subjectParameters = map[string]interface{}{ 120 "SiteName": siteName, 121 "SenderDisplayName": senderName, 122 "Month": t.Month, 123 "Day": t.Day, 124 "Year": t.Year, 125 } 126 return translateFunc("app.notification.subject.direct.full", subjectParameters) 127 } 128 129 /** 130 * Computes the subject line for group, public, and private email messages 131 */ 132 func getNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string, useMilitaryTime bool) string { 133 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 134 var subjectParameters = map[string]interface{}{ 135 "SiteName": siteName, 136 "TeamName": teamName, 137 "Month": t.Month, 138 "Day": t.Day, 139 "Year": t.Year, 140 } 141 return translateFunc("app.notification.subject.notification.full", subjectParameters) 142 } 143 144 /** 145 * Computes the subject line for group email messages 146 */ 147 func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, channelName string, emailNotificationContentsType string, useMilitaryTime bool) string { 148 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 149 var subjectParameters = map[string]interface{}{ 150 "SiteName": siteName, 151 "Month": t.Month, 152 "Day": t.Day, 153 "Year": t.Year, 154 } 155 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 156 subjectParameters["ChannelName"] = channelName 157 return translateFunc("app.notification.subject.group_message.full", subjectParameters) 158 } 159 return translateFunc("app.notification.subject.group_message.generic", subjectParameters) 160 } 161 162 /** 163 * Computes the email body for notification messages 164 */ 165 func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc) string { 166 // only include message contents in notification email if email notification contents type is set to full 167 var bodyPage *utils.HTMLTemplate 168 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 169 bodyPage = a.Srv().EmailService.newEmailTemplate("post_body_full", recipient.Locale) 170 postMessage := a.GetMessageForNotification(post, translateFunc) 171 postMessage = html.EscapeString(postMessage) 172 normalizedPostMessage := a.generateHyperlinkForChannels(postMessage, teamName, landingURL) 173 bodyPage.Props["PostMessage"] = template.HTML(normalizedPostMessage) 174 } else { 175 bodyPage = a.Srv().EmailService.newEmailTemplate("post_body_generic", recipient.Locale) 176 } 177 178 bodyPage.Props["SiteURL"] = a.GetSiteURL() 179 if teamName != "select_team" { 180 bodyPage.Props["TeamLink"] = landingURL + "/pl/" + post.Id 181 } else { 182 bodyPage.Props["TeamLink"] = landingURL 183 } 184 185 t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) 186 187 info := map[string]interface{}{ 188 "Hour": t.Hour, 189 "Minute": t.Minute, 190 "TimeZone": t.TimeZone, 191 "Month": t.Month, 192 "Day": t.Day, 193 } 194 if channel.Type == model.CHANNEL_DIRECT { 195 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 196 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") 197 bodyPage.Props["Info1"] = "" 198 info["SenderName"] = senderName 199 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", info) 200 } else { 201 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ 202 "SenderName": senderName, 203 }) 204 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", info) 205 } 206 } else if channel.Type == model.CHANNEL_GROUP { 207 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 208 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.full") 209 bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.group_message.full", 210 map[string]interface{}{ 211 "ChannelName": channelName, 212 }) 213 info["SenderName"] = senderName 214 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", info) 215 } else { 216 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ 217 "SenderName": senderName, 218 }) 219 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", info) 220 } 221 } else { 222 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 223 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") 224 bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", 225 map[string]interface{}{ 226 "ChannelName": channelName, 227 }) 228 info["SenderName"] = senderName 229 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", info) 230 } else { 231 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ 232 "SenderName": senderName, 233 }) 234 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", info) 235 } 236 } 237 238 bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") 239 240 return bodyPage.Render() 241 } 242 243 type formattedPostTime struct { 244 Time time.Time 245 Year string 246 Month string 247 Day string 248 Hour string 249 Minute string 250 TimeZone string 251 } 252 253 func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bool, translateFunc i18n.TranslateFunc) formattedPostTime { 254 preferredTimezone := user.GetPreferredTimezone() 255 postTime := time.Unix(post.CreateAt/1000, 0) 256 zone, _ := postTime.Zone() 257 258 localTime := postTime 259 if preferredTimezone != "" { 260 loc, _ := time.LoadLocation(preferredTimezone) 261 if loc != nil { 262 localTime = postTime.In(loc) 263 zone, _ = localTime.Zone() 264 } 265 } 266 267 hour := localTime.Format("15") 268 period := "" 269 if !useMilitaryTime { 270 hour = localTime.Format("3") 271 period = " " + localTime.Format("PM") 272 } 273 274 return formattedPostTime{ 275 Time: localTime, 276 Year: fmt.Sprintf("%d", localTime.Year()), 277 Month: translateFunc(localTime.Month().String()), 278 Day: fmt.Sprintf("%d", localTime.Day()), 279 Hour: hour, 280 Minute: fmt.Sprintf("%02d"+period, localTime.Minute()), 281 TimeZone: zone, 282 } 283 } 284 285 func (a *App) generateHyperlinkForChannels(postMessage, teamName, teamURL string) string { 286 team, err := a.GetTeamByName(teamName) 287 if err != nil { 288 mlog.Error("Encountered error while looking up team by name", mlog.String("team_name", teamName), mlog.Err(err)) 289 return postMessage 290 } 291 292 channelNames := model.ChannelMentions(postMessage) 293 if len(channelNames) == 0 { 294 return postMessage 295 } 296 297 channels, err := a.GetChannelsByNames(channelNames, team.Id) 298 if err != nil { 299 mlog.Error("Encountered error while getting channels", mlog.Err(err)) 300 return postMessage 301 } 302 303 visited := make(map[string]bool) 304 for _, ch := range channels { 305 if !visited[ch.Id] && ch.Type == model.CHANNEL_OPEN { 306 channelURL := teamURL + "/channels/" + ch.Name 307 channelHyperLink := fmt.Sprintf("<a href='%s'>%s</a>", channelURL, "~"+ch.Name) 308 postMessage = strings.Replace(postMessage, "~"+ch.Name, channelHyperLink, -1) 309 visited[ch.Id] = true 310 } 311 } 312 return postMessage 313 } 314 315 func (s *Server) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { 316 if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { 317 return post.Message 318 } 319 320 // extract the filenames from their paths and determine what type of files are attached 321 infos, err := s.Store.FileInfo().GetForPost(post.Id, true, false, true) 322 if err != nil { 323 mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err)) 324 } 325 326 filenames := make([]string, len(infos)) 327 onlyImages := true 328 for i, info := range infos { 329 if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { 330 // this should never error since filepath was escaped using url.QueryEscape 331 filenames[i] = escaped 332 } else { 333 filenames[i] = info.Name 334 } 335 336 onlyImages = onlyImages && info.IsImage() 337 } 338 339 props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} 340 341 if onlyImages { 342 return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) 343 } 344 return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) 345 } 346 347 func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { 348 return a.Srv().GetMessageForNotification(post, translateFunc) 349 }