diff --git a/app/email.go b/app/email.go index fd31526394..f2230d2b08 100644 --- a/app/email.go +++ b/app/email.go @@ -815,6 +815,33 @@ func (es *EmailService) SendAtUserLimitWarningEmail(email string, locale string, return true, nil } +func (es *EmailService) SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) (bool, *model.AppError) { + T := i18n.GetUserTranslations(locale) + subject := T("api.templates.license_up_for_renewal_subject") + + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.license_up_for_renewal_title") + data.Props["SubTitle"] = T("api.templates.license_up_for_renewal_subtitle", map[string]interface{}{"UserName": name, "Days": daysToExpiration}) + data.Props["SubTitleTwo"] = T("api.templates.license_up_for_renewal_subtitle_two") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["Button"] = T("api.templates.license_up_for_renewal_renew_now") + data.Props["ButtonURL"] = renewalLink + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + + body, err := es.srv.TemplatesContainer().RenderToString("license_up_for_renewal", data) + if err != nil { + return false, model.NewAppError("SendLicenseUpForRenewalEmail", "api.user.send_license_up_for_renewal_email.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { + return false, model.NewAppError("SendLicenseUpForRenewalEmail", "api.user.send_license_up_for_renewal_email.error", nil, err.Error(), http.StatusInternalServerError) + } + + return true, nil +} + // SendUpgradeEmail formats an email template and sends an email to an admin specified in the email arg func (es *EmailService) SendUpgradeEmail(user, email, locale, siteURL, action string) (bool, *model.AppError) { T := i18n.GetUserTranslations(locale) diff --git a/app/server.go b/app/server.go index 11d8a2fca0..4669285302 100644 --- a/app/server.go +++ b/app/server.go @@ -1781,6 +1781,52 @@ func (s *Server) startMetricsServer() { s.Log.Info("Metrics and profiling server is started", mlog.String("address", l.Addr().String())) } +func (s *Server) sendLicenseUpForRenewalEmail(users map[string]*model.User, license *model.License) *model.AppError { + key := model.LICENSE_UP_FOR_RENEWAL_EMAIL_SENT + license.Id + if _, err := s.Store.System().GetByName(key); err == nil { + // return early because the key already exists and that means we already executed the code below to send email successfully + return nil + } + + daysToExpiration := license.DaysToExpiration() + + renewalLink, appErr := s.GenerateLicenseRenewalLink() + if appErr != nil { + return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_generating_link", nil, appErr.Error(), http.StatusInternalServerError) + } + + // we want to at least have one email sent out to an admin + countNotOks := 0 + + for _, user := range users { + name := user.FirstName + if name == "" { + name = user.Username + } + ok, err := s.EmailService.SendLicenseUpForRenewalEmail(user.Email, name, user.Locale, *s.Config().ServiceSettings.SiteURL, renewalLink, daysToExpiration) + if !ok || err != nil { + mlog.Error("Error sending license up for renewal email to", mlog.String("user_email", user.Email)) + countNotOks++ + } + } + + // if not even one admin got an email, we consider that this operation errored + if countNotOks == len(users) { + return model.NewAppError("s.sendLicenseUpForRenewalEmail", "api.server.license_up_for_renewal.error_sending_email", nil, "", http.StatusInternalServerError) + } + + system := model.System{ + Name: key, + Value: "true", + } + + if err := s.Store.System().Save(&system); err != nil { + mlog.Debug("Failed to mark license up for renewal email sending as completed.", mlog.Err(err)) + } + + return nil +} + func (s *Server) doLicenseExpirationCheck() { s.LoadLicense() license := s.License() @@ -1790,17 +1836,24 @@ func (s *Server) doLicenseExpirationCheck() { 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 } + if license.IsWithinExpirationPeriod() { + appErr := s.sendLicenseUpForRenewalEmail(users, license) + if appErr != nil { + mlog.Debug(appErr.Error()) + } + } + + if !license.IsPastGracePeriod() { + mlog.Debug("License is not past the grace period.") + return + } + //send email to admin(s) for _, user := range users { user := user diff --git a/i18n/en.json b/i18n/en.json index e2738b54b0..1db27db311 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2458,6 +2458,14 @@ "id": "api.scheme.patch_scheme.license.error", "translation": "Your license does not support update permissions schemes" }, + { + "id": "api.server.license_up_for_renewal.error_generating_link", + "translation": "Failed to generate the license renewal link" + }, + { + "id": "api.server.license_up_for_renewal.error_sending_email", + "translation": "Failed to send license up for renewal emails" + }, { "id": "api.server.start_server.forward80to443.disabled_while_using_lets_encrypt", "translation": "Must enable Forward80To443 when using LetsEncrypt" @@ -3318,6 +3326,26 @@ "id": "api.templates.invite_subject", "translation": "[{{ .SiteName }}] {{ .SenderName }} invited you to join {{ .TeamDisplayName }} Team" }, + { + "id": "api.templates.license_up_for_renewal_renew_now", + "translation": "Renew now" + }, + { + "id": "api.templates.license_up_for_renewal_subject", + "translation": "Your license is up for renewal" + }, + { + "id": "api.templates.license_up_for_renewal_subtitle", + "translation": "{{.UserName}}, your subscription is set to expire in {{.Days}} days. We hope you’re experiencing the flexible, secure team collaboration that Mattermost enables. Renew soon to ensure your team can keep enjoying these benefits." + }, + { + "id": "api.templates.license_up_for_renewal_subtitle_two", + "translation": "Log in to your Customer Account to renew" + }, + { + "id": "api.templates.license_up_for_renewal_title", + "translation": "Your Mattermost subscription is up for renewal" + }, { "id": "api.templates.mfa_activated_body.info", "translation": "Multi-factor authentication has been added to your account on {{ .SiteURL }}." @@ -4102,6 +4130,10 @@ "id": "api.user.send_email_change_verify_email_and_forget.error", "translation": "Failed to send email change verification email successfully" }, + { + "id": "api.user.send_license_up_for_renewal_email.error", + "translation": "Failed to send license up for renewal email" + }, { "id": "api.user.send_mfa_change_email.error", "translation": "Unable to send email notification for MFA change." diff --git a/model/license.go b/model/license.go index 83b1e07f92..ab9e481a2a 100644 --- a/model/license.go +++ b/model/license.go @@ -5,6 +5,7 @@ package model import ( "encoding/json" + "fmt" "io" "net/http" "time" @@ -17,6 +18,12 @@ const ( LICENSE_RENEWAL_LINK = "https://mattermost.com/renew/" ) +const ( + SIXTY_DAYS = 60 + FIFTY_EIGHT = 58 + LICENSE_UP_FOR_RENEWAL_EMAIL_SENT = "LicenseUpForRenewalEmailSent" +) + var ( trialDuration = 30*(time.Hour*24) + (time.Hour * 8) // 720 hours (30 days) + 8 hours is trial license duration adminTrialDuration = 30*(time.Hour*24) + (time.Hour * 23) + (time.Minute * 59) + (time.Second * 59) // 720 hours (30 days) + 23 hours, 59 mins and 59 seconds @@ -265,6 +272,18 @@ func (l *License) IsPastGracePeriod() bool { return timeDiff > LICENSE_GRACE_PERIOD } +func (l *License) IsWithinExpirationPeriod() bool { + days := l.DaysToExpiration() + return days <= SIXTY_DAYS && days >= FIFTY_EIGHT +} + +func (l *License) DaysToExpiration() int { + dif := l.ExpiresAt - GetMillis() + d, _ := time.ParseDuration(fmt.Sprint(dif) + "ms") + days := d.Hours() / 24 + return int(days) +} + func (l *License) IsStarted() bool { return l.StartsAt < GetMillis() } diff --git a/templates/license_up_for_renewal.html b/templates/license_up_for_renewal.html new file mode 100644 index 0000000000..c3750dfc45 --- /dev/null +++ b/templates/license_up_for_renewal.html @@ -0,0 +1,497 @@ +{{define "license_up_for_renewal"}} + + + + + +
+|
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
+