github.com/jlevesy/mattermost-server@v5.3.2-0.20181003190404-7468f35cb0c8+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(notification *postNotification, user *model.User, team *model.Team) *model.AppError { 21 channel := notification.channel 22 post := notification.post 23 24 if channel.IsGroupOrDirect() { 25 result := <-a.Srv.Store.Team().GetTeamsByUserId(user.Id) 26 if result.Err != nil { 27 return result.Err 28 } 29 30 // if the recipient isn't in the current user's team, just pick one 31 teams := result.Data.([]*model.Team) 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 result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.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 = result.Data.(model.Preference).Value != model.PREFERENCE_EMAIL_INTERVAL_NO_BATCHING_SECONDS 57 } 58 59 if sendBatched { 60 if err := a.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 result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_USE_MILITARY_TIME); result.Err != nil { 72 useMilitaryTime = true 73 } else { 74 useMilitaryTime = result.Data.(model.Preference).Value == "true" 75 } 76 77 var nameFormat string 78 if result := <-a.Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, model.PREFERENCE_NAME_NAME_FORMAT); result.Err != nil { 79 nameFormat = *a.Config().TeamSettings.TeammateNameDisplay 80 } else { 81 nameFormat = result.Data.(model.Preference).Value 82 } 83 84 channelName := notification.GetChannelName(nameFormat, "") 85 senderName := notification.GetSenderName(nameFormat, a.Config().ServiceSettings.EnablePostUsernameOverride) 86 87 emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL 88 if license := a.License(); license != nil && *license.Features.EmailNotificationContents { 89 emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType 90 } 91 92 var subjectText string 93 if channel.Type == model.CHANNEL_DIRECT { 94 subjectText = getDirectMessageNotificationEmailSubject(user, post, translateFunc, a.Config().TeamSettings.SiteName, senderName, useMilitaryTime) 95 } else if channel.Type == model.CHANNEL_GROUP { 96 subjectText = getGroupMessageNotificationEmailSubject(user, post, translateFunc, a.Config().TeamSettings.SiteName, channelName, emailNotificationContentsType, useMilitaryTime) 97 } else if *a.Config().EmailSettings.UseChannelInEmailNotifications { 98 subjectText = getNotificationEmailSubject(user, post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName+" ("+channelName+")", useMilitaryTime) 99 } else { 100 subjectText = getNotificationEmailSubject(user, post, translateFunc, a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime) 101 } 102 103 teamURL := a.GetSiteURL() + "/" + team.Name 104 var bodyText = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, teamURL, emailNotificationContentsType, useMilitaryTime, translateFunc) 105 106 a.Go(func() { 107 if err := a.SendMail(user.Email, html.UnescapeString(subjectText), bodyText); err != nil { 108 mlog.Error(fmt.Sprint("Error to send the email", user.Email, err)) 109 } 110 }) 111 112 if a.Metrics != nil { 113 a.Metrics.IncrementPostSentEmail() 114 } 115 116 return nil 117 } 118 119 /** 120 * Computes the subject line for direct notification email messages 121 */ 122 func getDirectMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, senderName string, useMilitaryTime bool) string { 123 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 124 var subjectParameters = map[string]interface{}{ 125 "SiteName": siteName, 126 "SenderDisplayName": senderName, 127 "Month": t.Month, 128 "Day": t.Day, 129 "Year": t.Year, 130 } 131 return translateFunc("app.notification.subject.direct.full", subjectParameters) 132 } 133 134 /** 135 * Computes the subject line for group, public, and private email messages 136 */ 137 func getNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, teamName string, useMilitaryTime bool) string { 138 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 139 var subjectParameters = map[string]interface{}{ 140 "SiteName": siteName, 141 "TeamName": teamName, 142 "Month": t.Month, 143 "Day": t.Day, 144 "Year": t.Year, 145 } 146 return translateFunc("app.notification.subject.notification.full", subjectParameters) 147 } 148 149 /** 150 * Computes the subject line for group email messages 151 */ 152 func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, translateFunc i18n.TranslateFunc, siteName string, channelName string, emailNotificationContentsType string, useMilitaryTime bool) string { 153 t := getFormattedPostTime(user, post, useMilitaryTime, translateFunc) 154 var subjectText string 155 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 156 var subjectParameters = map[string]interface{}{ 157 "SiteName": siteName, 158 "ChannelName": channelName, 159 "Month": t.Month, 160 "Day": t.Day, 161 "Year": t.Year, 162 } 163 subjectText = translateFunc("app.notification.subject.group_message.full", subjectParameters) 164 } else { 165 var subjectParameters = map[string]interface{}{ 166 "SiteName": siteName, 167 "Month": t.Month, 168 "Day": t.Day, 169 "Year": t.Year, 170 } 171 subjectText = translateFunc("app.notification.subject.group_message.generic", subjectParameters) 172 } 173 return subjectText 174 } 175 176 /** 177 * Computes the email body for notification messages 178 */ 179 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 { 180 // only include message contents in notification email if email notification contents type is set to full 181 var bodyPage *utils.HTMLTemplate 182 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 183 bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale) 184 bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc) 185 } else { 186 bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale) 187 } 188 189 bodyPage.Props["SiteURL"] = a.GetSiteURL() 190 if teamName != "select_team" { 191 bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id 192 } else { 193 bodyPage.Props["TeamLink"] = teamURL 194 } 195 196 t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) 197 198 if channel.Type == model.CHANNEL_DIRECT { 199 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 200 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") 201 bodyPage.Props["Info1"] = "" 202 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", 203 map[string]interface{}{ 204 "SenderName": senderName, 205 "Hour": t.Hour, 206 "Minute": t.Minute, 207 "TimeZone": t.TimeZone, 208 "Month": t.Month, 209 "Day": t.Day, 210 }) 211 } else { 212 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ 213 "SenderName": senderName, 214 }) 215 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", 216 map[string]interface{}{ 217 "Hour": t.Hour, 218 "Minute": t.Minute, 219 "TimeZone": t.TimeZone, 220 "Month": t.Month, 221 "Day": t.Day, 222 }) 223 } 224 } else if channel.Type == model.CHANNEL_GROUP { 225 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 226 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.full") 227 bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.group_message.full", 228 map[string]interface{}{ 229 "ChannelName": channelName, 230 }) 231 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", 232 map[string]interface{}{ 233 "SenderName": senderName, 234 "Hour": t.Hour, 235 "Minute": t.Minute, 236 "TimeZone": t.TimeZone, 237 "Month": t.Month, 238 "Day": t.Day, 239 }) 240 } else { 241 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ 242 "SenderName": senderName, 243 }) 244 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", 245 map[string]interface{}{ 246 "Hour": t.Hour, 247 "Minute": t.Minute, 248 "TimeZone": t.TimeZone, 249 "Month": t.Month, 250 "Day": t.Day, 251 }) 252 } 253 } else { 254 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 255 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") 256 bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", 257 map[string]interface{}{ 258 "ChannelName": channelName, 259 }) 260 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", 261 map[string]interface{}{ 262 "SenderName": senderName, 263 "Hour": t.Hour, 264 "Minute": t.Minute, 265 "TimeZone": t.TimeZone, 266 "Month": t.Month, 267 "Day": t.Day, 268 }) 269 } else { 270 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ 271 "SenderName": senderName, 272 }) 273 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", 274 map[string]interface{}{ 275 "Hour": t.Hour, 276 "Minute": t.Minute, 277 "TimeZone": t.TimeZone, 278 "Month": t.Month, 279 "Day": t.Day, 280 }) 281 } 282 } 283 284 bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") 285 286 return bodyPage.Render() 287 } 288 289 type formattedPostTime struct { 290 Time time.Time 291 Year string 292 Month string 293 Day string 294 Hour string 295 Minute string 296 TimeZone string 297 } 298 299 func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bool, translateFunc i18n.TranslateFunc) formattedPostTime { 300 preferredTimezone := user.GetPreferredTimezone() 301 postTime := time.Unix(post.CreateAt/1000, 0) 302 zone, _ := postTime.Zone() 303 304 localTime := postTime 305 if preferredTimezone != "" { 306 loc, _ := time.LoadLocation(preferredTimezone) 307 if loc != nil { 308 localTime = postTime.In(loc) 309 zone, _ = localTime.Zone() 310 } 311 } 312 313 hour := localTime.Format("15") 314 period := "" 315 if !useMilitaryTime { 316 hour = localTime.Format("3") 317 period = " " + localTime.Format("PM") 318 } 319 320 return formattedPostTime{ 321 Time: localTime, 322 Year: fmt.Sprintf("%d", localTime.Year()), 323 Month: translateFunc(localTime.Month().String()), 324 Day: fmt.Sprintf("%d", localTime.Day()), 325 Hour: hour, 326 Minute: fmt.Sprintf("%02d"+period, localTime.Minute()), 327 TimeZone: zone, 328 } 329 } 330 331 func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { 332 if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { 333 return post.Message 334 } 335 336 // extract the filenames from their paths and determine what type of files are attached 337 var infos []*model.FileInfo 338 if result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, true); result.Err != nil { 339 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)) 340 } else { 341 infos = result.Data.([]*model.FileInfo) 342 } 343 344 filenames := make([]string, len(infos)) 345 onlyImages := true 346 for i, info := range infos { 347 if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { 348 // this should never error since filepath was escaped using url.QueryEscape 349 filenames[i] = escaped 350 } else { 351 filenames[i] = info.Name 352 } 353 354 onlyImages = onlyImages && info.IsImage() 355 } 356 357 props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} 358 359 if onlyImages { 360 return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) 361 } 362 return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) 363 }