diff --git a/app/app_iface.go b/app/app_iface.go index bae6bc8c88..1740a0a892 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -848,6 +848,7 @@ type AppIface interface { SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList) ([]string, error) SendPasswordReset(email string, siteURL string) (bool, *model.AppError) SendPasswordResetEmail(email string, token *model.Token, locale, siteURL string) (bool, *model.AppError) + SendRemoveExpiredLicenseEmail(email string, locale, siteURL string, licenseId string) *model.AppError SendSignInChangeEmail(email, method, locale, siteURL string) *model.AppError ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string) ServePluginRequest(w http.ResponseWriter, r *http.Request) diff --git a/app/email.go b/app/email.go index 738621af74..ae00690283 100644 --- a/app/email.go +++ b/app/email.go @@ -513,6 +513,24 @@ func (a *App) SendDeactivateAccountEmail(email string, locale, siteURL string) * return nil } +func (a *App) SendRemoveExpiredLicenseEmail(email string, locale, siteURL string, licenseId string) *model.AppError { + T := utils.GetUserTranslations(locale) + subject := T("api.templates.remove_expired_license.subject", + map[string]interface{}{"SiteName": a.ClientConfig()["SiteName"]}) + + bodyPage := a.newEmailTemplate("remove_expired_license", locale) + bodyPage.Props["SiteURL"] = siteURL + bodyPage.Props["Title"] = T("api.templates.remove_expired_license.body.title") + bodyPage.Props["Link"] = fmt.Sprintf("%s?id=%s", model.LICENSE_RENEWAL_LINK, licenseId) + bodyPage.Props["LinkButton"] = T("api.templates.remove_expired_license.body.renew_button") + + if err := a.sendMail(email, subject, bodyPage.Render()); err != nil { + return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + func (a *App) sendNotificationMail(to, subject, htmlBody string) *model.AppError { if !*a.Config().EmailSettings.SendEmailNotifications { return nil diff --git a/app/license.go b/app/license.go index 1e162e2543..2dc9b40d32 100644 --- a/app/license.go +++ b/app/license.go @@ -158,6 +158,8 @@ func (a *App) RemoveLicense() *model.AppError { return nil } + mlog.Info("Remove license.", mlog.String("id", model.SYSTEM_ACTIVE_LICENSE_ID)) + sysVar := &model.System{} sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID sysVar.Value = "" diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index 85be8bf41e..8311eb1554 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -12402,6 +12402,28 @@ func (a *OpenTracingAppLayer) SendPasswordResetEmail(email string, token *model. return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) SendRemoveExpiredLicenseEmail(email string, locale string, siteURL string, licenseId string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendRemoveExpiredLicenseEmail") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.SendRemoveExpiredLicenseEmail(email, locale, siteURL, licenseId) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) SendSignInChangeEmail(email string, method string, locale string, siteURL string) *model.AppError { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSignInChangeEmail") diff --git a/app/server.go b/app/server.go index 43e8564763..06d01330ec 100644 --- a/app/server.go +++ b/app/server.go @@ -348,6 +348,9 @@ func NewServer(options ...Option) (*Server, error) { s.Go(func() { runCommandWebhookCleanupJob(s) }) + s.Go(func() { + runLicenseExpirationCheckJob(s) + }) if complianceI := s.Compliance; complianceI != nil { complianceI.StartComplianceDailyJob() @@ -777,6 +780,13 @@ func runSessionCleanupJob(s *Server) { }, time.Hour*24) } +func runLicenseExpirationCheckJob(s *Server) { + doLicenseExpirationCheck(s) + model.CreateRecurringTask("License Expiration Check", func() { + doLicenseExpirationCheck(s) + }, time.Hour*24) +} + func doSecurity(s *Server) { s.DoSecurityUpdateCheck() } @@ -804,6 +814,46 @@ func doSessionCleanup(s *Server) { s.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE) } +func doLicenseExpirationCheck(s *Server) { + s.FakeApp().LoadLicense() + license := s.License() + + if license == nil { + mlog.Debug("License cannot be found.") + return + } + + if !license.IsPastGracePeriod() { + mlog.Debug("License is not past the grace period.") + return + } + + users, err := s.Store.User().GetSystemAdminProfiles() + if err != nil { + mlog.Error("Failed to get system admins for license expired message from Mattermost.") + return + } + + //send email to admin(s) + for _, user := range users { + user := user + if user.Email == "" { + mlog.Error("Invalid system admin email.", mlog.String("user_email", user.Email)) + continue + } + + mlog.Debug("Sending license expired email.", mlog.String("user_email", user.Email)) + s.Go(func() { + if err := s.FakeApp().SendRemoveExpiredLicenseEmail(user.Email, user.Locale, *s.Config().ServiceSettings.SiteURL, license.Id); err != nil { + mlog.Error("Error while sending the license expired email.", mlog.String("user_email", user.Email), mlog.Err(err)) + } + }) + } + + //remove the license + s.FakeApp().RemoveLicense() +} + func (s *Server) StartSearchEngine() (string, string) { if s.SearchEngine.ElasticsearchEngine != nil && s.SearchEngine.ElasticsearchEngine.IsActive() { s.Go(func() { diff --git a/i18n/en.json b/i18n/en.json index 6b60f6096a..650b9a5676 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1464,6 +1464,10 @@ "id": "api.license.client.old_format.app_error", "translation": "New format for the client license is not supported yet. Please specify format=old in the query string." }, + { + "id": "api.license.remove_expired_license.failed.error", + "translation": "Failed to send the disable license email successfully." + }, { "id": "api.marshal_error", "translation": "marshal error" @@ -2298,6 +2302,18 @@ "id": "api.templates.post_body.button", "translation": "Go To Post" }, + { + "id": "api.templates.remove_expired_license.body.renew_button", + "translation": "Renew License" + }, + { + "id": "api.templates.remove_expired_license.body.title", + "translation": "Enterprise license has expired and some features may be disabled. Please renew." + }, + { + "id": "api.templates.remove_expired_license.subject", + "translation": "Mattermost Enterprise license has been disabled." + }, { "id": "api.templates.reset_body.button", "translation": "Reset Password" diff --git a/model/license.go b/model/license.go index 16466e5340..6a7c75f611 100644 --- a/model/license.go +++ b/model/license.go @@ -12,6 +12,8 @@ import ( const ( EXPIRED_LICENSE_ERROR = "api.license.add_license.expired.app_error" INVALID_LICENSE_ERROR = "api.license.add_license.invalid.app_error" + LICENSE_GRACE_PERIOD = 1000 * 60 * 60 * 24 * 10 //10 days + LICENSE_RENEWAL_LINK = "https://licensing.mattermost.com/renew" ) type LicenseRecord struct { @@ -194,6 +196,11 @@ func (l *License) IsExpired() bool { return l.ExpiresAt < GetMillis() } +func (l *License) IsPastGracePeriod() bool { + timeDiff := GetMillis() - l.ExpiresAt + return timeDiff > LICENSE_GRACE_PERIOD +} + func (l *License) IsStarted() bool { return l.StartsAt < GetMillis() } diff --git a/model/license_test.go b/model/license_test.go index e5a0caaac5..7d1625660b 100644 --- a/model/license_test.go +++ b/model/license_test.go @@ -116,6 +116,15 @@ func TestLicenseIsExpired(t *testing.T) { assert.False(t, l1.IsExpired()) } +func TestLicenseIsPastGracePeriod(t *testing.T) { + l1 := License{} + l1.ExpiresAt = GetMillis() - LICENSE_GRACE_PERIOD - 1000 + assert.True(t, l1.IsPastGracePeriod()) + + l1.ExpiresAt = GetMillis() + 1000 + assert.False(t, l1.IsPastGracePeriod()) +} + func TestLicenseIsStarted(t *testing.T) { l1 := License{} l1.StartsAt = GetMillis() - 1000 diff --git a/templates/remove_expired_license.html b/templates/remove_expired_license.html new file mode 100644 index 0000000000..fd8adcdd0b --- /dev/null +++ b/templates/remove_expired_license.html @@ -0,0 +1,50 @@ +{{define "remove_expired_license"}} + + + + + +
+ + + + +
+ + + + + + + + + {{template "email_footer" . }} + +
+ +
+ + + + + + {{template "email_info" . }} + +
+

{{.Props.Title}}

+

{{.Props.Info}}

+

+ {{.Props.LinkButton}} +

+
+
+
+
+ +{{end}} \ No newline at end of file