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