Mono repo -> Master (#22553)
Combines the following repositories into one: https://github.com/mattermost/mattermost-server https://github.com/mattermost/mattermost-webapp https://github.com/mattermost/focalboard https://github.com/mattermost/mattermost-plugin-playbooks
Этот коммит содержится в:
1323
server/channels/app/email/email.go
Обычный файл
1323
server/channels/app/email/email.go
Обычный файл
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
378
server/channels/app/email/email_batching.go
Обычный файл
378
server/channels/app/email/email_batching.go
Обычный файл
@@ -0,0 +1,378 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
const (
|
||||
EmailBatchingTaskName = "Email Batching"
|
||||
)
|
||||
|
||||
type postData struct {
|
||||
SenderName string
|
||||
ChannelName string
|
||||
Message template.HTML
|
||||
MessageURL string
|
||||
SenderPhoto string
|
||||
PostPhoto string
|
||||
Time string
|
||||
ShowChannelIcon bool
|
||||
OtherChannelMembersCount int
|
||||
MessageAttachments []*EmailMessageAttachment
|
||||
}
|
||||
|
||||
func (es *Service) InitEmailBatching() {
|
||||
if *es.config().EmailSettings.EnableEmailBatching {
|
||||
if es.EmailBatching == nil {
|
||||
es.EmailBatching = NewEmailBatchingJob(es, *es.config().EmailSettings.EmailBatchingBufferSize)
|
||||
}
|
||||
|
||||
// note that we don't support changing EmailBatchingBufferSize without restarting the server
|
||||
|
||||
es.EmailBatching.Start()
|
||||
}
|
||||
}
|
||||
|
||||
func (es *Service) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
|
||||
if !*es.config().EmailSettings.EnableEmailBatching {
|
||||
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
if !es.EmailBatching.Add(user, post, team) {
|
||||
mlog.Error("Email batching job's receiving buffer was full. Please increase the EmailBatchingBufferSize. Falling back to sending immediate mail.")
|
||||
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.channel_full.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type batchedNotification struct {
|
||||
userID string
|
||||
post *model.Post
|
||||
teamName string
|
||||
}
|
||||
|
||||
type EmailBatchingJob struct {
|
||||
config func() *model.Config
|
||||
service *Service
|
||||
|
||||
newNotifications chan *batchedNotification
|
||||
pendingNotifications map[string][]*batchedNotification
|
||||
task *model.ScheduledTask
|
||||
taskMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
|
||||
return &EmailBatchingJob{
|
||||
config: es.config,
|
||||
service: es,
|
||||
newNotifications: make(chan *batchedNotification, bufferSize),
|
||||
pendingNotifications: make(map[string][]*batchedNotification),
|
||||
}
|
||||
}
|
||||
|
||||
func (job *EmailBatchingJob) Start() {
|
||||
mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.config().EmailSettings.EmailBatchingInterval))
|
||||
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.config().EmailSettings.EmailBatchingInterval)*time.Second)
|
||||
|
||||
job.taskMutex.Lock()
|
||||
oldTask := job.task
|
||||
job.task = newTask
|
||||
job.taskMutex.Unlock()
|
||||
|
||||
if oldTask != nil {
|
||||
oldTask.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
// Stop will cancel the task properly, flushing out any pending notifications.
|
||||
// Although this still won't send those notifications which are yet to be sent
|
||||
// due to a user's PreferenceNameEmailInterval.
|
||||
func (job *EmailBatchingJob) Stop() {
|
||||
job.taskMutex.Lock()
|
||||
if task := job.task; task != nil {
|
||||
task.Cancel()
|
||||
}
|
||||
job.taskMutex.Unlock()
|
||||
}
|
||||
|
||||
func (job *EmailBatchingJob) Add(user *model.User, post *model.Post, team *model.Team) bool {
|
||||
notification := &batchedNotification{
|
||||
userID: user.Id,
|
||||
post: post,
|
||||
teamName: team.Name,
|
||||
}
|
||||
|
||||
select {
|
||||
case job.newNotifications <- notification:
|
||||
return true
|
||||
default:
|
||||
// return false if we couldn't queue the email notification so that we can send an immediate email
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (job *EmailBatchingJob) CheckPendingEmails() {
|
||||
job.handleNewNotifications()
|
||||
|
||||
// it's a bit weird to pass the send email function through here, but it makes it so that we can test
|
||||
// without actually sending emails
|
||||
job.checkPendingNotifications(time.Now(), job.service.sendBatchedEmailNotification)
|
||||
|
||||
mlog.Debug("Email batching job ran. Notifications might be still pending.", mlog.Int("number_of_users", len(job.pendingNotifications)))
|
||||
}
|
||||
|
||||
func (job *EmailBatchingJob) handleNewNotifications() {
|
||||
receiving := true
|
||||
|
||||
// read in new notifications to send
|
||||
for receiving {
|
||||
select {
|
||||
case notification := <-job.newNotifications:
|
||||
userID := notification.userID
|
||||
|
||||
if _, ok := job.pendingNotifications[userID]; !ok {
|
||||
job.pendingNotifications[userID] = []*batchedNotification{notification}
|
||||
} else {
|
||||
job.pendingNotifications[userID] = append(job.pendingNotifications[userID], notification)
|
||||
}
|
||||
default:
|
||||
receiving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler func(string, []*batchedNotification)) {
|
||||
for userID, notifications := range job.pendingNotifications {
|
||||
// Defensive code.
|
||||
if len(notifications) == 0 {
|
||||
mlog.Warn("Unexpected result. Got 0 pending notifications for batched email.", mlog.String("user_id", userID))
|
||||
continue
|
||||
}
|
||||
|
||||
// get how long we need to wait to send notifications to the user
|
||||
var interval int64
|
||||
preference, err := job.service.store.Preference().Get(userID, model.PreferenceCategoryNotifications, model.PreferenceNameEmailInterval)
|
||||
if err != nil {
|
||||
// use the default batching interval if an error occurs while fetching user preferences
|
||||
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
|
||||
} else {
|
||||
if value, err := strconv.ParseInt(preference.Value, 10, 64); err != nil {
|
||||
// // use the default batching interval if an error occurs while deserializing user preferences
|
||||
interval, _ = strconv.ParseInt(model.PreferenceEmailIntervalBatchingSeconds, 10, 64)
|
||||
} else {
|
||||
interval = value
|
||||
}
|
||||
}
|
||||
|
||||
batchStartTime := notifications[0].post.CreateAt
|
||||
// Ignore if it isn't time yet to send.
|
||||
if now.Sub(time.UnixMilli(batchStartTime)) <= time.Duration(interval)*time.Second {
|
||||
continue
|
||||
}
|
||||
|
||||
// If the user has viewed any channels in this team since the notification was queued, delete
|
||||
// all queued notifications
|
||||
inspectedTeamNames := make(map[string]string)
|
||||
for _, notification := range notifications {
|
||||
// at most, we'll do one check for each team that notifications were sent for
|
||||
if inspectedTeamNames[notification.teamName] != "" {
|
||||
continue
|
||||
}
|
||||
|
||||
team, nErr := job.service.store.Team().GetByName(notifications[0].teamName)
|
||||
if nErr != nil {
|
||||
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
|
||||
continue
|
||||
}
|
||||
|
||||
if team != nil {
|
||||
inspectedTeamNames[notification.teamName] = team.Id
|
||||
}
|
||||
|
||||
channelMembers, err := job.service.store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
|
||||
if err != nil {
|
||||
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
|
||||
continue
|
||||
}
|
||||
|
||||
deleted := false
|
||||
for _, channelMember := range channelMembers {
|
||||
if channelMember.LastViewedAt >= batchStartTime {
|
||||
mlog.Debug("Deleted notifications for user", mlog.String("user_id", userID))
|
||||
delete(job.pendingNotifications, userID)
|
||||
deleted = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if deleted {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// The notifications might have been cleared from the above step.
|
||||
// We need to check again.
|
||||
if len(job.pendingNotifications[userID]) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
handler(userID, job.pendingNotifications[userID])
|
||||
delete(job.pendingNotifications, userID)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the name is longer than i characters, replace remaining characters with ...
|
||||
*/
|
||||
func truncateUserNames(name string, i int) string {
|
||||
runes := []rune(name)
|
||||
if len(runes) > i {
|
||||
newString := string(runes[:i])
|
||||
return newString + "..."
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
func (es *Service) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
|
||||
user, err := es.userService.GetUser(userID)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to find recipient for batched email notification")
|
||||
return
|
||||
}
|
||||
|
||||
translateFunc := i18n.GetUserTranslations(user.Locale)
|
||||
displayNameFormat := *es.config().TeamSettings.TeammateNameDisplay
|
||||
siteURL := *es.config().ServiceSettings.SiteURL
|
||||
|
||||
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
|
||||
embeddedFiles := make(map[string]io.Reader)
|
||||
|
||||
emailNotificationContentsType := model.EmailNotificationContentsFull
|
||||
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
|
||||
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
|
||||
}
|
||||
|
||||
// check if user has CRT set to ON
|
||||
appCRT := *es.config().ServiceSettings.CollapsedThreads
|
||||
threadsEnabled := appCRT == model.CollapsedThreadsAlwaysOn
|
||||
if !threadsEnabled && appCRT != model.CollapsedThreadsDisabled {
|
||||
threadsEnabled = appCRT == model.CollapsedThreadsDefaultOn
|
||||
// check if a participant has overridden collapsed threads settings
|
||||
if preference, errCrt := es.store.Preference().Get(userID, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); errCrt == nil {
|
||||
threadsEnabled = preference.Value == "on"
|
||||
}
|
||||
}
|
||||
|
||||
if emailNotificationContentsType == model.EmailNotificationContentsFull {
|
||||
for i, notification := range notifications {
|
||||
sender, errSender := es.userService.GetUser(notification.post.UserId)
|
||||
if errSender != nil {
|
||||
mlog.Warn("Unable to find sender of post for batched email notification")
|
||||
}
|
||||
|
||||
channel, errCh := es.store.Channel().Get(notification.post.ChannelId, true)
|
||||
if errCh != nil {
|
||||
mlog.Warn("Unable to find channel of post for batched email notification")
|
||||
}
|
||||
|
||||
senderProfileImage, _, errProfileImage := es.userService.GetProfileImage(sender)
|
||||
if errProfileImage != nil {
|
||||
mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage))
|
||||
}
|
||||
|
||||
senderPhoto := fmt.Sprintf("user-avatar-%d.png", i)
|
||||
if senderProfileImage != nil {
|
||||
embeddedFiles[senderPhoto] = bytes.NewReader(senderProfileImage)
|
||||
}
|
||||
|
||||
tm := time.Unix(notification.post.CreateAt/1000, 0)
|
||||
timezone, _ := tm.Zone()
|
||||
|
||||
t := translateFunc("api.email_batching.send_batched_email_notification.time", map[string]any{
|
||||
"Hour": tm.Hour(),
|
||||
"Minute": fmt.Sprintf("%02d", tm.Minute()),
|
||||
"Month": translateFunc(tm.Month().String()),
|
||||
"Day": tm.Day(),
|
||||
"Year": tm.Year(),
|
||||
"TimeZone": timezone,
|
||||
})
|
||||
|
||||
MessageURL := siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id
|
||||
|
||||
channelDisplayName := channel.DisplayName
|
||||
showChannelIcon := true
|
||||
otherChannelMembersCount := 0
|
||||
|
||||
if threadsEnabled && notification.post.RootId != "" {
|
||||
props := map[string]any{"channelName": channelDisplayName}
|
||||
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads", props)
|
||||
if channel.Type == model.ChannelTypeDirect {
|
||||
channelDisplayName = translateFunc("api.push_notification.title.collapsed_threads_dm")
|
||||
}
|
||||
}
|
||||
|
||||
if channel.Type == model.ChannelTypeGroup {
|
||||
otherChannelMembersCount = len(strings.Split(channelDisplayName, ",")) - 1
|
||||
showChannelIcon = false
|
||||
channelDisplayName = truncateUserNames(channel.DisplayName, 11)
|
||||
}
|
||||
|
||||
postsData = append(postsData, &postData{
|
||||
SenderPhoto: senderPhoto,
|
||||
SenderName: truncateUserNames(sender.GetDisplayName(displayNameFormat), 22),
|
||||
Time: t,
|
||||
ChannelName: channelDisplayName,
|
||||
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
|
||||
MessageURL: MessageURL,
|
||||
ShowChannelIcon: showChannelIcon,
|
||||
OtherChannelMembersCount: otherChannelMembersCount,
|
||||
MessageAttachments: ProcessMessageAttachments(notification.post, siteURL),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
|
||||
|
||||
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]any{
|
||||
"SiteName": es.config().TeamSettings.SiteName,
|
||||
"Year": tm.Year(),
|
||||
"Month": translateFunc(tm.Month().String()),
|
||||
"Day": tm.Day(),
|
||||
})
|
||||
|
||||
data := es.NewEmailTemplateData(user.Locale)
|
||||
data.Props["SiteURL"] = siteURL
|
||||
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1)
|
||||
data.Props["SubTitle"] = translateFunc("api.email_batching.send_batched_email_notification.subTitle")
|
||||
data.Props["Button"] = translateFunc("api.email_batching.send_batched_email_notification.button")
|
||||
data.Props["ButtonURL"] = siteURL
|
||||
data.Props["Posts"] = postsData
|
||||
data.Props["MessageButton"] = translateFunc("api.email_batching.send_batched_email_notification.messageButton")
|
||||
data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title")
|
||||
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
|
||||
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
|
||||
|
||||
renderedPage, renderErr := es.templatesContainer.RenderToString("messages_notification", data)
|
||||
if renderErr != nil {
|
||||
mlog.Error("Unable to render email", mlog.Err(renderErr))
|
||||
}
|
||||
|
||||
if nErr := es.SendMailWithEmbeddedFiles(user.Email, subject, renderedPage, embeddedFiles, "", "", "", "BatchedEmailNotification"); nErr != nil {
|
||||
mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr))
|
||||
}
|
||||
}
|
||||
285
server/channels/app/email/email_batching_test.go
Обычный файл
285
server/channels/app/email/email_batching_test.go
Обычный файл
@@ -0,0 +1,285 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func TestHandleNewNotifications(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
defer th.TearDown()
|
||||
|
||||
id1 := model.NewId()
|
||||
id2 := model.NewId()
|
||||
id3 := model.NewId()
|
||||
|
||||
// test queueing of received posts by user
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
job.handleNewNotifications()
|
||||
|
||||
require.Empty(t, job.pendingNotifications, "shouldn't have added any pending notifications")
|
||||
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"})
|
||||
require.Empty(t, job.pendingNotifications, "shouldn't have added any pending notifications")
|
||||
|
||||
job.handleNewNotifications()
|
||||
require.Len(t, job.pendingNotifications, 1, "should have received posts for 1 user")
|
||||
require.Len(t, job.pendingNotifications[id1], 1, "should have received 1 post for user")
|
||||
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.handleNewNotifications()
|
||||
require.Len(t, job.pendingNotifications, 1, "should have received posts for 1 user")
|
||||
require.Len(t, job.pendingNotifications[id1], 2, "should have received 2 posts for user1")
|
||||
|
||||
job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.handleNewNotifications()
|
||||
require.Len(t, job.pendingNotifications, 2, "should have received posts for 2 users")
|
||||
require.Len(t, job.pendingNotifications[id1], 2, "should have received 2 posts for user1")
|
||||
require.Len(t, job.pendingNotifications[id2], 1, "should have received 1 post for user2")
|
||||
|
||||
job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id3}, &model.Post{UserId: id3, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id2}, &model.Post{UserId: id2, Message: "test"}, &model.Team{Name: "team"})
|
||||
job.handleNewNotifications()
|
||||
require.Len(t, job.pendingNotifications, 3, "should have received posts for 3 users")
|
||||
require.Len(t, job.pendingNotifications[id1], 3, "should have received 3 posts for user1")
|
||||
require.Len(t, job.pendingNotifications[id2], 3, "should have received 3 posts for user2")
|
||||
require.Len(t, job.pendingNotifications[id3], 1, "should have received 1 post for user3")
|
||||
|
||||
// test ordering of received posts
|
||||
job = NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test1"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test2"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test3"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test4"}, &model.Team{Name: "team"})
|
||||
job.Add(&model.User{Id: id2}, &model.Post{UserId: id1, Message: "test5"}, &model.Team{Name: "team"})
|
||||
job.handleNewNotifications()
|
||||
assert.Equal(t, job.pendingNotifications[id1][0].post.Message, "test1", "incorrect order of received posts for user1")
|
||||
assert.Equal(t, job.pendingNotifications[id1][1].post.Message, "test2", "incorrect order of received posts for user1")
|
||||
assert.Equal(t, job.pendingNotifications[id1][2].post.Message, "test4", "incorrect order of received posts for user1")
|
||||
assert.Equal(t, job.pendingNotifications[id2][0].post.Message, "test3", "incorrect order of received posts for user2")
|
||||
assert.Equal(t, job.pendingNotifications[id2][1].post.Message, "test5", "incorrect order of received posts for user2")
|
||||
}
|
||||
|
||||
func TestCheckPendingNotifications(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
{
|
||||
post: &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: 10000000,
|
||||
},
|
||||
teamName: th.BasicTeam.Name,
|
||||
},
|
||||
}
|
||||
|
||||
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 9999999
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
nErr := th.store.Preference().Save(model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PreferenceCategoryNotifications,
|
||||
Name: model.PreferenceNameEmailInterval,
|
||||
Value: "60",
|
||||
}})
|
||||
require.NoError(t, nErr)
|
||||
|
||||
// test that notifications aren't sent before interval
|
||||
job.checkPendingNotifications(time.Unix(10001, 0), func(string, []*batchedNotification) {})
|
||||
|
||||
require.NotNil(t, job.pendingNotifications[th.BasicUser.Id])
|
||||
require.Len(t, job.pendingNotifications[th.BasicUser.Id], 1, "shouldn't have sent queued post")
|
||||
|
||||
// test that notifications are cleared if the user has acted
|
||||
channelMember, err = th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 10001000
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We reset the interval to something shorter
|
||||
nErr = th.store.Preference().Save(model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PreferenceCategoryNotifications,
|
||||
Name: model.PreferenceNameEmailInterval,
|
||||
Value: "10",
|
||||
}})
|
||||
require.NoError(t, nErr)
|
||||
|
||||
var wasCalled int32
|
||||
job.checkPendingNotifications(time.Unix(10050, 0), func(string, []*batchedNotification) {
|
||||
atomic.StoreInt32(&wasCalled, int32(1))
|
||||
})
|
||||
|
||||
// A hack to check whether the handler was called.
|
||||
// It's not straightforward to just wait for it using a channel because the test should
|
||||
// NOT call the handler, and it will be called only if the test fails.
|
||||
time.Sleep(1 * time.Second)
|
||||
// We do a check outside the email handler, because otherwise, failing from
|
||||
// inside the handler doesn't let the .Go() function exit cleanly, and it gets
|
||||
// stuck during server shutdown, trying to wait for the goroutine to exit
|
||||
require.Equal(t, int32(0), atomic.LoadInt32(&wasCalled), "email handler should not have been called")
|
||||
|
||||
require.Nil(t, job.pendingNotifications[th.BasicUser.Id])
|
||||
require.Empty(t, job.pendingNotifications[th.BasicUser.Id], "should've remove queued post since user acted")
|
||||
|
||||
// test that notifications are sent if enough time passes since the first message
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
{
|
||||
post: &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: 10060000,
|
||||
Message: "post1",
|
||||
},
|
||||
teamName: th.BasicTeam.Name,
|
||||
},
|
||||
{
|
||||
post: &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: 10090000,
|
||||
Message: "post2",
|
||||
},
|
||||
teamName: th.BasicTeam.Name,
|
||||
},
|
||||
}
|
||||
|
||||
received := make(chan *model.Post, 2)
|
||||
|
||||
job.checkPendingNotifications(time.Unix(10130, 0), func(s string, notifications []*batchedNotification) {
|
||||
for _, notification := range notifications {
|
||||
received <- notification.post
|
||||
}
|
||||
})
|
||||
|
||||
require.Nil(t, job.pendingNotifications[th.BasicUser.Id], "shouldn't have sent queued post")
|
||||
|
||||
select {
|
||||
case post := <-received:
|
||||
require.Equal(t, post.Message, "post1", "should've received post1 first")
|
||||
case <-time.After(5 * time.Second):
|
||||
require.Fail(t, "timed out waiting for first post notification")
|
||||
}
|
||||
|
||||
select {
|
||||
case post := <-received:
|
||||
require.Equal(t, post.Message, "post2", "should've received post2 second")
|
||||
case <-time.After(5 * time.Second):
|
||||
require.Fail(t, "timed out waiting for second post notification")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that email batch interval defaults to 15 minutes for users that haven't explicitly set this preference
|
||||
*/
|
||||
func TestCheckPendingNotificationsDefaultInterval(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
// bypasses recent user activity check
|
||||
require.NotNil(t, th.store)
|
||||
require.NotNil(t, th.store.Channel())
|
||||
|
||||
require.NotNil(t, th.BasicUser)
|
||||
require.NotNil(t, th.BasicChannel)
|
||||
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 9999000
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
{
|
||||
post: &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: 10000000,
|
||||
},
|
||||
teamName: th.BasicTeam.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// notifications should not be sent 1s after post was created, because default batch interval is 15mins
|
||||
job.checkPendingNotifications(time.Unix(10001, 0), func(string, []*batchedNotification) {})
|
||||
require.NotNil(t, job.pendingNotifications[th.BasicUser.Id])
|
||||
require.Len(t, job.pendingNotifications[th.BasicUser.Id], 1, "shouldn't have sent queued post")
|
||||
|
||||
// notifications should be sent 901s after post was created, because default batch interval is 15mins
|
||||
job.checkPendingNotifications(time.Unix(10901, 0), func(string, []*batchedNotification) {})
|
||||
require.Nil(t, job.pendingNotifications[th.BasicUser.Id])
|
||||
require.Empty(t, job.pendingNotifications[th.BasicUser.Id], "should have sent queued post")
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that email batch interval defaults to 15 minutes if user preference is invalid
|
||||
*/
|
||||
func TestCheckPendingNotificationsCantParseInterval(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
require.NotNil(t, th.store)
|
||||
require.NotNil(t, th.store.Channel())
|
||||
require.NotNil(t, th.BasicChannel)
|
||||
require.NotNil(t, th.BasicUser)
|
||||
// bypasses recent user activity check
|
||||
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 9999000
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
// preference value is not an integer, so we'll fall back to the default 15min value
|
||||
nErr := th.store.Preference().Save(model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PreferenceCategoryNotifications,
|
||||
Name: model.PreferenceNameEmailInterval,
|
||||
Value: "notAnIntegerValue",
|
||||
}})
|
||||
require.NoError(t, nErr)
|
||||
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
{
|
||||
post: &model.Post{
|
||||
UserId: th.BasicUser.Id,
|
||||
ChannelId: th.BasicChannel.Id,
|
||||
CreateAt: 10000000,
|
||||
},
|
||||
teamName: th.BasicTeam.Name,
|
||||
},
|
||||
}
|
||||
|
||||
// notifications should not be sent 1s after post was created, because default batch interval is 15mins
|
||||
job.checkPendingNotifications(time.Unix(10001, 0), func(string, []*batchedNotification) {})
|
||||
require.NotNil(t, job.pendingNotifications[th.BasicUser.Id])
|
||||
require.Len(t, job.pendingNotifications[th.BasicUser.Id], 1, "shouldn't have sent queued post")
|
||||
|
||||
// notifications should be sent 901s after post was created, because default batch interval is 15mins
|
||||
job.checkPendingNotifications(time.Unix(10901, 0), func(string, []*batchedNotification) {})
|
||||
|
||||
require.Nil(t, job.pendingNotifications[th.BasicUser.Id], "should have sent queued post")
|
||||
}
|
||||
426
server/channels/app/email/email_test.go
Обычный файл
426
server/channels/app/email/email_test.go
Обычный файл
@@ -0,0 +1,426 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
|
||||
)
|
||||
|
||||
func TestCondenseSiteURL(t *testing.T) {
|
||||
require.Equal(t, "", condenseSiteURL(""))
|
||||
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com"))
|
||||
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com/"))
|
||||
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com"))
|
||||
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com/"))
|
||||
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath"))
|
||||
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath/"))
|
||||
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath"))
|
||||
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath/"))
|
||||
|
||||
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080"))
|
||||
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080/"))
|
||||
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080"))
|
||||
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080/"))
|
||||
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath"))
|
||||
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath/"))
|
||||
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath"))
|
||||
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath/"))
|
||||
}
|
||||
|
||||
func TestSendInviteEmails(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "test@example.com"
|
||||
|
||||
retrieveEmail := func(t *testing.T) mail.JSONMessageInbucket {
|
||||
t.Helper()
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
return resultsEmail
|
||||
}
|
||||
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "http://testserver", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.HTML, "test-user", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.Text, "http://testserver", "Wrong received message %s", email.Body.Text)
|
||||
require.Contains(t, email.Body.Text, "test-user", "Wrong received message %s", email.Body.Text)
|
||||
}
|
||||
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
*cfg.EmailSettings.SendEmailNotifications = false
|
||||
})
|
||||
t.Run("SendInviteEmails", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails can return error when SMTP connection fails", func(t *testing.T) {
|
||||
originalPort := *th.service.config().EmailSettings.SMTPPort
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
|
||||
*cfg.EmailSettings.SMTPPort = "5432"
|
||||
})
|
||||
defer th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
|
||||
*cfg.EmailSettings.SMTPPort = originalPort
|
||||
})
|
||||
|
||||
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, true, false, false)
|
||||
require.Error(t, err)
|
||||
|
||||
err = th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false, false, false)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmails", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmail can return error when SMTP connection fails", func(t *testing.T) {
|
||||
originalPort := *th.service.config().EmailSettings.SMTPPort
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
|
||||
*cfg.EmailSettings.SMTPPort = "5432"
|
||||
})
|
||||
defer th.UpdateConfig(func(cfg *model.Config) {
|
||||
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
|
||||
*cfg.EmailSettings.SMTPPort = originalPort
|
||||
})
|
||||
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
"hello world",
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.Error(t, err)
|
||||
|
||||
})
|
||||
|
||||
t.Run("SendGuestInviteEmails should sanitize HTML input", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
message := `<a href="http://testserver">sanitized message</a>`
|
||||
err := th.service.SendGuestInviteEmails(
|
||||
th.BasicTeam,
|
||||
[]*model.Channel{th.BasicChannel},
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
nil,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
message,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.NotContains(t, email.Body.HTML, message)
|
||||
require.Contains(t, email.Body.HTML, "sanitized message")
|
||||
require.Contains(t, email.Body.Text, "sanitized message")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for system user", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=su")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for system admin", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=sa")
|
||||
})
|
||||
|
||||
t.Run("SendInviteEmails should contain button URL with 'started by role' param for first system admin", func(t *testing.T) {
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(
|
||||
th.BasicTeam,
|
||||
"test-user",
|
||||
th.BasicUser.Id,
|
||||
[]string{emailTo},
|
||||
"http://testserver",
|
||||
nil,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
email := retrieveEmail(t)
|
||||
require.Contains(t, email.Body.HTML, "&sbr=fa")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendCloudUpgradedEmail(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "testclouduser@example.com"
|
||||
emailToUsername := strings.Split(emailTo, "@")[0]
|
||||
|
||||
t.Run("SendCloudMonthlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "You'll be billed from", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "Open Mattermost", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Len(t, resultsEmail.Attachments, 0)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
// Send Update to Monthly Plan email
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", false, make(map[string]io.Reader))
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
|
||||
t.Run("SendCloudYearlyUpgradedEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Body.Text, "You are now upgraded!", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "SomeName workspace has now been upgraded", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "View your invoice", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Len(t, resultsEmail.Attachments, 1)
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
// Send Update to Monthly Plan email
|
||||
var embeddedFiles = map[string]io.Reader{
|
||||
"filename": bytes.NewReader([]byte("Test")),
|
||||
}
|
||||
err := th.service.SendCloudUpgradeConfirmationEmail(emailTo, emailToUsername, "June 23, 2200", th.BasicUser.Locale, "https://example.com", "SomeName", true, embeddedFiles)
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSendCloudWelcomeEmail(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
emailTo := "testclouduser@example.com"
|
||||
|
||||
t.Run("TestSendCloudWelcomeEmail", func(t *testing.T) {
|
||||
verifyMailbox := func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Skipf("No email was received, maybe due load on the server: %v", err2)
|
||||
}
|
||||
|
||||
require.Len(t, resultsMailbox, 1)
|
||||
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
|
||||
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
|
||||
require.NoError(t, err, "Could not get message from mailbox")
|
||||
require.Contains(t, resultsEmail.Subject, "Congratulations!", "Wrong subject message %s", resultsEmail.Subject)
|
||||
require.Contains(t, resultsEmail.Body.Text, "Your workspace is ready to go!", "Wrong body %s", resultsEmail.Body.Text)
|
||||
|
||||
}
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendCloudWelcomeEmail(emailTo, th.BasicUser.Locale, "inviteID", "SomeName", "example.com", "https://example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
verifyMailbox(t)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMailServiceConfig(t *testing.T) {
|
||||
configuredReplyTo := "feedbackexample@test.com"
|
||||
customReplyTo := "customreplyto@test.com"
|
||||
|
||||
emailService := Service{
|
||||
config: func() *model.Config {
|
||||
return &model.Config{
|
||||
ServiceSettings: model.ServiceSettings{
|
||||
SiteURL: model.NewString(""),
|
||||
},
|
||||
EmailSettings: model.EmailSettings{
|
||||
EnableSignUpWithEmail: new(bool),
|
||||
EnableSignInWithEmail: new(bool),
|
||||
EnableSignInWithUsername: new(bool),
|
||||
SendEmailNotifications: new(bool),
|
||||
UseChannelInEmailNotifications: new(bool),
|
||||
RequireEmailVerification: new(bool),
|
||||
FeedbackName: new(string),
|
||||
FeedbackEmail: new(string),
|
||||
ReplyToAddress: model.NewString(configuredReplyTo),
|
||||
FeedbackOrganization: new(string),
|
||||
EnableSMTPAuth: new(bool),
|
||||
SMTPUsername: new(string),
|
||||
SMTPPassword: new(string),
|
||||
SMTPServer: new(string),
|
||||
SMTPPort: new(string),
|
||||
SMTPServerTimeout: new(int),
|
||||
ConnectionSecurity: new(string),
|
||||
SendPushNotifications: new(bool),
|
||||
PushNotificationServer: new(string),
|
||||
PushNotificationContents: new(string),
|
||||
PushNotificationBuffer: new(int),
|
||||
EnableEmailBatching: new(bool),
|
||||
EmailBatchingBufferSize: new(int),
|
||||
EmailBatchingInterval: new(int),
|
||||
EnablePreviewModeBanner: new(bool),
|
||||
SkipServerCertificateVerification: new(bool),
|
||||
EmailNotificationContentsType: new(string),
|
||||
LoginButtonColor: new(string),
|
||||
LoginButtonBorderColor: new(string),
|
||||
LoginButtonTextColor: new(string),
|
||||
EnableInactivityEmail: new(bool),
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("use custom replyto instead of configured replyto", func(t *testing.T) {
|
||||
mailConfig := emailService.mailServiceConfig(customReplyTo)
|
||||
require.Equal(t, customReplyTo, mailConfig.ReplyToAddress)
|
||||
})
|
||||
|
||||
t.Run("use configured replyto", func(t *testing.T) {
|
||||
mailConfig := emailService.mailServiceConfig("")
|
||||
require.Equal(t, configuredReplyTo, mailConfig.ReplyToAddress)
|
||||
})
|
||||
}
|
||||
14
server/channels/app/email/errors.go
Обычный файл
14
server/channels/app/email/errors.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
var (
|
||||
CreateEmailTokenError = errors.New("could not create token")
|
||||
NoRateLimiterError = errors.New("the rate limit could not be found")
|
||||
SetupRateLimiterError = errors.New("the rate limiter could not be set")
|
||||
RateLimitExceededError = errors.New("the rate limit is exceeded")
|
||||
SendMailError = errors.New("could not send the email")
|
||||
)
|
||||
300
server/channels/app/email/helper_test.go
Обычный файл
300
server/channels/app/email/helper_test.go
Обычный файл
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
|
||||
"github.com/mattermost/mattermost-server/v6/server/config"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
|
||||
)
|
||||
|
||||
type TestHelper struct {
|
||||
service *Service
|
||||
configStore *config.Store
|
||||
store store.Store
|
||||
workspace string
|
||||
|
||||
BasicTeam *model.Team
|
||||
BasicChannel *model.Channel
|
||||
BasicUser *model.User
|
||||
BasicUser2 *model.User
|
||||
|
||||
SystemAdminUser *model.User
|
||||
LogBuffer *bytes.Buffer
|
||||
}
|
||||
|
||||
func Setup(tb testing.TB) *TestHelper {
|
||||
if testing.Short() {
|
||||
tb.SkipNow()
|
||||
}
|
||||
dbStore := mainHelper.GetStore()
|
||||
dbStore.DropAllTables()
|
||||
dbStore.MarkSystemRanUnitTests()
|
||||
mainHelper.PreloadMigrations()
|
||||
|
||||
return setupTestHelper(dbStore, tb)
|
||||
}
|
||||
|
||||
func SetupWithStoreMock(tb testing.TB) *TestHelper {
|
||||
mockStore := testlib.GetMockStoreForSetupFunctions()
|
||||
th := setupTestHelper(mockStore, tb)
|
||||
statusMock := mocks.StatusStore{}
|
||||
statusMock.On("UpdateExpiredDNDStatuses").Return([]*model.Status{}, nil)
|
||||
statusMock.On("Get", "user1").Return(&model.Status{UserId: "user1", Status: model.StatusOnline}, nil)
|
||||
statusMock.On("UpdateLastActivityAt", "user1", mock.Anything).Return(nil)
|
||||
statusMock.On("SaveOrUpdate", mock.AnythingOfType("*model.Status")).Return(nil)
|
||||
emptyMockStore := mocks.Store{}
|
||||
emptyMockStore.On("Close").Return(nil)
|
||||
emptyMockStore.On("Status").Return(&statusMock)
|
||||
th.service.store = &emptyMockStore
|
||||
return th
|
||||
}
|
||||
|
||||
func setupTestHelper(s store.Store, tb testing.TB) *TestHelper {
|
||||
tempWorkspace, err := os.MkdirTemp("", "userservicetest")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configStore := config.NewTestMemoryStore()
|
||||
|
||||
config := configStore.Get()
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.PluginSettings.AutomaticPrepackagedPlugins = false
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
*config.TeamSettings.MaxUsersPerTeam = 50
|
||||
*config.RateLimitSettings.Enable = false
|
||||
*config.TeamSettings.EnableOpenServer = true
|
||||
// Disable strict password requirements for test
|
||||
*config.PasswordSettings.MinimumLength = 5
|
||||
*config.PasswordSettings.Lowercase = false
|
||||
*config.PasswordSettings.Uppercase = false
|
||||
*config.PasswordSettings.Symbol = false
|
||||
*config.PasswordSettings.Number = false
|
||||
configStore.Set(config)
|
||||
|
||||
licenseFn := func() *model.License { return model.NewTestLicense() }
|
||||
|
||||
us, err := users.New(users.ServiceConfig{
|
||||
UserStore: s.User(),
|
||||
SessionStore: s.Session(),
|
||||
OAuthStore: s.OAuth(),
|
||||
ConfigFn: configStore.Get,
|
||||
LicenseFn: licenseFn,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
templatesDir, ok := templates.GetTemplateDirectory()
|
||||
if !ok {
|
||||
panic("failed find server templates")
|
||||
}
|
||||
htmlTemplateWatcher, errorsChan, err := templates.NewWithWatcher(templatesDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for err2 := range errorsChan {
|
||||
mlog.Error("Server templates error", mlog.Err(err2))
|
||||
}
|
||||
}()
|
||||
|
||||
service := &Service{
|
||||
store: s,
|
||||
userService: us,
|
||||
license: licenseFn,
|
||||
config: configStore.Get,
|
||||
templatesContainer: htmlTemplateWatcher,
|
||||
}
|
||||
|
||||
if err := service.setUpRateLimiters(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &TestHelper{
|
||||
service: service,
|
||||
configStore: configStore,
|
||||
store: s,
|
||||
LogBuffer: &bytes.Buffer{},
|
||||
workspace: tempWorkspace,
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) InitBasic() *TestHelper {
|
||||
th.BasicTeam = th.CreateTeam()
|
||||
|
||||
th.SystemAdminUser = th.CreateUser()
|
||||
th.SystemAdminUser, _ = th.service.userService.GetUser(th.SystemAdminUser.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.SystemAdminUser)
|
||||
|
||||
th.BasicUser = th.CreateUser()
|
||||
th.BasicUser, _ = th.service.userService.GetUser(th.BasicUser.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.BasicUser)
|
||||
|
||||
th.BasicUser2 = th.CreateUser()
|
||||
th.BasicUser2, _ = th.service.userService.GetUser(th.BasicUser2.Id)
|
||||
th.addUserToTeam(th.BasicTeam, th.BasicUser2)
|
||||
|
||||
th.BasicChannel = th.createChannel(th.BasicTeam, string(model.ChannelTypeOpen))
|
||||
th.addUserToChannel(th.BasicChannel, th.SystemAdminUser)
|
||||
th.addUserToChannel(th.BasicChannel, th.BasicUser)
|
||||
th.addUserToChannel(th.BasicChannel, th.BasicUser2)
|
||||
|
||||
return th
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateTeam() *model.Team {
|
||||
id := model.NewId()
|
||||
team := &model.Team{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name" + id,
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Type: model.TeamOpen,
|
||||
}
|
||||
|
||||
var err error
|
||||
if team, err = th.store.Team().Save(team); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return team
|
||||
}
|
||||
|
||||
func (th *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
|
||||
id := model.NewId()
|
||||
|
||||
channel := &model.Channel{
|
||||
DisplayName: "dn_" + id,
|
||||
Name: "name_" + id,
|
||||
Type: model.ChannelType(channelType),
|
||||
TeamId: team.Id,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
var err error
|
||||
if channel, err = th.store.Channel().Save(channel, *th.configStore.Get().TeamSettings.MaxChannelsPerTeam); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return channel
|
||||
}
|
||||
|
||||
func (th *TestHelper) addUserToChannel(channel *model.Channel, user *model.User) *model.ChannelMember {
|
||||
newMember := &model.ChannelMember{
|
||||
ChannelId: channel.Id,
|
||||
UserId: user.Id,
|
||||
NotifyProps: model.GetDefaultChannelNotifyProps(),
|
||||
SchemeGuest: user.IsGuest(),
|
||||
SchemeUser: !user.IsGuest(),
|
||||
}
|
||||
|
||||
var err error
|
||||
newMember, err = th.store.Channel().SaveMember(newMember)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return newMember
|
||||
}
|
||||
|
||||
func (th *TestHelper) addUserToTeam(team *model.Team, user *model.User) *model.TeamMember {
|
||||
tm := &model.TeamMember{
|
||||
TeamId: team.Id,
|
||||
UserId: user.Id,
|
||||
SchemeGuest: user.IsGuest(),
|
||||
SchemeUser: !user.IsGuest(),
|
||||
}
|
||||
|
||||
var err error
|
||||
tm, err = th.store.Team().SaveMember(tm, *th.service.config().TeamSettings.MaxUsersPerTeam)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return tm
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUser() *model.User {
|
||||
return th.CreateUserOrGuest(false)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateGuest() *model.User {
|
||||
return th.CreateUserOrGuest(true)
|
||||
}
|
||||
|
||||
func (th *TestHelper) CreateUserOrGuest(guest bool) *model.User {
|
||||
id := model.NewId()
|
||||
|
||||
user := &model.User{
|
||||
Email: "success+" + id + "@simulator.amazonses.com",
|
||||
Username: "un_" + id,
|
||||
Nickname: "nn_" + id,
|
||||
Password: "Password1",
|
||||
EmailVerified: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
if guest {
|
||||
if user, err = th.service.userService.CreateUser(user, users.UserCreateOptions{Guest: true}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
if user, err = th.service.userService.CreateUser(user, users.UserCreateOptions{}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func (th *TestHelper) TearDown() {
|
||||
th.configStore.Close()
|
||||
|
||||
th.store.Close()
|
||||
|
||||
if th.workspace != "" {
|
||||
os.RemoveAll(th.workspace)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) UpdateConfig(f func(*model.Config)) {
|
||||
if th.configStore.IsReadOnly() {
|
||||
return
|
||||
}
|
||||
old := th.configStore.Get()
|
||||
updated := old.Clone()
|
||||
f(updated)
|
||||
if _, _, err := th.configStore.Set(updated); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (th *TestHelper) ConfigureInbucketMail() {
|
||||
inbucket_host := os.Getenv("CI_INBUCKET_HOST")
|
||||
if inbucket_host == "" {
|
||||
inbucket_host = "localhost"
|
||||
}
|
||||
inbucket_port := os.Getenv("CI_INBUCKET_SMTP_PORT")
|
||||
if inbucket_port == "" {
|
||||
inbucket_port = "10025"
|
||||
}
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.EmailSettings.SMTPServer = inbucket_host
|
||||
*cfg.EmailSettings.SMTPPort = inbucket_port
|
||||
})
|
||||
}
|
||||
32
server/channels/app/email/main_test.go
Обычный файл
32
server/channels/app/email/main_test.go
Обычный файл
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/testlib"
|
||||
)
|
||||
|
||||
var mainHelper *testlib.MainHelper
|
||||
var replicaFlag bool
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if f := flag.Lookup("mysql-replica"); f == nil {
|
||||
flag.BoolVar(&replicaFlag, "mysql-replica", false, "")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
var options = testlib.HelperOptions{
|
||||
EnableStore: true,
|
||||
EnableResources: true,
|
||||
WithReadReplica: replicaFlag,
|
||||
}
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
560
server/channels/app/email/mocks/ServiceInterface.go
Обычный файл
560
server/channels/app/email/mocks/ServiceInterface.go
Обычный файл
@@ -0,0 +1,560 @@
|
||||
// Code generated by mockery v2.10.4. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make email-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
io "io"
|
||||
|
||||
i18n "github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
|
||||
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
model "github.com/mattermost/mattermost-server/v6/model"
|
||||
|
||||
templates "github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
|
||||
|
||||
throttled "github.com/throttled/throttled"
|
||||
)
|
||||
|
||||
// ServiceInterface is an autogenerated mock type for the ServiceInterface type
|
||||
type ServiceInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AddNotificationEmailToBatch provides a mock function with given fields: user, post, team
|
||||
func (_m *ServiceInterface) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
|
||||
ret := _m.Called(user, post, team)
|
||||
|
||||
var r0 *model.AppError
|
||||
if rf, ok := ret.Get(0).(func(*model.User, *model.Post, *model.Team) *model.AppError); ok {
|
||||
r0 = rf(user, post, team)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.AppError)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// CreateVerifyEmailToken provides a mock function with given fields: userID, newEmail
|
||||
func (_m *ServiceInterface) CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error) {
|
||||
ret := _m.Called(userID, newEmail)
|
||||
|
||||
var r0 *model.Token
|
||||
if rf, ok := ret.Get(0).(func(string, string) *model.Token); ok {
|
||||
r0 = rf(userID, newEmail)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*model.Token)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(userID, newEmail)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// GetMessageForNotification provides a mock function with given fields: post, translateFunc
|
||||
func (_m *ServiceInterface) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
ret := _m.Called(post, translateFunc)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(*model.Post, i18n.TranslateFunc) string); ok {
|
||||
r0 = rf(post, translateFunc)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetPerDayEmailRateLimiter provides a mock function with given fields:
|
||||
func (_m *ServiceInterface) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 *throttled.GCRARateLimiter
|
||||
if rf, ok := ret.Get(0).(func() *throttled.GCRARateLimiter); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*throttled.GCRARateLimiter)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// InitEmailBatching provides a mock function with given fields:
|
||||
func (_m *ServiceInterface) InitEmailBatching() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// NewEmailTemplateData provides a mock function with given fields: locale
|
||||
func (_m *ServiceInterface) NewEmailTemplateData(locale string) templates.Data {
|
||||
ret := _m.Called(locale)
|
||||
|
||||
var r0 templates.Data
|
||||
if rf, ok := ret.Get(0).(func(string) templates.Data); ok {
|
||||
r0 = rf(locale)
|
||||
} else {
|
||||
r0 = ret.Get(0).(templates.Data)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendChangeUsernameEmail provides a mock function with given fields: newUsername, _a1, locale, siteURL
|
||||
func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(newUsername, _a1, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(newUsername, _a1, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendCloudUpgradeConfirmationEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles
|
||||
func (_m *ServiceInterface) SendCloudUpgradeConfirmationEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error {
|
||||
ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, bool, map[string]io.Reader) error); ok {
|
||||
r0 = rf(userEmail, name, trialEndDate, locale, siteURL, workspaceName, isYearly, embeddedFiles)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendCloudWelcomeEmail provides a mock function with given fields: userEmail, locale, teamInviteID, workSpaceName, dns, siteURL
|
||||
func (_m *ServiceInterface) SendCloudWelcomeEmail(userEmail string, locale string, teamInviteID string, workSpaceName string, dns string, siteURL string) error {
|
||||
ret := _m.Called(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok {
|
||||
r0 = rf(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDeactivateAccountEmail provides a mock function with given fields: _a0, locale, siteURL
|
||||
func (_m *ServiceInterface) SendDeactivateAccountEmail(_a0 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail14 provides a mock function with given fields: _a0, locale, siteURL, planName
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail14(_a0 string, locale string, siteURL string, planName string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL, planName)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL, planName)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail30 provides a mock function with given fields: _a0, locale, siteURL, planName
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail30(_a0 string, locale string, siteURL string, planName string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL, planName)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL, planName)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail45 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail45(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail60 provides a mock function with given fields: _a0, locale, siteURL
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail60(_a0 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail7 provides a mock function with given fields: _a0, locale, siteURL, planName
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail7(_a0 string, locale string, siteURL string, planName string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL, planName)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL, planName)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail75 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail75(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendDelinquencyEmail90 provides a mock function with given fields: _a0, locale, siteURL
|
||||
func (_m *ServiceInterface) SendDelinquencyEmail90(_a0 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendEmailChangeEmail provides a mock function with given fields: oldEmail, newEmail, locale, siteURL
|
||||
func (_m *ServiceInterface) SendEmailChangeEmail(oldEmail string, newEmail string, locale string, siteURL string) error {
|
||||
ret := _m.Called(oldEmail, newEmail, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(oldEmail, newEmail, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendEmailChangeVerifyEmail provides a mock function with given fields: newUserEmail, locale, siteURL, token
|
||||
func (_m *ServiceInterface) SendEmailChangeVerifyEmail(newUserEmail string, locale string, siteURL string, token string) error {
|
||||
ret := _m.Called(newUserEmail, locale, siteURL, token)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(newUserEmail, locale, siteURL, token)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendGuestInviteEmails provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin
|
||||
func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error {
|
||||
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, string, bool, bool, bool) error); ok {
|
||||
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin
|
||||
func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error {
|
||||
ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*model.Team, string, string, []string, string, *model.TeamInviteReminderData, bool, bool, bool) error); ok {
|
||||
r0 = rf(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendInviteEmailsToTeamAndChannels provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin
|
||||
func (_m *ServiceInterface) SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error) {
|
||||
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
|
||||
var r0 []*model.EmailInviteWithError
|
||||
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) []*model.EmailInviteWithError); ok {
|
||||
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*model.EmailInviteWithError)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, *model.TeamInviteReminderData, string, bool, bool, bool) error); ok {
|
||||
r1 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, reminderData, message, errorWhenNotSent, isSystemAdmin, isFirstAdmin)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SendLicenseInactivityEmail provides a mock function with given fields: _a0, name, locale, siteURL
|
||||
func (_m *ServiceInterface) SendLicenseInactivityEmail(_a0 string, name string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, name, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, name, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendLicenseUpForRenewalEmail provides a mock function with given fields: _a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration
|
||||
func (_m *ServiceInterface) SendLicenseUpForRenewalEmail(_a0 string, name string, locale string, siteURL string, ctaTitle string, ctaLink string, ctaText string, daysToExpiration int) error {
|
||||
ret := _m.Called(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string, string, int) error); ok {
|
||||
r0 = rf(_a0, name, locale, siteURL, ctaTitle, ctaLink, ctaText, daysToExpiration)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendMailWithEmbeddedFiles provides a mock function with given fields: to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category
|
||||
func (_m *ServiceInterface) SendMailWithEmbeddedFiles(to string, subject string, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error {
|
||||
ret := _m.Called(to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, map[string]io.Reader, string, string, string, string) error); ok {
|
||||
r0 = rf(to, subject, htmlBody, embeddedFiles, messageID, inReplyTo, references, category)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendMfaChangeEmail provides a mock function with given fields: _a0, activated, locale, siteURL
|
||||
func (_m *ServiceInterface) SendMfaChangeEmail(_a0 string, activated bool, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, activated, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, bool, string, string) error); ok {
|
||||
r0 = rf(_a0, activated, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendNoCardPaymentFailedEmail provides a mock function with given fields: _a0, locale, siteURL
|
||||
func (_m *ServiceInterface) SendNoCardPaymentFailedEmail(_a0 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendNotificationMail provides a mock function with given fields: to, subject, htmlBody
|
||||
func (_m *ServiceInterface) SendNotificationMail(to string, subject string, htmlBody string) error {
|
||||
ret := _m.Called(to, subject, htmlBody)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(to, subject, htmlBody)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendPasswordChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
|
||||
func (_m *ServiceInterface) SendPasswordChangeEmail(_a0 string, method string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, method, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, method, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendPasswordResetEmail provides a mock function with given fields: _a0, token, locale, siteURL
|
||||
func (_m *ServiceInterface) SendPasswordResetEmail(_a0 string, token *model.Token, locale string, siteURL string) (bool, error) {
|
||||
ret := _m.Called(_a0, token, locale, siteURL)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, *model.Token, string, string) bool); ok {
|
||||
r0 = rf(_a0, token, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, *model.Token, string, string) error); ok {
|
||||
r1 = rf(_a0, token, locale, siteURL)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SendPaymentFailedEmail provides a mock function with given fields: _a0, locale, failedPayment, planName, siteURL
|
||||
func (_m *ServiceInterface) SendPaymentFailedEmail(_a0 string, locale string, failedPayment *model.FailedPayment, planName string, siteURL string) (bool, error) {
|
||||
ret := _m.Called(_a0, locale, failedPayment, planName, siteURL)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string, *model.FailedPayment, string, string) bool); ok {
|
||||
r0 = rf(_a0, locale, failedPayment, planName, siteURL)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, *model.FailedPayment, string, string) error); ok {
|
||||
r1 = rf(_a0, locale, failedPayment, planName, siteURL)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SendRemoveExpiredLicenseEmail provides a mock function with given fields: ctaText, ctaLink, _a2, locale, siteURL
|
||||
func (_m *ServiceInterface) SendRemoveExpiredLicenseEmail(ctaText string, ctaLink string, _a2 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(ctaText, ctaLink, _a2, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
|
||||
r0 = rf(ctaText, ctaLink, _a2, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendSignInChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
|
||||
func (_m *ServiceInterface) SendSignInChangeEmail(_a0 string, method string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, method, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
|
||||
r0 = rf(_a0, method, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendUserAccessTokenAddedEmail provides a mock function with given fields: _a0, locale, siteURL
|
||||
func (_m *ServiceInterface) SendUserAccessTokenAddedEmail(_a0 string, locale string, siteURL string) error {
|
||||
ret := _m.Called(_a0, locale, siteURL)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
|
||||
r0 = rf(_a0, locale, siteURL)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendVerifyEmail provides a mock function with given fields: userEmail, locale, siteURL, token, redirect
|
||||
func (_m *ServiceInterface) SendVerifyEmail(userEmail string, locale string, siteURL string, token string, redirect string) error {
|
||||
ret := _m.Called(userEmail, locale, siteURL, token, redirect)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
|
||||
r0 = rf(userEmail, locale, siteURL, token, redirect)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SendWelcomeEmail provides a mock function with given fields: userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect
|
||||
func (_m *ServiceInterface) SendWelcomeEmail(userID string, _a1 string, verified bool, disableWelcomeEmail bool, locale string, siteURL string, redirect string) error {
|
||||
ret := _m.Called(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, bool, bool, string, string, string) error); ok {
|
||||
r0 = rf(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Stop provides a mock function with given fields:
|
||||
func (_m *ServiceInterface) Stop() {
|
||||
_m.Called()
|
||||
}
|
||||
136
server/channels/app/email/notification_email.go
Обычный файл
136
server/channels/app/email/notification_email.go
Обычный файл
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"html"
|
||||
"html/template"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
)
|
||||
|
||||
type FieldRow struct {
|
||||
Cells []*model.SlackAttachmentField
|
||||
}
|
||||
|
||||
type EmailMessageAttachment struct {
|
||||
model.SlackAttachment
|
||||
|
||||
Pretext template.HTML
|
||||
Text template.HTML
|
||||
FieldRows []FieldRow
|
||||
}
|
||||
|
||||
func (es *Service) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
|
||||
return post.Message
|
||||
}
|
||||
|
||||
// extract the filenames from their paths and determine what type of files are attached
|
||||
infos, err := es.store.FileInfo().GetForPost(post.Id, true, false, true)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
}
|
||||
|
||||
filenames := make([]string, len(infos))
|
||||
onlyImages := true
|
||||
for i, info := range infos {
|
||||
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
|
||||
// this should never error since filepath was escaped using url.QueryEscape
|
||||
filenames[i] = escaped
|
||||
} else {
|
||||
filenames[i] = info.Name
|
||||
}
|
||||
|
||||
onlyImages = onlyImages && info.IsImage()
|
||||
}
|
||||
|
||||
props := map[string]any{"Filenames": strings.Join(filenames, ", ")}
|
||||
|
||||
if onlyImages {
|
||||
return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
|
||||
}
|
||||
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
|
||||
}
|
||||
|
||||
func ProcessMessageAttachments(post *model.Post, siteURL string) []*EmailMessageAttachment {
|
||||
emailMessageAttachments := []*EmailMessageAttachment{}
|
||||
|
||||
for _, messageAttachment := range post.Attachments() {
|
||||
emailMessageAttachment := &EmailMessageAttachment{
|
||||
SlackAttachment: *messageAttachment,
|
||||
Pretext: prepareTextForEmail(messageAttachment.Pretext, siteURL),
|
||||
Text: prepareTextForEmail(messageAttachment.Text, siteURL),
|
||||
}
|
||||
|
||||
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
|
||||
if err != nil {
|
||||
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||
stripedTitle = ""
|
||||
}
|
||||
|
||||
emailMessageAttachment.Title = stripedTitle
|
||||
|
||||
shortFieldRow := FieldRow{}
|
||||
|
||||
for i := range messageAttachment.Fields {
|
||||
// Create a new instance to avoid altering the original pointer reference
|
||||
// We update field value to parse markdown.
|
||||
// If we do that on the original pointer, the rendered text in mattermost
|
||||
// becomes invalid as its no longer a markdown string, but rather an HTML string.
|
||||
field := &model.SlackAttachmentField{
|
||||
Title: messageAttachment.Fields[i].Title,
|
||||
Value: messageAttachment.Fields[i].Value,
|
||||
Short: messageAttachment.Fields[i].Short,
|
||||
}
|
||||
|
||||
if stringValue, ok := field.Value.(string); ok {
|
||||
field.Value = prepareTextForEmail(stringValue, siteURL)
|
||||
}
|
||||
|
||||
if !field.Short {
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
|
||||
} else {
|
||||
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
|
||||
|
||||
if len(shortFieldRow.Cells) == 2 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// collect any leftover short fields
|
||||
if len(shortFieldRow.Cells) > 0 {
|
||||
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
|
||||
shortFieldRow = FieldRow{}
|
||||
}
|
||||
|
||||
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
|
||||
}
|
||||
|
||||
return emailMessageAttachments
|
||||
}
|
||||
|
||||
func prepareTextForEmail(text, siteURL string) template.HTML {
|
||||
escapedText := html.EscapeString(text)
|
||||
markdownText, err := utils.MarkdownToHTML(escapedText, siteURL)
|
||||
if err != nil {
|
||||
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
|
||||
return template.HTML(text)
|
||||
}
|
||||
|
||||
return template.HTML(markdownText)
|
||||
}
|
||||
73
server/channels/app/email/notification_email_test.go
Обычный файл
73
server/channels/app/email/notification_email_test.go
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
)
|
||||
|
||||
func TestProcessMessageAttachments(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
post := &model.Post{
|
||||
Message: "This is the message",
|
||||
}
|
||||
|
||||
messageAttachments := []*model.SlackAttachment{
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 1 pretext",
|
||||
AuthorName: "author name",
|
||||
AuthorLink: "https://example.com/slack_attachment_1/author_link",
|
||||
AuthorIcon: "https://example.com/slack_attachment_1/author_icon",
|
||||
Title: "message attachment 1 title",
|
||||
TitleLink: "https://example.com/slack_attachment_1/title_link",
|
||||
Text: "message attachment 1 text",
|
||||
ImageURL: "https://example.com/slack_attachment_1/image",
|
||||
ThumbURL: "https://example.com/slack_attachment_1/thumb",
|
||||
Fields: []*model.SlackAttachmentField{
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 1 title",
|
||||
Value: "message attachment 1 field 1 value",
|
||||
},
|
||||
{
|
||||
Short: false,
|
||||
Title: "message attachment 1 field 2 title",
|
||||
Value: "message attachment 1 field 2 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 3 title",
|
||||
Value: "message attachment 1 field 3 value",
|
||||
},
|
||||
{
|
||||
Short: true,
|
||||
Title: "message attachment 1 field 4 title",
|
||||
Value: "message attachment 1 field 4 value",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Color: "#FF0000",
|
||||
Pretext: "message attachment 2 pretext",
|
||||
AuthorName: "author name 2",
|
||||
Text: "message attachment 2 text",
|
||||
},
|
||||
}
|
||||
|
||||
model.ParseSlackAttachment(post, messageAttachments)
|
||||
|
||||
processedAttachmentsPost := ProcessMessageAttachments(post, "https://example.com")
|
||||
require.NotNil(t, processedAttachmentsPost)
|
||||
require.Len(t, processedAttachmentsPost, 2)
|
||||
require.Equal(t, processedAttachmentsPost[0].Color, "#FF0000")
|
||||
require.Equal(t, processedAttachmentsPost[0].FieldRows[0].Cells[0].Title, "message attachment 1 field 1 title")
|
||||
require.Equal(t, processedAttachmentsPost[1].Color, "#FF0000")
|
||||
}
|
||||
176
server/channels/app/email/service.go
Обычный файл
176
server/channels/app/email/service.go
Обычный файл
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/throttled/throttled"
|
||||
"github.com/throttled/throttled/store/memstore"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v6/model"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/app/users"
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/store"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/templates"
|
||||
)
|
||||
|
||||
const (
|
||||
emailRateLimitingMemstoreSize = 65536
|
||||
emailRateLimitingPerHour = 20
|
||||
emailRateLimitingMaxBurst = 20
|
||||
|
||||
TokenTypePasswordRecovery = "password_recovery"
|
||||
TokenTypeVerifyEmail = "verify_email"
|
||||
TokenTypeTeamInvitation = "team_invitation"
|
||||
TokenTypeGuestInvitation = "guest_invitation"
|
||||
TokenTypeCWSAccess = "cws_access_token"
|
||||
)
|
||||
|
||||
func condenseSiteURL(siteURL string) string {
|
||||
parsedSiteURL, _ := url.Parse(siteURL)
|
||||
if parsedSiteURL.Path == "" || parsedSiteURL.Path == "/" {
|
||||
return parsedSiteURL.Host
|
||||
}
|
||||
|
||||
return path.Join(parsedSiteURL.Host, parsedSiteURL.Path)
|
||||
}
|
||||
|
||||
type Service struct {
|
||||
config func() *model.Config
|
||||
license func() *model.License
|
||||
|
||||
userService *users.UserService
|
||||
store store.Store
|
||||
|
||||
templatesContainer *templates.Container
|
||||
perHourEmailRateLimiter *throttled.GCRARateLimiter
|
||||
perDayEmailRateLimiter *throttled.GCRARateLimiter
|
||||
EmailBatching *EmailBatchingJob
|
||||
}
|
||||
|
||||
type ServiceConfig struct {
|
||||
ConfigFn func() *model.Config
|
||||
LicenseFn func() *model.License
|
||||
|
||||
TemplatesContainer *templates.Container
|
||||
UserService *users.UserService
|
||||
Store store.Store
|
||||
}
|
||||
|
||||
func NewService(config ServiceConfig) (*Service, error) {
|
||||
if err := config.validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service := &Service{
|
||||
config: config.ConfigFn,
|
||||
templatesContainer: config.TemplatesContainer,
|
||||
license: config.LicenseFn,
|
||||
store: config.Store,
|
||||
userService: config.UserService,
|
||||
}
|
||||
if err := service.setUpRateLimiters(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.InitEmailBatching()
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (es *Service) Stop() {
|
||||
mlog.Info("Shutting down Email batching service...")
|
||||
if es.EmailBatching != nil {
|
||||
es.EmailBatching.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if c.ConfigFn == nil || c.Store == nil || c.LicenseFn == nil || c.TemplatesContainer == nil {
|
||||
return errors.New("invalid service config")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *Service) setUpRateLimiters() error {
|
||||
store, err := memstore.New(emailRateLimitingMemstoreSize)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
|
||||
}
|
||||
|
||||
perHourQuota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerHour(emailRateLimitingPerHour),
|
||||
MaxBurst: emailRateLimitingMaxBurst,
|
||||
}
|
||||
|
||||
perDayQuota := throttled.RateQuota{
|
||||
MaxRate: throttled.PerDay(1),
|
||||
MaxBurst: 0,
|
||||
}
|
||||
|
||||
perHourRateLimiter, err := throttled.NewGCRARateLimiter(store, perHourQuota)
|
||||
if err != nil || perHourRateLimiter == nil {
|
||||
return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
|
||||
}
|
||||
|
||||
perDayRateLimiter, err := throttled.NewGCRARateLimiter(store, perDayQuota)
|
||||
if err != nil || perDayRateLimiter == nil {
|
||||
return errors.Wrap(err, "Unable to setup per day email rate limiting GCRA rate limiter.")
|
||||
}
|
||||
|
||||
es.perHourEmailRateLimiter = perHourRateLimiter
|
||||
es.perDayEmailRateLimiter = perDayRateLimiter
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServiceInterface interface {
|
||||
GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter
|
||||
NewEmailTemplateData(locale string) templates.Data
|
||||
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
|
||||
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
|
||||
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
|
||||
SendSignInChangeEmail(email, method, locale, siteURL string) error
|
||||
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
|
||||
SendCloudUpgradeConfirmationEmail(userEmail, name, trialEndDate, locale, siteURL, workspaceName string, isYearly bool, embeddedFiles map[string]io.Reader) error
|
||||
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
|
||||
SendPasswordChangeEmail(email, method, locale, siteURL string) error
|
||||
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
|
||||
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
|
||||
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
|
||||
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
|
||||
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) error
|
||||
SendInviteEmailsToTeamAndChannels(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, message string, errorWhenNotSent bool, isSystemAdmin bool, isFirstAdmin bool) ([]*model.EmailInviteWithError, error)
|
||||
SendDeactivateAccountEmail(email string, locale, siteURL string) error
|
||||
SendNotificationMail(to, subject, htmlBody string) error
|
||||
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string, category string) error
|
||||
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, ctaTitle, ctaLink, ctaText string, daysToExpiration int) error
|
||||
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, planName, siteURL string) (bool, error)
|
||||
// Cloud delinquency email sequence
|
||||
SendDelinquencyEmail7(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail14(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail30(email, locale, siteURL, planName string) error
|
||||
SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error
|
||||
SendDelinquencyEmail60(email, locale, siteURL string) error
|
||||
SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
|
||||
SendDelinquencyEmail90(email, locale, siteURL string) error
|
||||
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
|
||||
SendRemoveExpiredLicenseEmail(ctaText, ctaLink, email, locale, siteURL string) error
|
||||
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
|
||||
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
|
||||
InitEmailBatching()
|
||||
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
|
||||
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
|
||||
SendLicenseInactivityEmail(email, name, locale, siteURL string) error
|
||||
Stop()
|
||||
}
|
||||
|
||||
func (es *Service) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
|
||||
return es.perDayEmailRateLimiter
|
||||
}
|
||||
|
||||
func (es *Service) GetPerHourEmailRateLimiter() *throttled.GCRARateLimiter {
|
||||
return es.perHourEmailRateLimiter
|
||||
}
|
||||
48
server/channels/app/email/utils.go
Обычный файл
48
server/channels/app/email/utils.go
Обычный файл
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v6/server/channels/utils"
|
||||
"github.com/mattermost/mattermost-server/v6/server/platform/shared/mail"
|
||||
)
|
||||
|
||||
func (es *Service) mailServiceConfig(replyToAddress string) *mail.SMTPConfig {
|
||||
emailSettings := es.config().EmailSettings
|
||||
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
|
||||
|
||||
if replyToAddress == "" {
|
||||
replyToAddress = *emailSettings.ReplyToAddress
|
||||
}
|
||||
|
||||
cfg := mail.SMTPConfig{
|
||||
Hostname: hostname,
|
||||
ConnectionSecurity: *emailSettings.ConnectionSecurity,
|
||||
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
|
||||
ServerName: *emailSettings.SMTPServer,
|
||||
Server: *emailSettings.SMTPServer,
|
||||
Port: *emailSettings.SMTPPort,
|
||||
ServerTimeout: *emailSettings.SMTPServerTimeout,
|
||||
Username: *emailSettings.SMTPUsername,
|
||||
Password: *emailSettings.SMTPPassword,
|
||||
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
|
||||
SendEmailNotifications: *emailSettings.SendEmailNotifications,
|
||||
FeedbackName: *emailSettings.FeedbackName,
|
||||
FeedbackEmail: *emailSettings.FeedbackEmail,
|
||||
ReplyToAddress: replyToAddress,
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (es *Service) GetTrackFlowStartedByRole(isFirstAdmin bool, isSystemAdmin bool) string {
|
||||
trackFlowStartedByRole := "su"
|
||||
|
||||
if isFirstAdmin {
|
||||
trackFlowStartedByRole = "fa"
|
||||
} else if isSystemAdmin {
|
||||
trackFlowStartedByRole = "sa"
|
||||
}
|
||||
|
||||
return trackFlowStartedByRole
|
||||
}
|
||||
Ссылка в новой задаче
Block a user