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
Этот коммит содержится в:
Ibrahim Serdar Acikgoz
2021-07-19 18:26:06 +03:00
коммит произвёл GitHub
родитель a78a7c7b54
Коммит 41dc05a6bd
35 изменённых файлов: 1156 добавлений и 555 удалений

Просмотреть файл

@@ -435,8 +435,8 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if appErr := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); appErr != nil {
c.Err = appErr
if err := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS, *c.App.Config().ServiceSettings.SiteURL); err != nil {
c.Err = model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError)
return
}
case model.EventTypeTrialWillEnd:

Просмотреть файл

@@ -4,11 +4,13 @@
package api4
import (
"fmt"
"net/http"
"strings"
"github.com/pkg/errors"
"github.com/mattermost/mattermost-server/v5/app/email"
"github.com/mattermost/mattermost-server/v5/audit"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
@@ -126,7 +128,14 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
if len(goodEmails) > 0 {
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = err
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge)
}
return
}
}
@@ -147,7 +156,14 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
}
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL)
if err != nil {
c.Err = err
switch {
case errors.Is(err, email.NoRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s", team.Id), http.StatusInternalServerError)
case errors.Is(err, email.SetupRateLimiterError):
c.Err = model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusInternalServerError)
default:
c.Err = model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("team_id=%s, error=%v", team.Id, err), http.StatusRequestEntityTooLarge)
}
return
}
ReturnStatusOK(w)

Просмотреть файл

@@ -1398,8 +1398,8 @@ func updateUserActive(c *Context, w http.ResponseWriter, r *http.Request) {
if isSelfDeactive {
c.App.Srv().Go(func() {
if err = c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(err)
if err := c.App.Srv().EmailService.SendDeactivateAccountEmail(user.Email, user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError))
}
})
}

Просмотреть файл

@@ -3245,7 +3245,7 @@ func TestVerifyUserEmail(t *testing.T) {
ruser, _ := th.Client.CreateUser(&user)
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(ruser.Id, email)
require.Nil(t, err, "Unable to create email verify token")
require.NoError(t, err, "Unable to create email verify token")
_, resp := th.Client.VerifyUserEmail(token.Token)
CheckNoError(t, resp)

Просмотреть файл

@@ -369,7 +369,7 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User,
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, i18n.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusInternalServerError)
}
T := i18n.GetUserTranslations(sender.Locale)
data := a.Srv().EmailService.newEmailTemplateData(sender.Locale)
data := a.Srv().EmailService.NewEmailTemplateData(sender.Locale)
data.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header")
data.Props["ContactNameValue"] = sender.GetFullName()
data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header")

Просмотреть файл

@@ -1977,6 +1977,7 @@ func TestMarkChannelsAsViewedPanic(t *testing.T) {
SessionStore: &mockSessionStore,
OAuthStore: &mockOAuthStore,
ConfigFn: th.App.srv.Config,
LicenseFn: th.App.srv.License,
})
require.NoError(t, err)
mockPreferenceStore := mocks.PreferenceStore{}

Просмотреть файл

