Decouple emailservice from app package (#17827)
* decouple emailservice from app package * fix some escaped errors * move email package under app directory * fix i18n * reflect review comments
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
a78a7c7b54
Коммит
41dc05a6bd
@@ -435,8 +435,8 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); appErr != nil {
|
||||
c.Err = appErr
|
||||
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
|
||||
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
case model.EventTypeTrialWillEnd:
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
package api4
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||
"github.com/mattermost/mattermost-server/v5/audit"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
@@ -126,7 +128,14 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
if len(goodEmails) > 0 {
|
||||
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
switch {
|
||||
case errors.Is(err, email.NoRateLimiterError):
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(err, email.SetupRateLimiterError):
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError)
|
||||
default:
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -147,7 +156,14 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
switch {
|
||||
case errors.Is(err, email.NoRateLimiterError):
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(err, email.SetupRateLimiterError):
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError)
|
||||
default:
|
||||
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
return
|
||||
}
|
||||
ReturnStatusOK(w)
|
||||
|
||||
@@ -1398,8 +1398,8 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if isSelfDeactive {
|
||||
c.App.Srv().Go(func() {
|
||||
if err = c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
|
||||
c.LogErrorByCode(err)
|
||||
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
|
||||
c.LogErrorByCode(model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3245,7 +3245,7 @@ func TestVerifyUserEmail(t *testing.T) {
|
||||
ruser, _ := th.Client.CreateUser(&user)
|
||||
|
||||
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(ruser.Id, email)
|
||||
require.Nil(t, err, "Unable to create email verify token")
|
||||
require.NoError(t, err, "Unable to create email verify token")
|
||||
|
||||
_, resp := th.Client.VerifyUserEmail(token.Token)
|
||||
CheckNoError(t, resp)
|
||||
|
||||
@@ -369,7 +369,7 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User,
|
||||
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, i18n.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusInternalServerError)
|
||||
}
|
||||
T := i18n.GetUserTranslations(sender.Locale)
|
||||
data := a.Srv().EmailService.newEmailTemplateData(sender.Locale)
|
||||
data := a.Srv().EmailService.NewEmailTemplateData(sender.Locale)
|
||||
data.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
|
||||
data.Props["ContactNameValue"] = sender.GetFullName()
|
||||
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
|
||||
|
||||
@@ -1977,6 +1977,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
|
||||
SessionStore: &mockSessionStore,
|
||||
OAuthStore: &mockOAuthStore,
|
||||
ConfigFn: th.App.srv.Config,
|
||||
LicenseFn: th.App.srv.License,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
mockPreferenceStore := mocks.PreferenceStore{}
|
||||
|
||||
@@ -179,8 +179,8 @@ func (a *App) CheckAndSendUserLimitWarningEmails(c *request.Context) *model.AppE
|
||||
} else if remainingUsers == 0 {
|
||||
// At limit
|
||||
for admin := range sysAdmins {
|
||||
_, appErr := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
if appErr != nil {
|
||||
_, err := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
||||
if err != nil {
|
||||
a.Log().Error(
|
||||
"Error sending user limit warning email to admin",
|
||||
mlog.String("username", sysAdmins[admin].Username),
|
||||
|
||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
@@ -23,10 +22,20 @@ const (
|
||||
EmailBatchingTaskName = "Email Batching"
|
||||
)
|
||||
|
||||
func (es *EmailService) InitEmailBatching() {
|
||||
if *es.srv.Config().EmailSettings.EnableEmailBatching {
|
||||
type postData struct {
|
||||
SenderName string
|
||||
ChannelName string
|
||||
Message template.HTML
|
||||
MessageURL string
|
||||
SenderPhoto string
|
||||
PostPhoto string
|
||||
Time string
|
||||
}
|
||||
|
||||
func (es *Service) InitEmailBatching() {
|
||||
if *es.config().EmailSettings.EnableEmailBatching {
|
||||
if es.EmailBatching == nil {
|
||||
es.EmailBatching = NewEmailBatchingJob(es, *es.srv.Config().EmailSettings.EmailBatchingBufferSize)
|
||||
es.EmailBatching = NewEmailBatchingJob(es, *es.config().EmailSettings.EmailBatchingBufferSize)
|
||||
}
|
||||
|
||||
// note that we don't support changing EmailBatchingBufferSize without restarting the server
|
||||
@@ -35,8 +44,8 @@ func (es *EmailService) InitEmailBatching() {
|
||||
}
|
||||
}
|
||||
|
||||
func (es *EmailService) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
|
||||
if !*es.srv.Config().EmailSettings.EnableEmailBatching {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -55,24 +64,27 @@ type batchedNotification struct {
|
||||
}
|
||||
|
||||
type EmailBatchingJob struct {
|
||||
server *Server
|
||||
config func() *model.Config
|
||||
service *Service
|
||||
|
||||
newNotifications chan *batchedNotification
|
||||
pendingNotifications map[string][]*batchedNotification
|
||||
task *model.ScheduledTask
|
||||
taskMutex sync.Mutex
|
||||
}
|
||||
|
||||
func NewEmailBatchingJob(es *EmailService, bufferSize int) *EmailBatchingJob {
|
||||
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
|
||||
return &EmailBatchingJob{
|
||||
server: es.srv,
|
||||
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.server.Config().EmailSettings.EmailBatchingInterval))
|
||||
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.server.Config().EmailSettings.EmailBatchingInterval)*time.Second)
|
||||
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
|
||||
@@ -105,7 +117,7 @@ func (job *EmailBatchingJob) CheckPendingEmails() {
|
||||
|
||||
// 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.server.EmailService.sendBatchedEmailNotification)
|
||||
job.checkPendingNotifications(time.Now(), job.service.sendBatchedEmailNotification)
|
||||
|
||||
mlog.Debug("Email batching job ran. Some users still have notifications pending.", mlog.Int("number_of_users", len(job.pendingNotifications)))
|
||||
}
|
||||
@@ -140,7 +152,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
||||
continue
|
||||
}
|
||||
|
||||
team, nErr := job.server.Store.Team().GetByName(notifications[0].teamName)
|
||||
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
|
||||
@@ -152,7 +164,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
||||
|
||||
// if the user has viewed any channels in this team since the notification was queued, delete
|
||||
// all queued notifications
|
||||
channelMembers, err := job.server.Store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
|
||||
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
|
||||
@@ -169,7 +181,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
||||
|
||||
// get how long we need to wait to send notifications to the user
|
||||
var interval int64
|
||||
preference, err := job.server.Store.Preference().Get(userID, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
|
||||
preference, err := job.service.store.Preference().Get(userID, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
|
||||
if err != nil {
|
||||
// use the default batching interval if an error ocurrs while fetching user preferences
|
||||
interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
|
||||
@@ -184,7 +196,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
||||
|
||||
// send the email notification if there are notifications to send AND it's been long enough
|
||||
if len(job.pendingNotifications[userID]) > 0 && now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second {
|
||||
job.server.Go(func(userID string, notifications []*batchedNotification) func() {
|
||||
job.service.goFn(func(userID string, notifications []*batchedNotification) func() {
|
||||
return func() {
|
||||
handler(userID, notifications)
|
||||
}
|
||||
@@ -194,38 +206,38 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
||||
}
|
||||
}
|
||||
|
||||
func (es *EmailService) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
|
||||
user, err := es.srv.Store.User().Get(context.Background(), userID)
|
||||
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.srv.Config().TeamSettings.TeammateNameDisplay
|
||||
siteURL := *es.srv.Config().ServiceSettings.SiteURL
|
||||
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.EMAIL_NOTIFICATION_CONTENTS_FULL
|
||||
if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents {
|
||||
emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType
|
||||
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
|
||||
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
|
||||
}
|
||||
|
||||
if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
|
||||
for i, notification := range notifications {
|
||||
sender, errSender := es.srv.Store.User().Get(context.Background(), notification.post.UserId)
|
||||
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.srv.Store.Channel().Get(notification.post.ChannelId, true)
|
||||
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.srv.GetProfileImage(sender)
|
||||
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))
|
||||
}
|
||||
@@ -254,7 +266,7 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
|
||||
SenderName: sender.GetDisplayName(displayNameFormat),
|
||||
Time: t,
|
||||
ChannelName: channel.DisplayName,
|
||||
Message: template.HTML(es.srv.GetMessageForNotification(notification.post, translateFunc)),
|
||||
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
|
||||
MessageURL: MessageURL,
|
||||
})
|
||||
}
|
||||
@@ -263,18 +275,18 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
|
||||
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
|
||||
|
||||
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
|
||||
"SiteName": es.srv.Config().TeamSettings.SiteName,
|
||||
"SiteName": es.config().TeamSettings.SiteName,
|
||||
"Year": tm.Year(),
|
||||
"Month": translateFunc(tm.Month().String()),
|
||||
"Day": tm.Day(),
|
||||
})
|
||||
|
||||
firstSender, err := es.srv.Store.User().Get(context.Background(), notifications[0].post.UserId)
|
||||
firstSender, err := es.userService.GetUser(notifications[0].post.UserId)
|
||||
if err != nil {
|
||||
mlog.Warn("Unable to find sender of post for batched email notification")
|
||||
}
|
||||
|
||||
data := es.newEmailTemplateData(user.Locale)
|
||||
data := es.NewEmailTemplateData(user.Locale)
|
||||
data.Props["SiteURL"] = siteURL
|
||||
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1, map[string]interface{}{
|
||||
"SenderName": firstSender.GetDisplayName(displayNameFormat),
|
||||
@@ -288,12 +300,12 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
|
||||
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
|
||||
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
|
||||
|
||||
renderedPage, renderErr := es.srv.TemplatesContainer().RenderToString("messages_notification", data)
|
||||
renderedPage, renderErr := es.templatesContainer.RenderToString("messages_notification", data)
|
||||
if renderErr != nil {
|
||||
mlog.Error("Unable to render email", mlog.Err(renderErr))
|
||||
}
|
||||
|
||||
if nErr := es.sendNotificationMail(user.Email, subject, renderedPage); nErr != nil {
|
||||
if nErr := es.SendNotificationMail(user.Email, subject, renderedPage); nErr != nil {
|
||||
mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -24,7 +24,7 @@ func TestHandleNewNotifications(t *testing.T) {
|
||||
id3 := model.NewId()
|
||||
|
||||
// test queueing of received posts by user
|
||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
job.handleNewNotifications()
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestHandleNewNotifications(t *testing.T) {
|
||||
require.Len(t, job.pendingNotifications[id3], 1, "should have received 1 post for user3")
|
||||
|
||||
// test ordering of received posts
|
||||
job = NewEmailBatchingJob(th.Server.EmailService, 128)
|
||||
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"})
|
||||
@@ -78,7 +78,7 @@ func TestCheckPendingNotifications(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
{
|
||||
post: &model.Post{
|
||||
@@ -90,13 +90,13 @@ func TestCheckPendingNotifications(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
channelMember, err := th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 9999999
|
||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
nErr := th.App.Srv().Store.Preference().Save(&model.Preferences{{
|
||||
nErr := th.store.Preference().Save(&model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
||||
@@ -111,14 +111,14 @@ func TestCheckPendingNotifications(t *testing.T) {
|
||||
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.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
channelMember, err = th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 10001000
|
||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We reset the interval to something shorter
|
||||
nErr = th.App.Srv().Store.Preference().Save(&model.Preferences{{
|
||||
nErr = th.store.Preference().Save(&model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
||||
@@ -197,13 +197,18 @@ func TestCheckPendingNotificationsDefaultInterval(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
||||
job := NewEmailBatchingJob(th.service, 128)
|
||||
|
||||
// bypasses recent user activity check
|
||||
channelMember, err := th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
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.App.Srv().Store.Channel().UpdateMember(channelMember)
|
||||
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||
require.NoError(t, err)
|
||||
|
||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||
@@ -235,17 +240,21 @@ func TestCheckPendingNotificationsCantParseInterval(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
||||
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.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
|
||||
require.NoError(t, err)
|
||||
channelMember.LastViewedAt = 9999000
|
||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
||||
_, 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.App.Srv().Store.Preference().Save(&model.Preferences{{
|
||||
nErr := th.store.Preference().Save(&model.Preferences{{
|
||||
UserId: th.BasicUser.Id,
|
||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
||||
73
app/email/email_test.go
Обычный файл
73
app/email/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/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/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()
|
||||
|
||||
th.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
})
|
||||
|
||||
require.NotNil(t, th.BasicUser)
|
||||
require.NotNil(t, th.BasicChannel)
|
||||
|
||||
emailTo := "test@example.com"
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver")
|
||||
require.NoError(t, err)
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Log(err2)
|
||||
t.Log("No email was received, maybe due load on the server. Skipping this verification")
|
||||
} else if len(resultsMailbox) > 0 {
|
||||
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.HTML, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.HTML, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
}
|
||||
}
|
||||
13
app/email/errors.go
Обычный файл
13
app/email/errors.go
Обычный файл
@@ -0,0 +1,13 @@
|
||||
// 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")
|
||||
)
|
||||
307
app/email/helper_test.go
Обычный файл
307
app/email/helper_test.go
Обычный файл
@@ -0,0 +1,307 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/config"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/plugin/plugintest/mock"
|
||||
"github.com/mattermost/mattermost-server/v5/services/users"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/templates"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"github.com/mattermost/mattermost-server/v5/store/storetest/mocks"
|
||||
"github.com/mattermost/mattermost-server/v5/testlib"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
)
|
||||
|
||||
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.STATUS_ONLINE}, 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 := ioutil.TempDir("", "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,
|
||||
goFn: func(f func()) { go f() },
|
||||
}
|
||||
|
||||
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, model.CHANNEL_OPEN)
|
||||
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.TEAM_OPEN,
|
||||
}
|
||||
|
||||
utils.DisableDebugLogForTest()
|
||||
var err error
|
||||
if team, err = th.store.Team().Save(team); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
utils.EnableDebugLogForTest()
|
||||
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: channelType,
|
||||
TeamId: team.Id,
|
||||
CreatorId: th.BasicUser.Id,
|
||||
}
|
||||
|
||||
utils.DisableDebugLogForTest()
|
||||
var err error
|
||||
if channel, err = th.store.Channel().Save(channel, *th.configStore.Get().TeamSettings.MaxChannelsPerTeam); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
utils.EnableDebugLogForTest()
|
||||
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
|
||||
})
|
||||
}
|
||||
35
app/email/main_test.go
Обычный файл
35
app/email/main_test.go
Обычный файл
@@ -0,0 +1,35 @@
|
||||
// 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/v5/shared/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/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,
|
||||
}
|
||||
|
||||
mlog.DisableZap()
|
||||
|
||||
mainHelper = testlib.NewMainHelperWithOptions(&options)
|
||||
defer mainHelper.Close()
|
||||
|
||||
mainHelper.Main(m)
|
||||
}
|
||||
46
app/email/notification_email.go
Обычный файл
46
app/email/notification_email.go
Обычный файл
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/i18n"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mlog"
|
||||
)
|
||||
|
||||
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]interface{}{"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)
|
||||
}
|
||||
120
app/email/service.go
Обычный файл
120
app/email/service.go
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/throttled/throttled"
|
||||
"github.com/throttled/throttled/store/memstore"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/services/users"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/templates"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
)
|
||||
|
||||
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
|
||||
goFn func(f func())
|
||||
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
|
||||
GoFn func(f func())
|
||||
|
||||
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,
|
||||
goFn: config.GoFn,
|
||||
store: config.Store,
|
||||
userService: config.UserService,
|
||||
}
|
||||
if err := service.setUpRateLimiters(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service.InitEmailBatching()
|
||||
return service, nil
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if c.ConfigFn == nil || c.GoFn == 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
|
||||
}
|
||||
31
app/email/utils.go
Обычный файл
31
app/email/utils.go
Обычный файл
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/shared/mail"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
)
|
||||
|
||||
func (es *Service) mailServiceConfig() *mail.SMTPConfig {
|
||||
emailSettings := es.config().EmailSettings
|
||||
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
|
||||
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: *emailSettings.ReplyToAddress,
|
||||
}
|
||||
return &cfg
|
||||
}
|
||||
@@ -8,34 +8,11 @@ import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/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 TestSendInviteEmailRateLimits(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
@@ -134,39 +111,3 @@ func TestSendAdminUpgradeRequestEmailOnJoin(t *testing.T) {
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
|
||||
}
|
||||
|
||||
func TestSendInviteEmails(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.ConfigureInbucketMail()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.EnableEmailInvitations = true
|
||||
})
|
||||
|
||||
emailTo := "test@example.com"
|
||||
mail.DeleteMailBox(emailTo)
|
||||
|
||||
appErr := th.App.Srv().EmailService.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver")
|
||||
require.Nil(t, appErr)
|
||||
|
||||
var resultsMailbox mail.JSONMessageHeaderInbucket
|
||||
err2 := mail.RetryInbucket(5, func() error {
|
||||
var err error
|
||||
resultsMailbox, err = mail.GetMailBox(emailTo)
|
||||
return err
|
||||
})
|
||||
if err2 != nil {
|
||||
t.Log(err2)
|
||||
t.Log("No email was received, maybe due load on the server. Skipping this verification")
|
||||
} else if len(resultsMailbox) > 0 {
|
||||
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.HTML, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.HTML, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
require.Contains(t, resultsEmail.Body.Text, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ import (
|
||||
"html"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -114,7 +112,7 @@ func (a *App) sendNotificationEmail(notification *PostNotification, user *model.
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
if nErr := a.Srv().EmailService.sendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles); nErr != nil {
|
||||
if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles); nErr != nil {
|
||||
mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr))
|
||||
}
|
||||
})
|
||||
@@ -212,7 +210,7 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
|
||||
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
|
||||
}
|
||||
|
||||
data := a.Srv().EmailService.newEmailTemplateData(recipient.Locale)
|
||||
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
|
||||
data.Props["SiteURL"] = a.GetSiteURL()
|
||||
if teamName != "select_team" {
|
||||
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
|
||||
@@ -321,38 +319,6 @@ func (a *App) generateHyperlinkForChannels(postMessage, teamName, teamURL string
|
||||
return postMessage, nil
|
||||
}
|
||||
|
||||
func (s *Server) 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 := s.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]interface{}{"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 (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
|
||||
return a.Srv().GetMessageForNotification(post, translateFunc)
|
||||
return a.Srv().EmailService.GetMessageForNotification(post, translateFunc)
|
||||
}
|
||||
|
||||
@@ -256,10 +256,6 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing
|
||||
zone, _ := tm.Zone()
|
||||
|
||||
formattedTime := formattedPostTime{
|
||||
Time: tm,
|
||||
Year: fmt.Sprintf("%d", tm.Year()),
|
||||
Month: translateFunc(tm.Month().String()),
|
||||
Day: fmt.Sprintf("%d", tm.Day()),
|
||||
Hour: fmt.Sprintf("%02d", tm.Hour()),
|
||||
Minute: fmt.Sprintf("%02d", tm.Minute()),
|
||||
TimeZone: zone,
|
||||
|
||||
@@ -780,7 +780,7 @@ func (api *PluginAPI) SendMail(to, subject, htmlBody string) *model.AppError {
|
||||
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := api.app.Srv().EmailService.sendNotificationMail(to, subject, htmlBody); err != nil {
|
||||
if err := api.app.Srv().EmailService.SendNotificationMail(to, subject, htmlBody); err != nil {
|
||||
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"github.com/rs/cors"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||
"github.com/mattermost/mattermost-server/v5/app/featureflag"
|
||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
@@ -113,7 +114,7 @@ type Server struct {
|
||||
PluginConfigListenerId string
|
||||
PluginsLock sync.RWMutex
|
||||
|
||||
EmailService *EmailService
|
||||
EmailService *email.Service
|
||||
|
||||
hubs []*Hub
|
||||
hashSeed maphash.Seed
|
||||
@@ -416,6 +417,7 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
ConfigFn: s.Config,
|
||||
Metrics: s.Metrics,
|
||||
Cluster: s.Cluster,
|
||||
LicenseFn: s.License,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to create users service")
|
||||
@@ -452,7 +454,14 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
|
||||
s.telemetryService = telemetry.New(s, s.Store, s.SearchEngine, s.Log)
|
||||
|
||||
emailService, err := NewEmailService(s)
|
||||
emailService, err := email.NewService(email.ServiceConfig{
|
||||
ConfigFn: s.Config,
|
||||
LicenseFn: s.License,
|
||||
GoFn: s.Go,
|
||||
TemplatesContainer: s.TemplatesContainer(),
|
||||
UserService: s.userService,
|
||||
Store: s.GetStore(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to initialize email service")
|
||||
}
|
||||
@@ -1803,9 +1812,8 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice
|
||||
if name == "" {
|
||||
name = user.Username
|
||||
}
|
||||
ok, err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration)
|
||||
if !ok || err != nil {
|
||||
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email))
|
||||
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil {
|
||||
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
|
||||
countNotOks++
|
||||
}
|
||||
}
|
||||
@@ -1864,7 +1872,7 @@ func (s *Server) doLicenseExpirationCheck() {
|
||||
|
||||
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
|
||||
s.Go(func() {
|
||||
if err := s.EmailService.SendRemoveExpiredLicenseEmail(user.Email, user.Locale, *s.Config().ServiceSettings.SiteURL); err != nil {
|
||||
if err := s.SendRemoveExpiredLicenseEmail(user.Email, user.Locale, *s.Config().ServiceSettings.SiteURL); err != nil {
|
||||
mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1874,6 +1882,21 @@ func (s *Server) doLicenseExpirationCheck() {
|
||||
s.RemoveLicense()
|
||||
}
|
||||
|
||||
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
|
||||
// to renew the user license
|
||||
func (s *Server) SendRemoveExpiredLicenseEmail(email string, locale, siteURL string) *model.AppError {
|
||||
renewalLink, err := s.GenerateLicenseRenewalLink()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.EmailService.SendRemoveExpiredLicenseEmail(renewalLink, email, locale, siteURL); err != nil {
|
||||
return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) StartSearchEngine() (string, string) {
|
||||
if s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() {
|
||||
s.Go(func() {
|
||||
|
||||
@@ -362,7 +362,7 @@ func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAc
|
||||
|
||||
// Don't send emails to bot users.
|
||||
if !user.IsBot {
|
||||
if err := a.Srv().EmailService.sendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
|
||||
a.Log().Error("Unable to send user access token added email", mlog.Err(err), mlog.String("user_id", user.Id))
|
||||
}
|
||||
}
|
||||
|
||||
53
app/team.go
53
app/team.go
@@ -15,6 +15,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
@@ -1434,9 +1435,16 @@ func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderI
|
||||
|
||||
if len(goodEmails) > 0 {
|
||||
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
||||
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
|
||||
if eErr != nil {
|
||||
switch {
|
||||
case errors.Is(eErr, email.NoRateLimiterError):
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(eErr, email.SetupRateLimiterError):
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
|
||||
default:
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1536,9 +1544,16 @@ func (a *App) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *mode
|
||||
if err != nil {
|
||||
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
|
||||
}
|
||||
err = a.Srv().EmailService.sendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message)
|
||||
if eErr != nil {
|
||||
switch {
|
||||
case errors.Is(eErr, email.NoRateLimiterError):
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(eErr, email.SetupRateLimiterError):
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
|
||||
default:
|
||||
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1575,9 +1590,16 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string)
|
||||
}
|
||||
|
||||
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
||||
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
|
||||
if err != nil {
|
||||
return err
|
||||
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
|
||||
if eErr != nil {
|
||||
switch {
|
||||
case errors.Is(eErr, email.NoRateLimiterError):
|
||||
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(eErr, email.SetupRateLimiterError):
|
||||
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1610,9 +1632,16 @@ func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsIn
|
||||
if err != nil {
|
||||
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
|
||||
}
|
||||
err = a.Srv().EmailService.sendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message)
|
||||
if err != nil {
|
||||
return err
|
||||
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message)
|
||||
if eErr != nil {
|
||||
switch {
|
||||
case errors.Is(eErr, email.NoRateLimiterError):
|
||||
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
|
||||
case errors.Is(eErr, email.SetupRateLimiterError):
|
||||
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusRequestEntityTooLarge)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
48
app/user.go
48
app/user.go
@@ -15,6 +15,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
||||
@@ -143,7 +144,7 @@ func (a *App) CreateUserWithInviteId(c *request.Context, user *model.User, invit
|
||||
|
||||
a.AddDirectChannels(team.Id, ruser)
|
||||
|
||||
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
mlog.Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -156,7 +157,7 @@ func (a *App) CreateUserAsAdmin(c *request.Context, user *model.User, redirect s
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
mlog.Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -180,7 +181,7 @@ func (a *App) CreateUserFromSignup(c *request.Context, user *model.User, redirec
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
|
||||
mlog.Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
|
||||
}
|
||||
|
||||
@@ -1101,7 +1102,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
|
||||
})
|
||||
} else {
|
||||
a.Srv().Go(func() {
|
||||
if err := a.Srv().EmailService.sendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
|
||||
mlog.Error("Failed to send email change email", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1110,7 +1111,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
|
||||
|
||||
if userUpdate.New.Username != userUpdate.Old.Username {
|
||||
a.Srv().Go(func() {
|
||||
if err := a.Srv().EmailService.sendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
|
||||
mlog.Error("Failed to send change username email", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1171,7 +1172,7 @@ func (a *App) UpdateMfa(activate bool, userID, token string) *model.AppError {
|
||||
return
|
||||
}
|
||||
|
||||
if err := a.Srv().EmailService.sendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
|
||||
mlog.Error("Failed to send mfa change email", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1210,7 +1211,7 @@ func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method stri
|
||||
}
|
||||
|
||||
a.Srv().Go(func() {
|
||||
if err := a.Srv().EmailService.sendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
|
||||
mlog.Error("Failed to send password change email", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1297,7 +1298,12 @@ func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppE
|
||||
return false, err
|
||||
}
|
||||
|
||||
return a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
|
||||
result, eErr := a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
|
||||
if eErr != nil {
|
||||
return result, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+eErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a *App) CreatePasswordRecoveryToken(userID, email string) (*model.Token, *model.AppError) {
|
||||
@@ -1540,13 +1546,27 @@ func (a *App) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
|
||||
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
|
||||
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
|
||||
if err != nil {
|
||||
return err
|
||||
switch {
|
||||
case errors.Is(err, email.CreateEmailTokenError):
|
||||
return model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
|
||||
default:
|
||||
return model.NewAppError("CreateVerifyEmailToken", "app.recover.save.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := a.GetStatus(user.Id); err != nil {
|
||||
return a.Srv().EmailService.sendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
|
||||
eErr := a.Srv().EmailService.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
|
||||
if eErr != nil {
|
||||
return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, eErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return a.Srv().EmailService.sendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token)
|
||||
|
||||
if err := a.Srv().EmailService.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token); err != nil {
|
||||
return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError {
|
||||
@@ -1580,7 +1600,7 @@ func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppErr
|
||||
|
||||
if user.Email != tokenData.Email {
|
||||
a.Srv().Go(func() {
|
||||
if err := a.Srv().EmailService.sendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
|
||||
if err := a.Srv().EmailService.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
|
||||
mlog.Error("Failed to send email change email", mlog.Err(err))
|
||||
}
|
||||
})
|
||||
@@ -1989,7 +2009,7 @@ func (a *App) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestricti
|
||||
// PromoteGuestToUser Convert user's roles and all his mermbership's roles from
|
||||
// guest roles to regular user roles.
|
||||
func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
|
||||
nErr := a.Srv().Store.User().PromoteGuestToUser(user.Id)
|
||||
nErr := a.srv.userService.PromoteGuestToUser(user)
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
if nErr != nil {
|
||||
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
@@ -2045,7 +2065,7 @@ func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestor
|
||||
// DemoteUserToGuest Convert user's roles and all his mermbership's roles from
|
||||
// regular user roles to guest roles.
|
||||
func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
|
||||
demotedUser, nErr := a.Srv().Store.User().DemoteUserToGuest(user.Id)
|
||||
demotedUser, nErr := a.srv.userService.DemoteUserToGuest(user)
|
||||
a.InvalidateCacheForUser(user.Id)
|
||||
if nErr != nil {
|
||||
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
||||
|
||||
@@ -398,19 +398,19 @@ func TestUpdateUserEmail(t *testing.T) {
|
||||
newEmail := th.MakeEmail()
|
||||
|
||||
user.Email = newEmail
|
||||
user2, err := th.App.UpdateUser(user, false)
|
||||
assert.Nil(t, err)
|
||||
user2, appErr := th.App.UpdateUser(user, false)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, currentEmail, user2.Email)
|
||||
assert.True(t, user2.EmailVerified)
|
||||
|
||||
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(user2.Id, newEmail)
|
||||
assert.Nil(t, err)
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = th.App.VerifyEmailFromToken(token.Token)
|
||||
assert.Nil(t, err)
|
||||
appErr = th.App.VerifyEmailFromToken(token.Token)
|
||||
assert.Nil(t, appErr)
|
||||
|
||||
user2, err = th.App.GetUser(user2.Id)
|
||||
assert.Nil(t, err)
|
||||
user2, appErr = th.App.GetUser(user2.Id)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, newEmail, user2.Email)
|
||||
assert.True(t, user2.EmailVerified)
|
||||
|
||||
@@ -425,8 +425,8 @@ func TestUpdateUserEmail(t *testing.T) {
|
||||
|
||||
newBotEmail := th.MakeEmail()
|
||||
botuser.Email = newBotEmail
|
||||
botuser2, err := th.App.UpdateUser(&botuser, false)
|
||||
assert.Nil(t, err)
|
||||
botuser2, appErr := th.App.UpdateUser(&botuser, false)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, botuser2.Email, newBotEmail)
|
||||
|
||||
})
|
||||
|
||||
@@ -168,6 +168,7 @@ func TestHubSessionRevokeRace(t *testing.T) {
|
||||
ConfigFn: th.App.srv.Config,
|
||||
Metrics: th.App.Metrics(),
|
||||
Cluster: th.App.Cluster(),
|
||||
LicenseFn: th.App.srv.License,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
th.App.srv.userService = userService
|
||||
|
||||
@@ -7,7 +7,7 @@ require (
|
||||
github.com/golang-migrate/migrate/v4 v4.14.1 // indirect
|
||||
github.com/jstemmer/go-junit-report v0.9.1 // indirect
|
||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9 // indirect
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833 // indirect
|
||||
github.com/philhofer/fwd v1.0.0 // indirect
|
||||
github.com/reflog/struct2interface v0.6.1 // indirect
|
||||
github.com/spf13/cobra v1.1.3 // indirect
|
||||
|
||||
@@ -344,6 +344,8 @@ github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d764
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d7640e8538/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9 h1:EdA8k1LBxdk1SslBITXYiGVIptfPWFt7fRwxiy2BsTk=
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833 h1:Cgx5Md/4umqKYAgu8oPTZ+vDPZ5DaaRpjUjR+CUsmNI=
|
||||
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
|
||||
40
i18n/en.json
40
i18n/en.json
@@ -3866,14 +3866,6 @@
|
||||
"id": "api.user.check_user_password.invalid.app_error",
|
||||
"translation": "Login failed because of invalid password."
|
||||
},
|
||||
{
|
||||
"id": "api.user.cloud_trial_ended_email.error",
|
||||
"translation": "Failed to send trial ended email"
|
||||
},
|
||||
{
|
||||
"id": "api.user.cloud_trial_ending_email.error",
|
||||
"translation": "Failed to send trial ending warning email"
|
||||
},
|
||||
{
|
||||
"id": "api.user.complete_switch_with_oauth.blank_email.app_error",
|
||||
"translation": "Blank email."
|
||||
@@ -4130,30 +4122,10 @@
|
||||
"id": "api.user.send_deactivate_email_and_forget.failed.error",
|
||||
"translation": "Failed to send the deactivate account email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_email_change_email_and_forget.error",
|
||||
"translation": "Failed to send email change notification email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_email_change_username_and_forget.error",
|
||||
"translation": "Failed to send username change notification email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_email_change_verify_email_and_forget.error",
|
||||
"translation": "Failed to send email change verification email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_license_up_for_renewal_email.error",
|
||||
"translation": "Failed to send license up for renewal email"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_mfa_change_email.error",
|
||||
"translation": "Unable to send email notification for MFA change."
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_password_change_email_and_forget.error",
|
||||
"translation": "Failed to send update password email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_password_reset.send.app_error",
|
||||
"translation": "Failed to send password reset email successfully."
|
||||
@@ -4166,22 +4138,10 @@
|
||||
"id": "api.user.send_sign_in_change_email_and_forget.error",
|
||||
"translation": "Failed to send update password email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_upgrade_request_email.error",
|
||||
"translation": "Failed to send email to user limit notification to admin"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_user_access_token.error",
|
||||
"translation": "Failed to send \"Personal access token added\" email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_verify_email_and_forget.failed.error",
|
||||
"translation": "Failed to send verification email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.send_welcome_email_and_forget.failed.error",
|
||||
"translation": "Failed to send welcome email successfully"
|
||||
},
|
||||
{
|
||||
"id": "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error",
|
||||
"translation": "You cannot activate a guest account because Guest Access feature is not enabled."
|
||||
|
||||
@@ -10,13 +10,16 @@ import (
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/golang/freetype"
|
||||
"github.com/golang/freetype/truetype"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||
)
|
||||
|
||||
@@ -24,6 +27,68 @@ const (
|
||||
imageProfilePixelDimension = 128
|
||||
)
|
||||
|
||||
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
|
||||
if *us.config().FileSettings.DriverName == "" {
|
||||
img, err := us.GetDefaultProfileImage(user)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
return img, false, nil
|
||||
}
|
||||
|
||||
path := path.Join("users", user.Id, "profile.png")
|
||||
data, err := us.ReadFile(path)
|
||||
if err != nil {
|
||||
img, appErr := us.GetDefaultProfileImage(user)
|
||||
if appErr != nil {
|
||||
return nil, false, appErr
|
||||
}
|
||||
|
||||
if user.LastPictureUpdate == 0 {
|
||||
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
return img, true, nil
|
||||
}
|
||||
|
||||
return data, false, nil
|
||||
}
|
||||
|
||||
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
|
||||
license := us.license()
|
||||
backend, err := filestore.NewFileBackend(us.config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
func (us *UserService) ReadFile(path string) ([]byte, error) {
|
||||
backend, err := us.FileBackend()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, nErr := backend.ReadFile(path)
|
||||
if nErr != nil {
|
||||
return nil, nErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
|
||||
backend, err := us.FileBackend()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
result, nErr := backend.WriteFile(fr, path)
|
||||
if nErr != nil {
|
||||
return result, nErr
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
|
||||
if user.IsBot {
|
||||
return botDefaultImage, nil
|
||||
|
||||
@@ -24,6 +24,7 @@ type UserService struct {
|
||||
metrics einterfaces.MetricsInterface
|
||||
cluster einterfaces.ClusterInterface
|
||||
config func() *model.Config
|
||||
license func() *model.License
|
||||
}
|
||||
|
||||
// ServiceConfig is used to initialize the UserService.
|
||||
@@ -33,6 +34,7 @@ type ServiceConfig struct {
|
||||
SessionStore store.SessionStore
|
||||
OAuthStore store.OAuthStore
|
||||
ConfigFn func() *model.Config
|
||||
LicenseFn func() *model.License
|
||||
// Optional fields
|
||||
Metrics einterfaces.MetricsInterface
|
||||
Cluster einterfaces.ClusterInterface
|
||||
@@ -62,6 +64,7 @@ func New(c ServiceConfig) (*UserService, error) {
|
||||
sessionStore: c.SessionStore,
|
||||
oAuthStore: c.OAuthStore,
|
||||
config: c.ConfigFn,
|
||||
license: c.LicenseFn,
|
||||
metrics: c.Metrics,
|
||||
cluster: c.Cluster,
|
||||
sessionCache: sessionCache,
|
||||
@@ -74,7 +77,7 @@ func New(c ServiceConfig) (*UserService, error) {
|
||||
}
|
||||
|
||||
func (c *ServiceConfig) validate() error {
|
||||
if in := c; in.ConfigFn == nil || in.UserStore == nil || in.SessionStore == nil || in.OAuthStore == nil {
|
||||
if c.ConfigFn == nil || c.UserStore == nil || c.SessionStore == nil || c.OAuthStore == nil || c.LicenseFn == nil {
|
||||
return errors.New("required parameters are not provided")
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,16 @@ func TestNew(t *testing.T) {
|
||||
return &model.Config{}
|
||||
}
|
||||
|
||||
lfn := func() *model.License {
|
||||
return model.NewTestLicense()
|
||||
}
|
||||
|
||||
_, err = New(ServiceConfig{
|
||||
UserStore: dbStore.User(),
|
||||
SessionStore: dbStore.Session(),
|
||||
OAuthStore: dbStore.OAuth(),
|
||||
ConfigFn: cfn,
|
||||
LicenseFn: lfn,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -242,3 +242,11 @@ func (us *UserService) ActivateMfa(user *model.User, token string) error {
|
||||
func (us *UserService) DeactivateMfa(user *model.User) error {
|
||||
return mfa.New(us.store).Deactivate(user.Id)
|
||||
}
|
||||
|
||||
func (us *UserService) PromoteGuestToUser(user *model.User) error {
|
||||
return us.store.PromoteGuestToUser(user.Id)
|
||||
}
|
||||
|
||||
func (us *UserService) DemoteUserToGuest(user *model.User) (*model.User, error) {
|
||||
return us.store.DemoteUserToGuest(user.Id)
|
||||
}
|
||||
|
||||
@@ -154,8 +154,8 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
|
||||
c.App.Srv().Go(func() {
|
||||
if err = c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
|
||||
c.LogErrorByCode(err)
|
||||
if err := c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
|
||||
c.LogErrorByCode(model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user