From 1ec295f88ca99e9423ffd91019cecf802ae3dc77 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 6 Feb 2018 17:25:49 -0600 Subject: add App.License, remove utils.IsLicensed / utils.License calls (#8203) --- api/context.go | 4 ++-- api/team.go | 3 +-- api/user.go | 2 +- api4/context.go | 4 ++-- api4/user.go | 2 +- app/authentication.go | 7 ++++--- app/cluster_discovery.go | 3 +-- app/compliance.go | 7 +++---- app/config.go | 8 ++++++++ app/diagnostics.go | 17 ++++++++--------- app/email_batching.go | 2 +- app/ldap.go | 9 +++++---- app/license_test.go | 4 +--- app/notification.go | 4 ++-- app/oauth.go | 4 ++-- app/post.go | 8 ++++---- app/session.go | 3 ++- app/user.go | 3 ++- app/web_conn.go | 2 +- app/webhook.go | 2 +- cmd/platform/server.go | 6 ++++-- 21 files changed, 56 insertions(+), 48 deletions(-) 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/team.go b/api/team.go index 48377f970..f590b8e8c 100644 --- a/api/team.go +++ b/api/team.go @@ -13,7 +13,6 @@ import ( "github.com/gorilla/mux" "github.com/mattermost/mattermost-server/model" - "github.com/mattermost/mattermost-server/utils" ) func (api *API) InitTeam() { @@ -117,7 +116,7 @@ func getAll(c *Context, w http.ResponseWriter, r *http.Request) { func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) { invites := model.InvitesFromJson(r.Body) - if utils.IsLicensed() && !c.App.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_INVITE_USER) { + if c.App.License() != nil && !c.App.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_INVITE_USER) { errorId := "" if *c.App.Config().TeamSettings.RestrictTeamInvite == model.PERMISSIONS_SYSTEM_ADMIN { errorId = "api.team.invite_members.restricted_system_admin.app_error" 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/context.go b/api4/context.go index 980897062..82c8d9e6c 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/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/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..809d9ff1e 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" ) @@ -509,17 +508,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/post.go b/app/post.go index 6890d1dd9..005624605 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.CheckIfRolesGrantPermission(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/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 64e49e293..69c6d072b 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/server.go b/cmd/platform/server.go index 7ac075502..e3742cef6 100644 --- a/cmd/platform/server.go +++ b/cmd/platform/server.go @@ -93,14 +93,16 @@ func runServer(configFileLocation string, disableConfigWatch bool) error { 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 }) -- cgit v1.2.3-1-g7c22 From 9a73f9988588b6b1be5711634239381fe9e01d16 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Tue, 6 Feb 2018 18:45:14 -0500 Subject: ICU-715 Change ExperimentalGroupUnreadChannels setting to allow for default on/off (#8211) --- config/default.json | 2 +- i18n/en.json | 4 ++++ model/config.go | 18 ++++++++++++++++-- model/config_test.go | 32 ++++++++++++++++++++++++++++++++ utils/config.go | 2 +- 5 files changed, 54 insertions(+), 4 deletions(-) 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 d3bc69d0a..d983e8855 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -4758,6 +4758,10 @@ "id": "model.config.is_valid.file_thumb_width.app_error", "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." diff --git a/model/config.go b/model/config.go index b7888ab13..20011f7cb 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 @@ -214,7 +218,7 @@ type ServiceSettings struct { EnablePreviewFeatures *bool EnableTutorial *bool ExperimentalEnableDefaultChannelLeaveJoinMessages *bool - ExperimentalGroupUnreadChannels *bool + ExperimentalGroupUnreadChannels *string ImageProxyType *string ImageProxyURL *string ImageProxyOptions *string @@ -424,7 +428,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 { @@ -2070,6 +2078,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": diff --git a/model/config_test.go b/model/config_test.go index ceede6be4..5510c40d0 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{} 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) -- cgit v1.2.3-1-g7c22 From 809a16458f7483a2b762cd546493780fea6220ea Mon Sep 17 00:00:00 2001 From: Pierre de La Morinerie Date: Wed, 7 Feb 2018 13:41:15 +0530 Subject: Abort on critical error during server startup (#8204) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only a handful of critical errors are present in the codebase. They all occur during server startup (in `app.StartServer()`). Currently, when one of these critical error occurs, it is simpled mentionned in the logs – then the error is discarded, and the app attempts to continue the execution (and probably fails pretty quickly in a weird way). Rather than continuing operations in an unknow state, these errors should trigger a clean exit. This commit rewrites critical startup errors to be correctly propagated, logged, and then terminate the command execution. Additionnaly, it makes the server return a proper error code to the shell. --- api/apitestlib.go | 6 +++++- api4/apitestlib.go | 6 +++++- app/app_test.go | 3 ++- app/apptestlib.go | 6 +++++- app/server.go | 12 +++++++----- app/server_test.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ cmd/platform/server.go | 9 +++++++-- cmd/platform/test.go | 12 ++++++++++-- web/web_test.go | 5 ++++- 9 files changed, 95 insertions(+), 14 deletions(-) create mode 100644 app/server_test.go diff --git a/api/apitestlib.go b/api/apitestlib.go index 8d7f54902..bae00927a 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/api4/apitestlib.go b/api4/apitestlib.go index a7e64ae84..ccdb4c206 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) diff --git a/app/app_test.go b/app/app_test.go index 25b19ead8..09f8725d7 100644 --- a/app/app_test.go +++ b/app/app_test.go @@ -51,7 +51,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 09afc8f76..016a68bec 100644 --- a/app/apptestlib.go +++ b/app/apptestlib.go @@ -96,7 +96,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.Srv.Store.MarkSystemRanUnitTests() 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/cmd/platform/server.go b/cmd/platform/server.go index e3742cef6..a8a6e8923 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() @@ -87,7 +87,12 @@ 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) 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/web/web_test.go b/web/web_test.go index 21a7968b3..c8d64c61d 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) -- cgit v1.2.3-1-g7c22 From 121712ce5d61cd0bb35efa5bacd59f7f9daff326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Wed, 7 Feb 2018 10:42:55 +0100 Subject: Remove fmt.Println trace on api4 tests (#8212) --- api4/apitestlib.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/api4/apitestlib.go b/api4/apitestlib.go index ccdb4c206..b55ce7cbf 100644 --- a/api4/apitestlib.go +++ b/api4/apitestlib.go @@ -302,8 +302,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() -- cgit v1.2.3-1-g7c22 From b2ee5077931013d308aaf60d790d341e2cb0c3e3 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 7 Feb 2018 07:21:40 -0600 Subject: allow plugins to set command hints (#8214) --- app/plugin.go | 1 + 1 file changed, 1 insertion(+) 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, } -- cgit v1.2.3-1-g7c22 From d3e934d07ac0a58a24a435ea7c5b3bd222ef509a Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 7 Feb 2018 09:02:46 -0500 Subject: XYZ-35: Added Support for GlobalRelay Compliance Export Format * Added username to ChannelMemberHistory struct in anticipation of supporting GlobalRelay in Compliance Export * Removed translation from debug output - this makes it complicated to use utils functions from tests in the enterprise repo * Added an advanced email function that allows for greater control over message details. Updated MessageExport config to support GlobalRelay. Added attachment support to InBucket unit tests * Moving templates in from enterprise to solve test issues * Added export format to diagnostics * Changed email attachment code to use FileBackend so that S3 storage is properly supported --- app/diagnostics.go | 1 + i18n/en.json | 28 +++---- model/channel_member_history.go | 5 +- model/config.go | 21 +++++ model/config_test.go | 52 ++++++++++++- model/message_export.go | 1 + store/sqlstore/channel_member_history_store.go | 12 +-- store/sqlstore/compliance_store.go | 3 +- store/storetest/channel_member_history_store.go | 16 ++++ store/storetest/compliance_store.go | 9 ++- templates/globalrelay_compliance_export.html | 91 ++++++++++++++++++++++ .../globalrelay_compliance_export_message.html | 8 ++ ...balrelay_compliance_export_participant_row.html | 10 +++ utils/html.go | 8 +- utils/inbucket.go | 52 +++++++++++-- utils/mail.go | 60 +++++++++++--- utils/mail_test.go | 87 ++++++++++++++++++++- 17 files changed, 413 insertions(+), 51 deletions(-) create mode 100644 templates/globalrelay_compliance_export.html create mode 100644 templates/globalrelay_compliance_export_message.html create mode 100644 templates/globalrelay_compliance_export_participant_row.html diff --git a/app/diagnostics.go b/app/diagnostics.go index 809d9ff1e..6d83d3a89 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -501,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, diff --git a/i18n/en.json b/i18n/en.json index d983e8855..1a04aeeed 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -131,10 +131,6 @@ "id": "api.admin.upload_brand_image.too_large.app_error", "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" @@ -4858,6 +4854,14 @@ "id": "model.config.is_valid.message_export.batch_size.app_error", "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." @@ -7082,6 +7086,10 @@ "id": "utils.mail.new_client.auth.app_error", "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" @@ -7158,10 +7166,6 @@ "id": "web.create_dir.error", "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" @@ -7266,18 +7270,10 @@ "id": "web.parsing_templates.debug", "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 20011f7cb..9010eaeae 100644 --- a/model/config.go +++ b/model/config.go @@ -158,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 { @@ -1623,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() { @@ -1633,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") } @@ -2170,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 5510c40d0..919f73fd7 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -136,7 +136,7 @@ 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 } @@ -147,6 +147,55 @@ func TestMessageExportSettingsIsValid(t *testing.T) { 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), + } + + // should pass because everything is valid + 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)) } @@ -159,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/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"}} + + +

Mattermost Compliance Export

+ +

Conversation Summary

+
+
    +
  • Channel: {{.Props.ChannelName}}
  • +
  • Started: {{.Props.Started}}
  • +
  • Ended: {{.Props.Ended}}
  • +
  • Duration: {{.Props.Duration}} Minutes
  • +
+
+ + + + + + + + + + {{.Props.ParticipantRows}} +
Username
JoinedLeftDurationMessages
+ +

Messages

+
+
    + {{.Props.Messages}} +
+
+ +

Exported on {{.Props.ExportDate}}

+{{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"}} +
  • + {{.Props.SentTime}} + @{{.Props.Username}} + + {{.Props.Message}} +
  • +{{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"}} + + @{{.Props.Username}} + {{.Props.Email}} + {{.Props.Joined}} + {{.Props.Left}} + {{.Props.DurationMinutes}} Minutes + {{.Props.NumMessages}} + +{{end}} \ No newline at end of file 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" + htmlBody + "" - 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) + } + } + } + } +} -- cgit v1.2.3-1-g7c22 From 7bd298ceaa24c0721e0acd65692cb2d1ca4983f3 Mon Sep 17 00:00:00 2001 From: Vordimous Date: Wed, 7 Feb 2018 09:17:18 -0500 Subject: PLT-7537: Move channel CLI command posts system message to channel. (#8161) * [PTL-7537] implement feature and test * [PTL-7537] Update feature to post the the room requiring a username flag to be used * [PTL-7537] update tests with username * update test to remove changes to the test helper struct * use the basic team and user --- app/channel.go | 30 ++++++++++++++++++++++++++++-- app/channel_test.go | 4 ++-- cmd/platform/channel.go | 17 +++++++++++++---- cmd/platform/channel_test.go | 27 +++++++++++++++++++++++++++ i18n/en.json | 8 ++++++++ model/post.go | 2 ++ 6 files changed, 80 insertions(+), 8 deletions(-) diff --git a/app/channel.go b/app/channel.go index e4bf48654..8ac1f421c 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1359,7 +1359,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 @@ -1378,11 +1378,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/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/i18n/en.json b/i18n/en.json index 1a04aeeed..21f0661cc 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -3130,6 +3130,14 @@ "id": "app.channel.move_channel.members_do_not_match.error", "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" 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, -- cgit v1.2.3-1-g7c22