@@ -179,8 +179,8 @@ func (a *App) CheckAndSendUserLimitWarningEmails(c *request.Context) *model.AppE
} else if remainingUsers == 0 {
// At limit
for admin := range sysAdmins {
_, appErr := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
if appErr != nil {
_, err := a.Srv().EmailService.SendAtUserLimitWarningEmail(sysAdmins[admin].Email, sysAdmins[admin].Locale, *a.Config().ServiceSettings.SiteURL)
if err != nil {
a.Log().Error(
"Error sending user limit warning email to admin",
mlog.String("username", sysAdmins[admin].Username),

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@@ -1,11 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
package email
import (
"bytes"
"context"
"fmt"
"html/template"
"io"
@@ -23,10 +22,20 @@ const (
EmailBatchingTaskName = "Email Batching"
)
func (es *EmailService) InitEmailBatching() {
if *es.srv.Config().EmailSettings.EnableEmailBatching {
type postData struct {
SenderName string
ChannelName string
Message template.HTML
MessageURL string
SenderPhoto string
PostPhoto string
Time string
}
func (es *Service) InitEmailBatching() {
if *es.config().EmailSettings.EnableEmailBatching {
if es.EmailBatching == nil {
es.EmailBatching = NewEmailBatchingJob(es, *es.srv.Config().EmailSettings.EmailBatchingBufferSize)
es.EmailBatching = NewEmailBatchingJob(es, *es.config().EmailSettings.EmailBatchingBufferSize)
}
// note that we don't support changing EmailBatchingBufferSize without restarting the server
@@ -35,8 +44,8 @@ func (es *EmailService) InitEmailBatching() {
}
}
func (es *EmailService) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
if !*es.srv.Config().EmailSettings.EnableEmailBatching {
func (es *Service) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
if !*es.config().EmailSettings.EnableEmailBatching {
return model.NewAppError("AddNotificationEmailToBatch", "api.email_batching.add_notification_email_to_batch.disabled.app_error", nil, "", http.StatusNotImplemented)
}
@@ -55,24 +64,27 @@ type batchedNotification struct {
}
type EmailBatchingJob struct {
server *Server
config func() *model.Config
service *Service
newNotifications chan *batchedNotification
pendingNotifications map[string][]*batchedNotification
task *model.ScheduledTask
taskMutex sync.Mutex
}
func NewEmailBatchingJob(es *EmailService, bufferSize int) *EmailBatchingJob {
func NewEmailBatchingJob(es *Service, bufferSize int) *EmailBatchingJob {
return &EmailBatchingJob{
server: es.srv,
config: es.config,
service: es,
newNotifications: make(chan *batchedNotification, bufferSize),
pendingNotifications: make(map[string][]*batchedNotification),
}
}
func (job *EmailBatchingJob) Start() {
mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.server.Config().EmailSettings.EmailBatchingInterval))
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.server.Config().EmailSettings.EmailBatchingInterval)*time.Second)
mlog.Debug("Email batching job starting. Checking for pending emails periodically.", mlog.Int("interval_in_seconds", *job.config().EmailSettings.EmailBatchingInterval))
newTask := model.CreateRecurringTask(EmailBatchingTaskName, job.CheckPendingEmails, time.Duration(*job.config().EmailSettings.EmailBatchingInterval)*time.Second)
job.taskMutex.Lock()
oldTask := job.task
@@ -105,7 +117,7 @@ func (job *EmailBatchingJob) CheckPendingEmails() {
// it's a bit weird to pass the send email function through here, but it makes it so that we can test
// without actually sending emails
job.checkPendingNotifications(time.Now(), job.server.EmailService.sendBatchedEmailNotification)
job.checkPendingNotifications(time.Now(), job.service.sendBatchedEmailNotification)
mlog.Debug("Email batching job ran. Some users still have notifications pending.", mlog.Int("number_of_users", len(job.pendingNotifications)))
}
@@ -140,7 +152,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
continue
}
team, nErr := job.server.Store.Team().GetByName(notifications[0].teamName)
team, nErr := job.service.store.Team().GetByName(notifications[0].teamName)
if nErr != nil {
mlog.Error("Unable to find Team id for notification", mlog.Err(nErr))
continue
@@ -152,7 +164,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
// if the user has viewed any channels in this team since the notification was queued, delete
// all queued notifications
channelMembers, err := job.server.Store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
channelMembers, err := job.service.store.Channel().GetMembersForUser(inspectedTeamNames[notification.teamName], userID)
if err != nil {
mlog.Error("Unable to find ChannelMembers for user", mlog.Err(err))
continue
@@ -169,7 +181,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
// get how long we need to wait to send notifications to the user
var interval int64
preference, err := job.server.Store.Preference().Get(userID, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
preference, err := job.service.store.Preference().Get(userID, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL)
if err != nil {
// use the default batching interval if an error ocurrs while fetching user preferences
interval, _ = strconv.ParseInt(model.PREFERENCE_EMAIL_INTERVAL_BATCHING_SECONDS, 10, 64)
@@ -184,7 +196,7 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
// send the email notification if there are notifications to send AND it's been long enough
if len(job.pendingNotifications[userID]) > 0 && now.Sub(time.Unix(batchStartTime/1000, 0)) > time.Duration(interval)*time.Second {
job.server.Go(func(userID string, notifications []*batchedNotification) func() {
job.service.goFn(func(userID string, notifications []*batchedNotification) func() {
return func() {
handler(userID, notifications)
}
@@ -194,38 +206,38 @@ func (job *EmailBatchingJob) checkPendingNotifications(now time.Time, handler fu
}
}
func (es *EmailService) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
user, err := es.srv.Store.User().Get(context.Background(), userID)
func (es *Service) sendBatchedEmailNotification(userID string, notifications []*batchedNotification) {
user, err := es.userService.GetUser(userID)
if err != nil {
mlog.Warn("Unable to find recipient for batched email notification")
return
}
translateFunc := i18n.GetUserTranslations(user.Locale)
displayNameFormat := *es.srv.Config().TeamSettings.TeammateNameDisplay
siteURL := *es.srv.Config().ServiceSettings.SiteURL
displayNameFormat := *es.config().TeamSettings.TeammateNameDisplay
siteURL := *es.config().ServiceSettings.SiteURL
postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */)
embeddedFiles := make(map[string]io.Reader)
emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType
if license := es.license(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *es.config().EmailSettings.EmailNotificationContentsType
}
if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL {
for i, notification := range notifications {
sender, errSender := es.srv.Store.User().Get(context.Background(), notification.post.UserId)
sender, errSender := es.userService.GetUser(notification.post.UserId)
if errSender != nil {
mlog.Warn("Unable to find sender of post for batched email notification")
}
channel, errCh := es.srv.Store.Channel().Get(notification.post.ChannelId, true)
channel, errCh := es.store.Channel().Get(notification.post.ChannelId, true)
if errCh != nil {
mlog.Warn("Unable to find channel of post for batched email notification")
}
senderProfileImage, _, errProfileImage := es.srv.GetProfileImage(sender)
senderProfileImage, _, errProfileImage := es.userService.GetProfileImage(sender)
if errProfileImage != nil {
mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage))
}
@@ -254,7 +266,7 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
SenderName: sender.GetDisplayName(displayNameFormat),
Time: t,
ChannelName: channel.DisplayName,
Message: template.HTML(es.srv.GetMessageForNotification(notification.post, translateFunc)),
Message: template.HTML(es.GetMessageForNotification(notification.post, translateFunc)),
MessageURL: MessageURL,
})
}
@@ -263,18 +275,18 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
tm := time.Unix(notifications[0].post.CreateAt/1000, 0)
subject := translateFunc("api.email_batching.send_batched_email_notification.subject", len(notifications), map[string]interface{}{
"SiteName": es.srv.Config().TeamSettings.SiteName,
"SiteName": es.config().TeamSettings.SiteName,
"Year": tm.Year(),
"Month": translateFunc(tm.Month().String()),
"Day": tm.Day(),
})
firstSender, err := es.srv.Store.User().Get(context.Background(), notifications[0].post.UserId)
firstSender, err := es.userService.GetUser(notifications[0].post.UserId)
if err != nil {
mlog.Warn("Unable to find sender of post for batched email notification")
}
data := es.newEmailTemplateData(user.Locale)
data := es.NewEmailTemplateData(user.Locale)
data.Props["SiteURL"] = siteURL
data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1, map[string]interface{}{
"SenderName": firstSender.GetDisplayName(displayNameFormat),
@@ -288,12 +300,12 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
renderedPage, renderErr := es.srv.TemplatesContainer().RenderToString("messages_notification", data)
renderedPage, renderErr := es.templatesContainer.RenderToString("messages_notification", data)
if renderErr != nil {
mlog.Error("Unable to render email", mlog.Err(renderErr))
}
if nErr := es.sendNotificationMail(user.Email, subject, renderedPage); nErr != nil {
if nErr := es.SendNotificationMail(user.Email, subject, renderedPage); nErr != nil {
mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr))
}
}

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
package email
import (
"context"
@@ -24,7 +24,7 @@ func TestHandleNewNotifications(t *testing.T) {
id3 := model.NewId()
// test queueing of received posts by user
job := NewEmailBatchingJob(th.Server.EmailService, 128)
job := NewEmailBatchingJob(th.service, 128)
job.handleNewNotifications()
@@ -59,7 +59,7 @@ func TestHandleNewNotifications(t *testing.T) {
require.Len(t, job.pendingNotifications[id3], 1, "should have received 1 post for user3")
// test ordering of received posts
job = NewEmailBatchingJob(th.Server.EmailService, 128)
job = NewEmailBatchingJob(th.service, 128)
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test1"}, &model.Team{Name: "team"})
job.Add(&model.User{Id: id1}, &model.Post{UserId: id1, Message: "test2"}, &model.Team{Name: "team"})
@@ -78,7 +78,7 @@ func TestCheckPendingNotifications(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
job := NewEmailBatchingJob(th.Server.EmailService, 128)
job := NewEmailBatchingJob(th.service, 128)
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
{
post: &model.Post{
@@ -90,13 +90,13 @@ func TestCheckPendingNotifications(t *testing.T) {
},
}
channelMember, err := th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
channelMember.LastViewedAt = 9999999
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
_, err = th.store.Channel().UpdateMember(channelMember)
require.NoError(t, err)
nErr := th.App.Srv().Store.Preference().Save(&model.Preferences{{
nErr := th.store.Preference().Save(&model.Preferences{{
UserId: th.BasicUser.Id,
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
@@ -111,14 +111,14 @@ func TestCheckPendingNotifications(t *testing.T) {
require.Len(t, job.pendingNotifications[th.BasicUser.Id], 1, "shouldn't have sent queued post")
// test that notifications are cleared if the user has acted
channelMember, err = th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
channelMember, err = th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
channelMember.LastViewedAt = 10001000
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
_, err = th.store.Channel().UpdateMember(channelMember)
require.NoError(t, err)
// We reset the interval to something shorter
nErr = th.App.Srv().Store.Preference().Save(&model.Preferences{{
nErr = th.store.Preference().Save(&model.Preferences{{
UserId: th.BasicUser.Id,
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
@@ -197,13 +197,18 @@ func TestCheckPendingNotificationsDefaultInterval(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
job := NewEmailBatchingJob(th.Server.EmailService, 128)
job := NewEmailBatchingJob(th.service, 128)
// bypasses recent user activity check
channelMember, err := th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NotNil(t, th.store)
require.NotNil(t, th.store.Channel())
require.NotNil(t, th.BasicUser)
require.NotNil(t, th.BasicChannel)
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
channelMember.LastViewedAt = 9999000
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
_, err = th.store.Channel().UpdateMember(channelMember)
require.NoError(t, err)
job.pendingNotifications[th.BasicUser.Id] = []*batchedNotification{
@@ -235,17 +240,21 @@ func TestCheckPendingNotificationsCantParseInterval(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
job := NewEmailBatchingJob(th.Server.EmailService, 128)
job := NewEmailBatchingJob(th.service, 128)
require.NotNil(t, th.store)
require.NotNil(t, th.store.Channel())
require.NotNil(t, th.BasicChannel)
require.NotNil(t, th.BasicUser)
// bypasses recent user activity check
channelMember, err := th.App.Srv().Store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
channelMember, err := th.store.Channel().GetMember(context.Background(), th.BasicChannel.Id, th.BasicUser.Id)
require.NoError(t, err)
channelMember.LastViewedAt = 9999000
_, err = th.App.Srv().Store.Channel().UpdateMember(channelMember)
_, err = th.store.Channel().UpdateMember(channelMember)
require.NoError(t, err)
// preference value is not an integer, so we'll fall back to the default 15min value
nErr := th.App.Srv().Store.Preference().Save(&model.Preferences{{
nErr := th.store.Preference().Save(&model.Preferences{{
UserId: th.BasicUser.Id,
Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,

73
app/email/email_test.go Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -0,0 +1,31 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package email
import (
"github.com/mattermost/mattermost-server/v5/shared/mail"
"github.com/mattermost/mattermost-server/v5/utils"
)
func (es *Service) mailServiceConfig() *mail.SMTPConfig {
emailSettings := es.config().EmailSettings
hostname := utils.GetHostnameFromSiteURL(*es.config().ServiceSettings.SiteURL)
cfg := mail.SMTPConfig{
Hostname: hostname,
ConnectionSecurity: *emailSettings.ConnectionSecurity,
SkipServerCertificateVerification: *emailSettings.SkipServerCertificateVerification,
ServerName: *emailSettings.SMTPServer,
Server: *emailSettings.SMTPServer,
Port: *emailSettings.SMTPPort,
ServerTimeout: *emailSettings.SMTPServerTimeout,
Username: *emailSettings.SMTPUsername,
Password: *emailSettings.SMTPPassword,
EnableSMTPAuth: *emailSettings.EnableSMTPAuth,
SendEmailNotifications: *emailSettings.SendEmailNotifications,
FeedbackName: *emailSettings.FeedbackName,
FeedbackEmail: *emailSettings.FeedbackEmail,
ReplyToAddress: *emailSettings.ReplyToAddress,
}
return &cfg
}

Просмотреть файл

@@ -8,34 +8,11 @@ import (
"strconv"
"testing"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/shared/mail"
)
func TestCondenseSiteURL(t *testing.T) {
require.Equal(t, "", condenseSiteURL(""))
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com"))
require.Equal(t, "mattermost.com", condenseSiteURL("mattermost.com/"))
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com"))
require.Equal(t, "chat.mattermost.com", condenseSiteURL("chat.mattermost.com/"))
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath"))
require.Equal(t, "mattermost.com/subpath", condenseSiteURL("mattermost.com/subpath/"))
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath"))
require.Equal(t, "chat.mattermost.com/subpath", condenseSiteURL("chat.mattermost.com/subpath/"))
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080"))
require.Equal(t, "mattermost.com:8080", condenseSiteURL("http://mattermost.com:8080/"))
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080"))
require.Equal(t, "chat.mattermost.com:8080", condenseSiteURL("http://chat.mattermost.com:8080/"))
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath"))
require.Equal(t, "mattermost.com:8080/subpath", condenseSiteURL("http://mattermost.com:8080/subpath/"))
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath"))
require.Equal(t, "chat.mattermost.com:8080/subpath", condenseSiteURL("http://chat.mattermost.com:8080/subpath/"))
}
func TestSendInviteEmailRateLimits(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
@@ -134,39 +111,3 @@ func TestSendAdminUpgradeRequestEmailOnJoin(t *testing.T) {
require.NotNil(t, err)
assert.Equal(t, err.Id, "app.email.rate_limit_exceeded.app_error")
}
func TestSendInviteEmails(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.ConfigureInbucketMail()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableEmailInvitations = true
})
emailTo := "test@example.com"
mail.DeleteMailBox(emailTo)
appErr := th.App.Srv().EmailService.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver")
require.Nil(t, appErr)
var resultsMailbox mail.JSONMessageHeaderInbucket
err2 := mail.RetryInbucket(5, func() error {
var err error
resultsMailbox, err = mail.GetMailBox(emailTo)
return err
})
if err2 != nil {
t.Log(err2)
t.Log("No email was received, maybe due load on the server. Skipping this verification")
} else if len(resultsMailbox) > 0 {
require.Len(t, resultsMailbox, 1)
require.Contains(t, resultsMailbox[0].To[0], emailTo, "Wrong To: recipient")
resultsEmail, err := mail.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID)
require.NoError(t, err, "Could not get message from mailbox")
require.Contains(t, resultsEmail.Body.HTML, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
require.Contains(t, resultsEmail.Body.HTML, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
require.Contains(t, resultsEmail.Body.Text, "http://testserver", "Wrong received message %s", resultsEmail.Body.Text)
require.Contains(t, resultsEmail.Body.Text, "test-user", "Wrong received message %s", resultsEmail.Body.Text)
}
}

Просмотреть файл

@@ -9,8 +9,6 @@ import (
"html"
"html/template"
"io"
"net/url"
"path/filepath"
"strings"
"time"
@@ -114,7 +112,7 @@ func (a *App) sendNotificationEmail(notification *PostNotification, user *model.
}
a.Srv().Go(func() {
if nErr := a.Srv().EmailService.sendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles); nErr != nil {
if nErr := a.Srv().EmailService.SendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles); nErr != nil {
mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr))
}
})
@@ -212,7 +210,7 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
}
data := a.Srv().EmailService.newEmailTemplateData(recipient.Locale)
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
data.Props["SiteURL"] = a.GetSiteURL()
if teamName != "select_team" {
data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id
@@ -321,38 +319,6 @@ func (a *App) generateHyperlinkForChannels(postMessage, teamName, teamURL string
return postMessage, nil
}
func (s *Server) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
if strings.TrimSpace(post.Message) != "" || len(post.FileIds) == 0 {
return post.Message
}
// extract the filenames from their paths and determine what type of files are attached
infos, err := s.Store.FileInfo().GetForPost(post.Id, true, false, true)
if err != nil {
mlog.Warn("Encountered error when getting files for notification message", mlog.String("post_id", post.Id), mlog.Err(err))
}
filenames := make([]string, len(infos))
onlyImages := true
for i, info := range infos {
if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
// this should never error since filepath was escaped using url.QueryEscape
filenames[i] = escaped
} else {
filenames[i] = info.Name
}
onlyImages = onlyImages && info.IsImage()
}
props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
if onlyImages {
return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
}
return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
}
func (a *App) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
return a.Srv().GetMessageForNotification(post, translateFunc)
return a.Srv().EmailService.GetMessageForNotification(post, translateFunc)
}

Просмотреть файл

@@ -256,10 +256,6 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing
zone, _ := tm.Zone()
formattedTime := formattedPostTime{
Time: tm,
Year: fmt.Sprintf("%d", tm.Year()),
Month: translateFunc(tm.Month().String()),
Day: fmt.Sprintf("%d", tm.Day()),
Hour: fmt.Sprintf("%02d", tm.Hour()),
Minute: fmt.Sprintf("%02d", tm.Minute()),
TimeZone: zone,

Просмотреть файл

@@ -780,7 +780,7 @@ func (api *PluginAPI) SendMail(to, subject, htmlBody string) *model.AppError {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, "", http.StatusBadRequest)
}
if err := api.app.Srv().EmailService.sendNotificationMail(to, subject, htmlBody); err != nil {
if err := api.app.Srv().EmailService.SendNotificationMail(to, subject, htmlBody); err != nil {
return model.NewAppError("SendMail", "plugin_api.send_mail.missing_htmlbody", nil, err.Error(), http.StatusInternalServerError)
}

Просмотреть файл

@@ -36,6 +36,7 @@ import (
"github.com/rs/cors"
"golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/v5/app/email"
"github.com/mattermost/mattermost-server/v5/app/featureflag"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request"
@@ -113,7 +114,7 @@ type Server struct {
PluginConfigListenerId string
PluginsLock sync.RWMutex
EmailService *EmailService
EmailService *email.Service
hubs []*Hub
hashSeed maphash.Seed
@@ -416,6 +417,7 @@ func NewServer(options ...Option) (*Server, error) {
ConfigFn: s.Config,
Metrics: s.Metrics,
Cluster: s.Cluster,
LicenseFn: s.License,
})
if err != nil {
return nil, errors.Wrapf(err, "unable to create users service")
@@ -452,7 +454,14 @@ func NewServer(options ...Option) (*Server, error) {
s.telemetryService = telemetry.New(s, s.Store, s.SearchEngine, s.Log)
emailService, err := NewEmailService(s)
emailService, err := email.NewService(email.ServiceConfig{
ConfigFn: s.Config,
LicenseFn: s.License,
GoFn: s.Go,
TemplatesContainer: s.TemplatesContainer(),
UserService: s.userService,
Store: s.GetStore(),
})
if err != nil {
return nil, errors.Wrapf(err, "unable to initialize email service")
}
@@ -1803,9 +1812,8 @@ func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, lice
if name == "" {
name = user.Username
}
ok, err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration)
if !ok || err != nil {
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email))
if err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration); err != nil {
mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email), mlog.Err(err))
countNotOks++
}
}
@@ -1864,7 +1872,7 @@ func (s *Server) doLicenseExpirationCheck() {
mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email))
s.Go(func() {
if err := s.EmailService.SendRemoveExpiredLicenseEmail(user.Email, user.Locale, *s.Config().ServiceSettings.SiteURL); err != nil {
if err := s.SendRemoveExpiredLicenseEmail(user.Email, user.Locale, *s.Config().ServiceSettings.SiteURL); err != nil {
mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err))
}
})
@@ -1874,6 +1882,21 @@ func (s *Server) doLicenseExpirationCheck() {
s.RemoveLicense()
}
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (s *Server) SendRemoveExpiredLicenseEmail(email string, locale, siteURL string) *model.AppError {
renewalLink, err := s.GenerateLicenseRenewalLink()
if err != nil {
return err
}
if err := s.EmailService.SendRemoveExpiredLicenseEmail(renewalLink, email, locale, siteURL); err != nil {
return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (s *Server) StartSearchEngine() (string, string) {
if s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() {
s.Go(func() {

Просмотреть файл

@@ -362,7 +362,7 @@ func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAc
// Don't send emails to bot users.
if !user.IsBot {
if err := a.Srv().EmailService.sendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendUserAccessTokenAddedEmail(user.Email, user.Locale, a.GetSiteURL()); err != nil {
a.Log().Error("Unable to send user access token added email", mlog.Err(err), mlog.String("user_id", user.Id))
}
}

Просмотреть файл

@@ -15,6 +15,7 @@ import (
"net/url"
"strings"
"github.com/mattermost/mattermost-server/v5/app/email"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/model"
@@ -1434,9 +1435,16 @@ func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderI
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
if err != nil {
return nil, err
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL())
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
@@ -1536,9 +1544,16 @@ func (a *App) InviteGuestsToChannelsGracefully(teamID string, guestsInvite *mode
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
err = a.Srv().EmailService.sendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message)
if err != nil {
return nil, err
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, goodEmails, a.GetSiteURL(), guestsInvite.Message)
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return nil, model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return nil, model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
}
@@ -1575,9 +1590,16 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
err = a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
if err != nil {
return err
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, eErr), http.StatusRequestEntityTooLarge)
}
}
return nil
@@ -1610,9 +1632,16 @@ func (a *App) InviteGuestsToChannels(teamID string, guestsInvite *model.GuestsIn
if err != nil {
a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", user.Id), mlog.String("team_id", team.Id), mlog.Err(err))
}
err = a.Srv().EmailService.sendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message)
if err != nil {
return err
eErr := a.Srv().EmailService.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, senderProfileImage, guestsInvite.Emails, a.GetSiteURL(), guestsInvite.Message)
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s", user.Id, team.Id), http.StatusInternalServerError)
case errors.Is(eErr, email.SetupRateLimiterError):
return model.NewAppError("SendInviteEmails", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusInternalServerError)
default:
return model.NewAppError("SendInviteEmails", "app.email.rate_limit_exceeded.app_error", nil, fmt.Sprintf("user_id=%s, team_id=%s, error=%v", user.Id, team.Id, err), http.StatusRequestEntityTooLarge)
}
}
return nil

Просмотреть файл

@@ -15,6 +15,7 @@ import (
"strconv"
"strings"
"github.com/mattermost/mattermost-server/v5/app/email"
"github.com/mattermost/mattermost-server/v5/app/imaging"
"github.com/mattermost/mattermost-server/v5/app/request"
"github.com/mattermost/mattermost-server/v5/einterfaces"
@@ -143,7 +144,7 @@ func (a *App) CreateUserWithInviteId(c *request.Context, user *model.User, invit
a.AddDirectChannels(team.Id, ruser)
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err))
}
@@ -156,7 +157,7 @@ func (a *App) CreateUserAsAdmin(c *request.Context, user *model.User, redirect s
return nil, err
}
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err))
}
@@ -180,7 +181,7 @@ func (a *App) CreateUserFromSignup(c *request.Context, user *model.User, redirec
return nil, err
}
if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
if err := a.Srv().EmailService.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil {
mlog.Warn("Failed to send welcome email on create user from signup", mlog.Err(err))
}
@@ -1101,7 +1102,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
})
} else {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendEmailChangeEmail(userUpdate.Old.Email, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
@@ -1110,7 +1111,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
if userUpdate.New.Username != userUpdate.Old.Username {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendChangeUsernameEmail(userUpdate.New.Username, userUpdate.New.Email, userUpdate.New.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send change username email", mlog.Err(err))
}
})
@@ -1171,7 +1172,7 @@ func (a *App) UpdateMfa(activate bool, userID, token string) *model.AppError {
return
}
if err := a.Srv().EmailService.sendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendMfaChangeEmail(user.Email, activate, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send mfa change email", mlog.Err(err))
}
})
@@ -1210,7 +1211,7 @@ func (a *App) UpdatePasswordSendEmail(user *model.User, newPassword, method stri
}
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendPasswordChangeEmail(user.Email, method, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send password change email", mlog.Err(err))
}
})
@@ -1297,7 +1298,12 @@ func (a *App) SendPasswordReset(email string, siteURL string) (bool, *model.AppE
return false, err
}
return a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
result, eErr := a.Srv().EmailService.SendPasswordResetEmail(user.Email, token, user.Locale, siteURL)
if eErr != nil {
return result, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+eErr.Error(), http.StatusInternalServerError)
}
return result, nil
}
func (a *App) CreatePasswordRecoveryToken(userID, email string) (*model.Token, *model.AppError) {
@@ -1540,13 +1546,27 @@ func (a *App) PermanentDeleteAllUsers(c *request.Context) *model.AppError {
func (a *App) SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError {
token, err := a.Srv().EmailService.CreateVerifyEmailToken(user.Id, newEmail)
if err != nil {
return err
switch {
case errors.Is(err, email.CreateEmailTokenError):
return model.NewAppError("CreateVerifyEmailToken", "api.user.create_email_token.error", nil, "", http.StatusInternalServerError)
default:
return model.NewAppError("CreateVerifyEmailToken", "app.recover.save.app_error", nil, err.Error(), http.StatusInternalServerError)
}
}
if _, err := a.GetStatus(user.Id); err != nil {
return a.Srv().EmailService.sendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
eErr := a.Srv().EmailService.SendVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token, redirect)
if eErr != nil {
return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, eErr.Error(), http.StatusInternalServerError)
}
return nil
}
return a.Srv().EmailService.sendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token)
if err := a.Srv().EmailService.SendEmailChangeVerifyEmail(newEmail, user.Locale, a.GetSiteURL(), token.Token); err != nil {
return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError)
}
return nil
}
func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppError {
@@ -1580,7 +1600,7 @@ func (a *App) VerifyEmailFromToken(userSuppliedTokenString string) *model.AppErr
if user.Email != tokenData.Email {
a.Srv().Go(func() {
if err := a.Srv().EmailService.sendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
if err := a.Srv().EmailService.SendEmailChangeEmail(user.Email, tokenData.Email, user.Locale, a.GetSiteURL()); err != nil {
mlog.Error("Failed to send email change email", mlog.Err(err))
}
})
@@ -1989,7 +2009,7 @@ func (a *App) GetViewUsersRestrictions(userID string) (*model.ViewUsersRestricti
// PromoteGuestToUser Convert user's roles and all his mermbership's roles from
// guest roles to regular user roles.
func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestorId string) *model.AppError {
nErr := a.Srv().Store.User().PromoteGuestToUser(user.Id)
nErr := a.srv.userService.PromoteGuestToUser(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("PromoteGuestToUser", "app.user.promote_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)
@@ -2045,7 +2065,7 @@ func (a *App) PromoteGuestToUser(c *request.Context, user *model.User, requestor
// DemoteUserToGuest Convert user's roles and all his mermbership's roles from
// regular user roles to guest roles.
func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
demotedUser, nErr := a.Srv().Store.User().DemoteUserToGuest(user.Id)
demotedUser, nErr := a.srv.userService.DemoteUserToGuest(user)
a.InvalidateCacheForUser(user.Id)
if nErr != nil {
return model.NewAppError("DemoteUserToGuest", "app.user.demote_user_to_guest.user_update.app_error", nil, nErr.Error(), http.StatusInternalServerError)

Просмотреть файл

@@ -398,19 +398,19 @@ func TestUpdateUserEmail(t *testing.T) {
newEmail := th.MakeEmail()
user.Email = newEmail
user2, err := th.App.UpdateUser(user, false)
assert.Nil(t, err)
user2, appErr := th.App.UpdateUser(user, false)
assert.Nil(t, appErr)
assert.Equal(t, currentEmail, user2.Email)
assert.True(t, user2.EmailVerified)
token, err := th.App.Srv().EmailService.CreateVerifyEmailToken(user2.Id, newEmail)
assert.Nil(t, err)
assert.NoError(t, err)
err = th.App.VerifyEmailFromToken(token.Token)
assert.Nil(t, err)
appErr = th.App.VerifyEmailFromToken(token.Token)
assert.Nil(t, appErr)
user2, err = th.App.GetUser(user2.Id)
assert.Nil(t, err)
user2, appErr = th.App.GetUser(user2.Id)
assert.Nil(t, appErr)
assert.Equal(t, newEmail, user2.Email)
assert.True(t, user2.EmailVerified)
@@ -425,8 +425,8 @@ func TestUpdateUserEmail(t *testing.T) {
newBotEmail := th.MakeEmail()
botuser.Email = newBotEmail
botuser2, err := th.App.UpdateUser(&botuser, false)
assert.Nil(t, err)
botuser2, appErr := th.App.UpdateUser(&botuser, false)
assert.Nil(t, appErr)
assert.Equal(t, botuser2.Email, newBotEmail)
})

Просмотреть файл

@@ -168,6 +168,7 @@ func TestHubSessionRevokeRace(t *testing.T) {
ConfigFn: th.App.srv.Config,
Metrics: th.App.Metrics(),
Cluster: th.App.Cluster(),
LicenseFn: th.App.srv.License,
})
require.NoError(t, err)
th.App.srv.userService = userService

Просмотреть файл

@@ -7,7 +7,7 @@ require (
github.com/golang-migrate/migrate/v4 v4.14.1 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/jteeuwen/go-bindata v3.0.7+incompatible // indirect
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9 // indirect
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/reflog/struct2interface v0.6.1 // indirect
github.com/spf13/cobra v1.1.3 // indirect

Просмотреть файл

@@ -344,6 +344,8 @@ github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d764
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210218104610-40d7640e8538/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9 h1:EdA8k1LBxdk1SslBITXYiGVIptfPWFt7fRwxiy2BsTk=
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210309083648-c1e5575135f9/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833 h1:Cgx5Md/4umqKYAgu8oPTZ+vDPZ5DaaRpjUjR+CUsmNI=
github.com/mattermost/mattermost-utilities/mmgotool v0.0.0-20210714114450-fbc82c4cf833/go.mod h1:3gKozJI8n2Y/vW37GfnFWAdehGXe5yZlt+HykK6Y3DM=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=

Просмотреть файл

@@ -3866,14 +3866,6 @@
"id": "api.user.check_user_password.invalid.app_error",
"translation": "Login failed because of invalid password."
},
{
"id": "api.user.cloud_trial_ended_email.error",
"translation": "Failed to send trial ended email"
},
{
"id": "api.user.cloud_trial_ending_email.error",
"translation": "Failed to send trial ending warning email"
},
{
"id": "api.user.complete_switch_with_oauth.blank_email.app_error",
"translation": "Blank email."
@@ -4130,30 +4122,10 @@
"id": "api.user.send_deactivate_email_and_forget.failed.error",
"translation": "Failed to send the deactivate account email successfully"
},
{
"id": "api.user.send_email_change_email_and_forget.error",
"translation": "Failed to send email change notification email successfully"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
"translation": "Failed to send username change notification email successfully"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
"translation": "Failed to send email change verification email successfully"
},
{
"id": "api.user.send_license_up_for_renewal_email.error",
"translation": "Failed to send license up for renewal email"
},
{
"id": "api.user.send_mfa_change_email.error",
"translation": "Unable to send email notification for MFA change."
},
{
"id": "api.user.send_password_change_email_and_forget.error",
"translation": "Failed to send update password email successfully"
},
{
"id": "api.user.send_password_reset.send.app_error",
"translation": "Failed to send password reset email successfully."
@@ -4166,22 +4138,10 @@
"id": "api.user.send_sign_in_change_email_and_forget.error",
"translation": "Failed to send update password email successfully"
},
{
"id": "api.user.send_upgrade_request_email.error",
"translation": "Failed to send email to user limit notification to admin"
},
{
"id": "api.user.send_user_access_token.error",
"translation": "Failed to send \"Personal access token added\" email successfully"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
"translation": "Failed to send verification email successfully"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
"translation": "Failed to send welcome email successfully"
},
{
"id": "api.user.update_active.cannot_enable_guest_when_guest_feature_is_disabled.app_error",
"translation": "You cannot activate a guest account because Guest Access feature is not enabled."

Просмотреть файл

@@ -10,13 +10,16 @@ import (
"image/color"
"image/draw"
"image/png"
"io"
"io/ioutil"
"path"
"path/filepath"
"strings"
"github.com/golang/freetype"
"github.com/golang/freetype/truetype"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/shared/filestore"
"github.com/mattermost/mattermost-server/v5/utils/fileutils"
)
@@ -24,6 +27,68 @@ const (
imageProfilePixelDimension = 128
)
func (us *UserService) GetProfileImage(user *model.User) ([]byte, bool, error) {
if *us.config().FileSettings.DriverName == "" {
img, err := us.GetDefaultProfileImage(user)
if err != nil {
return nil, false, err
}
return img, false, nil
}
path := path.Join("users", user.Id, "profile.png")
data, err := us.ReadFile(path)
if err != nil {
img, appErr := us.GetDefaultProfileImage(user)
if appErr != nil {
return nil, false, appErr
}
if user.LastPictureUpdate == 0 {
if _, err := us.writeFile(bytes.NewReader(img), path); err != nil {
return nil, false, err
}
}
return img, true, nil
}
return data, false, nil
}
func (us *UserService) FileBackend() (filestore.FileBackend, error) {
license := us.license()
backend, err := filestore.NewFileBackend(us.config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance))
if err != nil {
return nil, err
}
return backend, nil
}
func (us *UserService) ReadFile(path string) ([]byte, error) {
backend, err := us.FileBackend()
if err != nil {
return nil, err
}
result, nErr := backend.ReadFile(path)
if nErr != nil {
return nil, nErr
}
return result, nil
}
func (us *UserService) writeFile(fr io.Reader, path string) (int64, error) {
backend, err := us.FileBackend()
if err != nil {
return 0, err
}
result, nErr := backend.WriteFile(fr, path)
if nErr != nil {
return result, nErr
}
return result, nil
}
func (us *UserService) GetDefaultProfileImage(user *model.User) ([]byte, error) {
if user.IsBot {
return botDefaultImage, nil

Просмотреть файл

@@ -24,6 +24,7 @@ type UserService struct {
metrics einterfaces.MetricsInterface
cluster einterfaces.ClusterInterface
config func() *model.Config
license func() *model.License
}
// ServiceConfig is used to initialize the UserService.
@@ -33,6 +34,7 @@ type ServiceConfig struct {
SessionStore store.SessionStore
OAuthStore store.OAuthStore
ConfigFn func() *model.Config
LicenseFn func() *model.License
// Optional fields
Metrics einterfaces.MetricsInterface
Cluster einterfaces.ClusterInterface
@@ -62,6 +64,7 @@ func New(c ServiceConfig) (*UserService, error) {
sessionStore: c.SessionStore,
oAuthStore: c.OAuthStore,
config: c.ConfigFn,
license: c.LicenseFn,
metrics: c.Metrics,
cluster: c.Cluster,
sessionCache: sessionCache,
@@ -74,7 +77,7 @@ func New(c ServiceConfig) (*UserService, error) {
}
func (c *ServiceConfig) validate() error {
if in := c; in.ConfigFn == nil || in.UserStore == nil || in.SessionStore == nil || in.OAuthStore == nil {
if c.ConfigFn == nil || c.UserStore == nil || c.SessionStore == nil || c.OAuthStore == nil || c.LicenseFn == nil {
return errors.New("required parameters are not provided")
}

Просмотреть файл

@@ -20,11 +20,16 @@ func TestNew(t *testing.T) {
return &model.Config{}
}
lfn := func() *model.License {
return model.NewTestLicense()
}
_, err = New(ServiceConfig{
UserStore: dbStore.User(),
SessionStore: dbStore.Session(),
OAuthStore: dbStore.OAuth(),
ConfigFn: cfn,
LicenseFn: lfn,
})
require.NoError(t, err)
}

Просмотреть файл

@@ -242,3 +242,11 @@ func (us *UserService) ActivateMfa(user *model.User, token string) error {
func (us *UserService) DeactivateMfa(user *model.User) error {
return mfa.New(us.store).Deactivate(user.Id)
}
func (us *UserService) PromoteGuestToUser(user *model.User) error {
return us.store.PromoteGuestToUser(user.Id)
}
func (us *UserService) DemoteUserToGuest(user *model.User) (*model.User, error) {
return us.store.DemoteUserToGuest(user.Id)
}

Просмотреть файл

@@ -154,8 +154,8 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
c.App.Srv().Go(func() {
if err = c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(err)
if err := c.App.Srv().EmailService.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.App.GetSiteURL()); err != nil {
c.LogErrorByCode(model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError))
}
})
}