github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/app/email_batching.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/template"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/mattermost/mattermost-server/v5/mlog"
    14  	"github.com/mattermost/mattermost-server/v5/model"
    15  	"github.com/mattermost/mattermost-server/v5/utils"
    16  
    17  	"net/http"
    18  
    19  	"github.com/mattermost/go-i18n/i18n"
    20  )
    21  
    22  const (
    23  	EMAIL_BATCHING_TASK_NAME = "Email Batching"
    24  )
    25  
    26  func (es *EmailService) InitEmailBatching() {
    27  	if *es.srv.Config().EmailSettings.EnableEmailBatching {
    28  		if es.EmailBatching == nil {
    29  			es.EmailBatching = NewEmailBatchingJob(es, *es.srv.Config().EmailSettings.EmailBatchingBufferSize)
    30  		}
    31  
    32  		// note that we don't support changing EmailBatchingBufferSize without restarting the server
    33  
    34  		es.EmailBatching.Start()
    35  	}
    36  }
    37  
    38  func (es *EmailService) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
    39  	if !*es.srv.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 !es.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  	server               *Server
    59  	newNotifications     chan *batchedNotification
    60  	pendingNotifications map[string][]*batchedNotification
    61  	task                 *model.ScheduledTask
    62  	taskMutex            sync.Mutex
    63  }
    64  
    65  func NewEmailBatchingJob(es *EmailService, bufferSize int) *EmailBatchingJob {
    66  	return &EmailBatchingJob{
    67  		server:               es.srv,
    68  		newNotifications:     make(chan *batchedNotification, bufferSize),
    69  		pendingNotifications: make(map[string][]*batchedNotification),
    70  	}
    71  }
    72  
    73  func (job *EmailBatchingJob) Start() {
    74  	mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.server.Config().EmailSettings.EmailBatchingInterval))
    75  	newTask := model.CreateRecurringTask(EMAIL_BATCHING_TASK_NAME, job.CheckPendingEmails, time.Duration(*job.server.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.server.EmailService.sendBatchedEmailNotification)
   109  
   110  	mlog.Debug("Email batching job ran. Some users still have notifications pending.", mlog.Int("number_of_users", 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  			team, err := job.server.Store.Team().GetByName(notifications[0].teamName)
   144  			if err != nil {
   145  				mlog.Error("Unable to find Team id for notification", mlog.Err(err))
   146  				continue
   147  			}
   148  
   149  			if team != nil {
   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  			channelMembers, err := job.server.Store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userId)
   156  			if err != nil {
   157  				mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
   158  				continue
   159  			}
   160  
   161  			for _, channelMember := range *channelMembers {
   162  				if channelMember.LastViewedAt >= batchStartTime {
   163  					mlog.Debug("Deleted notifications for user", mlog.String("user_id", userId))
   164  					delete(job.pendingNotifications, userId)
   165  					break
   166  				}
   167  			}
   168  		}
   169  
   170  		// get how long we need to wait to send notifications to the user
   171  		var interval int64
   172  		preference, err := job.server.Store.Preference().Get(userId, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
   173  		if err != nil {
   174  			// use the default batching interval if an error ocurrs while fetching user preferences
   175  			interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   176  		} else {
   177  			if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
   178  				// // use the default batching interval if an error ocurrs while deserializing user preferences
   179  				interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
   180  			} else {
   181  				interval = value
   182  			}
   183  		}
   184  
   185  		// send the email notification if there are notifications to send AND it's been long enough
   186  		if len(job.pendingNotifications[userId]) > 0 && now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second {
   187  			job.server.Go(func(userId string, notifications []*batchedNotification) func() {
   188  				return func() {
   189  					handler(userId, notifications)
   190  				}
   191  			}(userId, job.pendingNotifications[userId]))
   192  			delete(job.pendingNotifications, userId)
   193  		}
   194  	}
   195  }
   196  
   197  func (es *EmailService) sendBatchedEmailNotification(userId string, notifications []*batchedNotification) {
   198  	user, err := es.srv.Store.User().Get(userId)
   199  	if err != nil {
   200  		mlog.Warn("Unable to find recipient for batched email notification")
   201  		return
   202  	}
   203  
   204  	translateFunc := utils.GetUserTranslations(user.Locale)
   205  	displayNameFormat := *es.srv.Config().TeamSettings.TeammateNameDisplay
   206  
   207  	var contents string
   208  	for _, notification := range notifications {
   209  		sender, err := es.srv.Store.User().Get(notification.post.UserId)
   210  		if err != nil {
   211  			mlog.Warn("Unable to find sender of post for batched email notification")
   212  			continue
   213  		}
   214  
   215  		channel, errCh := es.srv.Store.Channel().Get(notification.post.ChannelId, true)
   216  		if errCh != nil {
   217  			mlog.Warn("Unable to find channel of post for batched email notification")
   218  			continue
   219  		}
   220  
   221  		emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
   222  		if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents {
   223  			emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType
   224  		}
   225  
   226  		contents += es.renderBatchedPost(notification, channel, sender, *es.srv.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType)
   227  	}
   228  
   229  	tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
   230  
   231  	subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
   232  		"SiteName": es.srv.Config().TeamSettings.SiteName,
   233  		"Year":     tm.Year(),
   234  		"Month":    translateFunc(tm.Month().String()),
   235  		"Day":      tm.Day(),
   236  	})
   237  
   238  	body := es.newEmailTemplate("post_batched_body", user.Locale)
   239  	body.Props["SiteURL"] = *es.srv.Config().ServiceSettings.SiteURL
   240  	body.Props["Posts"] = template.HTML(contents)
   241  	body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications))
   242  
   243  	if err := es.sendNotificationMail(user.Email, subject, body.Render()); err != nil {
   244  		mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(err))
   245  	}
   246  }
   247  
   248  func (es *EmailService) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) string {
   249  	// don't include message contents if email notification contents type is set to generic
   250  	var template *utils.HTMLTemplate
   251  	if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   252  		template = es.newEmailTemplate("post_batched_post_full", userLocale)
   253  	} else {
   254  		template = es.newEmailTemplate("post_batched_post_generic", userLocale)
   255  	}
   256  
   257  	template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post")
   258  	template.Props["PostMessage"] = es.srv.GetMessageForNotification(notification.post, translateFunc)
   259  	template.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
   260  	template.Props["SenderName"] = sender.GetDisplayName(displayNameFormat)
   261  
   262  	tm := time.Unix(notification.post.CreateAt/1000, 0)
   263  	timezone, _ := tm.Zone()
   264  
   265  	template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{
   266  		"Year":     tm.Year(),
   267  		"Month":    translateFunc(tm.Month().String()),
   268  		"Day":      tm.Day(),
   269  		"Hour":     tm.Hour(),
   270  		"Minute":   fmt.Sprintf("%02d", tm.Minute()),
   271  		"Timezone": timezone,
   272  	})
   273  
   274  	if channel.Type == model.CHANNEL_DIRECT {
   275  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message")
   276  	} else if channel.Type == model.CHANNEL_GROUP {
   277  		template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message")
   278  	} else {
   279  		// don't include channel name if email notification contents type is set to generic
   280  		if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   281  			template.Props["ChannelName"] = channel.DisplayName
   282  		} else {
   283  			template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification")
   284  		}
   285  	}
   286  
   287  	return template.Render()
   288  }