github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/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/vnforks/kid/v5/mlog"
    14  	"github.com/vnforks/kid/v5/model"
    15  	"github.com/vnforks/kid/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 (s *Server) InitEmailBatching() {
    27  	if *s.Config().EmailSettings.EnableEmailBatching {
    28  		if s.EmailBatching == nil {
    29  			s.EmailBatching = NewEmailBatchingJob(s, *s.Config().EmailSettings.EmailBatchingBufferSize)
    30  		}
    31  
    32  		// note that we don't support changing EmailBatchingBufferSize without restarting the server
    33  
    34  		s.EmailBatching.Start()
    35  	}
    36  }
    37  
    38  func (a *App) AddNotificationEmailToBatch(user *model.User, post *model.Post, branch *model.Branch) *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.Srv().EmailBatching.Add(user, post, branch) {
    44  		mlog.Error("Email batching job's receiving class was full. Please increase the EmailBatchingBufferSize.")
    45  		return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.class_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  	branchName 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(s *Server, bufferSize int) *EmailBatchingJob {
    66  	return &EmailBatchingJob{
    67  		server:               s,
    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, branch *model.Branch) bool {
    88  	notification := &batchedNotification{
    89  		userId:     user.Id,
    90  		post:       post,
    91  		branchName: branch.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.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  		inspectedBranchNames := make(map[string]string)
   137  		for _, notification := range notifications {
   138  			// at most, we'll do one check for each branch that notifications were sent for
   139  			if inspectedBranchNames[notification.branchName] != "" {
   140  				continue
   141  			}
   142  
   143  			branch, err := job.server.Store.Branch().GetByName(notifications[0].branchName)
   144  			if err != nil {
   145  				mlog.Error("Unable to find Branch id for notification", mlog.Err(err))
   146  				continue
   147  			}
   148  
   149  			if branch != nil {
   150  				inspectedBranchNames[notification.branchName] = branch.Id
   151  			}
   152  
   153  			// if the user has viewed any classes in this branch since the notification was queued, delete
   154  			// all queued notifications
   155  			classMembers, err := job.server.Store.Class().GetMembersForUser(inspectedBranchNames[notification.branchName], userId)
   156  			if err != nil {
   157  				mlog.Error("Unable to find ClassMembers for user", mlog.Err(err))
   158  				continue
   159  			}
   160  
   161  			for _, classMember := range *classMembers {
   162  				if classMember.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 it's been long enough
   186  		if 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, notifications))
   192  			delete(job.pendingNotifications, userId)
   193  		}
   194  	}
   195  }
   196  
   197  func (s *Server) sendBatchedEmailNotification(userId string, notifications []*batchedNotification) {
   198  	user, err := s.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 := *s.Config().BranchSettings.BranchmateNameDisplay
   206  
   207  	var contents string
   208  	for _, notification := range notifications {
   209  		sender, err := s.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  		class, errCh := s.Store.Class().Get(notification.post.ClassId, true)
   216  		if errCh != nil {
   217  			mlog.Warn("Unable to find class of post for batched email notification")
   218  			continue
   219  		}
   220  
   221  		emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
   222  		if license := s.License(); license != nil && *license.Features.EmailNotificationContents {
   223  			emailNotificationContentsType = *s.Config().EmailSettings.EmailNotificationContentsType
   224  		}
   225  
   226  		contents += s.renderBatchedPost(notification, class, sender, *s.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": s.Config().BranchSettings.SiteName,
   233  		"Year":     tm.Year(),
   234  		"Month":    translateFunc(tm.Month().String()),
   235  		"Day":      tm.Day(),
   236  	})
   237  
   238  	body := s.FakeApp().newEmailTemplate("post_batched_body", user.Locale)
   239  	body.Props["SiteURL"] = *s.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 := s.FakeApp().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 (s *Server) renderBatchedPost(notification *batchedNotification, class *model.Class, 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 = s.FakeApp().newEmailTemplate("post_batched_post_full", userLocale)
   253  	} else {
   254  		template = s.FakeApp().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"] = s.FakeApp().GetMessageForNotification(notification.post, translateFunc)
   259  	template.Props["PostLink"] = siteURL + "/" + notification.branchName + "/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  	// don't include class name if email notification contents type is set to generic
   275  	if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
   276  		template.Props["ClassName"] = class.DisplayName
   277  	} else {
   278  		template.Props["ClassName"] = translateFunc("api.email_batching.render_batched_post.notification")
   279  	}
   280  
   281  	return template.Render()
   282  }