summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJesús Espino <jespinog@gmail.com>2018-02-07 18:05:23 +0100
committerJesús Espino <jespinog@gmail.com>2018-02-07 18:05:23 +0100
commita04b02081a77497ecfc7a5ae9ffb0ca28404dd0e (patch)
tree985cb699d278f68522b08b60b1e7b84e0bd243fc
parent7941c30117efe1b957ac0458c2f0479e3824196d (diff)
parent7bd298ceaa24c0721e0acd65692cb2d1ca4983f3 (diff)
downloadchat-a04b02081a77497ecfc7a5ae9ffb0ca28404dd0e.tar.gz
chat-a04b02081a77497ecfc7a5ae9ffb0ca28404dd0e.tar.bz2
chat-a04b02081a77497ecfc7a5ae9ffb0ca28404dd0e.zip
Merge remote-tracking branch 'origin/master' into advanced-permissions-phase-1
-rw-r--r--api/apitestlib.go6
-rw-r--r--api/context.go4
-rw-r--r--api/user.go2
-rw-r--r--api4/apitestlib.go9
-rw-r--r--api4/context.go4
-rw-r--r--api4/user.go2
-rw-r--r--app/app_test.go3
-rw-r--r--app/apptestlib.go6
-rw-r--r--app/authentication.go7
-rw-r--r--app/channel.go30
-rw-r--r--app/channel_test.go4
-rw-r--r--app/cluster_discovery.go3
-rw-r--r--app/compliance.go7
-rw-r--r--app/config.go8
-rw-r--r--app/diagnostics.go18
-rw-r--r--app/email_batching.go2
-rw-r--r--app/ldap.go9
-rw-r--r--app/license_test.go4
-rw-r--r--app/notification.go4
-rw-r--r--app/oauth.go4
-rw-r--r--app/plugin.go1
-rw-r--r--app/post.go8
-rw-r--r--app/server.go12
-rw-r--r--app/server_test.go50
-rw-r--r--app/session.go3
-rw-r--r--app/user.go3
-rw-r--r--app/web_conn.go2
-rw-r--r--app/webhook.go2
-rw-r--r--cmd/platform/channel.go17
-rw-r--r--cmd/platform/channel_test.go27
-rw-r--r--cmd/platform/server.go15
-rw-r--r--cmd/platform/test.go12
-rw-r--r--config/default.json2
-rw-r--r--i18n/en.json40
-rw-r--r--model/channel_member_history.go5
-rw-r--r--model/config.go39
-rw-r--r--model/config_test.go84
-rw-r--r--model/message_export.go1
-rw-r--r--model/post.go2
-rw-r--r--store/sqlstore/channel_member_history_store.go12
-rw-r--r--store/sqlstore/compliance_store.go3
-rw-r--r--store/storetest/channel_member_history_store.go16
-rw-r--r--store/storetest/compliance_store.go9
-rw-r--r--templates/globalrelay_compliance_export.html91
-rw-r--r--templates/globalrelay_compliance_export_message.html8
-rw-r--r--templates/globalrelay_compliance_export_participant_row.html10
-rw-r--r--utils/config.go2
-rw-r--r--utils/html.go8
-rw-r--r--utils/inbucket.go52
-rw-r--r--utils/mail.go60
-rw-r--r--utils/mail_test.go87
-rw-r--r--web/web_test.go5
52 files changed, 699 insertions, 125 deletions
diff --git a/api/apitestlib.go b/api/apitestlib.go
index f53e7b237..dece29b89 100644
--- a/api/apitestlib.go
+++ b/api/apitestlib.go
@@ -105,7 +105,11 @@ func setupTestHelper(enterprise bool) *TestHelper {
if testStore != nil {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
}
- th.App.StartServer()
+ serverErr := th.App.StartServer()
+ if serverErr != nil {
+ panic(serverErr)
+ }
+
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
api4.Init(th.App, th.App.Srv.Router, false)
Init(th.App, th.App.Srv.Router)
diff --git a/api/context.go b/api/context.go
index 84967659d..b28a24731 100644
--- a/api/context.go
+++ b/api/context.go
@@ -127,7 +127,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
- w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), utils.IsLicensed()))
+ w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))
// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
if !h.isApi {
@@ -292,7 +292,7 @@ func (c *Context) UserRequired() {
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
- if !utils.IsLicensed() || !*utils.License().Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
+ if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
diff --git a/api/user.go b/api/user.go
index a2e06c013..440ea5858 100644
--- a/api/user.go
+++ b/api/user.go
@@ -1057,7 +1057,7 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
}
func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
- if !utils.IsLicensed() || !*utils.License().Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication {
+ if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication {
rdata := map[string]string{}
rdata["mfa_required"] = "false"
w.Write([]byte(model.MapToJson(rdata)))
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
index bdca072c5..67dbc0419 100644
--- a/api4/apitestlib.go
+++ b/api4/apitestlib.go
@@ -113,7 +113,11 @@ func setupTestHelper(enterprise bool) *TestHelper {
if testStore != nil {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
}
- th.App.StartServer()
+ serverErr := th.App.StartServer()
+ if serverErr != nil {
+ panic(serverErr)
+ }
+
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
Init(th.App, th.App.Srv.Router, true)
wsapi.Init(th.App, th.App.Srv.WebSocketRouter)
@@ -299,8 +303,7 @@ func (me *TestHelper) CreateUserWithClient(client *model.Client4) *model.User {
}
utils.DisableDebugLogForTest()
- ruser, r := client.CreateUser(user)
- fmt.Println(r)
+ ruser, _ := client.CreateUser(user)
ruser.Password = "Password1"
store.Must(me.App.Srv.Store.User().VerifyEmail(ruser.Id))
utils.EnableDebugLogForTest()
diff --git a/api4/context.go b/api4/context.go
index 971180b3e..19778dda3 100644
--- a/api4/context.go
+++ b/api4/context.go
@@ -112,7 +112,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.SetSiteURLHeader(app.GetProtocol(r) + "://" + r.Host)
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
- w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), utils.IsLicensed()))
+ w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, c.App.ClientConfigHash(), c.App.License() != nil))
w.Header().Set("Content-Type", "application/json")
@@ -249,7 +249,7 @@ func (c *Context) SessionRequired() {
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
- if !utils.IsLicensed() || !*utils.License().Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
+ if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication || !*c.App.Config().ServiceSettings.EnforceMultifactorAuthentication {
return
}
diff --git a/api4/user.go b/api4/user.go
index a664acfac..cfb2a5b3f 100644
--- a/api4/user.go
+++ b/api4/user.go
@@ -738,7 +738,7 @@ func checkUserMfa(c *Context, w http.ResponseWriter, r *http.Request) {
resp := map[string]interface{}{}
resp["mfa_required"] = false
- if !utils.IsLicensed() || !*utils.License().Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication {
+ if license := c.App.License(); license == nil || !*license.Features.MFA || !*c.App.Config().ServiceSettings.EnableMultifactorAuthentication {
w.Write([]byte(model.StringInterfaceToJson(resp)))
return
}
diff --git a/app/app_test.go b/app/app_test.go
index 6d62bf249..09e002791 100644
--- a/app/app_test.go
+++ b/app/app_test.go
@@ -50,7 +50,8 @@ func TestAppRace(t *testing.T) {
a, err := New()
require.NoError(t, err)
a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
- a.StartServer()
+ serverErr := a.StartServer()
+ require.NoError(t, serverErr)
a.Shutdown()
}
}
diff --git a/app/apptestlib.go b/app/apptestlib.go
index 2b56c7ded..9aef50ce6 100644
--- a/app/apptestlib.go
+++ b/app/apptestlib.go
@@ -101,7 +101,11 @@ func setupTestHelper(enterprise bool) *TestHelper {
if testStore != nil {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
}
- th.App.StartServer()
+ serverErr := th.App.StartServer()
+ if serverErr != nil {
+ panic(serverErr)
+ }
+
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
th.App.DoAdvancedPermissionsMigration()
diff --git a/app/authentication.go b/app/authentication.go
index 0b3659449..5c91f8038 100644
--- a/app/authentication.go
+++ b/app/authentication.go
@@ -36,7 +36,7 @@ func (tl TokenLocation) String() string {
}
func (a *App) IsPasswordValid(password string) *model.AppError {
- if utils.IsLicensed() && *utils.License().Features.PasswordRequirements {
+ if license := a.License(); license != nil && *license.Features.PasswordRequirements {
return utils.IsPasswordValidWithSettings(password, &a.Config().PasswordSettings)
}
return utils.IsPasswordValid(password)
@@ -150,7 +150,7 @@ func (a *App) CheckUserPostflightAuthenticationCriteria(user *model.User) *model
}
func (a *App) CheckUserMfa(user *model.User, token string) *model.AppError {
- if !user.MfaActive || !utils.IsLicensed() || !*utils.License().Features.MFA || !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
+ if license := a.License(); !user.MfaActive || license == nil || !*license.Features.MFA || !*a.Config().ServiceSettings.EnableMultifactorAuthentication {
return nil
}
@@ -183,7 +183,8 @@ func checkUserNotDisabled(user *model.User) *model.AppError {
}
func (a *App) authenticateUser(user *model.User, password, mfaToken string) (*model.User, *model.AppError) {
- ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && utils.IsLicensed() && *utils.License().Features.LDAP
+ license := a.License()
+ ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && license != nil && *license.Features.LDAP
if user.AuthService == model.USER_AUTH_SERVICE_LDAP {
if !ldapAvailable {
diff --git a/app/channel.go b/app/channel.go
index 21bb386f3..af36774de 100644
--- a/app/channel.go
+++ b/app/channel.go
@@ -1363,7 +1363,7 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {
// This function is intended for use from the CLI. It is not robust against people joining the channel while the move
// is in progress, and therefore should not be used from the API without first fixing this potential race condition.
-func (a *App) MoveChannel(team *model.Team, channel *model.Channel) *model.AppError {
+func (a *App) MoveChannel(team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
// Check that all channel members are in the destination team.
if channelMembers, err := a.GetChannelMembersPage(channel.Id, 0, 10000000); err != nil {
return err
@@ -1382,11 +1382,37 @@ func (a *App) MoveChannel(team *model.Team, channel *model.Channel) *model.AppEr
}
}
- // Change the Team ID of the channel.
+ // keep instance of the previous team
+ var previousTeam *model.Team
+ if result := <-a.Srv.Store.Team().Get(channel.TeamId); result.Err != nil {
+ return result.Err
+ } else {
+ previousTeam = result.Data.(*model.Team)
+ }
channel.TeamId = team.Id
if result := <-a.Srv.Store.Channel().Update(channel); result.Err != nil {
return result.Err
}
+ a.postChannelMoveMessage(user, channel, previousTeam)
+
+ return nil
+}
+
+func (a *App) postChannelMoveMessage(user *model.User, channel *model.Channel, previousTeam *model.Team) *model.AppError {
+
+ post := &model.Post{
+ ChannelId: channel.Id,
+ Message: fmt.Sprintf(utils.T("api.team.move_channel.success"), previousTeam.Name),
+ Type: model.POST_MOVE_CHANNEL,
+ UserId: user.Id,
+ Props: model.StringInterface{
+ "username": user.Username,
+ },
+ }
+
+ if _, err := a.CreatePost(post, channel, false); err != nil {
+ return model.NewAppError("postChannelMoveMessage", "api.team.move_channel.post.error", nil, err.Error(), http.StatusInternalServerError)
+ }
return nil
}
diff --git a/app/channel_test.go b/app/channel_test.go
index a414fbb35..d315fbae6 100644
--- a/app/channel_test.go
+++ b/app/channel_test.go
@@ -97,7 +97,7 @@ func TestMoveChannel(t *testing.T) {
t.Fatal(err)
}
- if err := th.App.MoveChannel(targetTeam, channel1); err == nil {
+ if err := th.App.MoveChannel(targetTeam, channel1, th.BasicUser); err == nil {
t.Fatal("Should have failed due to mismatched members.")
}
@@ -105,7 +105,7 @@ func TestMoveChannel(t *testing.T) {
t.Fatal(err)
}
- if err := th.App.MoveChannel(targetTeam, channel1); err != nil {
+ if err := th.App.MoveChannel(targetTeam, channel1, th.BasicUser); err != nil {
t.Fatal(err)
}
}
diff --git a/app/cluster_discovery.go b/app/cluster_discovery.go
index 6418ab2be..2682425f5 100644
--- a/app/cluster_discovery.go
+++ b/app/cluster_discovery.go
@@ -9,7 +9,6 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/utils"
)
const (
@@ -80,7 +79,7 @@ func (me *ClusterDiscoveryService) Stop() {
}
func (a *App) IsLeader() bool {
- if utils.IsLicensed() && *a.Config().ClusterSettings.Enable && a.Cluster != nil {
+ if a.License() != nil && *a.Config().ClusterSettings.Enable && a.Cluster != nil {
return a.Cluster.IsLeader()
} else {
return true
diff --git a/app/compliance.go b/app/compliance.go
index 2a1b86a6b..5c62a49d9 100644
--- a/app/compliance.go
+++ b/app/compliance.go
@@ -9,11 +9,10 @@ import (
"net/http"
"github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/utils"
)
func (a *App) GetComplianceReports(page, perPage int) (model.Compliances, *model.AppError) {
- if !*a.Config().ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance {
+ if license := a.License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance {
return nil, model.NewAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
@@ -25,7 +24,7 @@ func (a *App) GetComplianceReports(page, perPage int) (model.Compliances, *model
}
func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
- if !*a.Config().ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance || a.Compliance == nil {
+ if license := a.License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance == nil {
return nil, model.NewAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
@@ -44,7 +43,7 @@ func (a *App) SaveComplianceReport(job *model.Compliance) (*model.Compliance, *m
}
func (a *App) GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
- if !*a.Config().ComplianceSettings.Enable || !utils.IsLicensed() || !*utils.License().Features.Compliance || a.Compliance == nil {
+ if license := a.License(); !*a.Config().ComplianceSettings.Enable || license == nil || !*license.Features.Compliance || a.Compliance == nil {
return nil, model.NewAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "", http.StatusNotImplemented)
}
diff --git a/app/config.go b/app/config.go
index 526d47a77..a2398f9e9 100644
--- a/app/config.go
+++ b/app/config.go
@@ -166,3 +166,11 @@ func (a *App) Desanitize(cfg *model.Config) {
cfg.SqlSettings.DataSourceSearchReplicas[i] = actual.SqlSettings.DataSourceSearchReplicas[i]
}
}
+
+// License returns the currently active license or nil if the application is unlicensed.
+func (a *App) License() *model.License {
+ if utils.IsLicensed() {
+ return utils.License()
+ }
+ return nil
+}
diff --git a/app/diagnostics.go b/app/diagnostics.go
index c74224f19..6d83d3a89 100644
--- a/app/diagnostics.go
+++ b/app/diagnostics.go
@@ -11,7 +11,6 @@ import (
"sync/atomic"
"github.com/mattermost/mattermost-server/model"
- "github.com/mattermost/mattermost-server/utils"
"github.com/segmentio/analytics-go"
)
@@ -502,6 +501,7 @@ func (a *App) trackConfig() {
a.SendDiagnostic(TRACK_CONFIG_MESSAGE_EXPORT, map[string]interface{}{
"enable_message_export": *cfg.MessageExportSettings.EnableExport,
+ "export_format": *cfg.MessageExportSettings.ExportFormat,
"daily_run_time": *cfg.MessageExportSettings.DailyRunTime,
"default_export_from_timestamp": *cfg.MessageExportSettings.ExportFromTimestamp,
"batch_size": *cfg.MessageExportSettings.BatchSize,
@@ -509,17 +509,17 @@ func (a *App) trackConfig() {
}
func (a *App) trackLicense() {
- if utils.IsLicensed() {
+ if license := a.License(); license != nil {
data := map[string]interface{}{
- "customer_id": utils.License().Customer.Id,
- "license_id": utils.License().Id,
- "issued": utils.License().IssuedAt,
- "start": utils.License().StartsAt,
- "expire": utils.License().ExpiresAt,
- "users": *utils.License().Features.Users,
+ "customer_id": license.Customer.Id,
+ "license_id": license.Id,
+ "issued": license.IssuedAt,
+ "start": license.StartsAt,
+ "expire": license.ExpiresAt,
+ "users": *license.Features.Users,
}
- features := utils.License().Features.ToMap()
+ features := license.Features.ToMap()
for featureName, featureValue := range features {
data["feature_"+featureName] = featureValue
}
diff --git a/app/email_batching.go b/app/email_batching.go
index 70fd7eb40..2a33d7d3e 100644
--- a/app/email_batching.go
+++ b/app/email_batching.go
@@ -220,7 +220,7 @@ func (a *App) sendBatchedEmailNotification(userId string, notifications []*batch
}
emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
- if utils.IsLicensed() && *utils.License().Features.EmailNotificationContents {
+ if license := a.License(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
diff --git a/app/ldap.go b/app/ldap.go
index 49f3d034a..179529c52 100644
--- a/app/ldap.go
+++ b/app/ldap.go
@@ -14,7 +14,7 @@ import (
func (a *App) SyncLdap() {
a.Go(func() {
- if utils.IsLicensed() && *utils.License().Features.LDAP && *a.Config().LdapSettings.EnableSync {
+ if license := a.License(); license != nil && *license.Features.LDAP && *a.Config().LdapSettings.EnableSync {
if ldapI := a.Ldap; ldapI != nil {
ldapI.StartSynchronizeJob(false)
} else {
@@ -25,7 +25,8 @@ func (a *App) SyncLdap() {
}
func (a *App) TestLdap() *model.AppError {
- if ldapI := a.Ldap; ldapI != nil && utils.IsLicensed() && *utils.License().Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
+ license := a.License()
+ if ldapI := a.Ldap; ldapI != nil && license != nil && *license.Features.LDAP && (*a.Config().LdapSettings.Enable || *a.Config().LdapSettings.EnableSync) {
if err := ldapI.RunTest(); err != nil {
err.StatusCode = 500
return err
@@ -39,7 +40,7 @@ func (a *App) TestLdap() *model.AppError {
}
func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword string) (string, *model.AppError) {
- if utils.IsLicensed() && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
+ if a.License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToLdap", "api.user.email_to_ldap.not_available.app_error", nil, "", http.StatusForbidden)
}
@@ -75,7 +76,7 @@ func (a *App) SwitchEmailToLdap(email, password, code, ldapId, ldapPassword stri
}
func (a *App) SwitchLdapToEmail(ldapPassword, code, email, newPassword string) (string, *model.AppError) {
- if utils.IsLicensed() && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
+ if a.License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("ldapToEmail", "api.user.ldap_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
diff --git a/app/license_test.go b/app/license_test.go
index 632034b11..5b73d9d18 100644
--- a/app/license_test.go
+++ b/app/license_test.go
@@ -6,8 +6,6 @@ package app
import (
//"github.com/mattermost/mattermost-server/model"
"testing"
-
- "github.com/mattermost/mattermost-server/utils"
)
func TestLoadLicense(t *testing.T) {
@@ -15,7 +13,7 @@ func TestLoadLicense(t *testing.T) {
defer th.TearDown()
th.App.LoadLicense()
- if utils.IsLicensed() {
+ if th.App.License() != nil {
t.Fatal("shouldn't have a valid license")
}
}
diff --git a/app/notification.go b/app/notification.go
index 19f7894c5..1318308f8 100644
--- a/app/notification.go
+++ b/app/notification.go
@@ -233,7 +233,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
sendPushNotifications := false
if *a.Config().EmailSettings.SendPushNotifications {
pushServer := *a.Config().EmailSettings.PushNotificationServer
- if pushServer == model.MHPNS && (!utils.IsLicensed() || !*utils.License().Features.MHPNS) {
+ if license := a.License(); pushServer == model.MHPNS && (license == nil || !*license.Features.MHPNS) {
l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
sendPushNotifications = false
} else {
@@ -358,7 +358,7 @@ func (a *App) sendNotificationEmail(post *model.Post, user *model.User, channel
}
emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL
- if utils.IsLicensed() && *utils.License().Features.EmailNotificationContents {
+ if license := a.License(); license != nil && *license.Features.EmailNotificationContents {
emailNotificationContentsType = *a.Config().EmailSettings.EmailNotificationContentsType
}
diff --git a/app/oauth.go b/app/oauth.go
index 1cce5a3a0..5a66f542e 100644
--- a/app/oauth.go
+++ b/app/oauth.go
@@ -717,7 +717,7 @@ func (a *App) AuthorizeOAuthUser(w http.ResponseWriter, r *http.Request, service
}
func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email, password, code, service string) (string, *model.AppError) {
- if utils.IsLicensed() && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
+ if a.License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("emailToOAuth", "api.user.email_to_oauth.not_available.app_error", nil, "", http.StatusForbidden)
}
@@ -747,7 +747,7 @@ func (a *App) SwitchEmailToOAuth(w http.ResponseWriter, r *http.Request, email,
}
func (a *App) SwitchOAuthToEmail(email, password, requesterId string) (string, *model.AppError) {
- if utils.IsLicensed() && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
+ if a.License() != nil && !*a.Config().ServiceSettings.ExperimentalEnableAuthenticationTransfer {
return "", model.NewAppError("oauthToEmail", "api.user.oauth_to_email.not_available.app_error", nil, "", http.StatusForbidden)
}
diff --git a/app/plugin.go b/app/plugin.go
index 3f06a000f..fe671d26a 100644
--- a/app/plugin.go
+++ b/app/plugin.go
@@ -565,6 +565,7 @@ func (a *App) RegisterPluginCommand(pluginId string, command *model.Command) err
TeamId: command.TeamId,
AutoComplete: command.AutoComplete,
AutoCompleteDesc: command.AutoCompleteDesc,
+ AutoCompleteHint: command.AutoCompleteHint,
DisplayName: command.DisplayName,
}
diff --git a/app/post.go b/app/post.go
index 9e32badad..01abb21cf 100644
--- a/app/post.go
+++ b/app/post.go
@@ -124,7 +124,7 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
user = result.Data.(*model.User)
}
- if utils.IsLicensed() && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
+ if a.License() != nil && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
!post.IsSystemMessage() &&
channel.Name == model.DEFAULT_CHANNEL &&
!a.RolesGrantPermission(user.GetRoles(), model.PERMISSION_MANAGE_SYSTEM.Id) {
@@ -332,7 +332,7 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
} else {
oldPost = result.Data.(*model.PostList).Posts[post.Id]
- if utils.IsLicensed() {
+ if a.License() != nil {
if *a.Config().ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER && post.Message != oldPost.Message {
err := model.NewAppError("UpdatePost", "api.post.update_post.permissions_denied.app_error", nil, "", http.StatusForbidden)
return nil, err
@@ -354,7 +354,7 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
return nil, err
}
- if utils.IsLicensed() {
+ if a.License() != nil {
if *a.Config().ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*a.Config().ServiceSettings.PostEditTimeLimit*1000) && post.Message != oldPost.Message {
err := model.NewAppError("UpdatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *a.Config().ServiceSettings.PostEditTimeLimit}, "", http.StatusBadRequest)
return nil, err
@@ -613,7 +613,7 @@ func (a *App) SearchPostsInTeam(terms string, userId string, teamId string, isOr
paramsList := model.ParseSearchParams(terms)
esInterface := a.Elasticsearch
- if esInterface != nil && *a.Config().ElasticsearchSettings.EnableSearching && utils.IsLicensed() && *utils.License().Features.Elasticsearch {
+ if license := a.License(); esInterface != nil && *a.Config().ElasticsearchSettings.EnableSearching && license != nil && *license.Features.Elasticsearch {
finalParamsList := []*model.SearchParams{}
for _, params := range paramsList {
diff --git a/app/server.go b/app/server.go
index 1659908b6..afa282ad6 100644
--- a/app/server.go
+++ b/app/server.go
@@ -17,6 +17,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
+ "github.com/pkg/errors"
"golang.org/x/crypto/acme/autocert"
"github.com/mattermost/mattermost-server/model"
@@ -116,7 +117,7 @@ func redirectHTTPToHTTPS(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, url.String(), http.StatusFound)
}
-func (a *App) StartServer() {
+func (a *App) StartServer() error {
l4g.Info(utils.T("api.server.start_server.starting.info"))
var handler http.Handler = &CorsWrapper{a.Config, a.Srv.Router}
@@ -126,8 +127,7 @@ func (a *App) StartServer() {
rateLimiter, err := NewRateLimiter(&a.Config().RateLimitSettings)
if err != nil {
- l4g.Critical(err.Error())
- return
+ return err
}
a.Srv.RateLimiter = rateLimiter
@@ -151,8 +151,8 @@ func (a *App) StartServer() {
listener, err := net.Listen("tcp", addr)
if err != nil {
- l4g.Critical(utils.T("api.server.start_server.starting.critical"), err)
- return
+ errors.Wrapf(err, utils.T("api.server.start_server.starting.critical"), err)
+ return err
}
a.Srv.ListenAddr = listener.Addr().(*net.TCPAddr)
@@ -214,6 +214,8 @@ func (a *App) StartServer() {
}
close(a.Srv.didFinishListen)
}()
+
+ return nil
}
type tcpKeepAliveListener struct {
diff --git a/app/server_test.go b/app/server_test.go
new file mode 100644
index 000000000..de358b976
--- /dev/null
+++ b/app/server_test.go
@@ -0,0 +1,50 @@
+// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/stretchr/testify/require"
+)
+
+func TestStartServerSuccess(t *testing.T) {
+ a, err := New()
+ require.NoError(t, err)
+
+ a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
+ serverErr := a.StartServer()
+ a.Shutdown()
+ require.NoError(t, serverErr)
+}
+
+func TestStartServerRateLimiterCriticalError(t *testing.T) {
+ a, err := New()
+ require.NoError(t, err)
+
+ // Attempt to use Rate Limiter with an invalid config
+ a.UpdateConfig(func(cfg *model.Config) {
+ *cfg.RateLimitSettings.Enable = true
+ *cfg.RateLimitSettings.MaxBurst = -100
+ })
+
+ serverErr := a.StartServer()
+ a.Shutdown()
+ require.Error(t, serverErr)
+}
+
+func TestStartServerPortUnavailable(t *testing.T) {
+ a, err := New()
+ require.NoError(t, err)
+
+ // Attempt to listen on a system-reserved port
+ a.UpdateConfig(func(cfg *model.Config) {
+ *cfg.ServiceSettings.ListenAddress = ":21"
+ })
+
+ serverErr := a.StartServer()
+ a.Shutdown()
+ require.Error(t, serverErr)
+}
diff --git a/app/session.go b/app/session.go
index 1c5daf29e..459618439 100644
--- a/app/session.go
+++ b/app/session.go
@@ -69,8 +69,9 @@ func (a *App) GetSession(token string) (*model.Session, *model.AppError) {
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "", http.StatusUnauthorized)
}
+ license := a.License()
if *a.Config().ServiceSettings.SessionIdleTimeoutInMinutes > 0 &&
- utils.IsLicensed() && *utils.License().Features.Compliance &&
+ license != nil && *license.Features.Compliance &&
session != nil && !session.IsOAuth && !session.IsMobileApp() &&
session.Props[model.SESSION_PROP_TYPE] != model.SESSION_TYPE_USER_ACCESS_TOKEN {
diff --git a/app/user.go b/app/user.go
index 156503fb0..70ed83c0b 100644
--- a/app/user.go
+++ b/app/user.go
@@ -374,7 +374,8 @@ func (a *App) GetUserByAuth(authData *string, authService string) (*model.User,
}
func (a *App) GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) {
- ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && utils.IsLicensed() && *utils.License().Features.LDAP
+ license := a.License()
+ ldapAvailable := *a.Config().LdapSettings.Enable && a.Ldap != nil && license != nil && *license.Features.LDAP
if result := <-a.Srv.Store.User().GetForLogin(
loginId,
diff --git a/app/web_conn.go b/app/web_conn.go
index e625e61b5..33c285af3 100644
--- a/app/web_conn.go
+++ b/app/web_conn.go
@@ -277,7 +277,7 @@ func (webCon *WebConn) IsAuthenticated() bool {
func (webCon *WebConn) SendHello() {
msg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_HELLO, "", "", webCon.UserId, nil)
- msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, webCon.App.ClientConfigHash(), utils.IsLicensed()))
+ msg.Add("server_version", fmt.Sprintf("%v.%v.%v.%v", model.CurrentVersion, model.BuildNumber, webCon.App.ClientConfigHash(), webCon.App.License() != nil))
webCon.Send <- msg
}
diff --git a/app/webhook.go b/app/webhook.go
index a9bb32f35..f3777ab48 100644
--- a/app/webhook.go
+++ b/app/webhook.go
@@ -632,7 +632,7 @@ func (a *App) HandleIncomingWebhook(hookId string, req *model.IncomingWebhookReq
}
}
- if utils.IsLicensed() && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
+ if a.License() != nil && *a.Config().TeamSettings.ExperimentalTownSquareIsReadOnly &&
channel.Name == model.DEFAULT_CHANNEL {
return model.NewAppError("HandleIncomingWebhook", "api.post.create_post.town_square_read_only", nil, "", http.StatusForbidden)
}
diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go
index 98bdcebb8..5d86ad9da 100644
--- a/cmd/platform/channel.go
+++ b/cmd/platform/channel.go
@@ -106,6 +106,8 @@ func init() {
channelCreateCmd.Flags().String("purpose", "", "Channel purpose")
channelCreateCmd.Flags().Bool("private", false, "Create a private channel.")
+ moveChannelsCmd.Flags().String("username", "", "Required. Username who is moving the channel.")
+
deleteChannelsCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the channels.")
modifyChannelCmd.Flags().Bool("private", false, "Convert the channel to a private channel")
@@ -319,26 +321,33 @@ func moveChannelsCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find destination team '" + args[0] + "'")
}
+ username, erru := cmd.Flags().GetString("username")
+ if erru != nil || username == "" {
+ return errors.New("Username is required")
+ }
+ user := getUserFromUserArg(a, username)
+
channels := getChannelsFromChannelArgs(a, args[1:])
for i, channel := range channels {
if channel == nil {
CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
- if err := moveChannel(a, team, channel); err != nil {
+ originTeamID := channel.TeamId
+ if err := moveChannel(a, team, channel, user); err != nil {
CommandPrintErrorln("Unable to move channel '" + channel.Name + "' error: " + err.Error())
} else {
- CommandPrettyPrintln("Moved channel '" + channel.Name + "'")
+ CommandPrettyPrintln("Moved channel '" + channel.Name + "' to " + team.Name + "(" + team.Id + ") from " + originTeamID + ".")
}
}
return nil
}
-func moveChannel(a *app.App, team *model.Team, channel *model.Channel) *model.AppError {
+func moveChannel(a *app.App, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
oldTeamId := channel.TeamId
- if err := a.MoveChannel(team, channel); err != nil {
+ if err := a.MoveChannel(team, channel, user); err != nil {
return err
}
diff --git a/cmd/platform/channel_test.go b/cmd/platform/channel_test.go
index 1e6915679..cf8603cf3 100644
--- a/cmd/platform/channel_test.go
+++ b/cmd/platform/channel_test.go
@@ -44,6 +44,33 @@ func TestRemoveChannel(t *testing.T) {
checkCommand(t, "channel", "remove", th.BasicTeam.Name+":"+channel.Name, th.BasicUser2.Email)
}
+func TestMoveChannel(t *testing.T) {
+ th := api.Setup().InitBasic()
+ defer th.TearDown()
+
+ client := th.BasicClient
+ team1 := th.BasicTeam
+ team2 := th.CreateTeam(client)
+ user1 := th.BasicUser
+ th.LinkUserToTeam(user1, team2)
+ channel := th.BasicChannel
+
+ th.LinkUserToTeam(user1, team1)
+ th.LinkUserToTeam(user1, team2)
+
+ adminEmail := user1.Email
+ adminUsername := user1.Username
+ origin := team1.Name + ":" + channel.Name
+ dest := team2.Name
+
+ checkCommand(t, "channel", "add", origin, adminEmail)
+
+ // should fail with nill because errors are logged instead of returned when a channel does not exist
+ require.Nil(t, runCommand(t, "channel", "move", dest, team1.Name+":doesnotexist", "--username", adminUsername))
+
+ checkCommand(t, "channel", "move", dest, origin, "--username", adminUsername)
+}
+
func TestListChannels(t *testing.T) {
th := api.Setup().InitBasic()
defer th.TearDown()
diff --git a/cmd/platform/server.go b/cmd/platform/server.go
index e55ad70d0..54a7d6ae3 100644
--- a/cmd/platform/server.go
+++ b/cmd/platform/server.go
@@ -53,7 +53,7 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
a, err := app.New(options...)
if err != nil {
- l4g.Error(err.Error())
+ l4g.Critical(err.Error())
return err
}
defer a.Shutdown()
@@ -89,20 +89,27 @@ func runServer(configFileLocation string, disableConfigWatch bool) error {
}
})
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ l4g.Critical(serverErr.Error())
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api3 := api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
web.Init(api3)
- if !utils.IsLicensed() && len(a.Config().SqlSettings.DataSourceReplicas) > 1 {
+ license := a.License()
+
+ if license == nil && len(a.Config().SqlSettings.DataSourceReplicas) > 1 {
l4g.Warn(utils.T("store.sql.read_replicas_not_licensed.critical"))
a.UpdateConfig(func(cfg *model.Config) {
cfg.SqlSettings.DataSourceReplicas = cfg.SqlSettings.DataSourceReplicas[:1]
})
}
- if !utils.IsLicensed() {
+ if license == nil {
a.UpdateConfig(func(cfg *model.Config) {
cfg.TeamSettings.MaxNotificationsPerChannel = &MaxNotificationsPerChannelDefault
})
diff --git a/cmd/platform/test.go b/cmd/platform/test.go
index 036df07de..9ab3fbb36 100644
--- a/cmd/platform/test.go
+++ b/cmd/platform/test.go
@@ -53,7 +53,11 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error {
defer a.Shutdown()
utils.InitTranslations(a.Config().LocalizationSettings)
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
@@ -71,7 +75,11 @@ func serverForWebClientTestsCmdF(cmd *cobra.Command, args []string) error {
defer a.Shutdown()
utils.InitTranslations(a.Config().LocalizationSettings)
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ return serverErr
+ }
+
api4.Init(a, a.Srv.Router, false)
api.Init(a, a.Srv.Router)
wsapi.Init(a, a.Srv.WebSocketRouter)
diff --git a/config/default.json b/config/default.json
index 0c07f9793..934635cb9 100644
--- a/config/default.json
+++ b/config/default.json
@@ -57,7 +57,7 @@
"CloseUnusedDirectMessages": false,
"EnableTutorial": true,
"ExperimentalEnableDefaultChannelLeaveJoinMessages": true,
- "ExperimentalGroupUnreadChannels": false,
+ "ExperimentalGroupUnreadChannels": "disabled",
"ImageProxyType": "",
"ImageProxyOptions": "",
"ImageProxyURL": ""
diff --git a/i18n/en.json b/i18n/en.json
index 58bfc3d86..4708f321e 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -132,10 +132,6 @@
"translation": "Unable to upload file. File is too large."
},
{
- "id": "api.api.init.parsing_templates.debug",
- "translation": "Parsing server templates at %v"
- },
- {
"id": "api.api.init.parsing_templates.error",
"translation": "Failed to parse server templates %v"
},
@@ -3143,6 +3139,14 @@
"translation": "Cannot move a channel unless all its members are already members of the destination team."
},
{
+ "id": "api.team.move_channel.success",
+ "translation": "This channel has been moved to this team from %v."
+ },
+ {
+ "id": "api.team.move_channel.post.error",
+ "translation": "Failed to post channel move message."
+ },
+ {
"id": "app.channel.post_update_channel_purpose_message.post.error",
"translation": "Failed to post channel purpose message"
},
@@ -4767,6 +4771,10 @@
"translation": "Invalid thumbnail width for file settings. Must be a positive number."
},
{
+ "id": "model.config.is_valid.group_unread_channels.app_error",
+ "translation": "Invalid group unread channels for service settings. Must be 'disabled', 'default_on', or 'default_off'."
+ },
+ {
"id": "model.config.is_valid.image_proxy_type.app_error",
"translation": "Invalid image proxy type for service settings."
},
@@ -4863,6 +4871,14 @@
"translation": "Message export job BatchSize must be a positive integer"
},
{
+ "id": "model.config.is_valid.message_export.export_type.app_error",
+ "translation": "Message export job ExportFormat must be one of either 'actiance' or 'globalrelay'"
+ },
+ {
+ "id": "model.config.is_valid.message_export.global_relay_email_address.app_error",
+ "translation": "Message export job GlobalRelayEmailAddress must be set to a valid email address"
+ },
+ {
"id": "model.config.is_valid.message_export.daily_runtime.app_error",
"translation": "Message export job DailyRuntime must be a 24-hour time stamp in the form HH:MM."
},
@@ -7107,6 +7123,10 @@
"translation": "Failed to authenticate on SMTP server"
},
{
+ "id": "utils.mail.sendMail.attachments.write_error",
+ "translation": "Failed to write attachment to email"
+ },
+ {
"id": "utils.mail.new_client.helo.error",
"translation": "Failed to to set the HELO to SMTP server %v"
},
@@ -7183,10 +7203,6 @@
"translation": "Failed to create directory watcher %v"
},
{
- "id": "web.dir_fail.error",
- "translation": "Failed in directory watcher %v"
- },
- {
"id": "web.do_load_channel.error",
"translation": "Error in getting users profile for id=%v forcing logout"
},
@@ -7291,18 +7307,10 @@
"translation": "Parsing templates at %v"
},
{
- "id": "web.parsing_templates.error",
- "translation": "Failed to parse templates %v"
- },
- {
"id": "web.post_permalink.app_error",
"translation": "Invalid Post ID"
},
{
- "id": "web.reparse_templates.info",
- "translation": "Re-parsing templates because of modified file %v"
- },
- {
"id": "web.reset_password.expired_link.app_error",
"translation": "The password reset link has expired"
},
diff --git a/model/channel_member_history.go b/model/channel_member_history.go
index bc71b580a..47c59d54e 100644
--- a/model/channel_member_history.go
+++ b/model/channel_member_history.go
@@ -6,7 +6,10 @@ package model
type ChannelMemberHistory struct {
ChannelId string
UserId string
- UserEmail string `db:"Email"`
JoinTime int64
LeaveTime *int64
+
+ // these two fields are never set in the database - when we SELECT, we join on Users to get them
+ UserEmail string `db:"Email"`
+ Username string
}
diff --git a/model/config.go b/model/config.go
index b7888ab13..9010eaeae 100644
--- a/model/config.go
+++ b/model/config.go
@@ -69,6 +69,10 @@ const (
ALLOW_EDIT_POST_NEVER = "never"
ALLOW_EDIT_POST_TIME_LIMIT = "time_limit"
+ GROUP_UNREAD_CHANNELS_DISABLED = "disabled"
+ GROUP_UNREAD_CHANNELS_DEFAULT_ON = "default_on"
+ GROUP_UNREAD_CHANNELS_DEFAULT_OFF = "default_off"
+
EMAIL_BATCHING_BUFFER_SIZE = 256
EMAIL_BATCHING_INTERVAL = 30
@@ -154,6 +158,9 @@ const (
PLUGIN_SETTINGS_DEFAULT_DIRECTORY = "./plugins"
PLUGIN_SETTINGS_DEFAULT_CLIENT_DIRECTORY = "./client/plugins"
+
+ COMPLIANCE_EXPORT_TYPE_ACTIANCE = "actiance"
+ COMPLIANCE_EXPORT_TYPE_GLOBALRELAY = "globalrelay"
)
type ServiceSettings struct {
@@ -214,7 +221,7 @@ type ServiceSettings struct {
EnablePreviewFeatures *bool
EnableTutorial *bool
ExperimentalEnableDefaultChannelLeaveJoinMessages *bool
- ExperimentalGroupUnreadChannels *bool
+ ExperimentalGroupUnreadChannels *string
ImageProxyType *string
ImageProxyURL *string
ImageProxyOptions *string
@@ -424,7 +431,11 @@ func (s *ServiceSettings) SetDefaults() {
}
if s.ExperimentalGroupUnreadChannels == nil {
- s.ExperimentalGroupUnreadChannels = NewBool(false)
+ s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED)
+ } else if *s.ExperimentalGroupUnreadChannels == "0" {
+ s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DISABLED)
+ } else if *s.ExperimentalGroupUnreadChannels == "1" {
+ s.ExperimentalGroupUnreadChannels = NewString(GROUP_UNREAD_CHANNELS_DEFAULT_ON)
}
if s.ImageProxyType == nil {
@@ -1615,9 +1626,13 @@ func (s *PluginSettings) SetDefaults() {
type MessageExportSettings struct {
EnableExport *bool
+ ExportFormat *string
DailyRunTime *string
ExportFromTimestamp *int64
BatchSize *int
+
+ // formatter-specific settings - these are only expected to be non-nil if ExportFormat is set to the associated format
+ GlobalRelayEmailAddress *string
}
func (s *MessageExportSettings) SetDefaults() {
@@ -1625,6 +1640,10 @@ func (s *MessageExportSettings) SetDefaults() {
s.EnableExport = NewBool(false)
}
+ if s.ExportFormat == nil {
+ s.ExportFormat = NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE)
+ }
+
if s.DailyRunTime == nil {
s.DailyRunTime = NewString("01:00")
}
@@ -2070,6 +2089,12 @@ func (ss *ServiceSettings) isValid() *AppError {
return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest)
}
+ if *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED &&
+ *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_ON &&
+ *ss.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_OFF {
+ return NewAppError("Config.IsValid", "model.config.is_valid.group_unread_channels.app_error", nil, "", http.StatusBadRequest)
+ }
+
switch *ss.ImageProxyType {
case "", "willnorris/imageproxy":
case "atmos/camo":
@@ -2156,6 +2181,16 @@ func (mes *MessageExportSettings) isValid(fs FileSettings) *AppError {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.daily_runtime.app_error", nil, err.Error(), http.StatusBadRequest)
} else if mes.BatchSize == nil || *mes.BatchSize < 0 {
return NewAppError("Config.IsValid", "model.config.is_valid.message_export.batch_size.app_error", nil, "", http.StatusBadRequest)
+ } else if mes.ExportFormat == nil || (*mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_ACTIANCE && *mes.ExportFormat != COMPLIANCE_EXPORT_TYPE_GLOBALRELAY) {
+ return NewAppError("Config.IsValid", "model.config.is_valid.message_export.export_type.app_error", nil, "", http.StatusBadRequest)
+ }
+
+ if *mes.ExportFormat == COMPLIANCE_EXPORT_TYPE_GLOBALRELAY {
+ // validating email addresses is hard - just make sure it contains an '@' sign
+ // see https://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address
+ if mes.GlobalRelayEmailAddress == nil || !strings.Contains(*mes.GlobalRelayEmailAddress, "@") {
+ return NewAppError("Config.IsValid", "model.config.is_valid.message_export.global_relay_email_address.app_error", nil, "", http.StatusBadRequest)
+ }
}
}
return nil
diff --git a/model/config_test.go b/model/config_test.go
index ceede6be4..919f73fd7 100644
--- a/model/config_test.go
+++ b/model/config_test.go
@@ -36,6 +36,38 @@ func TestConfigDefaultFileSettingsS3SSE(t *testing.T) {
}
}
+func TestConfigDefaultServiceSettingsExperimentalGroupUnreadChannels(t *testing.T) {
+ c1 := Config{}
+ c1.SetDefaults()
+
+ if *c1.ServiceSettings.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED {
+ t.Fatal("ServiceSettings.ExperimentalGroupUnreadChannels should default to 'disabled'")
+ }
+
+ // This setting was briefly a boolean, so ensure that those values still work as expected
+ c1 = Config{
+ ServiceSettings: ServiceSettings{
+ ExperimentalGroupUnreadChannels: NewString("1"),
+ },
+ }
+ c1.SetDefaults()
+
+ if *c1.ServiceSettings.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DEFAULT_ON {
+ t.Fatal("ServiceSettings.ExperimentalGroupUnreadChannels should set true to 'default on'")
+ }
+
+ c1 = Config{
+ ServiceSettings: ServiceSettings{
+ ExperimentalGroupUnreadChannels: NewString("0"),
+ },
+ }
+ c1.SetDefaults()
+
+ if *c1.ServiceSettings.ExperimentalGroupUnreadChannels != GROUP_UNREAD_CHANNELS_DISABLED {
+ t.Fatal("ServiceSettings.ExperimentalGroupUnreadChannels should set false to 'disabled'")
+ }
+}
+
func TestMessageExportSettingsIsValidEnableExportNotSet(t *testing.T) {
fs := &FileSettings{}
mes := &MessageExportSettings{}
@@ -104,12 +136,44 @@ func TestMessageExportSettingsIsValidBatchSizeInvalid(t *testing.T) {
require.Error(t, mes.isValid(*fs))
}
-func TestMessageExportSettingsIsValid(t *testing.T) {
+func TestMessageExportSettingsIsValidExportFormatInvalid(t *testing.T) {
+ fs := &FileSettings{
+ DriverName: NewString("foo"), // bypass file location check
+ }
+ mes := &MessageExportSettings{
+ EnableExport: NewBool(true),
+ ExportFromTimestamp: NewInt64(0),
+ DailyRunTime: NewString("15:04"),
+ BatchSize: NewInt(100),
+ }
+
+ // should fail fast because export format isn't set
+ require.Error(t, mes.isValid(*fs))
+}
+
+func TestMessageExportSettingsIsValidGlobalRelayEmailAddressInvalid(t *testing.T) {
+ fs := &FileSettings{
+ DriverName: NewString("foo"), // bypass file location check
+ }
+ mes := &MessageExportSettings{
+ EnableExport: NewBool(true),
+ ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY),
+ ExportFromTimestamp: NewInt64(0),
+ DailyRunTime: NewString("15:04"),
+ BatchSize: NewInt(100),
+ }
+
+ // should fail fast because global relay email address isn't set
+ require.Error(t, mes.isValid(*fs))
+}
+
+func TestMessageExportSettingsIsValidActiance(t *testing.T) {
fs := &FileSettings{
DriverName: NewString("foo"), // bypass file location check
}
mes := &MessageExportSettings{
EnableExport: NewBool(true),
+ ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_ACTIANCE),
ExportFromTimestamp: NewInt64(0),
DailyRunTime: NewString("15:04"),
BatchSize: NewInt(100),
@@ -119,6 +183,23 @@ func TestMessageExportSettingsIsValid(t *testing.T) {
require.Nil(t, mes.isValid(*fs))
}
+func TestMessageExportSettingsIsValidGlobalRelay(t *testing.T) {
+ fs := &FileSettings{
+ DriverName: NewString("foo"), // bypass file location check
+ }
+ mes := &MessageExportSettings{
+ EnableExport: NewBool(true),
+ ExportFormat: NewString(COMPLIANCE_EXPORT_TYPE_GLOBALRELAY),
+ ExportFromTimestamp: NewInt64(0),
+ DailyRunTime: NewString("15:04"),
+ BatchSize: NewInt(100),
+ GlobalRelayEmailAddress: NewString("test@mattermost.com"),
+ }
+
+ // should pass because everything is valid
+ require.Nil(t, mes.isValid(*fs))
+}
+
func TestMessageExportSetDefaults(t *testing.T) {
mes := &MessageExportSettings{}
mes.SetDefaults()
@@ -127,6 +208,7 @@ func TestMessageExportSetDefaults(t *testing.T) {
require.Equal(t, "01:00", *mes.DailyRunTime)
require.Equal(t, int64(0), *mes.ExportFromTimestamp)
require.Equal(t, 10000, *mes.BatchSize)
+ require.Equal(t, COMPLIANCE_EXPORT_TYPE_ACTIANCE, *mes.ExportFormat)
}
func TestMessageExportSetDefaultsExportEnabledExportFromTimestampNil(t *testing.T) {
diff --git a/model/message_export.go b/model/message_export.go
index b59b114d4..22641deee 100644
--- a/model/message_export.go
+++ b/model/message_export.go
@@ -9,6 +9,7 @@ type MessageExport struct {
UserId *string
UserEmail *string
+ Username *string
PostId *string
PostCreateAt *int64
diff --git a/model/post.go b/model/post.go
index 391b948f4..7cf0f1b35 100644
--- a/model/post.go
+++ b/model/post.go
@@ -28,6 +28,7 @@ const (
POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead
POST_ADD_TO_CHANNEL = "system_add_to_channel"
POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel"
+ POST_MOVE_CHANNEL = "system_move_channel"
POST_ADD_TO_TEAM = "system_add_to_team"
POST_REMOVE_FROM_TEAM = "system_remove_from_team"
POST_HEADER_CHANGE = "system_header_change"
@@ -196,6 +197,7 @@ func (o *Post) IsValid() *AppError {
POST_LEAVE_TEAM,
POST_ADD_TO_CHANNEL,
POST_REMOVE_FROM_CHANNEL,
+ POST_MOVE_CHANNEL,
POST_ADD_TO_TEAM,
POST_REMOVE_FROM_TEAM,
POST_SLACK_ATTACHMENT,
diff --git a/store/sqlstore/channel_member_history_store.go b/store/sqlstore/channel_member_history_store.go
index 182f37ce9..0b86aac28 100644
--- a/store/sqlstore/channel_member_history_store.go
+++ b/store/sqlstore/channel_member_history_store.go
@@ -110,7 +110,8 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime
query := `
SELECT
cmh.*,
- u.Email
+ u.Email,
+ u.Username
FROM ChannelMemberHistory cmh
INNER JOIN Users u ON cmh.UserId = u.Id
WHERE cmh.ChannelId = :ChannelId
@@ -130,9 +131,10 @@ func (s SqlChannelMemberHistoryStore) getFromChannelMemberHistoryTable(startTime
func (s SqlChannelMemberHistoryStore) getFromChannelMembersTable(startTime int64, endTime int64, channelId string) ([]*model.ChannelMemberHistory, error) {
query := `
SELECT DISTINCT
- ch.ChannelId,
- ch.UserId,
- u.email
+ ch.ChannelId,
+ ch.UserId,
+ u.Email,
+ u.Username
FROM ChannelMembers AS ch
INNER JOIN Users AS u ON ch.UserId = u.id
WHERE ch.ChannelId = :ChannelId`
@@ -158,7 +160,7 @@ func (s SqlChannelMemberHistoryStore) PermanentDeleteBatch(endTime int64, limit
query =
`DELETE FROM ChannelMemberHistory
WHERE ctid IN (
- SELECT ctid FROM ChannelMemberHistory
+ SELECT ctid FROM ChannelMemberHistory
WHERE LeaveTime IS NOT NULL
AND LeaveTime <= :EndTime
LIMIT :Limit
diff --git a/store/sqlstore/compliance_store.go b/store/sqlstore/compliance_store.go
index a25b01548..03d92d5e1 100644
--- a/store/sqlstore/compliance_store.go
+++ b/store/sqlstore/compliance_store.go
@@ -225,7 +225,8 @@ func (s SqlComplianceStore) MessageExport(after int64, limit int) store.StoreCha
Channels.Id AS ChannelId,
Channels.DisplayName AS ChannelDisplayName,
Users.Id AS UserId,
- Users.Email AS UserEmail
+ Users.Email AS UserEmail,
+ Users.Username
FROM
Posts
LEFT OUTER JOIN Channels ON Posts.ChannelId = Channels.Id
diff --git a/store/storetest/channel_member_history_store.go b/store/storetest/channel_member_history_store.go
index 6fe73478c..fa2e7a8fa 100644
--- a/store/storetest/channel_member_history_store.go
+++ b/store/storetest/channel_member_history_store.go
@@ -35,6 +35,7 @@ func testLogJoinEvent(t *testing.T, ss store.Store) {
user := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -57,6 +58,7 @@ func testLogLeaveEvent(t *testing.T, ss store.Store) {
user := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -82,6 +84,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
user := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -108,6 +111,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
@@ -117,6 +121,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Nil(t, channelMembers[0].LeaveTime)
@@ -129,6 +134,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
@@ -138,6 +144,7 @@ func testGetUsersInChannelAtChannelMemberHistory(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime, *channelMembers[0].LeaveTime)
@@ -160,6 +167,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
user := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user = *store.Must(ss.User().Save(&user)).(*model.User)
@@ -192,6 +200,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-500, channelMembers[0].JoinTime)
assert.Equal(t, joinTime-100, *channelMembers[0].LeaveTime)
@@ -201,6 +210,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
@@ -210,6 +220,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, joinTime+500, *channelMembers[0].LeaveTime)
@@ -219,6 +230,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime-100, *channelMembers[0].LeaveTime)
@@ -228,6 +240,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, joinTime-100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+100, *channelMembers[0].LeaveTime)
@@ -237,6 +250,7 @@ func testGetUsersInChannelAtChannelMembers(t *testing.T, ss store.Store) {
assert.Equal(t, channel.Id, channelMembers[0].ChannelId)
assert.Equal(t, user.Id, channelMembers[0].UserId)
assert.Equal(t, user.Email, channelMembers[0].UserEmail)
+ assert.Equal(t, user.Username, channelMembers[0].Username)
assert.Equal(t, leaveTime+100, channelMembers[0].JoinTime)
assert.Equal(t, leaveTime+200, *channelMembers[0].LeaveTime)
}
@@ -255,12 +269,14 @@ func testPermanentDeleteBatch(t *testing.T, ss store.Store) {
user := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user = *store.Must(ss.User().Save(&user)).(*model.User)
user2 := model.User{
Email: model.NewId() + "@mattermost.com",
Nickname: model.NewId(),
+ Username: model.NewId(),
}
user2 = *store.Must(ss.User().Save(&user2)).(*model.User)
diff --git a/store/storetest/compliance_store.go b/store/storetest/compliance_store.go
index c5bd60f05..eb29bedc7 100644
--- a/store/storetest/compliance_store.go
+++ b/store/storetest/compliance_store.go
@@ -341,7 +341,8 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
// and two users that are a part of that team
user1 := &model.User{
- Email: model.NewId(),
+ Email: model.NewId(),
+ Username: model.NewId(),
}
user1 = store.Must(ss.User().Save(user1)).(*model.User)
store.Must(ss.Team().SaveMember(&model.TeamMember{
@@ -350,7 +351,8 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
}, -1))
user2 := &model.User{
- Email: model.NewId(),
+ Email: model.NewId(),
+ Username: model.NewId(),
}
user2 = store.Must(ss.User().Save(user2)).(*model.User)
store.Must(ss.Team().SaveMember(&model.TeamMember{
@@ -415,6 +417,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, channel.DisplayName, *messageExportMap[post1.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post1.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post1.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post1.Id].Username)
// post2 was made by user1 in channel1 and team1
assert.Equal(t, post2.Id, *messageExportMap[post2.Id].PostId)
@@ -424,6 +427,7 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, channel.DisplayName, *messageExportMap[post2.Id].ChannelDisplayName)
assert.Equal(t, user1.Id, *messageExportMap[post2.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post2.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post2.Id].Username)
// post3 is a DM between user1 and user2
assert.Equal(t, post3.Id, *messageExportMap[post3.Id].PostId)
@@ -432,4 +436,5 @@ func testComplianceMessageExport(t *testing.T, ss store.Store) {
assert.Equal(t, directMessageChannel.Id, *messageExportMap[post3.Id].ChannelId)
assert.Equal(t, user1.Id, *messageExportMap[post3.Id].UserId)
assert.Equal(t, user1.Email, *messageExportMap[post3.Id].UserEmail)
+ assert.Equal(t, user1.Username, *messageExportMap[post3.Id].Username)
}
diff --git a/templates/globalrelay_compliance_export.html b/templates/globalrelay_compliance_export.html
new file mode 100644
index 000000000..91028d11c
--- /dev/null
+++ b/templates/globalrelay_compliance_export.html
@@ -0,0 +1,91 @@
+{{define "globalrelay_compliance_export"}}
+<style type="text/css">
+ body {
+ font-family:Arial, sans-serif;
+ font-size:14px;
+ font-weight:normal;
+ }
+
+ .summary-list ul {
+ padding: 0px;
+ list-style:none;
+ }
+ .summary-list li {
+ display: inline;
+ padding: 0 1em 0 0;
+ }
+ .summary-list .bold {
+ font-weight: bold;
+ }
+
+ .participants {
+ border-collapse:collapse;
+ border-spacing:0;
+ }
+ .participants td {
+ padding:10px 5px;
+ border:1px solid black;
+ overflow:hidden;
+ text-align: center;
+ word-break:normal;
+ }
+ .participants th {
+ padding:10px 5px;
+ border:1px solid black;
+ overflow:hidden;
+ word-break:normal;
+ }
+ .participants th,td {
+ vertical-align:top
+ }
+
+ .message-list ul {
+ list-style:none;
+ padding: 0;
+ }
+ .message-list li {
+ padding: 0 0 1em 0;
+ }
+ .message .sent_time {
+ font-weight:bold;
+ }
+ .message .username {
+ font-weight:bold;
+ }
+ .message .email {
+ font-weight: bold;
+ }
+</style>
+
+<h1>Mattermost Compliance Export</h1>
+
+<h2>Conversation Summary</h2>
+<div class="summary-list">
+ <ul>
+ <li><span class="bold">Channel:&nbsp;</span>{{.Props.ChannelName}}</li>
+ <li><span class="bold">Started:&nbsp;</span>{{.Props.Started}}</li>
+ <li><span class="bold">Ended:&nbsp;</span>{{.Props.Ended}}</li>
+ <li><span class="bold">Duration:&nbsp;</span>{{.Props.Duration}}&nbsp;Minutes</li>
+ </ul>
+</div>
+<table class="participants">
+ <tr>
+ <th class="username">Username<br></th>
+ <th class="email">Email</th>
+ <th class="joined">Joined</th>
+ <th class="left">Left</th>
+ <th class="duration">Duration</th>
+ <th class="messages">Messages</th>
+ </tr>
+ {{.Props.ParticipantRows}}
+</table>
+
+<h2>Messages</h2>
+<div class="message-list">
+ <ul>
+ {{.Props.Messages}}
+ </ul>
+</div>
+
+<p>Exported on {{.Props.ExportDate}}</p>
+{{end}} \ No newline at end of file
diff --git a/templates/globalrelay_compliance_export_message.html b/templates/globalrelay_compliance_export_message.html
new file mode 100644
index 000000000..3a47b29b7
--- /dev/null
+++ b/templates/globalrelay_compliance_export_message.html
@@ -0,0 +1,8 @@
+{{define "globalrelay_compliance_export_message"}}
+<li class="message">
+ <span class="sent_time">{{.Props.SentTime}}</span>
+ <span class="username">@{{.Props.Username}}</span>
+ <span class="email">({{.Props.Email}}):</span>
+ <span class="message">{{.Props.Message}}</span>
+</li>
+{{end}}
diff --git a/templates/globalrelay_compliance_export_participant_row.html b/templates/globalrelay_compliance_export_participant_row.html
new file mode 100644
index 000000000..7a61e23eb
--- /dev/null
+++ b/templates/globalrelay_compliance_export_participant_row.html
@@ -0,0 +1,10 @@
+{{define "globalrelay_compliance_export_participant_row"}}
+<tr>
+ <td class="username">@{{.Props.Username}}</td>
+ <td class="email">{{.Props.Email}}</td>
+ <td class="joined">{{.Props.Joined}}</td>
+ <td class="left">{{.Props.Left}}</td>
+ <td class="duration">{{.Props.DurationMinutes}} Minutes</td>
+ <td class="messages">{{.Props.NumMessages}}</td>
+</tr>
+{{end}} \ No newline at end of file
diff --git a/utils/config.go b/utils/config.go
index b93d673a3..9e962eef4 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -397,7 +397,7 @@ func GenerateClientConfig(c *model.Config, diagnosticId string) map[string]strin
props["EnablePreviewFeatures"] = strconv.FormatBool(*c.ServiceSettings.EnablePreviewFeatures)
props["EnableTutorial"] = strconv.FormatBool(*c.ServiceSettings.EnableTutorial)
props["ExperimentalEnableDefaultChannelLeaveJoinMessages"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalEnableDefaultChannelLeaveJoinMessages)
- props["ExperimentalGroupUnreadChannels"] = strconv.FormatBool(*c.ServiceSettings.ExperimentalGroupUnreadChannels)
+ props["ExperimentalGroupUnreadChannels"] = *c.ServiceSettings.ExperimentalGroupUnreadChannels
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
diff --git a/utils/html.go b/utils/html.go
index 02db8c97a..6bbe55c6d 100644
--- a/utils/html.go
+++ b/utils/html.go
@@ -23,7 +23,7 @@ type HTMLTemplateWatcher struct {
func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) {
templatesDir, _ := FindDir(directory)
- l4g.Debug(T("api.api.init.parsing_templates.debug"), templatesDir)
+ l4g.Debug("Parsing server templates at %v", templatesDir)
ret := &HTMLTemplateWatcher{
stop: make(chan struct{}),
@@ -55,15 +55,15 @@ func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) {
return
case event := <-watcher.Events:
if event.Op&fsnotify.Write == fsnotify.Write {
- l4g.Info(T("web.reparse_templates.info"), event.Name)
+ l4g.Info("Re-parsing templates because of modified file %v", event.Name)
if htmlTemplates, err := template.ParseGlob(templatesDir + "*.html"); err != nil {
- l4g.Error(T("web.parsing_templates.error"), err)
+ l4g.Error("Failed to parse templates %v", err)
} else {
ret.templates.Store(htmlTemplates)
}
}
case err := <-watcher.Errors:
- l4g.Error(T("web.dir_fail.error"), err)
+ l4g.Error("Failed in directory watcher %s", err)
}
}
}()
diff --git a/utils/inbucket.go b/utils/inbucket.go
index 46011989b..5c40d5757 100644
--- a/utils/inbucket.go
+++ b/utils/inbucket.go
@@ -4,6 +4,7 @@
package utils
import (
+ "bytes"
"encoding/json"
"fmt"
"io"
@@ -37,6 +38,12 @@ type JSONMessageInbucket struct {
Text string
HTML string `json:"Html"`
}
+ Attachments []struct {
+ Filename string
+ ContentType string `json:"content-type"`
+ DownloadLink string `json:"download-link"`
+ Bytes []byte `json:"-"`
+ }
}
func ParseEmail(email string) string {
@@ -89,21 +96,54 @@ func GetMessageFromMailbox(email, id string) (results JSONMessageInbucket, err e
var record JSONMessageInbucket
url := fmt.Sprintf("%s%s%s/%s", getInbucketHost(), INBUCKET_API, parsedEmail, id)
- req, err := http.NewRequest("GET", url, nil)
+ emailResponse, err := get(url)
if err != nil {
return record, err
}
+ defer emailResponse.Body.Close()
+
+ err = json.NewDecoder(emailResponse.Body).Decode(&record)
+
+ // download attachments
+ if record.Attachments != nil && len(record.Attachments) > 0 {
+ for i := range record.Attachments {
+ if bytes, err := downloadAttachment(record.Attachments[i].DownloadLink); err != nil {
+ return record, err
+ } else {
+ record.Attachments[i].Bytes = make([]byte, len(bytes))
+ copy(record.Attachments[i].Bytes, bytes)
+ }
+ }
+ }
- client := &http.Client{}
+ return record, err
+}
+
+func downloadAttachment(url string) ([]byte, error) {
+ attachmentResponse, err := get(url)
+ if err != nil {
+ return nil, err
+ }
+ defer attachmentResponse.Body.Close()
+
+ buf := new(bytes.Buffer)
+ io.Copy(buf, attachmentResponse.Body)
+ return buf.Bytes(), nil
+}
+
+func get(url string) (*http.Response, error) {
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+ client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
- return record, err
+ return nil, err
}
- defer resp.Body.Close()
- err = json.NewDecoder(resp.Body).Decode(&record)
- return record, err
+ return resp, nil
}
func DeleteMailBox(email string) (err error) {
diff --git a/utils/mail.go b/utils/mail.go
index b0289da5e..4c8a505af 100644
--- a/utils/mail.go
+++ b/utils/mail.go
@@ -15,6 +15,8 @@ import (
"net/http"
+ "io"
+
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/html2text"
"github.com/mattermost/mattermost-server/model"
@@ -104,36 +106,72 @@ func TestConnection(config *model.Config) {
}
func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *model.AppError {
+ fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail}
+ return sendMail(to, to, fromMail, subject, htmlBody, nil, nil, config)
+}
+
+// allows for sending an email with attachments and differing MIME/SMTP recipients
+func SendMailUsingConfigAdvanced(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError {
+ return sendMail(mimeTo, smtpTo, from, subject, htmlBody, attachments, mimeHeaders, config)
+}
+
+func sendMail(mimeTo, smtpTo string, from mail.Address, subject, htmlBody string, attachments []*model.FileInfo, mimeHeaders map[string]string, config *model.Config) *model.AppError {
if !config.EmailSettings.SendEmailNotifications || len(config.EmailSettings.SMTPServer) == 0 {
return nil
}
- l4g.Debug(T("utils.mail.send_mail.sending.debug"), to, subject)
+ l4g.Debug(T("utils.mail.send_mail.sending.debug"), mimeTo, subject)
htmlMessage := "\r\n<html><body>" + htmlBody + "</body></html>"
- fromMail := mail.Address{Name: config.EmailSettings.FeedbackName, Address: config.EmailSettings.FeedbackEmail}
-
txtBody, err := html2text.FromString(htmlBody)
if err != nil {
l4g.Warn(err)
txtBody = ""
}
- m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
- m.SetHeaders(map[string][]string{
- "From": {fromMail.String()},
- "To": {to},
+ headers := map[string][]string{
+ "From": {from.String()},
+ "To": {mimeTo},
"Subject": {encodeRFC2047Word(subject)},
"Content-Transfer-Encoding": {"8bit"},
"Auto-Submitted": {"auto-generated"},
"Precedence": {"bulk"},
- })
- m.SetDateHeader("Date", time.Now())
+ }
+ if mimeHeaders != nil {
+ for k, v := range mimeHeaders {
+ headers[k] = []string{encodeRFC2047Word(v)}
+ }
+ }
+ m := gomail.NewMessage(gomail.SetCharset("UTF-8"))
+ m.SetHeaders(headers)
+ m.SetDateHeader("Date", time.Now())
m.SetBody("text/plain", txtBody)
m.AddAlternative("text/html", htmlMessage)
+ if attachments != nil {
+ fileBackend, err := NewFileBackend(&config.FileSettings)
+ if err != nil {
+ return err
+ }
+
+ for _, fileInfo := range attachments {
+ m.Attach(fileInfo.Name, gomail.SetCopyFunc(func(writer io.Writer) error {
+ bytes, err := fileBackend.ReadFile(fileInfo.Path)
+ if err != nil {
+ return err
+ }
+ if _, err := writer.Write(bytes); err != nil {
+ return model.NewAppError("SendMail", "utils.mail.sendMail.attachments.write_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ return nil
+ }))
+
+ }
+
+ }
+
conn, err1 := connectToSMTPServer(config)
if err1 != nil {
return err1
@@ -147,11 +185,11 @@ func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config) *mo
defer c.Quit()
defer c.Close()
- if err := c.Mail(fromMail.Address); err != nil {
+ if err := c.Mail(from.Address); err != nil {
return model.NewAppError("SendMail", "utils.mail.send_mail.from_address.app_error", nil, err.Error(), http.StatusInternalServerError)
}
- if err := c.Rcpt(to); err != nil {
+ if err := c.Rcpt(smtpTo); err != nil {
return model.NewAppError("SendMail", "utils.mail.send_mail.to_address.app_error", nil, err.Error(), http.StatusInternalServerError)
}
diff --git a/utils/mail_test.go b/utils/mail_test.go
index 574f71f46..207fe32a5 100644
--- a/utils/mail_test.go
+++ b/utils/mail_test.go
@@ -7,6 +7,10 @@ import (
"strings"
"testing"
+ "net/mail"
+
+ "github.com/mattermost/mattermost-server/model"
+ "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -39,9 +43,9 @@ func TestSendMailUsingConfig(t *testing.T) {
require.Nil(t, err)
T = GetUserTranslations("en")
- var emailTo string = "test@example.com"
- var emailSubject string = "Testing this email"
- var emailBody string = "This is a test from autobot"
+ var emailTo = "test@example.com"
+ var emailSubject = "Testing this email"
+ var emailBody = "This is a test from autobot"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
@@ -50,7 +54,7 @@ func TestSendMailUsingConfig(t *testing.T) {
t.Log(err)
t.Fatal("Should connect to the STMP Server")
} else {
- //Check if the email was send to the rigth email address
+ //Check if the email was send to the right email address
var resultsMailbox JSONMessageHeaderInbucket
err := RetryInbucket(5, func() error {
var err error
@@ -75,3 +79,78 @@ func TestSendMailUsingConfig(t *testing.T) {
}
}
}
+
+func TestSendMailUsingConfigAdvanced(t *testing.T) {
+ cfg, _, err := LoadConfig("config.json")
+ require.Nil(t, err)
+ T = GetUserTranslations("en")
+
+ var mimeTo = "test@example.com"
+ var smtpTo = "test2@example.com"
+ var from = mail.Address{Name: "Nobody", Address: "nobody@mattermost.com"}
+ var emailSubject = "Testing this email"
+ var emailBody = "This is a test from autobot"
+
+ //Delete all the messages before check the sample email
+ DeleteMailBox(smtpTo)
+
+ // create a file that will be attached to the email
+ fileBackend, err := NewFileBackend(&cfg.FileSettings)
+ assert.Nil(t, err)
+ fileContents := []byte("hello world")
+ fileName := "file.txt"
+ assert.Nil(t, fileBackend.WriteFile(fileContents, fileName))
+ defer fileBackend.RemoveFile(fileName)
+
+ attachments := make([]*model.FileInfo, 1)
+ attachments[0] = &model.FileInfo{
+ Name: fileName,
+ Path: fileName,
+ }
+
+ headers := make(map[string]string)
+ headers["TestHeader"] = "TestValue"
+
+ if err := SendMailUsingConfigAdvanced(mimeTo, smtpTo, from, emailSubject, emailBody, attachments, headers, cfg); err != nil {
+ t.Log(err)
+ t.Fatal("Should connect to the STMP Server")
+ } else {
+ //Check if the email was send to the right email address
+ var resultsMailbox JSONMessageHeaderInbucket
+ err := RetryInbucket(5, func() error {
+ var err error
+ resultsMailbox, err = GetMailBox(smtpTo)
+ return err
+ })
+ if err != nil {
+ t.Log(err)
+ t.Fatal("No emails found for address " + smtpTo)
+ }
+ if err == nil && len(resultsMailbox) > 0 {
+ if !strings.ContainsAny(resultsMailbox[0].To[0], smtpTo) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := GetMessageFromMailbox(smtpTo, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Body.Text, emailBody) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Received message")
+ }
+
+ // verify that the To header of the email message is set to the MIME recipient, even though we got it out of the SMTP recipient's email inbox
+ assert.Equal(t, mimeTo, resultsEmail.Header["To"][0])
+
+ // verify that the MIME from address is correct - unfortunately, we can't verify the SMTP from address
+ assert.Equal(t, from.String(), resultsEmail.Header["From"][0])
+
+ // check that the custom mime headers came through - header case seems to get mutated
+ assert.Equal(t, "TestValue", resultsEmail.Header["Testheader"][0])
+
+ // ensure that the attachment was successfully sent
+ assert.Len(t, resultsEmail.Attachments, 1)
+ assert.Equal(t, fileName, resultsEmail.Attachments[0].Filename)
+ assert.Equal(t, fileContents, resultsEmail.Attachments[0].Bytes)
+ }
+ }
+ }
+ }
+}
diff --git a/web/web_test.go b/web/web_test.go
index f43251143..e4b9d820f 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -44,7 +44,10 @@ func Setup() *app.App {
}
prevListenAddress := *a.Config().ServiceSettings.ListenAddress
a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = ":0" })
- a.StartServer()
+ serverErr := a.StartServer()
+ if serverErr != nil {
+ panic(serverErr)
+ }
a.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ListenAddress = prevListenAddress })
api4.Init(a, a.Srv.Router, false)
api3 := api.Init(a, a.Srv.Router)