diff --git a/api4/cloud.go b/api4/cloud.go
index 655eddd4ad..9e10aafc08 100644
--- a/api4/cloud.go
+++ b/api4/cloud.go
@@ -665,6 +665,18 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
c.Logger.Info("Updated subscription from webhook event")
+ case model.EventTypeTriggerDelinquencyEmail:
+ var emailToTrigger model.DelinquencyEmail
+ if event.DelinquencyEmail != nil {
+ emailToTrigger = model.DelinquencyEmail(event.DelinquencyEmail.EmailToTrigger)
+ } else {
+ c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.delinquency_email.missing_email_to_trigger", nil, "", http.StatusInternalServerError)
+ return
+ }
+ if nErr := c.App.SendDelinquencyEmail(emailToTrigger); nErr != nil {
+ c.Err = nErr
+ return
+ }
default:
c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound)
diff --git a/app/app_iface.go b/app/app_iface.go
index 561ec1b7c3..fa2a68696b 100644
--- a/app/app_iface.go
+++ b/app/app_iface.go
@@ -1017,6 +1017,7 @@ type AppIface interface {
SendAckToPushProxy(ack *model.PushNotificationAck) error
SendAutoResponse(c request.CTX, channel *model.Channel, receiver *model.User, post *model.Post) (bool, *model.AppError)
SendAutoResponseIfNecessary(c request.CTX, channel *model.Channel, sender *model.User, post *model.Post) (bool, *model.AppError)
+ SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError
SendEmailVerification(user *model.User, newEmail, redirect string) *model.AppError
SendEphemeralPost(c request.CTX, userID string, post *model.Post) *model.Post
SendNotifications(c request.CTX, post *model.Post, team *model.Team, channel *model.Channel, sender *model.User, parentPostList *model.PostList, setOnline bool) ([]string, error)
diff --git a/app/cloud.go b/app/cloud.go
index bb38e6bb22..fed00db27b 100644
--- a/app/cloud.go
+++ b/app/cloud.go
@@ -148,6 +148,87 @@ func (a *App) SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.
return nil
}
+func getCurrentProduct(subscriptionProductID string, products []*model.Product) *model.Product {
+ for _, product := range products {
+ if product.ID == subscriptionProductID {
+ return product
+ }
+ }
+ return nil
+}
+
+func (a *App) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
+ sysAdmins, aErr := a.getSysAdminsEmailRecipients()
+ if aErr != nil {
+ return aErr
+ }
+ subscription, err := a.Cloud().GetSubscription("")
+ if err != nil {
+ return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ if subscription == nil {
+ return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ products, err := a.Cloud().GetCloudProducts("", false)
+ if err != nil {
+ return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_cloud_products.app_error", nil, err.Error(), http.StatusInternalServerError)
+ }
+ if products == nil {
+ return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_cloud_products.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ planName := getCurrentProduct(subscription.ProductID, products).Name
+
+ if subscription.DelinquentSince == nil {
+ return model.NewAppError("SendDelinquencyEmail", "app.cloud.get_subscription_delinquency_date.app_error", nil, "", http.StatusInternalServerError)
+ }
+
+ delinquentSince := time.Unix(*subscription.DelinquentSince, 0)
+
+ delinquencyDate := delinquentSince.Format("01/02/2006")
+ for _, admin := range sysAdmins {
+ switch emailToSend {
+ case model.DelinquencyEmail7:
+ err := a.Srv().EmailService.SendDelinquencyEmail7(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 7", mlog.Err(err))
+ }
+ case model.DelinquencyEmail14:
+ err := a.Srv().EmailService.SendDelinquencyEmail14(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 14", mlog.Err(err))
+ }
+ case model.DelinquencyEmail30:
+ err := a.Srv().EmailService.SendDelinquencyEmail30(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 30", mlog.Err(err))
+ }
+ case model.DelinquencyEmail45:
+ err := a.Srv().EmailService.SendDelinquencyEmail45(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 45", mlog.Err(err))
+ }
+ case model.DelinquencyEmail60:
+ err := a.Srv().EmailService.SendDelinquencyEmail60(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 60", mlog.Err(err))
+ }
+ case model.DelinquencyEmail75:
+ err := a.Srv().EmailService.SendDelinquencyEmail75(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL, planName, delinquencyDate)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 75", mlog.Err(err))
+ }
+ case model.DelinquencyEmail90:
+ err := a.Srv().EmailService.SendDelinquencyEmail90(admin.Email, admin.Locale, *a.Config().ServiceSettings.SiteURL)
+ if err != nil {
+ a.Log().Error("Error sending delinquency email 90", mlog.Err(err))
+ }
+ }
+ }
+ return nil
+}
+
func (a *App) AdjustInProductLimits(limits *model.ProductLimits, subscription *model.Subscription) *model.AppError {
if limits.Teams != nil && limits.Teams.Active != nil && *limits.Teams.Active > 0 {
err := a.AdjustTeamsFromProductLimits(limits.Teams)
diff --git a/app/email/email.go b/app/email/email.go
index 10c3ce0d9f..e3928d9b9b 100644
--- a/app/email/email.go
+++ b/app/email/email.go
@@ -1000,6 +1000,225 @@ func (es *Service) SendNoCardPaymentFailedEmail(email string, locale string, sit
return nil
}
+func (es *Service) SendDelinquencyEmail7(email, locale, siteURL, planName string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.payment_failed.subject")
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_7.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_7.subtitle1")
+ data.Props["SubTitle2"] = T("api.templates.delinquency_7.subtitle2", map[string]any{"Plan": planName})
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_7.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ body, err := es.templatesContainer.RenderToString("cloud_7_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+func (es *Service) SendDelinquencyEmail14(email, locale, siteURL, planName string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_14.subject", map[string]any{"Plan": planName})
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_14.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_14.subtitle1")
+ data.Props["SubTitle2"] = T("api.templates.delinquency_14.subtitle2")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_14.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ body, err := es.templatesContainer.RenderToString("cloud_14_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+func (es *Service) SendDelinquencyEmail30(email, locale, siteURL, planName string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_30.subject", map[string]any{"Plan": planName})
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_30.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_30.subtitle1", map[string]any{"Plan": planName})
+ data.Props["SubTitle2"] = T("api.templates.delinquency_30.subtitle2")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_30.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+ data.Props["BulletListItems"] = []string{T("api.templates.delinquency_30.bullet.message_history"), T("api.templates.delinquency_30.bullet.files"), T("api.templates.delinquency_30.bullet.cards"), T("api.templates.delinquency_30.bullet.plugins")}
+ data.Props["LimitsDocs"] = T("api.templates.delinquency_30.limits_documentation")
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ body, err := es.templatesContainer.RenderToString("cloud_30_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (es *Service) SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_45.subject", map[string]any{"Plan": planName})
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_45.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_45.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
+ data.Props["SubTitle2"] = T("api.templates.delinquency_45.subtitle2")
+ data.Props["SubTitle3"] = T("api.templates.delinquency_45.subtitle3")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_45.button")
+ data.Props["IncludeSecondaryActionButton"] = false
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (es *Service) SendDelinquencyEmail60(email, locale, siteURL string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_60.subject")
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_60.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_60.subtitle1")
+ data.Props["SubTitle2"] = T("api.templates.delinquency_60.subtitle2")
+ data.Props["SubTitle3"] = T("api.templates.delinquency_60.subtitle3")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_60.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+ data.Props["IncludeSecondaryActionButton"] = true
+ data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_60.downgrade_to_starter")
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ // 45 day template is the same as the 60 day one so its reused
+ body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (es *Service) SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_75.subject", map[string]any{"Plan": planName})
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_75.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_75.subtitle1", map[string]any{"DelinquencyDate": delinquencyDate})
+ data.Props["SubTitle2"] = T("api.templates.delinquency_75.subtitle2", map[string]any{"Plan": planName})
+ data.Props["SubTitle3"] = T("api.templates.delinquency_75.subtitle3")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_75.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+ data.Props["IncludeSecondaryActionButton"] = true
+ data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_75.downgrade_to_starter")
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ // 45 day template is the same as the 75 day one so its reused
+ body, err := es.templatesContainer.RenderToString("cloud_45_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (es *Service) SendDelinquencyEmail90(email, locale, siteURL string) error {
+ T := i18n.GetUserTranslations(locale)
+
+ subject := T("api.templates.delinquency_90.subject")
+
+ data := es.NewEmailTemplateData(locale)
+ data.Props["SiteURL"] = siteURL
+ data.Props["Title"] = T("api.templates.delinquency_90.title")
+ data.Props["SubTitle1"] = T("api.templates.delinquency_90.subtitle1", map[string]any{"SiteURL": siteURL})
+ data.Props["SubTitle2"] = T("api.templates.delinquency_90.subtitle2")
+ data.Props["SubTitle3"] = T("api.templates.delinquency_90.subtitle3")
+ data.Props["QuestionTitle"] = T("api.templates.questions_footer.title")
+ data.Props["QuestionInfo"] = T("api.templates.questions_footer.info")
+ data.Props["SupportEmail"] = *es.config().SupportSettings.SupportEmail
+ data.Props["Button"] = T("api.templates.delinquency_90.button")
+ data.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
+ data.Props["IncludeSecondaryActionButton"] = true
+ data.Props["SecondaryActionButtonText"] = T("api.templates.delinquency_90.secondary_action_button")
+ data.Props["Footer"] = T("api.templates.copyright")
+
+ body, err := es.templatesContainer.RenderToString("cloud_90_day_arrears", data)
+ if err != nil {
+ return err
+ }
+
+ if err := es.sendEmailWithCustomReplyTo(email, subject, body, *es.config().SupportSettings.SupportEmail); err != nil {
+ return err
+ }
+
+ return nil
+}
+
// SendRemoveExpiredLicenseEmail formats an email and uses the email service to send the email to user with link pointing to CWS
// to renew the user license
func (es *Service) SendRemoveExpiredLicenseEmail(renewalLink, email string, locale, siteURL string) error {
diff --git a/app/email/mocks/ServiceInterface.go b/app/email/mocks/ServiceInterface.go
index 68b97109fc..fc95b9ad47 100644
--- a/app/email/mocks/ServiceInterface.go
+++ b/app/email/mocks/ServiceInterface.go
@@ -167,6 +167,104 @@ func (_m *ServiceInterface) SendDeactivateAccountEmail(_a0 string, locale string
return r0
}
+// SendDelinquencyEmail14 provides a mock function with given fields: _a0, locale, siteURL, planName
+func (_m *ServiceInterface) SendDelinquencyEmail14(_a0 string, locale string, siteURL string, planName string) error {
+ ret := _m.Called(_a0, locale, siteURL, planName)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL, planName)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail30 provides a mock function with given fields: _a0, locale, siteURL, planName
+func (_m *ServiceInterface) SendDelinquencyEmail30(_a0 string, locale string, siteURL string, planName string) error {
+ ret := _m.Called(_a0, locale, siteURL, planName)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL, planName)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail45 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
+func (_m *ServiceInterface) SendDelinquencyEmail45(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
+ ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail60 provides a mock function with given fields: _a0, locale, siteURL
+func (_m *ServiceInterface) SendDelinquencyEmail60(_a0 string, locale string, siteURL string) error {
+ ret := _m.Called(_a0, locale, siteURL)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail7 provides a mock function with given fields: _a0, locale, siteURL, planName
+func (_m *ServiceInterface) SendDelinquencyEmail7(_a0 string, locale string, siteURL string, planName string) error {
+ ret := _m.Called(_a0, locale, siteURL, planName)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL, planName)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail75 provides a mock function with given fields: _a0, locale, siteURL, planName, delinquencyDate
+func (_m *ServiceInterface) SendDelinquencyEmail75(_a0 string, locale string, siteURL string, planName string, delinquencyDate string) error {
+ ret := _m.Called(_a0, locale, siteURL, planName, delinquencyDate)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL, planName, delinquencyDate)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
+// SendDelinquencyEmail90 provides a mock function with given fields: _a0, locale, siteURL
+func (_m *ServiceInterface) SendDelinquencyEmail90(_a0 string, locale string, siteURL string) error {
+ ret := _m.Called(_a0, locale, siteURL)
+
+ var r0 error
+ if rf, ok := ret.Get(0).(func(string, string, string) error); ok {
+ r0 = rf(_a0, locale, siteURL)
+ } else {
+ r0 = ret.Error(0)
+ }
+
+ return r0
+}
+
// SendEmailChangeEmail provides a mock function with given fields: oldEmail, newEmail, locale, siteURL
func (_m *ServiceInterface) SendEmailChangeEmail(oldEmail string, newEmail string, locale string, siteURL string) error {
ret := _m.Called(oldEmail, newEmail, locale, siteURL)
diff --git a/app/email/service.go b/app/email/service.go
index c4001af6be..78a7232e52 100644
--- a/app/email/service.go
+++ b/app/email/service.go
@@ -143,6 +143,14 @@ type ServiceInterface interface {
SendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, messageID string, inReplyTo string, references string) error
SendLicenseUpForRenewalEmail(email, name, locale, siteURL, renewalLink string, daysToExpiration int) error
SendPaymentFailedEmail(email string, locale string, failedPayment *model.FailedPayment, siteURL string) (bool, error)
+ // Cloud delinquency email sequence
+ SendDelinquencyEmail7(email, locale, siteURL, planName string) error
+ SendDelinquencyEmail14(email, locale, siteURL, planName string) error
+ SendDelinquencyEmail30(email, locale, siteURL, planName string) error
+ SendDelinquencyEmail45(email, locale, siteURL, planName, delinquencyDate string) error
+ SendDelinquencyEmail60(email, locale, siteURL string) error
+ SendDelinquencyEmail75(email, locale, siteURL, planName, delinquencyDate string) error
+ SendDelinquencyEmail90(email, locale, siteURL string) error
SendNoCardPaymentFailedEmail(email string, locale string, siteURL string) error
SendRemoveExpiredLicenseEmail(renewalLink, email string, locale, siteURL string) error
AddNotificationEmailToBatch(user *model.User, post *model.Post, team *model.Team) *model.AppError
diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go
index 20c1ba1b8a..a02d4d443a 100644
--- a/app/opentracing/opentracing_layer.go
+++ b/app/opentracing/opentracing_layer.go
@@ -15084,6 +15084,28 @@ func (a *OpenTracingAppLayer) SendAutoResponseIfNecessary(c request.CTX, channel
return resultVar0, resultVar1
}
+func (a *OpenTracingAppLayer) SendDelinquencyEmail(emailToSend model.DelinquencyEmail) *model.AppError {
+ origCtx := a.ctx
+ span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendDelinquencyEmail")
+
+ 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.SendDelinquencyEmail(emailToSend)
+
+ if resultVar0 != nil {
+ span.LogFields(spanlog.Error(resultVar0))
+ ext.Error.Set(span, true)
+ }
+
+ return resultVar0
+}
+
func (a *OpenTracingAppLayer) SendEmailVerification(user *model.User, newEmail string, redirect string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendEmailVerification")
diff --git a/i18n/en.json b/i18n/en.json
index 3873589405..31cfab8f01 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -471,6 +471,10 @@
"id": "api.cloud.cws_webhook_event_missing_error",
"translation": "Webhook event was not handled. Either it is missing or it is not valid."
},
+ {
+ "id": "api.cloud.delinquency_email.missing_email_to_trigger",
+ "translation": "Missing required fields to send delinquency email."
+ },
{
"id": "api.cloud.license_error",
"translation": "Your license does not support cloud requests."
@@ -3247,6 +3251,190 @@
"id": "api.templates.deactivate_subject",
"translation": "[{{ .SiteName }}] Your account at {{ .ServerURL }} has been deactivated"
},
+ {
+ "id": "api.templates.delinquency_14.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_14.subject",
+ "translation": "Payment is overdue for your Mattermost {{.Plan}}."
+ },
+ {
+ "id": "api.templates.delinquency_14.subtitle1",
+ "translation": "We weren't able to charge the credit card we have on file. This means your workspace is at risk of being downgraded to Cloud Starter."
+ },
+ {
+ "id": "api.templates.delinquency_14.subtitle2",
+ "translation": "Please contact your financial institution to resolve any issues. Then, update your payment details as needed."
+ },
+ {
+ "id": "api.templates.delinquency_14.title",
+ "translation": "Payment not received"
+ },
+ {
+ "id": "api.templates.delinquency_30.bullet.cards",
+ "translation": "Cards from your Boards"
+ },
+ {
+ "id": "api.templates.delinquency_30.bullet.files",
+ "translation": "Files"
+ },
+ {
+ "id": "api.templates.delinquency_30.bullet.message_history",
+ "translation": "Message history"
+ },
+ {
+ "id": "api.templates.delinquency_30.bullet.plugins",
+ "translation": "Active plugins and integrations"
+ },
+ {
+ "id": "api.templates.delinquency_30.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_30.limits_documentation",
+ "translation": "View all limits documentation."
+ },
+ {
+ "id": "api.templates.delinquency_30.subject",
+ "translation": "Act to keep your Mattermost {{.Plan}} Features"
+ },
+ {
+ "id": "api.templates.delinquency_30.subtitle1",
+ "translation": "You have time to keep your Mattermost {{.Plan}} active but you'll need to resolve issues with your payment method."
+ },
+ {
+ "id": "api.templates.delinquency_30.subtitle2",
+ "translation": "if no action is taken, your workspace will be downgraded and the following data may be archived:"
+ },
+ {
+ "id": "api.templates.delinquency_30.title",
+ "translation": "Your workspace will be downgraded soon"
+ },
+ {
+ "id": "api.templates.delinquency_45.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_45.subject",
+ "translation": "Notice: Your Mattermost {{.Plan}} will be downgraded soon"
+ },
+ {
+ "id": "api.templates.delinquency_45.subtitle1",
+ "translation": "We've been unable to collect payment for outstanding invoices dated {{.DelinquencyDate}}. Your workspace is at risk of being downgraded."
+ },
+ {
+ "id": "api.templates.delinquency_45.subtitle2",
+ "translation": "A downgraded workspace might negatively affect critical workflows, integrations and other business critical activities carried at your workspace."
+ },
+ {
+ "id": "api.templates.delinquency_45.subtitle3",
+ "translation": "Update your credit card information now."
+ },
+ {
+ "id": "api.templates.delinquency_45.title",
+ "translation": "Your workspace will be downgraded soon"
+ },
+ {
+ "id": "api.templates.delinquency_60.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_60.downgrade_to_starter",
+ "translation": "Downgrade to Cloud Starter"
+ },
+ {
+ "id": "api.templates.delinquency_60.subject",
+ "translation": "Action Required: Workspace will be downgraded in 30 days"
+ },
+ {
+ "id": "api.templates.delinquency_60.subtitle1",
+ "translation": "Please update your payment information soon to process your outstanding invoices."
+ },
+ {
+ "id": "api.templates.delinquency_60.subtitle2",
+ "translation": "We will downgrade your workspace automatically in 30 days if we are unable to process your payment."
+ },
+ {
+ "id": "api.templates.delinquency_60.subtitle3",
+ "translation": "Update your payment information now or downgrade to Cloud Starter below."
+ },
+ {
+ "id": "api.templates.delinquency_60.title",
+ "translation": "Your Mattermost workspace will be downgraded in 30 days"
+ },
+ {
+ "id": "api.templates.delinquency_7.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_7.subtitle1",
+ "translation": "We couldn't process your most recent payment"
+ },
+ {
+ "id": "api.templates.delinquency_7.subtitle2",
+ "translation": "To keep your {{.Plan}} plan active, please contact your financial institution as soon as possible. Then, update your payment details as needed."
+ },
+ {
+ "id": "api.templates.delinquency_7.title",
+ "translation": "Your payment wasn't completed"
+ },
+ {
+ "id": "api.templates.delinquency_75.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_75.downgrade_to_starter",
+ "translation": "Downgrade to Cloud Starter"
+ },
+ {
+ "id": "api.templates.delinquency_75.subject",
+ "translation": "Your Mattermost {{.Plan}} will be downgraded in 15 days"
+ },
+ {
+ "id": "api.templates.delinquency_75.subtitle1",
+ "translation": "This is a final reminder that we haven’t received payment for your Mattermost Cloud workspace since {{.DelinquencyDate}}"
+ },
+ {
+ "id": "api.templates.delinquency_75.subtitle2",
+ "translation": "Your workspace will be downgraded to Cloud Starter. Your {{.Plan}} features will be locked and some of your workspace data may be archived until your full outstanding balance is settled."
+ },
+ {
+ "id": "api.templates.delinquency_75.subtitle3",
+ "translation": "Update your payment information now, or downgrade to Cloud Starter."
+ },
+ {
+ "id": "api.templates.delinquency_75.title",
+ "translation": "Your workspace will be downgraded in 15 days"
+ },
+ {
+ "id": "api.templates.delinquency_90.button",
+ "translation": "Update payment"
+ },
+ {
+ "id": "api.templates.delinquency_90.secondary_action_button",
+ "translation": "View Plans & Pricing"
+ },
+ {
+ "id": "api.templates.delinquency_90.subject",
+ "translation": "Your Mattermost Cloud workspace has been downgraded"
+ },
+ {
+ "id": "api.templates.delinquency_90.subtitle1",
+ "translation": "If you use Cloud Professional or Enterprise features for important business operations, these will no longer be available and you'll experience degraded performance."
+ },
+ {
+ "id": "api.templates.delinquency_90.subtitle2",
+ "translation": "In addition, your data may have been archived due to Cloud Starter limitations."
+ },
+ {
+ "id": "api.templates.delinquency_90.subtitle3",
+ "translation": "To unarchive your data and keep paid features, update your payment information."
+ },
+ {
+ "id": "api.templates.delinquency_90.title",
+ "translation": "Your Mattermost workspace has been downgraded"
+ },
{
"id": "api.templates.email_change_body.info",
"translation": "Your email address for {{.TeamDisplayName}} has been changed to {{.NewEmail}}."
@@ -4651,6 +4839,18 @@
"id": "app.channel_member_history.log_leave_event.internal_error",
"translation": "Failed to record channel member history. Failed to update existing join record"
},
+ {
+ "id": "app.cloud.get_cloud_products.app_error",
+ "translation": "Couldn't retrieve cloud products"
+ },
+ {
+ "id": "app.cloud.get_subscription.app_error",
+ "translation": "Couldn't retrieve cloud subscription"
+ },
+ {
+ "id": "app.cloud.get_subscription_delinquency_date.app_error",
+ "translation": "Subscription is not delinquent"
+ },
{
"id": "app.command.createcommand.internal_error",
"translation": "Unable to save the command."
diff --git a/model/cloud.go b/model/cloud.go
index 8aa16ebc2e..940da261e8 100644
--- a/model/cloud.go
+++ b/model/cloud.go
@@ -15,6 +15,7 @@ const (
EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email"
EventTypeSendUpgradeConfirmationEmail = "send-upgrade-confirmation-email"
EventTypeSubscriptionChanged = "subscription-changed"
+ EventTypeTriggerDelinquencyEmail = "trigger-delinquency-email"
)
var MockCWS string
@@ -189,13 +190,30 @@ type InvoiceLineItem struct {
Metadata map[string]any `json:"metadata"`
}
+type DelinquencyEmailTrigger struct {
+ EmailToTrigger string `json:"email_to_send"`
+}
+
+type DelinquencyEmail string
+
+const (
+ DelinquencyEmail7 DelinquencyEmail = "7"
+ DelinquencyEmail14 DelinquencyEmail = "14"
+ DelinquencyEmail30 DelinquencyEmail = "30"
+ DelinquencyEmail45 DelinquencyEmail = "45"
+ DelinquencyEmail60 DelinquencyEmail = "60"
+ DelinquencyEmail75 DelinquencyEmail = "75"
+ DelinquencyEmail90 DelinquencyEmail = "90"
+)
+
type CWSWebhookPayload struct {
- Event string `json:"event"`
- FailedPayment *FailedPayment `json:"failed_payment"`
- CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
- ProductLimits *ProductLimits `json:"product_limits"`
- Subscription *Subscription `json:"subscription"`
- SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
+ Event string `json:"event"`
+ FailedPayment *FailedPayment `json:"failed_payment"`
+ CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"`
+ ProductLimits *ProductLimits `json:"product_limits"`
+ Subscription *Subscription `json:"subscription"`
+ SubscriptionTrialEndUnixTimeStamp int64 `json:"trial_end_time_stamp"`
+ DelinquencyEmail *DelinquencyEmailTrigger `json:"delinquency_email"`
}
type FailedPayment struct {
diff --git a/templates/cloud_14_day_arrears.html b/templates/cloud_14_day_arrears.html
new file mode 100644
index 0000000000..85f7643376
--- /dev/null
+++ b/templates/cloud_14_day_arrears.html
@@ -0,0 +1,582 @@
+{{define "cloud_14_day_arrears"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Title}}
+ |
+
+
+ |
+ {{.Props.SubTitle1}}
+ |
+
+
+ |
+ {{.Props.SubTitle2}}
+ |
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.QuestionTitle}}
+ |
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/cloud_14_day_arrears.mjml b/templates/cloud_14_day_arrears.mjml
new file mode 100644
index 0000000000..1c6617ca81
--- /dev/null
+++ b/templates/cloud_14_day_arrears.mjml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/cloud_30_day_arrears.html b/templates/cloud_30_day_arrears.html
new file mode 100644
index 0000000000..2388bc3b67
--- /dev/null
+++ b/templates/cloud_30_day_arrears.html
@@ -0,0 +1,608 @@
+{{define "cloud_30_day_arrears"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Title}}
+ |
+
+
+ |
+ {{.Props.SubTitle1}}
+ |
+
+
+ |
+ {{.Props.SubTitle2}}
+ |
+
+
+
+
+
+
+ {{ range .Props.BulletListItems}}
+ - {{.}}
+ {{end}}
+
+
+
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.QuestionTitle}}
+ |
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/cloud_30_day_arrears.mjml b/templates/cloud_30_day_arrears.mjml
new file mode 100644
index 0000000000..8f1fff3ca1
--- /dev/null
+++ b/templates/cloud_30_day_arrears.mjml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{.Props.Title}}
+
+
+ {{.Props.SubTitle1}}
+
+
+ {{.Props.SubTitle2}}
+
+
+
+
+ {{ range .Props.BulletListItems}}
+ - {{.}}
+ {{end}}
+
+
+
+
+ {{.Props.LimitsDocs}}
+
+
+ {{.Props.Button}}
+
+
+
+
+
+
+
+
diff --git a/templates/cloud_45_day_arrears.html b/templates/cloud_45_day_arrears.html
new file mode 100644
index 0000000000..97df400064
--- /dev/null
+++ b/templates/cloud_45_day_arrears.html
@@ -0,0 +1,600 @@
+{{define "cloud_45_day_arrears"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Title}}
+ |
+
+
+ |
+ {{.Props.SubTitle1}}
+ |
+
+
+ |
+ {{.Props.SubTitle2}}
+ |
+
+
+ |
+ {{.Props.SubTitle3}}
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.QuestionTitle}}
+ |
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/cloud_45_day_arrears.mjml b/templates/cloud_45_day_arrears.mjml
new file mode 100644
index 0000000000..3f4799fd04
--- /dev/null
+++ b/templates/cloud_45_day_arrears.mjml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/cloud_7_day_arrears.html b/templates/cloud_7_day_arrears.html
new file mode 100644
index 0000000000..db1d3447bd
--- /dev/null
+++ b/templates/cloud_7_day_arrears.html
@@ -0,0 +1,582 @@
+{{define "cloud_7_day_arrears"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Title}}
+ |
+
+
+ |
+ {{.Props.SubTitle1}}
+ |
+
+
+ |
+ {{.Props.SubTitle2}}
+ |
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.QuestionTitle}}
+ |
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/cloud_7_day_arrears.mjml b/templates/cloud_7_day_arrears.mjml
new file mode 100644
index 0000000000..1c6617ca81
--- /dev/null
+++ b/templates/cloud_7_day_arrears.mjml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/cloud_90_day_arrears.html b/templates/cloud_90_day_arrears.html
new file mode 100644
index 0000000000..5832fd1538
--- /dev/null
+++ b/templates/cloud_90_day_arrears.html
@@ -0,0 +1,600 @@
+{{define "cloud_90_day_arrears"}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Title}}
+ |
+
+
+ |
+ {{.Props.SubTitle1}}
+ |
+
+
+ |
+ {{.Props.SubTitle2}}
+ |
+
+
+ |
+ {{.Props.SubTitle3}}
+ |
+
+
+ |
+
+ |
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+
+
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.QuestionTitle}}
+ |
+
+
+ |
+
+ |
+
+
+
+ |
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+ |
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+{{end}}
diff --git a/templates/cloud_90_day_arrears.mjml b/templates/cloud_90_day_arrears.mjml
new file mode 100644
index 0000000000..be785ed1d1
--- /dev/null
+++ b/templates/cloud_90_day_arrears.mjml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/templates/cloud_upgrade_confirmation.html b/templates/cloud_upgrade_confirmation.html
index 7bd9b57765..49d53b82c2 100644
--- a/templates/cloud_upgrade_confirmation.html
+++ b/templates/cloud_upgrade_confirmation.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
@@ -445,7 +445,7 @@
|
diff --git a/templates/inactivity_body.html b/templates/inactivity_body.html
index 266da3b339..e742a9e81e 100644
--- a/templates/inactivity_body.html
+++ b/templates/inactivity_body.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
@@ -481,7 +481,7 @@
|
|
diff --git a/templates/invite_body.html b/templates/invite_body.html
index 0866ebd594..730240f958 100644
--- a/templates/invite_body.html
+++ b/templates/invite_body.html
@@ -118,7 +118,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
diff --git a/templates/license_up_for_renewal.html b/templates/license_up_for_renewal.html
index aa455ddc98..f91f3c2b4d 100644
--- a/templates/license_up_for_renewal.html
+++ b/templates/license_up_for_renewal.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
@@ -470,7 +470,7 @@
|
|
diff --git a/templates/messages_notification.html b/templates/messages_notification.html
index 6eff990157..f36e8e986d 100644
--- a/templates/messages_notification.html
+++ b/templates/messages_notification.html
@@ -118,7 +118,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
diff --git a/templates/partials/cloud_laptop_error.mjml b/templates/partials/cloud_laptop_error.mjml
new file mode 100644
index 0000000000..d184f38011
--- /dev/null
+++ b/templates/partials/cloud_laptop_error.mjml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/templates/partials/cloud_laptop_warning.mjml b/templates/partials/cloud_laptop_warning.mjml
new file mode 100644
index 0000000000..e205abbc5d
--- /dev/null
+++ b/templates/partials/cloud_laptop_warning.mjml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/templates/partials/cloud_questions_hr_email_footer.mjml b/templates/partials/cloud_questions_hr_email_footer.mjml
new file mode 100644
index 0000000000..f8bf2890d3
--- /dev/null
+++ b/templates/partials/cloud_questions_hr_email_footer.mjml
@@ -0,0 +1,23 @@
+
+
+
+ {{.Props.QuestionTitle}}
+
+
+ {{.Props.QuestionInfo}}
+
+ {{.Props.SupportEmail}}
+
+
+
+
+
+
+
+
+
+ {{.Props.Organization}}
+ {{.Props.FooterV2}}
+
+
+
diff --git a/templates/partials/cloud_title_2subtitles_button.mjml b/templates/partials/cloud_title_2subtitles_button.mjml
new file mode 100644
index 0000000000..6f1f2bb22b
--- /dev/null
+++ b/templates/partials/cloud_title_2subtitles_button.mjml
@@ -0,0 +1,16 @@
+
+
+
+ {{.Props.Title}}
+
+
+ {{.Props.SubTitle1}}
+
+
+ {{.Props.SubTitle2}}
+
+
+ {{.Props.Button}}
+
+
+
diff --git a/templates/partials/cloud_title_3subtitles_button.mjml b/templates/partials/cloud_title_3subtitles_button.mjml
new file mode 100644
index 0000000000..6e41aef92f
--- /dev/null
+++ b/templates/partials/cloud_title_3subtitles_button.mjml
@@ -0,0 +1,24 @@
+
+
+
+ {{.Props.Title}}
+
+
+ {{.Props.SubTitle1}}
+
+
+ {{.Props.SubTitle2}}
+
+
+ {{.Props.SubTitle3}}
+
+
+ {{.Props.Button}}
+
+ {{if .IncludeSecondaryActionButton}}
+
+ {{.Props.SecondaryActionButtonText}}
+
+ {{end}}
+
+
diff --git a/templates/partials/cloud_upgrade_person.mjml b/templates/partials/cloud_upgrade_person.mjml
new file mode 100644
index 0000000000..ecd94d8735
--- /dev/null
+++ b/templates/partials/cloud_upgrade_person.mjml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/templates/partials/style.css b/templates/partials/style.css
index 0e303632a4..f353ecf4a6 100644
--- a/templates/partials/style.css
+++ b/templates/partials/style.css
@@ -6,7 +6,7 @@
.emailBody a{
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
diff --git a/templates/reset_body.html b/templates/reset_body.html
index 740e9574eb..3f1d3c95ce 100644
--- a/templates/reset_body.html
+++ b/templates/reset_body.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
diff --git a/templates/verify_body.html b/templates/verify_body.html
index 27811f5a82..8f0acf64cc 100644
--- a/templates/verify_body.html
+++ b/templates/verify_body.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
diff --git a/templates/welcome_body.html b/templates/welcome_body.html
index 91775108cb..4954cf7b85 100644
--- a/templates/welcome_body.html
+++ b/templates/welcome_body.html
@@ -98,7 +98,7 @@
.emailBody a {
text-decoration: none !important;
- color: #1C58D9 !important;
+ color: #1C58D9;
}
.title div {
|