github.com/nmintoh/dserver@v5.11.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(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.Srv.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 subjectParameters = map[string]interface{}{ 155 "SiteName": siteName, 156 "Month": t.Month, 157 "Day": t.Day, 158 "Year": t.Year, 159 } 160 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 161 subjectParameters["ChannelName"] = channelName 162 return translateFunc("app.notification.subject.group_message.full", subjectParameters) 163 } 164 return translateFunc("app.notification.subject.group_message.generic", subjectParameters) 165 } 166 167 /** 168 * Computes the email body for notification messages 169 */ 170 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 { 171 // only include message contents in notification email if email notification contents type is set to full 172 var bodyPage *utils.HTMLTemplate 173 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 174 bodyPage = a.NewEmailTemplate("post_body_full", recipient.Locale) 175 bodyPage.Props["PostMessage"] = a.GetMessageForNotification(post, translateFunc) 176 } else { 177 bodyPage = a.NewEmailTemplate("post_body_generic", recipient.Locale) 178 } 179 180 bodyPage.Props["SiteURL"] = a.GetSiteURL() 181 if teamName != "select_team" { 182 bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id 183 } else { 184 bodyPage.Props["TeamLink"] = teamURL 185 } 186 187 t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) 188 189 info := map[string]interface{}{ 190 "Hour": t.Hour, 191 "Minute": t.Minute, 192 "TimeZone": t.TimeZone, 193 "Month": t.Month, 194 "Day": t.Day, 195 } 196 if channel.Type == model.CHANNEL_DIRECT { 197 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 198 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") 199 bodyPage.Props["Info1"] = "" 200 info["SenderName"] = senderName 201 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", info) 202 } else { 203 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ 204 "SenderName": senderName, 205 }) 206 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", info) 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 info["SenderName"] = senderName 216 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", info) 217 } else { 218 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ 219 "SenderName": senderName, 220 }) 221 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", info) 222 } 223 } else { 224 if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { 225 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") 226 bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", 227 map[string]interface{}{ 228 "ChannelName": channelName, 229 }) 230 info["SenderName"] = senderName 231 bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", info) 232 } else { 233 bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ 234 "SenderName": senderName, 235 }) 236 bodyPage.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", info) 237 } 238 } 239 240 bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") 241 242 return bodyPage.Render() 243 } 244 245 type formattedPostTime struct { 246 Time time.Time 247 Year string 248 Month string 249 Day string 250 Hour string 251 Minute string 252 TimeZone string 253 } 254 255 func getFormattedPostTime(user *model.User, post *model.Post, useMilitaryTime bool, translateFunc i18n.TranslateFunc) formattedPostTime { 256 preferredTimezone := user.GetPreferredTimezone() 257 postTime := time.Unix(post.CreateAt/1000, 0) 258 zone, _ := postTime.Zone() 259 260 localTime := postTime 261 if preferredTimezone != "" { 262 loc, _ := time.LoadLocation(preferredTimezone) 263 if loc != nil { 264 localTime = postTime.In(loc) 265 zone, _ = localTime.Zone() 266 } 267 } 268 269 hour := localTime.Format("15") 270 period := "" 271 if !useMilitaryTime { 272 hour = localTime.Format("3") 273 period = " " + localTime.Format("PM") 274 } 275 276 return formattedPostTime{ 277 Time: localTime, 278 Year: fmt.Sprintf("%d", localTime.Year()), 279 Month: translateFunc(localTime.Month().String()), 280 Day: fmt.Sprintf("%d", localTime.Day()), 281 Hour: hour, 282 Minute: fmt.Sprintf("%02d"+period, localTime.Minute()), 283 TimeZone: zone, 284 } 285 } 286 287 func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string { 288 if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 { 289 return post.Message 290 } 291 292 // extract the filenames from their paths and determine what type of files are attached 293 result := <-a.Srv.Store.FileInfo().GetForPost(post.Id, true, true) 294 if result.Err != nil { 295 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)) 296 } 297 infos := result.Data.([]*model.FileInfo) 298 299 filenames := make([]string, len(infos)) 300 onlyImages := true 301 for i, info := range infos { 302 if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil { 303 // this should never error since filepath was escaped using url.QueryEscape 304 filenames[i] = escaped 305 } else { 306 filenames[i] = info.Name 307 } 308 309 onlyImages = onlyImages && info.IsImage() 310 } 311 312 props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")} 313 314 if onlyImages { 315 return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props) 316 } 317 return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props) 318 }