github.com/gigforks/mattermost-server@v4.9.1-0.20180619094218-800d97fa55d0+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  			tchan := job.app.Srv.Store.Team().GetByName(notifications[0].teamName)
   143  			if result := <-tchan; result.Err != nil {
   144  				mlog.Error(fmt.Sprint("Unable to find Team id for notification", result.Err))
   145  				continue
   146  			} else if team, ok := result.Data.(*model.Team); ok {
   147  				inspectedTeamNames[notification.teamName] = team.Id
   148  			}
   149  
   150  			// if the user has viewed any channels in this team since the notification was queued, delete
   151  			// all queued notifications
   152  			mchan := job.app.Srv.Store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userId)
   153  			if result := <-mchan; result.Err != nil {
   154  				mlog.Error(fmt.Sprint("Unable to find ChannelMembers for user", result.Err))
   155  				continue
   156  			} else if channelMembers, ok := result.Data.(*model.ChannelMembers); ok {
   157  				for _, channelMember := range *channelMembers {
   158  					if channelMember.LastViewedAt >= batchStartTime {
   159  						mlog.Debug(fmt.Sprintf("Deleted notifications for user %s", userId), mlog.String("user_id", userId))
   160  						delete(job.pendingNotifications, userId)
   161  						break
   162  					}
   163  				}
   164  			}
   165  		}
   166  
   167  		// get how long we need to wait to send notifications to the user
   168  		var interval int64
   169  		pchan := job.app.Srv.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
   170  		if result := <-pchan; result.Err != nil {
   171  			// use the default batching interval if an error ocurrs while fetching user preferences
   172  			interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   173  		} else {
   174  			preference := result.Data.(model.Preference)
   175  
   176  			if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
   177  				// // use the default batching interval if an error ocurrs while deserializing user preferences
   178  				interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   179  			} else {
   180  				interval = value
   181  			}
   182  		}
   183  
   184  		// send the email notification if it's been long enough
   185  		if now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second {
   186  			job.app.Go(func(userId string, notifications []*batchedNotification) func() {
   187  				return func() {
   188  					handler(userId, notifications)
   189  				}
   190  			}(userId, notifications))
   191  			delete(job.pendingNotifications, userId)
   192  		}
   193  	}
   194  }
   195  
   196  func (a *App) sendBatchedEmailNotification(userId string, notifications []*batchedNotification) {
   197  	uchan := a.Srv.Store.User().Get(userId)
   198  
   199  	var user *model.User
   200  	if result := <-uchan; result.Err != nil {
   201  		mlog.Warn("api.email_batching.send_batched_email_notification.user.app_error")
   202  		return
   203  	} else {
   204  		user = result.Data.(*model.User)
   205  	}
   206  
   207  	translateFunc := utils.GetUserTranslations(user.Locale)
   208  	displayNameFormat := *a.Config().TeamSettings.TeammateNameDisplay
   209  
   210  	var contents string
   211  	for _, notification := range notifications {
   212  		var sender *model.User
   213  		schan := a.Srv.Store.User().Get(notification.post.UserId)
   214  		if result := <-schan; result.Err != nil {
   215  			mlog.Warn("Unable to find sender of post for batched email notification")
   216  			continue
   217  		} else {
   218  			sender = result.Data.(*model.User)
   219  		}
   220  
   221  		var channel *model.Channel
   222  		cchan := a.Srv.Store.Channel().Get(notification.post.ChannelId, true)
   223  		if result := <-cchan; result.Err != nil {
   224  			mlog.Warn("Unable to find channel of post for batched email notification")
   225  			continue
   226  		} else {
   227  			channel = result.Data.(*model.Channel)
   228  		}
   229  
   230  		emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
   231  		if license := a.License(); license != nil && *license.Features.EmailNotificationContents {
   232  			emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
   233  		}
   234  
   235  		contents += a.renderBatchedPost(notification, channel, sender, *a.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType)
   236  	}
   237  
   238  	tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
   239  
   240  	subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
   241  		"SiteName": a.Config().TeamSettings.SiteName,
   242  		"Year":     tm.Year(),
   243  		"Month":    translateFunc(tm.Month().String()),
   244  		"Day":      tm.Day(),
   245  	})
   246  
   247  	body := a.NewEmailTemplate("post_batched_body", user.Locale)
   248  	body.Props["SiteURL"] = *a.Config().ServiceSettings.SiteURL
   249  	body.Props["Posts"] = template.HTML(contents)
   250  	body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications))
   251  
   252  	if err := a.SendMail(user.Email, subject, body.Render()); err != nil {
   253  		mlog.Warn(fmt.Sprintf("Unable to send batched email notification err=%v", err), mlog.String("email", user.Email))
   254  	}
   255  }
   256  
   257  func (a *App) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) string {
   258  	// don't include message contents if email notification contents type is set to generic
   259  	var template *utils.HTMLTemplate
   260  	if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   261  		template = a.NewEmailTemplate("post_batched_post_full", userLocale)
   262  	} else {
   263  		template = a.NewEmailTemplate("post_batched_post_generic", userLocale)
   264  	}
   265  
   266  	template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post")
   267  	template.Props["PostMessage"] = a.GetMessageForNotification(notification.post, translateFunc)
   268  	template.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
   269  	template.Props["SenderName"] = sender.GetDisplayName(displayNameFormat)
   270  
   271  	tm := time.Unix(notification.post.CreateAt/1000, 0)
   272  	timezone, _ := tm.Zone()
   273  
   274  	template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{
   275  		"Year":     tm.Year(),
   276  		"Month":    translateFunc(tm.Month().String()),
   277  		"Day":      tm.Day(),
   278  		"Hour":     tm.Hour(),
   279  		"Minute":   fmt.Sprintf("%02d", tm.Minute()),
   280  		"Timezone": timezone,
   281  	})
   282  
   283  	if channel.Type == model.CHANNEL_DIRECT {
   284  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message")
   285  	} else if channel.Type == model.CHANNEL_GROUP {
   286  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message")
   287  	} else {
   288  		// don't include channel name if email notification contents type is set to generic
   289  		if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   290  			template.Props["ChannelName"] = channel.DisplayName
   291  		} else {
   292  			template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification")
   293  		}
   294  	}
   295  
   296  	return template.Render()
   297  }