github.com/psyb0t/mattermost-server@v4.6.1-0.20180125161845-5503a1351abf+incompatible/app/email_batching.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/template"
     9  	"strconv"
    10  	"time"
    11  
    12  	"github.com/mattermost/mattermost-server/model"
    13  	"github.com/mattermost/mattermost-server/utils"
    14  
    15  	"net/http"
    16  
    17  	l4g "github.com/alecthomas/log4go"
    18  	"github.com/nicksnyder/go-i18n/i18n"
    19  )
    20  
    21  const (
    22  	EMAIL_BATCHING_TASK_NAME = "Email Batching"
    23  )
    24  
    25  func (a *App) InitEmailBatching() {
    26  	if *a.Config().EmailSettings.EnableEmailBatching {
    27  		if a.EmailBatching == nil {
    28  			a.EmailBatching = NewEmailBatchingJob(a, *a.Config().EmailSettings.EmailBatchingBufferSize)
    29  		}
    30  
    31  		// note that we don't support changing EmailBatchingBufferSize without restarting the server
    32  
    33  		a.EmailBatching.Start()
    34  	}
    35  }
    36  
    37  func (a *App) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
    38  	if !*a.Config().EmailSettings.EnableEmailBatching {
    39  		return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
    40  	}
    41  
    42  	if !a.EmailBatching.Add(user, post, team) {
    43  		l4g.Error(utils.T("api.email_batching.add_notification_email_to_batch.channel_full.app_error"))
    44  		return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "", http.StatusInternalServerError)
    45  	}
    46  
    47  	return nil
    48  }
    49  
    50  type batchedNotification struct {
    51  	userId   string
    52  	post     *model.Post
    53  	teamName string
    54  }
    55  
    56  type EmailBatchingJob struct {
    57  	app                  *App
    58  	newNotifications     chan *batchedNotification
    59  	pendingNotifications map[string][]*batchedNotification
    60  }
    61  
    62  func NewEmailBatchingJob(a *App, bufferSize int) *EmailBatchingJob {
    63  	return &EmailBatchingJob{
    64  		app:                  a,
    65  		newNotifications:     make(chan *batchedNotification, bufferSize),
    66  		pendingNotifications: make(map[string][]*batchedNotification),
    67  	}
    68  }
    69  
    70  func (job *EmailBatchingJob) Start() {
    71  	if task := model.GetTaskByName(EMAIL_BATCHING_TASK_NAME); task != nil {
    72  		task.Cancel()
    73  	}
    74  
    75  	l4g.Debug(utils.T("api.email_batching.start.starting"), *job.app.Config().EmailSettings.EmailBatchingInterval)
    76  	model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.app.Config().EmailSettings.EmailBatchingInterval)*time.Second)
    77  }
    78  
    79  func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
    80  	notification := &batchedNotification{
    81  		userId:   user.Id,
    82  		post:     post,
    83  		teamName: team.Name,
    84  	}
    85  
    86  	select {
    87  	case job.newNotifications <- notification:
    88  		return true
    89  	default:
    90  		// return false if we couldn't queue the email notification so that we can send an immediate email
    91  		return false
    92  	}
    93  }
    94  
    95  func (job *EmailBatchingJob) CheckPendingEmails() {
    96  	job.handleNewNotifications()
    97  
    98  	// it's a bit weird to pass the send email function through here, but it makes it so that we can test
    99  	// without actually sending emails
   100  	job.checkPendingNotifications(time.Now(), job.app.sendBatchedEmailNotification)
   101  
   102  	l4g.Debug(utils.T("api.email_batching.check_pending_emails.finished_running"), len(job.pendingNotifications))
   103  }
   104  
   105  func (job *EmailBatchingJob) handleNewNotifications() {
   106  	receiving := true
   107  
   108  	// read in new notifications to send
   109  	for receiving {
   110  		select {
   111  		case notification := <-job.newNotifications:
   112  			userId := notification.userId
   113  
   114  			if _, ok := job.pendingNotifications[userId]; !ok {
   115  				job.pendingNotifications[userId] = []*batchedNotification{notification}
   116  			} else {
   117  				job.pendingNotifications[userId] = append(job.pendingNotifications[userId], notification)
   118  			}
   119  		default:
   120  			receiving = false
   121  		}
   122  	}
   123  }
   124  
   125  func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) {
   126  	for userId, notifications := range job.pendingNotifications {
   127  		batchStartTime := notifications[0].post.CreateAt
   128  		inspectedTeamNames := make(map[string]string)
   129  		for _, notification := range notifications {
   130  			// at most, we'll do one check for each team that notifications were sent for
   131  			if inspectedTeamNames[notification.teamName] != "" {
   132  				continue
   133  			}
   134  			tchan := job.app.Srv.Store.Team().GetByName(notifications[0].teamName)
   135  			if result := <-tchan; result.Err != nil {
   136  				l4g.Error("Unable to find Team id for notification", result.Err)
   137  				continue
   138  			} else if team, ok := result.Data.(*model.Team); ok {
   139  				inspectedTeamNames[notification.teamName] = team.Id
   140  			}
   141  
   142  			// if the user has viewed any channels in this team since the notification was queued, delete
   143  			// all queued notifications
   144  			mchan := job.app.Srv.Store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userId)
   145  			if result := <-mchan; result.Err != nil {
   146  				l4g.Error("Unable to find ChannelMembers for user", result.Err)
   147  				continue
   148  			} else if channelMembers, ok := result.Data.(*model.ChannelMembers); ok {
   149  				for _, channelMember := range *channelMembers {
   150  					if channelMember.LastViewedAt >= batchStartTime {
   151  						l4g.Debug("Deleted notifications for user %s", userId)
   152  						delete(job.pendingNotifications, userId)
   153  						break
   154  					}
   155  				}
   156  			}
   157  		}
   158  
   159  		// get how long we need to wait to send notifications to the user
   160  		var interval int64
   161  		pchan := job.app.Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
   162  		if result := <-pchan; result.Err != nil {
   163  			// use the default batching interval if an error ocurrs while fetching user preferences
   164  			interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   165  		} else {
   166  			preference := result.Data.(model.Preference)
   167  
   168  			if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
   169  				// // use the default batching interval if an error ocurrs while deserializing user preferences
   170  				interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   171  			} else {
   172  				interval = value
   173  			}
   174  		}
   175  
   176  		// send the email notification if it's been long enough
   177  		if now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second {
   178  			job.app.Go(func(userId string, notifications []*batchedNotification) func() {
   179  				return func() {
   180  					handler(userId, notifications)
   181  				}
   182  			}(userId, notifications))
   183  			delete(job.pendingNotifications, userId)
   184  		}
   185  	}
   186  }
   187  
   188  func (a *App) sendBatchedEmailNotification(userId string, notifications []*batchedNotification) {
   189  	uchan := a.Srv.Store.User().Get(userId)
   190  
   191  	var user *model.User
   192  	if result := <-uchan; result.Err != nil {
   193  		l4g.Warn("api.email_batching.send_batched_email_notification.user.app_error")
   194  		return
   195  	} else {
   196  		user = result.Data.(*model.User)
   197  	}
   198  
   199  	translateFunc := utils.GetUserTranslations(user.Locale)
   200  	displayNameFormat := *a.Config().TeamSettings.TeammateNameDisplay
   201  
   202  	var contents string
   203  	for _, notification := range notifications {
   204  		var sender *model.User
   205  		schan := a.Srv.Store.User().Get(notification.post.UserId)
   206  		if result := <-schan; result.Err != nil {
   207  			l4g.Warn(utils.T("api.email_batching.render_batched_post.sender.app_error"))
   208  			continue
   209  		} else {
   210  			sender = result.Data.(*model.User)
   211  		}
   212  
   213  		var channel *model.Channel
   214  		cchan := a.Srv.Store.Channel().Get(notification.post.ChannelId, true)
   215  		if result := <-cchan; result.Err != nil {
   216  			l4g.Warn(utils.T("api.email_batching.render_batched_post.channel.app_error"))
   217  			continue
   218  		} else {
   219  			channel = result.Data.(*model.Channel)
   220  		}
   221  
   222  		emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
   223  		if utils.IsLicensed() && *utils.License().Features.EmailNotificationContents {
   224  			emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
   225  		}
   226  
   227  		contents += a.renderBatchedPost(notification, channel, sender, *a.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType)
   228  	}
   229  
   230  	tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
   231  
   232  	subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
   233  		"SiteName": a.Config().TeamSettings.SiteName,
   234  		"Year":     tm.Year(),
   235  		"Month":    translateFunc(tm.Month().String()),
   236  		"Day":      tm.Day(),
   237  	})
   238  
   239  	body := a.NewEmailTemplate("post_batched_body", user.Locale)
   240  	body.Props["SiteURL"] = *a.Config().ServiceSettings.SiteURL
   241  	body.Props["Posts"] = template.HTML(contents)
   242  	body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications))
   243  
   244  	if err := a.SendMail(user.Email, subject, body.Render()); err != nil {
   245  		l4g.Warn(utils.T("api.email_batchings.send_batched_email_notification.send.app_error"), user.Email, err)
   246  	}
   247  }
   248  
   249  func (a *App) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) string {
   250  	// don't include message contents if email notification contents type is set to generic
   251  	var template *utils.HTMLTemplate
   252  	if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   253  		template = a.NewEmailTemplate("post_batched_post_full", userLocale)
   254  	} else {
   255  		template = a.NewEmailTemplate("post_batched_post_generic", userLocale)
   256  	}
   257  
   258  	template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post")
   259  	template.Props["PostMessage"] = a.GetMessageForNotification(notification.post, translateFunc)
   260  	template.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
   261  	template.Props["SenderName"] = sender.GetDisplayName(displayNameFormat)
   262  
   263  	tm := time.Unix(notification.post.CreateAt/1000, 0)
   264  	timezone, _ := tm.Zone()
   265  
   266  	template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{
   267  		"Year":     tm.Year(),
   268  		"Month":    translateFunc(tm.Month().String()),
   269  		"Day":      tm.Day(),
   270  		"Hour":     tm.Hour(),
   271  		"Minute":   fmt.Sprintf("%02d", tm.Minute()),
   272  		"Timezone": timezone,
   273  	})
   274  
   275  	if channel.Type == model.CHANNEL_DIRECT {
   276  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message")
   277  	} else if channel.Type == model.CHANNEL_GROUP {
   278  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message")
   279  	} else {
   280  		// don't include channel name if email notification contents type is set to generic
   281  		if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   282  			template.Props["ChannelName"] = channel.DisplayName
   283  		} else {
   284  			template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification")
   285  		}
   286  	}
   287  
   288  	return template.Render()
   289  }