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
|
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 {
|
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 = appErr
|
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
case model.EventTypeTrialWillEnd:
|
case model.EventTypeTrialWillEnd:
|
||||||
|
|||||||
@@ -4,11 +4,13 @@
|
|||||||
package api4
|
package api4
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"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/audit"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
"github.com/mattermost/mattermost-server/v5/store"
|
"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 {
|
if len(goodEmails) > 0 {
|
||||||
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
|
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
|
||||||
if err != nil {
|
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
|
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)
|
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL)
|
||||||
if err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
ReturnStatusOK(w)
|
ReturnStatusOK(w)
|
||||||
|
|||||||
@@ -1398,8 +1398,8 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
if isSelfDeactive {
|
if isSelfDeactive {
|
||||||
c.App.Srv().Go(func() {
|
c.App.Srv().Go(func() {
|
||||||
if err = c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
|
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
|
||||||
c.LogErrorByCode(err)
|
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)
|
ruser, _ := th.Client.CreateUser(&user)
|
||||||
|
|
||||||
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(ruser.Id, email)
|
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)
|
_, resp := th.Client.VerifyUserEmail(token.Token)
|
||||||
CheckNoError(t, resp)
|
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)
|
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)
|
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["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
|
||||||
data.Props["ContactNameValue"] = sender.GetFullName()
|
data.Props["ContactNameValue"] = sender.GetFullName()
|
||||||
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
|
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")
|
||||||
|
|||||||
@@ -1977,6 +1977,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
|
|||||||
SessionStore: &mockSessionStore,
|
SessionStore: &mockSessionStore,
|
||||||
OAuthStore: &mockOAuthStore,
|
OAuthStore: &mockOAuthStore,
|
||||||
ConfigFn: th.App.srv.Config,
|
ConfigFn: th.App.srv.Config,
|
||||||
|
LicenseFn: th.App.srv.License,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mockPreferenceStore := mocks.PreferenceStore{}
|
mockPreferenceStore := mocks.PreferenceStore{}
|
||||||
|
|||||||
@@ -179,8 +179,8 @@ func (a *App) CheckAndSendUserLimitWarningEmails(c *request.Context) *model.AppE
|
|||||||
} else if remainingUsers == 0 {
|
} else if remainingUsers == 0 {
|
||||||
// At limit
|
// At limit
|
||||||
for admin := range sysAdmins {
|
for admin := range sysAdmins {
|
||||||
_, appErr := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
_, err := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
|
||||||
if appErr != nil {
|
if err != nil {
|
||||||
a.Log().Error(
|
a.Log().Error(
|
||||||
"Error sending user limit warning email to admin",
|
"Error sending user limit warning email to admin",
|
||||||
mlog.String("username", sysAdmins[admin].Username),
|
mlog.String("username", sysAdmins[admin].Username),
|
||||||
|
|||||||
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,11 +1,10 @@
|
|||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package app
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@@ -23,10 +22,20 @@ const (
|
|||||||
EmailBatchingTaskName = "Email Batching"
|
EmailBatchingTaskName = "Email Batching"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (es *EmailService) InitEmailBatching() {
|
type postData struct {
|
||||||
if *es.srv.Config().EmailSettings.EnableEmailBatching {
|
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 {
|
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
|
// 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 {
|
func (es *Service) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
|
||||||
if !*es.srv.Config().EmailSettings.EnableEmailBatching {
|
if !*es.config().EmailSettings.EnableEmailBatching {
|
||||||
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
|
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 {
|
type EmailBatchingJob struct {
|
||||||
server *Server
|
config func() *model.Config
|
||||||
|
service *Service
|
||||||
|
|
||||||
newNotifications chan *batchedNotification
|
newNotifications chan *batchedNotification
|
||||||
pendingNotifications map[string][]*batchedNotification
|
pendingNotifications map[string][]*batchedNotification
|
||||||
task *model.ScheduledTask
|
task *model.ScheduledTask
|
||||||
taskMutex sync.Mutex
|
taskMutex sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEmailBatchingJob(es *EmailService, bufferSize int) *EmailBatchingJob {
|
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
|
||||||
return &EmailBatchingJob{
|
return &EmailBatchingJob{
|
||||||
server: es.srv,
|
config: es.config,
|
||||||
|
service: es,
|
||||||
newNotifications: make(chan *batchedNotification, bufferSize),
|
newNotifications: make(chan *batchedNotification, bufferSize),
|
||||||
pendingNotifications: make(map[string][]*batchedNotification),
|
pendingNotifications: make(map[string][]*batchedNotification),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (job *EmailBatchingJob) Start() {
|
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))
|
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.server.Config().EmailSettings.EmailBatchingInterval)*time.Second)
|
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.config().EmailSettings.EmailBatchingInterval)*time.Second)
|
||||||
|
|
||||||
job.taskMutex.Lock()
|
job.taskMutex.Lock()
|
||||||
oldTask := job.task
|
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
|
// 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
|
// 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)))
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
team, nErr := job.server.Store.Team().GetByName(notifications[0].teamName)
|
team, nErr := job.service.store.Team().GetByName(notifications[0].teamName)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
|
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
|
||||||
continue
|
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
|
// if the user has viewed any channels in this team since the notification was queued, delete
|
||||||
// all queued notifications
|
// 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 {
|
if err != nil {
|
||||||
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
|
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
|
||||||
continue
|
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
|
// get how long we need to wait to send notifications to the user
|
||||||
var interval int64
|
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 {
|
if err != nil {
|
||||||
// use the default batching interval if an error ocurrs while fetching user preferences
|
// use the default batching interval if an error ocurrs while fetching user preferences
|
||||||
interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
|
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
|
// 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 {
|
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() {
|
return func() {
|
||||||
handler(userID, notifications)
|
handler(userID, notifications)
|
||||||
}
|
}
|
||||||
@@ -194,38 +206,38 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (es *EmailService) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
|
func (es *Service) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
|
||||||
user, err := es.srv.Store.User().Get(context.Background(), userID)
|
user, err := es.userService.GetUser(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mlog.Warn("Unable to find recipient for batched email notification")
|
mlog.Warn("Unable to find recipient for batched email notification")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
translateFunc := i18n.GetUserTranslations(user.Locale)
|
translateFunc := i18n.GetUserTranslations(user.Locale)
|
||||||
displayNameFormat := *es.srv.Config().TeamSettings.TeammateNameDisplay
|
displayNameFormat := *es.config().TeamSettings.TeammateNameDisplay
|
||||||
siteURL := *es.srv.Config().ServiceSettings.SiteURL
|
siteURL := *es.config().ServiceSettings.SiteURL
|
||||||
|
|
||||||
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
|
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
|
||||||
embeddedFiles := make(map[string]io.Reader)
|
embeddedFiles := make(map[string]io.Reader)
|
||||||
|
|
||||||
emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
|
emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
|
||||||
if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents {
|
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
|
||||||
emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType
|
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
|
||||||
}
|
}
|
||||||
|
|
||||||
if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
|
if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
|
||||||
for i, notification := range notifications {
|
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 {
|
if errSender != nil {
|
||||||
mlog.Warn("Unable to find sender of post for batched email notification")
|
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 {
|
if errCh != nil {
|
||||||
mlog.Warn("Unable to find channel of post for batched email notification")
|
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 {
|
if errProfileImage != nil {
|
||||||
mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage))
|
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),
|
SenderName: sender.GetDisplayName(displayNameFormat),
|
||||||
Time: t,
|
Time: t,
|
||||||
ChannelName: channel.DisplayName,
|
ChannelName: channel.DisplayName,
|
||||||
Message: template.HTML(es.srv.GetMessageForNotification(notification.post, translateFunc)),
|
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
|
||||||
MessageURL: MessageURL,
|
MessageURL: MessageURL,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -263,18 +275,18 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
|
|||||||
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
|
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
|
||||||
|
|
||||||
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
|
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(),
|
"Year": tm.Year(),
|
||||||
"Month": translateFunc(tm.Month().String()),
|
"Month": translateFunc(tm.Month().String()),
|
||||||
"Day": tm.Day(),
|
"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 {
|
if err != nil {
|
||||||
mlog.Warn("Unable to find sender of post for batched email notification")
|
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["SiteURL"] = siteURL
|
||||||
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1, map[string]interface{}{
|
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1, map[string]interface{}{
|
||||||
"SenderName": firstSender.GetDisplayName(displayNameFormat),
|
"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["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
|
||||||
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
|
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 {
|
if renderErr != nil {
|
||||||
mlog.Error("Unable to render email", mlog.Err(renderErr))
|
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))
|
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.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
package app
|
package email
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -24,7 +24,7 @@ func TestHandleNewNotifications(t *testing.T) {
|
|||||||
id3 := model.NewId()
|
id3 := model.NewId()
|
||||||
|
|
||||||
// test queueing of received posts by user
|
// test queueing of received posts by user
|
||||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
job := NewEmailBatchingJob(th.service, 128)
|
||||||
|
|
||||||
job.handleNewNotifications()
|
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")
|
require.Len(t, job.pendingNotifications[id3], 1, "should have received 1 post for user3")
|
||||||
|
|
||||||
// test ordering of received posts
|
// 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: "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: id1}, &model.Post{UserId: id1, Message: "test2"}, &model.Team{Name: "team"})
|
||||||
@@ -78,7 +78,7 @@ func TestCheckPendingNotifications(t *testing.T) {
|
|||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
job := NewEmailBatchingJob(th.service, 128)
|
||||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||||
{
|
{
|
||||||
post: &model.Post{
|
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)
|
require.NoError(t, err)
|
||||||
channelMember.LastViewedAt = 9999999
|
channelMember.LastViewedAt = 9999999
|
||||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nErr := th.App.Srv().Store.Preference().Save(&model.Preferences{{
|
nErr := th.store.Preference().Save(&model.Preferences{{
|
||||||
UserId: th.BasicUser.Id,
|
UserId: th.BasicUser.Id,
|
||||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
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")
|
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
|
// 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)
|
require.NoError(t, err)
|
||||||
channelMember.LastViewedAt = 10001000
|
channelMember.LastViewedAt = 10001000
|
||||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// We reset the interval to something shorter
|
// 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,
|
UserId: th.BasicUser.Id,
|
||||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
||||||
@@ -197,13 +197,18 @@ func TestCheckPendingNotificationsDefaultInterval(t *testing.T) {
|
|||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
|
||||||
job := NewEmailBatchingJob(th.Server.EmailService, 128)
|
job := NewEmailBatchingJob(th.service, 128)
|
||||||
|
|
||||||
// bypasses recent user activity check
|
// 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)
|
require.NoError(t, err)
|
||||||
channelMember.LastViewedAt = 9999000
|
channelMember.LastViewedAt = 9999000
|
||||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
|
||||||
@@ -235,17 +240,21 @@ func TestCheckPendingNotificationsCantParseInterval(t *testing.T) {
|
|||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
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
|
// 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)
|
require.NoError(t, err)
|
||||||
channelMember.LastViewedAt = 9999000
|
channelMember.LastViewedAt = 9999000
|
||||||
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
|
_, err = th.store.Channel().UpdateMember(channelMember)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// preference value is not an integer, so we'll fall back to the default 15min value
|
// 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,
|
UserId: th.BasicUser.Id,
|
||||||
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
|
||||||
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
|
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"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestSendInviteEmailRateLimits(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
@@ -134,39 +111,3 @@ func TestSendAdminUpgradeRequestEmailOnJoin(t *testing.T) {
|
|||||||
require.NotNil(t, err)
|
require.NotNil(t, err)
|
||||||
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
|
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"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/url"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -114,7 +112,7 @@ func (a *App) sendNotificationEmail(notification *PostNotification, user *model.
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.Srv().Go(func() {
|
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))
|
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)
|
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()
|
data.Props["SiteURL"] = a.GetSiteURL()
|
||||||
if teamName != "select_team" {
|
if teamName != "select_team" {
|
||||||
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
|
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
|
||||||
@@ -321,38 +319,6 @@ func (a *App) generateHyperlinkForChannels(postMessage, teamName, teamURL string
|
|||||||
return postMessage, nil
|
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 {
|
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()
|
zone, _ := tm.Zone()
|
||||||
|
|
||||||
formattedTime := formattedPostTime{
|
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()),
|
Hour: fmt.Sprintf("%02d", tm.Hour()),
|
||||||
Minute: fmt.Sprintf("%02d", tm.Minute()),
|
Minute: fmt.Sprintf("%02d", tm.Minute()),
|
||||||
TimeZone: zone,
|
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)
|
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)
|
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ import (
|
|||||||
"github.com/rs/cors"
|
"github.com/rs/cors"
|
||||||
"golang.org/x/crypto/acme/autocert"
|
"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/featureflag"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||||
@@ -113,7 +114,7 @@ type Server struct {
|
|||||||
PluginConfigListenerId string
|
PluginConfigListenerId string
|
||||||
PluginsLock sync.RWMutex
|
PluginsLock sync.RWMutex
|
||||||
|
|
||||||
EmailService *EmailService
|
EmailService *email.Service
|
||||||
|
|
||||||
hubs []*Hub
|
hubs []*Hub
|
||||||
hashSeed maphash.Seed
|
hashSeed maphash.Seed
|
||||||
@@ -416,6 +417,7 @@ func NewServer(options ...Option) (*Server, error) {
|
|||||||
ConfigFn: s.Config,
|
ConfigFn: s.Config,
|
||||||
Metrics: s.Metrics,
|
Metrics: s.Metrics,
|
||||||
Cluster: s.Cluster,
|
Cluster: s.Cluster,
|
||||||
|
LicenseFn: s.License,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "unable to create users service")
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "unable to initialize email service")
|
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 == "" {
|
if name == "" {
|
||||||
name = user.Username
|
name = user.Username
|
||||||
}
|
}
|
||||||
ok, err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration)
|
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil {
|
||||||
if !ok || err != nil {
|
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
|
||||||
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email))
|
|
||||||
countNotOks++
|
countNotOks++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1864,7 +1872,7 @@ func (s *Server) doLicenseExpirationCheck() {
|
|||||||
|
|
||||||
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
|
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
|
||||||
s.Go(func() {
|
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))
|
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()
|
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) {
|
func (s *Server) StartSearchEngine() (string, string) {
|
||||||
if s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() {
|
if s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() {
|
||||||
s.Go(func() {
|
s.Go(func() {
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAc
|
|||||||
|
|
||||||
// Don't send emails to bot users.
|
// Don't send emails to bot users.
|
||||||
if !user.IsBot {
|
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))
|
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"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
@@ -1434,9 +1435,16 @@ func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderI
|
|||||||
|
|
||||||
if len(goodEmails) > 0 {
|
if len(goodEmails) > 0 {
|
||||||
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
||||||
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
|
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
|
||||||
if err != nil {
|
if eErr != nil {
|
||||||
return nil, err
|
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 {
|
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))
|
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)
|
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message)
|
||||||
if err != nil {
|
if eErr != nil {
|
||||||
return nil, err
|
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
|
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
|
||||||
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
|
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
|
||||||
if err != nil {
|
if eErr != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
@@ -1610,9 +1632,16 @@ func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsIn
|
|||||||
if err != nil {
|
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))
|
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)
|
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message)
|
||||||
if err != nil {
|
if eErr != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
|
|||||||
48
app/user.go
48
app/user.go
@@ -15,6 +15,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattermost/mattermost-server/v5/app/email"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
"github.com/mattermost/mattermost-server/v5/app/imaging"
|
||||||
"github.com/mattermost/mattermost-server/v5/app/request"
|
"github.com/mattermost/mattermost-server/v5/app/request"
|
||||||
"github.com/mattermost/mattermost-server/v5/einterfaces"
|
"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)
|
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))
|
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
|
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))
|
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
|
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))
|
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 {
|
} else {
|
||||||
a.Srv().Go(func() {
|
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))
|
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 {
|
if userUpdate.New.Username != userUpdate.Old.Username {
|
||||||
a.Srv().Go(func() {
|
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))
|
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
|
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))
|
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() {
|
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))
|
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 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) {
|
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 {
|
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
|
||||||
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
|
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
|
||||||
if err != nil {
|
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 {
|
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 {
|
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 {
|
if user.Email != tokenData.Email {
|
||||||
a.Srv().Go(func() {
|
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))
|
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
|
// PromoteGuestToUser Convert user's roles and all his mermbership's roles from
|
||||||
// guest roles to regular user roles.
|
// guest roles to regular user roles.
|
||||||
func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
|
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)
|
a.InvalidateCacheForUser(user.Id)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
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
|
// DemoteUserToGuest Convert user's roles and all his mermbership's roles from
|
||||||
// regular user roles to guest roles.
|
// regular user roles to guest roles.
|
||||||
func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
|
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)
|
a.InvalidateCacheForUser(user.Id)
|
||||||
if nErr != nil {
|
if nErr != nil {
|
||||||
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
|
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()
|
newEmail := th.MakeEmail()
|
||||||
|
|
||||||
user.Email = newEmail
|
user.Email = newEmail
|
||||||
user2, err := th.App.UpdateUser(user, false)
|
user2, appErr := th.App.UpdateUser(user, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, appErr)
|
||||||
assert.Equal(t, currentEmail, user2.Email)
|
assert.Equal(t, currentEmail, user2.Email)
|
||||||
assert.True(t, user2.EmailVerified)
|
assert.True(t, user2.EmailVerified)
|
||||||
|
|
||||||
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(user2.Id, newEmail)
|
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(user2.Id, newEmail)
|
||||||
assert.Nil(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
err = th.App.VerifyEmailFromToken(token.Token)
|
appErr = th.App.VerifyEmailFromToken(token.Token)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, appErr)
|
||||||
|
|
||||||
user2, err = th.App.GetUser(user2.Id)
|
user2, appErr = th.App.GetUser(user2.Id)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, appErr)
|
||||||
assert.Equal(t, newEmail, user2.Email)
|
assert.Equal(t, newEmail, user2.Email)
|
||||||
assert.True(t, user2.EmailVerified)
|
assert.True(t, user2.EmailVerified)
|
||||||
|
|
||||||
@@ -425,8 +425,8 @@ func TestUpdateUserEmail(t *testing.T) {
|
|||||||
|
|
||||||
newBotEmail := th.MakeEmail()
|
newBotEmail := th.MakeEmail()
|
||||||
botuser.Email = newBotEmail
|
botuser.Email = newBotEmail
|
||||||
botuser2, err := th.App.UpdateUser(&botuser, false)
|
botuser2, appErr := th.App.UpdateUser(&botuser, false)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, appErr)
|
||||||
assert.Equal(t, botuser2.Email, newBotEmail)
|
assert.Equal(t, botuser2.Email, newBotEmail)
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -168,6 +168,7 @@ func TestHubSessionRevokeRace(t *testing.T) {
|
|||||||
ConfigFn: th.App.srv.Config,
|
ConfigFn: th.App.srv.Config,
|
||||||
Metrics: th.App.Metrics(),
|
Metrics: th.App.Metrics(),
|
||||||
Cluster: th.App.Cluster(),
|
Cluster: th.App.Cluster(),
|
||||||
|
LicenseFn: th.App.srv.License,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
th.App.srv.userService = userService
|
th.App.srv.userService = userService
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require (
|
|||||||
github.com/golang-migrate/migrate/v4 v4.14.1 // indirect
|
github.com/golang-migrate/migrate/v4 v4.14.1 // indirect
|
||||||
github.com/jstemmer/go-junit-report v0.9.1 // indirect
|
github.com/jstemmer/go-junit-report v0.9.1 // indirect
|
||||||
github.com/jteeuwen/go-bindata v3.0.7+incompatible // 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/philhofer/fwd v1.0.0 // indirect
|
||||||
github.com/reflog/struct2interface v0.6.1 // indirect
|
github.com/reflog/struct2interface v0.6.1 // indirect
|
||||||
github.com/spf13/cobra v1.1.3 // 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-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 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-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.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 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
|
||||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
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",
|
"id": "api.user.check_user_password.invalid.app_error",
|
||||||
"translation": "Login failed because of invalid password."
|
"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",
|
"id": "api.user.complete_switch_with_oauth.blank_email.app_error",
|
||||||
"translation": "Blank email."
|
"translation": "Blank email."
|
||||||
@@ -4130,30 +4122,10 @@
|
|||||||
"id": "api.user.send_deactivate_email_and_forget.failed.error",
|
"id": "api.user.send_deactivate_email_and_forget.failed.error",
|
||||||
"translation": "Failed to send the deactivate account email successfully"
|
"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",
|
"id": "api.user.send_email_change_verify_email_and_forget.error",
|
||||||
"translation": "Failed to send email change verification email successfully"
|
"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",
|
"id": "api.user.send_password_reset.send.app_error",
|
||||||
"translation": "Failed to send password reset email successfully."
|
"translation": "Failed to send password reset email successfully."
|
||||||
@@ -4166,22 +4138,10 @@
|
|||||||
"id": "api.user.send_sign_in_change_email_and_forget.error",
|
"id": "api.user.send_sign_in_change_email_and_forget.error",
|
||||||
"translation": "Failed to send update password email successfully"
|
"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",
|
"id": "api.user.send_verify_email_and_forget.failed.error",
|
||||||
"translation": "Failed to send verification email successfully"
|
"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",
|
"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."
|
"translation": "You cannot activate a guest account because Guest Access feature is not enabled."
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"image/draw"
|
"image/draw"
|
||||||
"image/png"
|
"image/png"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/golang/freetype"
|
"github.com/golang/freetype"
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/mattermost/mattermost-server/v5/model"
|
"github.com/mattermost/mattermost-server/v5/model"
|
||||||
|
"github.com/mattermost/mattermost-server/v5/shared/filestore"
|
||||||
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -24,6 +27,68 @@ const (
|
|||||||
imageProfilePixelDimension = 128
|
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) {
|
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
|
||||||
if user.IsBot {
|
if user.IsBot {
|
||||||
return botDefaultImage, nil
|
return botDefaultImage, nil
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type UserService struct {
|
|||||||
metrics einterfaces.MetricsInterface
|
metrics einterfaces.MetricsInterface
|
||||||
cluster einterfaces.ClusterInterface
|
cluster einterfaces.ClusterInterface
|
||||||
config func() *model.Config
|
config func() *model.Config
|
||||||
|
license func() *model.License
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig is used to initialize the UserService.
|
// ServiceConfig is used to initialize the UserService.
|
||||||
@@ -33,6 +34,7 @@ type ServiceConfig struct {
|
|||||||
SessionStore store.SessionStore
|
SessionStore store.SessionStore
|
||||||
OAuthStore store.OAuthStore
|
OAuthStore store.OAuthStore
|
||||||
ConfigFn func() *model.Config
|
ConfigFn func() *model.Config
|
||||||
|
LicenseFn func() *model.License
|
||||||
// Optional fields
|
// Optional fields
|
||||||
Metrics einterfaces.MetricsInterface
|
Metrics einterfaces.MetricsInterface
|
||||||
Cluster einterfaces.ClusterInterface
|
Cluster einterfaces.ClusterInterface
|
||||||
@@ -62,6 +64,7 @@ func New(c ServiceConfig) (*UserService, error) {
|
|||||||
sessionStore: c.SessionStore,
|
sessionStore: c.SessionStore,
|
||||||
oAuthStore: c.OAuthStore,
|
oAuthStore: c.OAuthStore,
|
||||||
config: c.ConfigFn,
|
config: c.ConfigFn,
|
||||||
|
license: c.LicenseFn,
|
||||||
metrics: c.Metrics,
|
metrics: c.Metrics,
|
||||||
cluster: c.Cluster,
|
cluster: c.Cluster,
|
||||||
sessionCache: sessionCache,
|
sessionCache: sessionCache,
|
||||||
@@ -74,7 +77,7 @@ func New(c ServiceConfig) (*UserService, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *ServiceConfig) validate() 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")
|
return errors.New("required parameters are not provided")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ func TestNew(t *testing.T) {
|
|||||||
return &model.Config{}
|
return &model.Config{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lfn := func() *model.License {
|
||||||
|
return model.NewTestLicense()
|
||||||
|
}
|
||||||
|
|
||||||
_, err = New(ServiceConfig{
|
_, err = New(ServiceConfig{
|
||||||
UserStore: dbStore.User(),
|
UserStore: dbStore.User(),
|
||||||
SessionStore: dbStore.Session(),
|
SessionStore: dbStore.Session(),
|
||||||
OAuthStore: dbStore.OAuth(),
|
OAuthStore: dbStore.OAuth(),
|
||||||
ConfigFn: cfn,
|
ConfigFn: cfn,
|
||||||
|
LicenseFn: lfn,
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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 {
|
func (us *UserService) DeactivateMfa(user *model.User) error {
|
||||||
return mfa.New(us.store).Deactivate(user.Id)
|
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.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
|
||||||
c.App.Srv().Go(func() {
|
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 {
|
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)
|
c.LogErrorByCode(model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user