[MM-41785] Show error in invitation popin when email can't be send (#19617)

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Julien Tant
2022-03-02 14:26:57 -07:00
коммит произвёл GitHub
родитель 2bf1334ac4
Коммит dd100a3a69
13 изменённых файлов: 920 добавлений и 22 удалений

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

@@ -333,6 +333,10 @@ misc-mocks: ## Creates mocks for misc interfaces.
$(GO) install github.com/vektra/mockery/...@v1.1.2
$(GOBIN)/mockery -dir utils --name LicenseValidatorIface -output utils/mocks -note 'Regenerate this file using `make misc-mocks`.'
email-mocks: ## Creates mocks for misc interfaces.
$(GO) install github.com/vektra/mockery/...@v1.1.2
$(GOBIN)/mockery -dir app/email --name ServiceInterface -output app/email/mocks -note 'Regenerate this file using `make email-mocks`.'
pluginapi: ## Generates api and hooks glue code for plugins
$(GO) generate $(GOFLAGS) ./plugin

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

@@ -128,7 +128,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
}
auditRec.AddMeta("errors", errList)
if len(goodEmails) > 0 {
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil)
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), goodEmails, *c.App.Config().ServiceSettings.SiteURL, nil, false)
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):
@@ -161,7 +161,7 @@ func localInviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request)
c.Err = model.NewAppError("localInviteUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": s}, "", http.StatusBadRequest)
return
}
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil)
err := c.App.Srv().EmailService.SendInviteEmails(team, "Administrator", "mmctl "+model.NewId(), emailList, *c.App.Config().ServiceSettings.SiteURL, nil, false)
if err != nil {
switch {
case errors.Is(err, email.NoRateLimiterError):

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

@@ -37,12 +37,12 @@ func (a *App) SendAdminUpgradeRequestEmail(username string, subscription *model.
year, month, day := time.Now().Date()
key := fmt.Sprintf("%s-%d-%s-%d", action, day, month, year)
if a.Srv().EmailService.PerDayEmailRateLimiter == nil {
if a.Srv().EmailService.GetPerDayEmailRateLimiter() == nil {
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.no_rate_limiter.app_error", nil, fmt.Sprintf("for key=%s", key), http.StatusInternalServerError)
}
// rate limit based on combination of date and action as key
rateLimited, result, err := a.Srv().EmailService.PerDayEmailRateLimiter.RateLimit(key, 1)
rateLimited, result, err := a.Srv().EmailService.GetPerDayEmailRateLimiter().RateLimit(key, 1)
if err != nil {
return model.NewAppError("app.SendAdminUpgradeRequestEmail", "app.email.setup_rate_limiter.app_error", nil, fmt.Sprintf("for key=%s, error=%v", key, err), http.StatusInternalServerError)
}

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

@@ -428,11 +428,11 @@ func (es *Service) SendMfaChangeEmail(email string, activated bool, locale, site
return nil
}
func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData) error {
if es.PerHourEmailRateLimiter == nil {
func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.PerHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
@@ -493,17 +493,20 @@ func (es *Service) SendInviteEmails(team *model.Team, senderName string, senderU
if err := es.sendMail(invite, subject, body); err != nil {
mlog.Error("Failed to send invite email successfully ", mlog.Err(err))
if errorWhenNotSent {
return SendMailError
}
}
}
}
return nil
}
func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string) error {
if es.PerHourEmailRateLimiter == nil {
func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error {
if es.perHourEmailRateLimiter == nil {
return NoRateLimiterError
}
rateLimited, result, err := es.PerHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
rateLimited, result, err := es.perHourEmailRateLimiter.RateLimit(senderUserId, len(invites))
if err != nil {
return SetupRateLimiterError
}
@@ -592,6 +595,9 @@ func (es *Service) SendGuestInviteEmails(team *model.Team, channels []*model.Cha
if nErr := es.SendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles); nErr != nil {
mlog.Error("Failed to send invite email successfully", mlog.Err(nErr))
if errorWhenNotSent {
return SendMailError
}
}
}
}

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

@@ -4,6 +4,7 @@
package email
import (
"os"
"strings"
"testing"
@@ -75,12 +76,30 @@ func TestSendInviteEmails(t *testing.T) {
t.Run("SendInviteEmails", func(t *testing.T) {
mail.DeleteMailBox(emailTo)
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil)
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false)
require.NoError(t, err)
verifyMailbox(t)
})
t.Run("SendInviteEmails can return error when SMTP connection fails", func(t *testing.T) {
originalPort := *th.service.config().EmailSettings.SMTPPort
th.UpdateConfig(func(cfg *model.Config) {
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
*cfg.EmailSettings.SMTPPort = "5432"
})
defer th.UpdateConfig(func(cfg *model.Config) {
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
*cfg.EmailSettings.SMTPPort = originalPort
})
err := th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, true)
require.Error(t, err)
err = th.service.SendInviteEmails(th.BasicTeam, "test-user", th.BasicUser.Id, []string{emailTo}, "http://testserver", nil, false)
require.NoError(t, err)
})
t.Run("SendGuestInviteEmails", func(t *testing.T) {
mail.DeleteMailBox(emailTo)
@@ -93,12 +112,52 @@ func TestSendInviteEmails(t *testing.T) {
[]string{emailTo},
"http://testserver",
"hello world",
false,
)
require.NoError(t, err)
verifyMailbox(t)
})
t.Run("SendGuestInviteEmail can return error when SMTP connection fails", func(t *testing.T) {
originalPort := *th.service.config().EmailSettings.SMTPPort
th.UpdateConfig(func(cfg *model.Config) {
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", "5432")
*cfg.EmailSettings.SMTPPort = "5432"
})
defer th.UpdateConfig(func(cfg *model.Config) {
os.Setenv("MM_EMAILSETTINGS_SMTPPORT", originalPort)
*cfg.EmailSettings.SMTPPort = originalPort
})
err := th.service.SendGuestInviteEmails(
th.BasicTeam,
[]*model.Channel{th.BasicChannel},
"test-user",
th.BasicUser.Id,
nil,
[]string{emailTo},
"http://testserver",
"hello world",
false,
)
require.NoError(t, err)
err = th.service.SendGuestInviteEmails(
th.BasicTeam,
[]*model.Channel{th.BasicChannel},
"test-user",
th.BasicUser.Id,
nil,
[]string{emailTo},
"http://testserver",
"hello world",
true,
)
require.Error(t, err)
})
t.Run("SendGuestInviteEmails should sanitize HTML input", func(t *testing.T) {
mail.DeleteMailBox(emailTo)
@@ -112,6 +171,7 @@ func TestSendInviteEmails(t *testing.T) {
[]string{emailTo},
"http://testserver",
message,
false,
)
require.NoError(t, err)

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

@@ -10,4 +10,5 @@ var (
NoRateLimiterError = errors.New("the rate limit could not be found")
SetupRateLimiterError = errors.New("the rate limiter could not be set")
RateLimitExceededError = errors.New("the rate limit is exceeded")
SendMailError = errors.New("could not send the email")
)

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

@@ -0,0 +1,637 @@
// Code generated by mockery v1.0.0. DO NOT EDIT.
// Regenerate this file using `make email-mocks`.
package mocks
import (
io "io"
i18n "github.com/mattermost/mattermost-server/v6/shared/i18n"
mock "github.com/stretchr/testify/mock"
model "github.com/mattermost/mattermost-server/v6/model"
templates "github.com/mattermost/mattermost-server/v6/shared/templates"
throttled "github.com/throttled/throttled"
)
// ServiceInterface is an autogenerated mock type for the ServiceInterface type
type ServiceInterface struct {
mock.Mock
}
// AddNotificationEmailToBatch provides a mock function with given fields: user, post, team
func (_m *ServiceInterface) AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError {
ret := _m.Called(user, post, team)
var r0 *model.AppError
if rf, ok := ret.Get(0).(func(*model.User, *model.Post, *model.Team) *model.AppError); ok {
r0 = rf(user, post, team)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.AppError)
}
}
return r0
}
// CreateVerifyEmailToken provides a mock function with given fields: userID, newEmail
func (_m *ServiceInterface) CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error) {
ret := _m.Called(userID, newEmail)
var r0 *model.Token
if rf, ok := ret.Get(0).(func(string, string) *model.Token); ok {
r0 = rf(userID, newEmail)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*model.Token)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string) error); ok {
r1 = rf(userID, newEmail)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetMessageForNotification provides a mock function with given fields: post, translateFunc
func (_m *ServiceInterface) GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
ret := _m.Called(post, translateFunc)
var r0 string
if rf, ok := ret.Get(0).(func(*model.Post, i18n.TranslateFunc) string); ok {
r0 = rf(post, translateFunc)
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// GetPerDayEmailRateLimiter provides a mock function with given fields:
func (_m *ServiceInterface) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
ret := _m.Called()
var r0 *throttled.GCRARateLimiter
if rf, ok := ret.Get(0).(func() *throttled.GCRARateLimiter); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*throttled.GCRARateLimiter)
}
}
return r0
}
// InitEmailBatching provides a mock function with given fields:
func (_m *ServiceInterface) InitEmailBatching() {
_m.Called()
}
// NewEmailTemplateData provides a mock function with given fields: locale
func (_m *ServiceInterface) NewEmailTemplateData(locale string) templates.Data {
ret := _m.Called(locale)
var r0 templates.Data
if rf, ok := ret.Get(0).(func(string) templates.Data); ok {
r0 = rf(locale)
} else {
r0 = ret.Get(0).(templates.Data)
}
return r0
}
// SendAtUserLimitWarningEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendAtUserLimitWarningEmail(_a0 string, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendChangeUsernameEmail provides a mock function with given fields: newUsername, _a1, locale, siteURL
func (_m *ServiceInterface) SendChangeUsernameEmail(newUsername string, _a1 string, locale string, siteURL string) error {
ret := _m.Called(newUsername, _a1, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(newUsername, _a1, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendCloudTrialEndWarningEmail provides a mock function with given fields: userEmail, name, trialEndDate, locale, siteURL
func (_m *ServiceInterface) SendCloudTrialEndWarningEmail(userEmail string, name string, trialEndDate string, locale string, siteURL string) error {
ret := _m.Called(userEmail, name, trialEndDate, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(userEmail, name, trialEndDate, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendCloudTrialEndedEmail provides a mock function with given fields: userEmail, name, locale, siteURL
func (_m *ServiceInterface) SendCloudTrialEndedEmail(userEmail string, name string, locale string, siteURL string) error {
ret := _m.Called(userEmail, name, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(userEmail, name, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendCloudWelcomeEmail provides a mock function with given fields: userEmail, locale, teamInviteID, workSpaceName, dns, siteURL
func (_m *ServiceInterface) SendCloudWelcomeEmail(userEmail string, locale string, teamInviteID string, workSpaceName string, dns string, siteURL string) error {
ret := _m.Called(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, string) error); ok {
r0 = rf(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendDeactivateAccountEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendDeactivateAccountEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendEmailChangeEmail provides a mock function with given fields: oldEmail, newEmail, locale, siteURL
func (_m *ServiceInterface) SendEmailChangeEmail(oldEmail string, newEmail string, locale string, siteURL string) error {
ret := _m.Called(oldEmail, newEmail, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(oldEmail, newEmail, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendEmailChangeVerifyEmail provides a mock function with given fields: newUserEmail, locale, siteURL, token
func (_m *ServiceInterface) SendEmailChangeVerifyEmail(newUserEmail string, locale string, siteURL string, token string) error {
ret := _m.Called(newUserEmail, locale, siteURL, token)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(newUserEmail, locale, siteURL, token)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendGuestInviteEmails provides a mock function with given fields: team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent
func (_m *ServiceInterface) SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error {
ret := _m.Called(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Team, []*model.Channel, string, string, []byte, []string, string, string, bool) error); ok {
r0 = rf(team, channels, senderName, senderUserId, senderProfileImage, invites, siteURL, message, errorWhenNotSent)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendInviteEmails provides a mock function with given fields: team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent
func (_m *ServiceInterface) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error {
ret := _m.Called(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent)
var r0 error
if rf, ok := ret.Get(0).(func(*model.Team, string, string, []string, string, *model.TeamInviteReminderData, bool) error); ok {
r0 = rf(team, senderName, senderUserId, invites, siteURL, reminderData, errorWhenNotSent)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendLicenseInactivityEmail provides a mock function with given fields: _a0, name, locale, siteURL
func (_m *ServiceInterface) SendLicenseInactivityEmail(_a0 string, name string, locale string, siteURL string) error {
ret := _m.Called(_a0, name, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, name, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendLicenseUpForRenewalEmail provides a mock function with given fields: _a0, name, locale, siteURL, renewalLink, daysToExpiration
func (_m *ServiceInterface) SendLicenseUpForRenewalEmail(_a0 string, name string, locale string, siteURL string, renewalLink string, daysToExpiration int) error {
ret := _m.Called(_a0, name, locale, siteURL, renewalLink, daysToExpiration)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string, int) error); ok {
r0 = rf(_a0, name, locale, siteURL, renewalLink, daysToExpiration)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendMailWithEmbeddedFiles provides a mock function with given fields: to, subject, htmlBody, embeddedFiles
func (_m *ServiceInterface) SendMailWithEmbeddedFiles(to string, subject string, htmlBody string, embeddedFiles map[string]io.Reader) error {
ret := _m.Called(to, subject, htmlBody, embeddedFiles)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, map[string]io.Reader) error); ok {
r0 = rf(to, subject, htmlBody, embeddedFiles)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendMfaChangeEmail provides a mock function with given fields: _a0, activated, locale, siteURL
func (_m *ServiceInterface) SendMfaChangeEmail(_a0 string, activated bool, locale string, siteURL string) error {
ret := _m.Called(_a0, activated, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, bool, string, string) error); ok {
r0 = rf(_a0, activated, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendNoCardPaymentFailedEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendNoCardPaymentFailedEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendNotificationMail provides a mock function with given fields: to, subject, htmlBody
func (_m *ServiceInterface) SendNotificationMail(to string, subject string, htmlBody string) error {
ret := _m.Called(to, subject, htmlBody)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(to, subject, htmlBody)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendOverUserFourteenDayWarningEmail provides a mock function with given fields: _a0, locale, siteURL, overLimitDate
func (_m *ServiceInterface) SendOverUserFourteenDayWarningEmail(_a0 string, locale string, siteURL string, overLimitDate string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL, overLimitDate)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL, overLimitDate)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL, overLimitDate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendOverUserLimitNinetyDayWarningEmail provides a mock function with given fields: _a0, locale, siteURL, overLimitDate
func (_m *ServiceInterface) SendOverUserLimitNinetyDayWarningEmail(_a0 string, locale string, siteURL string, overLimitDate string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL, overLimitDate)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL, overLimitDate)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL, overLimitDate)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendOverUserLimitThirtyDayWarningEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendOverUserLimitThirtyDayWarningEmail(_a0 string, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendOverUserLimitWarningEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendOverUserLimitWarningEmail(_a0 string, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendOverUserLimitWorkspaceSuspendedWarningEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendOverUserLimitWorkspaceSuspendedWarningEmail(_a0 string, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendOverUserSevenDayWarningEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendOverUserSevenDayWarningEmail(_a0 string, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
r1 = rf(_a0, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendPasswordChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
func (_m *ServiceInterface) SendPasswordChangeEmail(_a0 string, method string, locale string, siteURL string) error {
ret := _m.Called(_a0, method, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, method, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendPasswordResetEmail provides a mock function with given fields: _a0, token, locale, siteURL
func (_m *ServiceInterface) SendPasswordResetEmail(_a0 string, token *model.Token, locale string, siteURL string) (bool, error) {
ret := _m.Called(_a0, token, locale, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, *model.Token, string, string) bool); ok {
r0 = rf(_a0, token, locale, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, *model.Token, string, string) error); ok {
r1 = rf(_a0, token, locale, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendPaymentFailedEmail provides a mock function with given fields: _a0, locale, failedPayment, siteURL
func (_m *ServiceInterface) SendPaymentFailedEmail(_a0 string, locale string, failedPayment *model.FailedPayment, siteURL string) (bool, error) {
ret := _m.Called(_a0, locale, failedPayment, siteURL)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, *model.FailedPayment, string) bool); ok {
r0 = rf(_a0, locale, failedPayment, siteURL)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, *model.FailedPayment, string) error); ok {
r1 = rf(_a0, locale, failedPayment, siteURL)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendRemoveExpiredLicenseEmail provides a mock function with given fields: renewalLink, _a1, locale, siteURL
func (_m *ServiceInterface) SendRemoveExpiredLicenseEmail(renewalLink string, _a1 string, locale string, siteURL string) error {
ret := _m.Called(renewalLink, _a1, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(renewalLink, _a1, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendSignInChangeEmail provides a mock function with given fields: _a0, method, locale, siteURL
func (_m *ServiceInterface) SendSignInChangeEmail(_a0 string, method string, locale string, siteURL string) error {
ret := _m.Called(_a0, method, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
r0 = rf(_a0, method, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendSuspensionEmailToSupport provides a mock function with given fields: _a0, installationID, customerID, subscriptionID, siteURL, userCount
func (_m *ServiceInterface) SendSuspensionEmailToSupport(_a0 string, installationID string, customerID string, subscriptionID string, siteURL string, userCount int64) (bool, error) {
ret := _m.Called(_a0, installationID, customerID, subscriptionID, siteURL, userCount)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string, string, string, int64) bool); ok {
r0 = rf(_a0, installationID, customerID, subscriptionID, siteURL, userCount)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, string, string, int64) error); ok {
r1 = rf(_a0, installationID, customerID, subscriptionID, siteURL, userCount)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendUpgradeEmail provides a mock function with given fields: user, _a1, locale, siteURL, action
func (_m *ServiceInterface) SendUpgradeEmail(user string, _a1 string, locale string, siteURL string, action string) (bool, error) {
ret := _m.Called(user, _a1, locale, siteURL, action)
var r0 bool
if rf, ok := ret.Get(0).(func(string, string, string, string, string) bool); ok {
r0 = rf(user, _a1, locale, siteURL, action)
} else {
r0 = ret.Get(0).(bool)
}
var r1 error
if rf, ok := ret.Get(1).(func(string, string, string, string, string) error); ok {
r1 = rf(user, _a1, locale, siteURL, action)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendUserAccessTokenAddedEmail provides a mock function with given fields: _a0, locale, siteURL
func (_m *ServiceInterface) SendUserAccessTokenAddedEmail(_a0 string, locale string, siteURL string) error {
ret := _m.Called(_a0, locale, siteURL)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
r0 = rf(_a0, locale, siteURL)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendVerifyEmail provides a mock function with given fields: userEmail, locale, siteURL, token, redirect
func (_m *ServiceInterface) SendVerifyEmail(userEmail string, locale string, siteURL string, token string, redirect string) error {
ret := _m.Called(userEmail, locale, siteURL, token, redirect)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
r0 = rf(userEmail, locale, siteURL, token, redirect)
} else {
r0 = ret.Error(0)
}
return r0
}
// SendWelcomeEmail provides a mock function with given fields: userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect
func (_m *ServiceInterface) SendWelcomeEmail(userID string, _a1 string, verified bool, disableWelcomeEmail bool, locale string, siteURL string, redirect string) error {
ret := _m.Called(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
var r0 error
if rf, ok := ret.Get(0).(func(string, string, bool, bool, string, string, string) error); ok {
r0 = rf(userID, _a1, verified, disableWelcomeEmail, locale, siteURL, redirect)
} else {
r0 = ret.Error(0)
}
return r0
}

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

@@ -4,6 +4,7 @@
package email
import (
"io"
"net/url"
"path"
@@ -13,6 +14,7 @@ import (
"github.com/mattermost/mattermost-server/v6/app/users"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/i18n"
"github.com/mattermost/mattermost-server/v6/shared/templates"
"github.com/mattermost/mattermost-server/v6/store"
)
@@ -47,8 +49,8 @@ type Service struct {
store store.Store
templatesContainer *templates.Container
PerHourEmailRateLimiter *throttled.GCRARateLimiter
PerDayEmailRateLimiter *throttled.GCRARateLimiter
perHourEmailRateLimiter *throttled.GCRARateLimiter
perDayEmailRateLimiter *throttled.GCRARateLimiter
EmailBatching *EmailBatchingJob
}
@@ -114,7 +116,56 @@ func (es *Service) setUpRateLimiters() error {
return errors.Wrap(err, "Unable to setup per day email rate limiting GCRA rate limiter.")
}
es.PerHourEmailRateLimiter = perHourRateLimiter
es.PerDayEmailRateLimiter = perDayRateLimiter
es.perHourEmailRateLimiter = perHourRateLimiter
es.perDayEmailRateLimiter = perDayRateLimiter
return nil
}
type ServiceInterface interface {
GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter
NewEmailTemplateData(locale string) templates.Data
SendEmailChangeVerifyEmail(newUserEmail, locale, siteURL, token string) error
SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) error
SendVerifyEmail(userEmail, locale, siteURL, token, redirect string) error
SendSignInChangeEmail(email, method, locale, siteURL string) error
SendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) error
SendCloudTrialEndWarningEmail(userEmail, name, trialEndDate, locale, siteURL string) error
SendCloudTrialEndedEmail(userEmail, name, locale, siteURL string) error
SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns, siteURL string) error
SendPasswordChangeEmail(email, method, locale, siteURL string) error
SendUserAccessTokenAddedEmail(email, locale, siteURL string) error
SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, error)
SendMfaChangeEmail(email string, activated bool, locale, siteURL string) error
SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string, reminderData *model.TeamInviteReminderData, errorWhenNotSent bool) error
SendGuestInviteEmails(team *model.Team, channels []*model.Channel, senderName string, senderUserId string, senderProfileImage []byte, invites []string, siteURL string, message string, errorWhenNotSent bool) error
SendDeactivateAccountEmail(email string, locale, siteURL string) error
SendNotificationMail(to, subject, htmlBody string) error
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) error
SendAtUserLimitWarningEmail(email string, locale string, siteURL string) (bool, error)
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) error
SendUpgradeEmail(user, email, locale, siteURL, action string) (bool, error)
SendOverUserLimitWarningEmail(email string, locale string, siteURL string) (bool, error)
SendOverUserLimitThirtyDayWarningEmail(email string, locale string, siteURL string) (bool, error)
SendOverUserLimitNinetyDayWarningEmail(email string, locale string, siteURL string, overLimitDate string) (bool, error)
SendOverUserLimitWorkspaceSuspendedWarningEmail(email string, locale string, siteURL string) (bool, error)
SendOverUserFourteenDayWarningEmail(email string, locale string, siteURL string, overLimitDate string) (bool, error)
SendOverUserSevenDayWarningEmail(email string, locale string, siteURL string) (bool, error)
SendSuspensionEmailToSupport(email string, installationID string, customerID string, subscriptionID string, siteURL string, userCount int64) (bool, error)
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, siteURL string) (bool, error)
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
SendRemoveExpiredLicenseEmail(renewalLink, email string, locale, siteURL string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string
InitEmailBatching()
SendChangeUsernameEmail(newUsername, email, locale, siteURL string) error
CreateVerifyEmailToken(userID string, newEmail string) (*model.Token, error)
SendLicenseInactivityEmail(email, name, locale, siteURL string) error
}
func (es *Service) GetPerDayEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perDayEmailRateLimiter
}
func (es *Service) GetPerHourEmailRateLimiter() *throttled.GCRARateLimiter {
return es.perHourEmailRateLimiter
}

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

@@ -125,7 +125,7 @@ type Server struct {
goroutineCount int32
goroutineExitSignal chan struct{}
EmailService *email.Service
EmailService email.ServiceInterface
hubs []*Hub
hashSeed maphash.Seed

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

@@ -1325,9 +1325,19 @@ func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamID, senderI
if len(goodEmails) > 0 {
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData)
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), reminderData, true)
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("SendInviteEmails", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
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):
@@ -1434,9 +1444,20 @@ 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))
}
eErr := 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, true)
if eErr != nil {
switch {
case errors.Is(eErr, email.SendMailError):
for i := range inviteListWithErrors {
if inviteListWithErrors[i].Error == nil {
if *a.Config().EmailSettings.SMTPServer == model.EmailSMTPDefaultServer && *a.Config().EmailSettings.SMTPPort == model.EmailSMTPDefaultPort {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email_with_defaults.app_error", nil, "", http.StatusInternalServerError)
} else {
inviteListWithErrors[i].Error = model.NewAppError("InviteGuestsToChannelsGracefully", "api.team.invite_members.unable_to_send_email.app_error", nil, "", http.StatusInternalServerError)
}
}
}
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):
@@ -1480,7 +1501,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamID, senderId string)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil)
eErr := a.Srv().EmailService.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL(), nil, false)
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):
@@ -1522,7 +1543,7 @@ 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))
}
eErr := 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, false)
if eErr != nil {
switch {
case errors.Is(eErr, email.NoRateLimiterError):

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

@@ -16,6 +16,8 @@ import (
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost-server/v6/app/email"
emailmocks "github.com/mattermost/mattermost-server/v6/app/email/mocks"
"github.com/mattermost/mattermost-server/v6/app/teams"
"github.com/mattermost/mattermost-server/v6/app/users"
"github.com/mattermost/mattermost-server/v6/model"
@@ -1240,3 +1242,108 @@ func TestClearTeamMembersCache(t *testing.T) {
th.App.ClearTeamMembersCache("teamID")
}
func TestInviteNewUsersToTeamGracefully(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableEmailInvitations = true
})
t.Run("it return list of email with no error on success", func(t *testing.T) {
emailServiceMock := emailmocks.ServiceInterface{}
emailServiceMock.On("SendInviteEmails",
mock.AnythingOfType("*model.Team"),
mock.AnythingOfType("string"),
mock.AnythingOfType("string"),
[]string{"idontexist@mattermost.com"},
"",
mock.Anything,
true,
).Once().Return(nil)
th.App.Srv().EmailService = &emailServiceMock
res, err := th.App.InviteNewUsersToTeamGracefully([]string{"idontexist@mattermost.com"}, th.BasicTeam.Id, th.BasicUser.Id, "")
require.Nil(t, err)
require.Len(t, res, 1)
require.Nil(t, res[0].Error)
})
t.Run("it should assign errors to emails when failing to send", func(t *testing.T) {
emailServiceMock := emailmocks.ServiceInterface{}
emailServiceMock.On("SendInviteEmails",
mock.AnythingOfType("*model.Team"),
mock.AnythingOfType("string"),
mock.AnythingOfType("string"),
[]string{"idontexist@mattermost.com"},
"",
mock.Anything,
true,
).Once().Return(email.SendMailError)
th.App.Srv().EmailService = &emailServiceMock
res, err := th.App.InviteNewUsersToTeamGracefully([]string{"idontexist@mattermost.com"}, th.BasicTeam.Id, th.BasicUser.Id, "")
require.Nil(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0].Error)
})
}
func TestInviteGuestsToChannelsGracefully(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) {
*cfg.ServiceSettings.EnableEmailInvitations = true
})
t.Run("it return list of email with no error on success", func(t *testing.T) {
emailServiceMock := emailmocks.ServiceInterface{}
emailServiceMock.On("SendGuestInviteEmails",
mock.AnythingOfType("*model.Team"),
mock.AnythingOfType("[]*model.Channel"),
mock.AnythingOfType("string"),
mock.AnythingOfType("string"),
mock.AnythingOfType("[]uint8"),
[]string{"idontexist@mattermost.com"},
"",
"",
true,
).Once().Return(nil)
th.App.Srv().EmailService = &emailServiceMock
res, err := th.App.InviteGuestsToChannelsGracefully(th.BasicTeam.Id, &model.GuestsInvite{
Emails: []string{"idontexist@mattermost.com"},
Channels: []string{th.BasicChannel.Id},
}, th.BasicUser.Id)
require.Nil(t, err)
require.Len(t, res, 1)
require.Nil(t, res[0].Error)
})
t.Run("it should assign errors to emails when failing to send", func(t *testing.T) {
emailServiceMock := emailmocks.ServiceInterface{}
emailServiceMock.On("SendGuestInviteEmails",
mock.AnythingOfType("*model.Team"),
mock.AnythingOfType("[]*model.Channel"),
mock.AnythingOfType("string"),
mock.AnythingOfType("string"),
mock.AnythingOfType("[]uint8"),
[]string{"idontexist@mattermost.com"},
"",
"",
true,
).Once().Return(email.SendMailError)
th.App.Srv().EmailService = &emailServiceMock
res, err := th.App.InviteGuestsToChannelsGracefully(th.BasicTeam.Id, &model.GuestsInvite{
Emails: []string{"idontexist@mattermost.com"},
Channels: []string{th.BasicChannel.Id},
}, th.BasicUser.Id)
require.Nil(t, err)
require.Len(t, res, 1)
require.NotNil(t, res[0].Error)
})
}

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

@@ -2963,6 +2963,14 @@
"id": "api.team.invite_members.no_one.app_error",
"translation": "No one to invite."
},
{
"id": "api.team.invite_members.unable_to_send_email.app_error",
"translation": "Error while sending the email"
},
{
"id": "api.team.invite_members.unable_to_send_email_with_defaults.app_error",
"translation": "SMTP is not configured in System Console"
},
{
"id": "api.team.is_team_creation_allowed.disabled.app_error",
"translation": "Team creation has been disabled. Please ask your System Administrator for details."

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

@@ -93,6 +93,9 @@ const (
EmailNotificationContentsFull = "full"
EmailNotificationContentsGeneric = "generic"
EmailSMTPDefaultServer = "localhost"
EmailSMTPDefaultPort = "10025"
SitenameMaxLength = 30
ServiceSettingsDefaultSiteURL = "http://localhost:8065"
@@ -1598,11 +1601,11 @@ func (s *EmailSettings) SetDefaults(isUpdate bool) {
}
if s.SMTPServer == nil || *s.SMTPServer == "" {
s.SMTPServer = NewString("localhost")
s.SMTPServer = NewString(EmailSMTPDefaultServer)
}
if s.SMTPPort == nil || *s.SMTPPort == "" {
s.SMTPPort = NewString("10025")
s.SMTPPort = NewString(EmailSMTPDefaultPort)
}
if s.SMTPServerTimeout == nil || *s.SMTPServerTimeout == 0 {