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