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  }