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  }