summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api4/team_test.go9
-rw-r--r--app/app.go8
-rw-r--r--app/command_invite_people.go6
-rw-r--r--app/email.go48
-rw-r--r--app/team.go6
-rw-r--r--cmd/mattermost/commands/user.go6
-rw-r--r--config/default.json3
-rw-r--r--i18n/en.json8
-rw-r--r--model/config.go10
-rw-r--r--utils/config.go2
10 files changed, 100 insertions, 6 deletions
diff --git a/api4/team_test.go b/api4/team_test.go
index 48e3404eb..307e91635 100644
--- a/api4/team_test.go
+++ b/api4/team_test.go
@@ -1935,6 +1935,15 @@ func TestInviteUsersToTeam(t *testing.T) {
utils.DeleteMailBox(user1)
utils.DeleteMailBox(user2)
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = false })
+
+ _, resp := th.SystemAdminClient.InviteUsersToTeam(th.BasicTeam.Id, emailList)
+ if resp.Error == nil {
+ t.Fatal("Should be disabled")
+ }
+
+ th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableEmailInvitations = true })
+
okMsg, resp := th.SystemAdminClient.InviteUsersToTeam(th.BasicTeam.Id, emailList)
CheckNoError(t, resp)
if !okMsg {
diff --git a/app/app.go b/app/app.go
index 96b9b6d13..6f98d4234 100644
--- a/app/app.go
+++ b/app/app.go
@@ -17,6 +17,7 @@ import (
"github.com/gorilla/mux"
"github.com/pkg/errors"
+ "github.com/throttled/throttled"
"github.com/mattermost/mattermost-server/einterfaces"
ejobs "github.com/mattermost/mattermost-server/einterfaces/jobs"
@@ -46,7 +47,8 @@ type App struct {
IsPluginSandboxSupported bool
pluginStatuses map[string]*model.PluginStatus
- EmailBatching *EmailBatchingJob
+ EmailBatching *EmailBatchingJob
+ EmailRateLimiter *throttled.GCRARateLimiter
Hubs []*Hub
HubsStopCheckingForDeadlock chan bool
@@ -185,6 +187,10 @@ func New(options ...Option) (outApp *App, outErr error) {
})
+ if err := app.SetupInviteEmailRateLimiting(); err != nil {
+ return nil, err
+ }
+
mlog.Info("Server is initializing...")
app.initEnterprise()
diff --git a/app/command_invite_people.go b/app/command_invite_people.go
index c3dc4f469..9ced3c5a1 100644
--- a/app/command_invite_people.go
+++ b/app/command_invite_people.go
@@ -28,7 +28,7 @@ func (me *InvitePeopleProvider) GetTrigger() string {
func (me *InvitePeopleProvider) GetCommand(a *App, T goi18n.TranslateFunc) *model.Command {
autoComplete := true
- if !a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation {
+ if !a.Config().EmailSettings.SendEmailNotifications || !*a.Config().TeamSettings.EnableUserCreation || !*a.Config().ServiceSettings.EnableEmailInvitations {
autoComplete = false
}
return &model.Command{
@@ -49,6 +49,10 @@ func (me *InvitePeopleProvider) DoCommand(a *App, args *model.CommandArgs, messa
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.invite_off")}
}
+ if !*a.Config().ServiceSettings.EnableEmailInvitations {
+ return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: args.T("api.command.invite_people.email_invitations_off")}
+ }
+
emailList := strings.Fields(message)
for i := len(emailList) - 1; i >= 0; i-- {
diff --git a/app/email.go b/app/email.go
index b4e0a8983..569e6f454 100644
--- a/app/email.go
+++ b/app/email.go
@@ -10,12 +10,41 @@ import (
"net/http"
"github.com/nicksnyder/go-i18n/i18n"
+ "github.com/pkg/errors"
+ "github.com/throttled/throttled"
+ "github.com/throttled/throttled/store/memstore"
"github.com/mattermost/mattermost-server/mlog"
"github.com/mattermost/mattermost-server/model"
"github.com/mattermost/mattermost-server/utils"
)
+const (
+ emailRateLimitingMemstoreSize = 65536
+ emailRateLimitingPerHour = 20
+ emailRateLimitingMaxBurst = 20
+)
+
+func (a *App) SetupInviteEmailRateLimiting() error {
+ store, err := memstore.New(emailRateLimitingMemstoreSize)
+ if err != nil {
+ return errors.Wrap(err, "Unable to setup email rate limiting memstore.")
+ }
+
+ quota := throttled.RateQuota{
+ MaxRate: throttled.PerHour(emailRateLimitingPerHour),
+ MaxBurst: emailRateLimitingMaxBurst,
+ }
+
+ rateLimiter, err := throttled.NewGCRARateLimiter(store, quota)
+ if err != nil || rateLimiter == nil {
+ return errors.Wrap(err, "Unable to setup email rate limiting GCRA rate limiter.")
+ }
+
+ a.EmailRateLimiter = rateLimiter
+ return nil
+}
+
func (a *App) SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL string) *model.AppError {
T := utils.GetUserTranslations(locale)
@@ -247,7 +276,24 @@ func (a *App) SendMfaChangeEmail(email string, activated bool, locale, siteURL s
return nil
}
-func (a *App) SendInviteEmails(team *model.Team, senderName string, invites []string, siteURL string) {
+func (a *App) SendInviteEmails(team *model.Team, senderName string, senderUserId string, invites []string, siteURL string) {
+ if a.EmailRateLimiter == nil {
+ a.Log.Error("Email invite not sent, rate limiting could not be setup.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id))
+ return
+ }
+ rateLimited, result, err := a.EmailRateLimiter.RateLimit(senderUserId, len(invites))
+ if rateLimited {
+ a.Log.Error("Invite emails rate limited.",
+ mlog.String("user_id", senderUserId),
+ mlog.String("team_id", team.Id),
+ mlog.String("retry_after", result.RetryAfter.String()),
+ mlog.Err(err))
+ return
+ } else if err != nil {
+ a.Log.Error("Error rate limiting invite email.", mlog.String("user_id", senderUserId), mlog.String("team_id", team.Id), mlog.Err(err))
+ return
+ }
+
for _, invite := range invites {
if len(invite) > 0 {
senderRole := utils.T("api.team.invite_members.member")
diff --git a/app/team.go b/app/team.go
index beb4b1449..d9f19fab8 100644
--- a/app/team.go
+++ b/app/team.go
@@ -805,6 +805,10 @@ func (a *App) postRemoveFromTeamMessage(user *model.User, channel *model.Channel
}
func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) *model.AppError {
+ if !*a.Config().ServiceSettings.EnableEmailInvitations {
+ return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented)
+ }
+
if len(emailList) == 0 {
err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest)
return err
@@ -842,7 +846,7 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string)
}
nameFormat := *a.Config().TeamSettings.TeammateNameDisplay
- a.SendInviteEmails(team, user.GetDisplayName(nameFormat), emailList, a.GetSiteURL())
+ a.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, emailList, a.GetSiteURL())
return nil
}
diff --git a/cmd/mattermost/commands/user.go b/cmd/mattermost/commands/user.go
index 373fe7463..b3b43c076 100644
--- a/cmd/mattermost/commands/user.go
+++ b/cmd/mattermost/commands/user.go
@@ -384,7 +384,11 @@ func inviteUser(a *app.App, email string, team *model.Team, teamArg string) erro
return fmt.Errorf("Can't find team '%v'", teamArg)
}
- a.SendInviteEmails(team, "Administrator", invites, *a.Config().ServiceSettings.SiteURL)
+ if !*a.Config().ServiceSettings.EnableEmailInvitations {
+ return fmt.Errorf("Email invites are disabled.")
+ }
+
+ a.SendInviteEmails(team, "Administrator", "Mattermost CLI "+model.NewId(), invites, *a.Config().ServiceSettings.SiteURL)
CommandPrettyPrintln("Invites may or may not have been sent.")
return nil
diff --git a/config/default.json b/config/default.json
index a4487888e..cb60611ba 100644
--- a/config/default.json
+++ b/config/default.json
@@ -68,7 +68,8 @@
"ImageProxyURL": "",
"EnableAPITeamDeletion": false,
"ExperimentalEnableHardenedMode": false,
- "ExperimentalLimitClientConfig": false
+ "ExperimentalLimitClientConfig": false,
+ "EnableEmailInvitations": false
},
"TeamSettings": {
"SiteName": "Mattermost",
diff --git a/i18n/en.json b/i18n/en.json
index b12a67cde..f257a86a4 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -312,6 +312,10 @@
"translation": "Email has not been configured, no invite(s) sent"
},
{
+ "id": "api.command.invite_people.email_invitations_off",
+ "translation": "Email invitations are disabled, no invite(s) sent"
+ },
+ {
"id": "api.command.invite_people.fail",
"translation": "Encountered an error sending email invite(s)"
},
@@ -1615,6 +1619,10 @@
"translation": "No one to invite."
},
{
+ "id": "api.team.invite_members.disabled.app_error",
+ "translation": "Email invitations are disabled."
+ },
+ {
"id": "api.team.is_team_creation_allowed.disabled.app_error",
"translation": "Team creation has been disabled. Please ask your systems administrator for details."
},
diff --git a/model/config.go b/model/config.go
index be940d893..1388f896f 100644
--- a/model/config.go
+++ b/model/config.go
@@ -237,9 +237,19 @@ type ServiceSettings struct {
EnableAPITeamDeletion *bool
ExperimentalEnableHardenedMode *bool
ExperimentalLimitClientConfig *bool
+ EnableEmailInvitations *bool
}
func (s *ServiceSettings) SetDefaults() {
+ if s.EnableEmailInvitations == nil {
+ // If the site URL is also not present then assume this is a clean install
+ if s.SiteURL == nil {
+ s.EnableEmailInvitations = NewBool(false)
+ } else {
+ s.EnableEmailInvitations = NewBool(true)
+ }
+ }
+
if s.SiteURL == nil {
s.SiteURL = NewString(SERVICE_SETTINGS_DEFAULT_SITE_URL)
}
diff --git a/utils/config.go b/utils/config.go
index 10661aa54..f8cc6ec75 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -573,6 +573,8 @@ func GenerateClientConfig(c *model.Config, diagnosticId string, license *model.L
props["RunJobs"] = strconv.FormatBool(*c.JobSettings.RunJobs)
+ props["EnableEmailInvitations"] = strconv.FormatBool(*c.ServiceSettings.EnableEmailInvitations)
+
// Set default values for all options that require a license.
props["ExperimentalHideTownSquareinLHS"] = "false"
props["ExperimentalTownSquareIsReadOnly"] = "false"