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