[MM-29999] User Limit Email Notifications (Part 2) (#16220)
* Adding files commiting to do something else * Stashing to merge master * Adding files, commit of working code, pre-test * Changing up logic according to new conversations, adding template and functions for 7 day and 14 day emails * Add subscription active check * Add a comment around subscription status * Fix i18n * Update jobs/cloud/worker.go Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com> * Add a check for cloud license and exit early * remove log * Update jobs/cloud/worker.go Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com> * Remove case for 91st day * Change var name * Fix i18n * Change value for dates in email * Add email template and send logic for support email on 30th day of arrears * Fix EETests? * add one to the day so the count starts on first day of next cycle * use the license for the enabled check * Moved scheduler and worker into enterprise * Add the user count to the support email * remove a line * Use the date for the 14 day email * Fix for design review * More font changes * Fixes for PR * Change feature flag name * Add cloud_interface to einterfaces/jobs * Add back & for running job server * Remove unused constant Co-authored-by: Maria A Nunez <maria.nunez@mattermost.com> Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
@@ -119,6 +119,11 @@ func (a *App) initJobs() {
|
||||
if jobsActiveUsersInterface != nil {
|
||||
a.srv.Jobs.ActiveUsers = jobsActiveUsersInterface(a)
|
||||
}
|
||||
|
||||
if jobsCloudInterface != nil {
|
||||
a.srv.Jobs.Cloud = jobsCloudInterface(a.srv)
|
||||
}
|
||||
|
||||
a.srv.Jobs.Workers = a.srv.Jobs.InitWorkers()
|
||||
a.srv.Jobs.Schedulers = a.srv.Jobs.InitSchedulers()
|
||||
}
|
||||
|
||||
133
app/email.go
133
app/email.go
@@ -11,6 +11,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
|
||||
@@ -633,3 +634,135 @@ func (es *EmailService) SendOverUserLimitWarningEmail(email string, locale strin
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserLimitThirtyDayWarningEmail(email string, locale string, siteURL string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_30_days_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_30_days_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_30_days_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_30_days_info1")
|
||||
bodyPage.Props["Info2"] = T("api.templates.over_limit_30_days_info2")
|
||||
bodyPage.Props["Info2Item1"] = T("api.templates.over_limit_30_days_info2_item1")
|
||||
bodyPage.Props["Info2Item2"] = T("api.templates.over_limit_30_days_info2_item2")
|
||||
bodyPage.Props["Info2Item3"] = T("api.templates.over_limit_30_days_info2_item3")
|
||||
bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserLimitNinetyDayWarningEmail(email string, locale string, siteURL string, overLimitDate string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_90_days_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_90_days_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_90_days_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_90_days_info1", map[string]interface{}{"OverLimitDate": overLimitDate})
|
||||
bodyPage.Props["Info2"] = T("api.templates.over_limit_90_days_info2")
|
||||
bodyPage.Props["Info3"] = T("api.templates.over_limit_90_days_info3")
|
||||
bodyPage.Props["Info4"] = T("api.templates.over_limit_90_days_info4")
|
||||
bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserLimitWorkspaceSuspendedWarningEmail(email string, locale string, siteURL string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_suspended_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_workspace_suspended_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_suspended_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_suspended_info1")
|
||||
bodyPage.Props["Info2"] = T("api.templates.over_limit_suspended_info2")
|
||||
bodyPage.Props["Button"] = T("api.templates.over_limit_suspended_contact_support")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserFourteenDayWarningEmail(email string, locale string, siteURL string, overLimitDate string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_14_days_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_7_days_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_14_days_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_14_days_info1", map[string]interface{}{"OverLimitDate": overLimitDate})
|
||||
bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendOverUserSevenDayWarningEmail(email string, locale string, siteURL string) (bool, *model.AppError) {
|
||||
T := utils.GetUserTranslations(locale)
|
||||
|
||||
subject := T("api.templates.over_limit_7_days_subject")
|
||||
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_7_days_body", locale)
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["Title"] = T("api.templates.over_limit_7_days_title")
|
||||
bodyPage.Props["Info1"] = T("api.templates.over_limit_7_days_info1")
|
||||
bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now")
|
||||
bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at")
|
||||
|
||||
bodyPage.Props["Footer"] = T("api.templates.copyright")
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (es *EmailService) SendSuspensionEmailToSupport(email string, installationID string, customerID string, subscriptionID string, siteURL string, userCount int64) (bool, *model.AppError) {
|
||||
// Localization not needed
|
||||
|
||||
subject := fmt.Sprintf("Cloud Installation %s Scheduled Suspension", installationID)
|
||||
bodyPage := es.newEmailTemplate("over_user_limit_support_body", "en")
|
||||
bodyPage.Props["CustomerID"] = customerID
|
||||
bodyPage.Props["SiteURL"] = siteURL
|
||||
bodyPage.Props["SubscriptionID"] = subscriptionID
|
||||
bodyPage.Props["InstallationID"] = installationID
|
||||
bodyPage.Props["SuspensionDate"] = time.Now().AddDate(0, 0, 61).Format("2006-01-02")
|
||||
bodyPage.Props["UserCount"] = userCount
|
||||
|
||||
if err := es.sendMail(email, subject, bodyPage.Render()); err != nil {
|
||||
return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message, http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -96,6 +96,12 @@ func RegisterJobsActiveUsersInterface(f func(*App) tjobs.ActiveUsersJobInterface
|
||||
jobsActiveUsersInterface = f
|
||||
}
|
||||
|
||||
var jobsCloudInterface func(*Server) ejobs.CloudJobInterface
|
||||
|
||||
func RegisterJobsCloudInterface(f func(*Server) ejobs.CloudJobInterface) {
|
||||
jobsCloudInterface = f
|
||||
}
|
||||
|
||||
var jobsExpiryNotifyInterface func(*App) tjobs.ExpiryNotifyJobInterface
|
||||
|
||||
func RegisterJobsExpiryNotifyJobInterface(f func(*App) tjobs.ExpiryNotifyJobInterface) {
|
||||
|
||||
11
einterfaces/jobs/cloud_interface.go
Обычный файл
11
einterfaces/jobs/cloud_interface.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package jobs
|
||||
|
||||
import "github.com/mattermost/mattermost-server/v5/model"
|
||||
|
||||
type CloudJobInterface interface {
|
||||
MakeWorker() model.Worker
|
||||
MakeScheduler() model.Scheduler
|
||||
}
|
||||
100
i18n/en.json
100
i18n/en.json
@@ -2726,6 +2726,86 @@
|
||||
"id": "api.templates.mfa_deactivated_body.title",
|
||||
"translation": "Multi-factor authentication was removed"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_14_days_info1",
|
||||
"translation": "Just a reminder that we have not received payment for your Mattermost subscription invoiced on {{ .OverLimitDate }}. We will soon begin a process to suspend your service if payment is not received."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_14_days_subject",
|
||||
"translation": "Payment is overdue for your Mattermost Cloud subscription"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_14_days_title",
|
||||
"translation": "Payment not received"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_info1",
|
||||
"translation": "There’s still time to keep your Mattermost Cloud workspace active by resolving issues with your payment method. To avoid suspension update your method of payment."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_info2",
|
||||
"translation": "If no action is taken, your workspace will be suspended"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_info2_item1",
|
||||
"translation": "You won't be able to log into your workspace"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_info2_item2",
|
||||
"translation": "You won’t be able to update payment information without contacting our support team"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_info2_item3",
|
||||
"translation": "You will lose access to your message history"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_subject",
|
||||
"translation": " Act to keep your Mattermost Cloud subscription"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_30_days_title",
|
||||
"translation": "Mattermost Cloud Workspace Suspension"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_7_days_info1",
|
||||
"translation": "Mattermost couldn’t process your most recent automatic payment. To keep your service active, please add payment details as soon as possible or your service may be suspended."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_7_days_subject",
|
||||
"translation": "Payment is overdue for your Mattermost Cloud subscription"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_7_days_title",
|
||||
"translation": "No payment method on file"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_info1",
|
||||
"translation": "This is a final reminder that we have not received payment for your Mattermost Cloud workspace, which has been overdue since {{ .OverLimitDate }}. Tomorrow we will suspend your service."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_info2",
|
||||
"translation": "To avoid suspension, add your payment information"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_info3",
|
||||
"translation": "Once your workspace has been suspended, you will not be able to log in to your account or update payment information. You will need to contact our support team to re-activate your service."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_info4",
|
||||
"translation": "Once your workspace is suspended, all content and data within your workspace will be deleted within 1-3 months."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_subject",
|
||||
"translation": "Your Mattermost Cloud subscription will soon be suspended"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_90_days_title",
|
||||
"translation": "Your Mattermost Cloud workspace suspension is scheduled for tomorrow"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_fix_now",
|
||||
"translation": "Fix Now"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_info1",
|
||||
"translation": "It looks like you have more than 10 users in your workspace which is beyond the free tier limits of Mattermost Cloud Professional. To avoid any disruption in your Mattermost workspace, please upgrade."
|
||||
@@ -2738,6 +2818,26 @@
|
||||
"id": "api.templates.over_limit_subject",
|
||||
"translation": "Mattermost Cloud Workspace Over User Limit"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_suspended_contact_support",
|
||||
"translation": "Contact Support"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_suspended_info1",
|
||||
"translation": "Your Mattermost workspace has been suspended. All contents and data within your workspace will be deleted within 1-3 months."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_suspended_info2",
|
||||
"translation": "Contact us to reactivate your workspace."
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_suspended_subject",
|
||||
"translation": "Your Mattermost Cloud subscription has been suspended"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_suspended_title",
|
||||
"translation": "Mattermost Cloud Workspace suspended"
|
||||
},
|
||||
{
|
||||
"id": "api.templates.over_limit_title",
|
||||
"translation": "Your workspace is over the user limit for the free tier"
|
||||
|
||||
@@ -149,6 +149,13 @@ func (watcher *Watcher) PollAndNotify() {
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else if job.Type == model.JOB_TYPE_CLOUD {
|
||||
if watcher.workers.Cloud != nil {
|
||||
select {
|
||||
case watcher.workers.Cloud.JobChannel() <- *job:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,10 @@ func (srv *JobServer) InitSchedulers() *Schedulers {
|
||||
schedulers.schedulers = append(schedulers.schedulers, productNoticesInterface.MakeScheduler())
|
||||
}
|
||||
|
||||
if cloudInterface := srv.Cloud; cloudInterface != nil {
|
||||
schedulers.schedulers = append(schedulers.schedulers, cloudInterface.MakeScheduler())
|
||||
}
|
||||
|
||||
schedulers.nextRunTimes = make([]*time.Time, len(schedulers.schedulers))
|
||||
return schedulers
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ type JobServer struct {
|
||||
ExpiryNotify tjobs.ExpiryNotifyJobInterface
|
||||
ProductNotices tjobs.ProductNoticesJobInterface
|
||||
ActiveUsers tjobs.ActiveUsersJobInterface
|
||||
Cloud ejobs.CloudJobInterface
|
||||
}
|
||||
|
||||
func NewJobServer(configService configservice.ConfigService, store store.Store) *JobServer {
|
||||
|
||||
@@ -27,6 +27,7 @@ type Workers struct {
|
||||
ExpiryNotify model.Worker
|
||||
ProductNotices model.Worker
|
||||
ActiveUsers model.Worker
|
||||
Cloud model.Worker
|
||||
|
||||
listenerId string
|
||||
}
|
||||
@@ -80,6 +81,11 @@ func (srv *JobServer) InitWorkers() *Workers {
|
||||
if productNoticesInterface := srv.ProductNotices; productNoticesInterface != nil {
|
||||
workers.ProductNotices = productNoticesInterface.MakeWorker()
|
||||
}
|
||||
|
||||
if cloudInterface := srv.Cloud; cloudInterface != nil {
|
||||
workers.Cloud = cloudInterface.MakeWorker()
|
||||
}
|
||||
|
||||
return workers
|
||||
}
|
||||
|
||||
@@ -131,6 +137,10 @@ func (workers *Workers) Start() *Workers {
|
||||
go workers.ProductNotices.Run()
|
||||
}
|
||||
|
||||
if workers.Cloud != nil {
|
||||
go workers.Cloud.Run()
|
||||
}
|
||||
|
||||
go workers.Watcher.Start()
|
||||
})
|
||||
|
||||
@@ -239,6 +249,10 @@ func (workers *Workers) Stop() *Workers {
|
||||
workers.ProductNotices.Stop()
|
||||
}
|
||||
|
||||
if workers.Cloud != nil {
|
||||
workers.Cloud.Stop()
|
||||
}
|
||||
|
||||
mlog.Info("Stopped workers")
|
||||
|
||||
return workers
|
||||
|
||||
@@ -81,6 +81,7 @@ type Subscription struct {
|
||||
EndAt int64 `json:"end_at"`
|
||||
CreateAt int64 `json:"create_at"`
|
||||
Seats int `json:"seats"`
|
||||
Status string `json:"status"`
|
||||
DNS string `json:"dns"`
|
||||
IsPaidTier string `json:"is_paid_tier"`
|
||||
LastInvoice *Invoice `json:"last_invoice"`
|
||||
|
||||
@@ -7,8 +7,12 @@ type FeatureFlags struct {
|
||||
// Exists only for unit and manual testing.
|
||||
// When set to a value, will be returned by the ping endpoint.
|
||||
TestFeature string
|
||||
|
||||
// Toggle on and off scheduled jobs for cloud user limit emails see MM-29999
|
||||
CloudDelinquentEmailJobsEnabled bool
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
f.TestFeature = "off"
|
||||
f.CloudDelinquentEmailJobsEnabled = false
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
JOB_TYPE_EXPIRY_NOTIFY = "expiry_notify"
|
||||
JOB_TYPE_PRODUCT_NOTICES = "product_notices"
|
||||
JOB_TYPE_ACTIVE_USERS = "active_users"
|
||||
JOB_TYPE_CLOUD = "cloud"
|
||||
|
||||
JOB_STATUS_PENDING = "pending"
|
||||
JOB_STATUS_IN_PROGRESS = "in_progress"
|
||||
@@ -65,6 +66,7 @@ func (j *Job) IsValid() *AppError {
|
||||
case JOB_TYPE_PRODUCT_NOTICES:
|
||||
case JOB_TYPE_EXPIRY_NOTIFY:
|
||||
case JOB_TYPE_ACTIVE_USERS:
|
||||
case JOB_TYPE_CLOUD:
|
||||
default:
|
||||
return NewAppError("Job.IsValid", "model.job.is_valid.type.app_error", nil, "id="+j.Id, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ const (
|
||||
SYSTEM_WARN_METRIC_LAST_RUN_TIMESTAMP_KEY = "LastWarnMetricRunTimestamp"
|
||||
AWS_METERING_REPORT_INTERVAL = 1
|
||||
AWS_METERING_DIMENSION_USAGE_HRS = "UsageHrs"
|
||||
USER_LIMIT_OVERAGE_CYCLE_END_DATE = "UserLimitOverageCycleEndDate"
|
||||
OVER_USER_LIMIT_FORGIVEN_COUNT = "OverUserLimitForgivenCount"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
106
templates/over_user_limit_30_days_body.html
Обычный файл
106
templates/over_user_limit_30_days_body.html
Обычный файл
@@ -0,0 +1,106 @@
|
||||
{{define "over_user_limit_30_days_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5"
|
||||
width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 64px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2
|
||||
style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">
|
||||
{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription"
|
||||
target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 16px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
<p style="margin-top: 40px; margin-bottom: 24px; font-weight: bold; font-size: 16px; line-height: 24px; color: #3D3C40; font-family: Arial;">{{.Props.Info2}}</p>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="100%">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5; padding-top: 12px; padding-bottom: 12px; text-align:center; color:#3D3C40; font-size: 16px; font-weight: 400; font-family: Arial;" >{{ .Props.Info2Item1 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5; padding-top: 12px; padding-bottom: 12px; text-align:center; color:#3D3C40; font-size: 16px; font-weight: 400; font-family: Arial;">{{ .Props.Info2Item2 }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5; padding-top: 12px; padding-bottom: 12px; text-align:center; color:#3D3C40; font-size: 16px; font-weight: 400; font-family: Arial;">{{ .Props.Info2Item3 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a
|
||||
href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
92
templates/over_user_limit_7_days_body.html
Обычный файл
92
templates/over_user_limit_7_days_body.html
Обычный файл
@@ -0,0 +1,92 @@
|
||||
{{define "over_user_limit_7_days_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5"
|
||||
width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 32px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2
|
||||
style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">
|
||||
{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin-top: 16px; margin-bottom: 24px; font-weight: bold; font-size: 16px; line-height: 24px; color: #3D3C40">{{.Props.Info2}}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription"
|
||||
target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 16px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a
|
||||
href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
94
templates/over_user_limit_90_days_body.html
Обычный файл
94
templates/over_user_limit_90_days_body.html
Обычный файл
@@ -0,0 +1,94 @@
|
||||
{{define "over_user_limit_90_days_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5"
|
||||
width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 64px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2
|
||||
style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">
|
||||
{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin-top: 16px; margin-bottom: 24px; font-weight: bold; font-size: 16px; line-height: 24px; color: #3D3C40; font-family: Arial">{{.Props.Info2}}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription"
|
||||
target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 16px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
<p style="margin-top: 24px; margin-bottom: 24px; font-weight: normal; font-size: 16px; line-height: 24px; color: #3D3C40; font-family: Arial">{{.Props.Info3}}</p>
|
||||
<p style="margin-top: 16px; margin-bottom: 24px; font-weight: normal; font-size: 16px; line-height: 24px; color: #3D3C40; font-family: Arial">{{.Props.Info4}}</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a
|
||||
href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
6
templates/over_user_limit_support_body.html
Обычный файл
6
templates/over_user_limit_support_body.html
Обычный файл
@@ -0,0 +1,6 @@
|
||||
{{define "over_user_limit_support_body"}}
|
||||
<p>Installation with ID: {{ .Props.InstallationID }} has been in arrears for over 30 days, and is scheduled for suspension on {{ .Props.SuspensionDate }}</p><br>
|
||||
<p>Site URL: {{ .Props.SiteURL }}</p><br>
|
||||
<p>Subscription ID: {{ .Props.SubscriptionID }}</p><br>
|
||||
<p>Number of registered users: {{ .Props.UserCount }}</p>
|
||||
{{end}}
|
||||
92
templates/over_user_limit_workspace_suspended_body.html
Обычный файл
92
templates/over_user_limit_workspace_suspended_body.html
Обычный файл
@@ -0,0 +1,92 @@
|
||||
{{define "over_user_limit_workspace_suspended_body"}}
|
||||
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="margin-top: 20px; line-height: 1.7; color: #555;font-family: Arial; font-style: normal; font-weight: bold;">
|
||||
<tr>
|
||||
<td>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
|
||||
<tr>
|
||||
<td style="border: 1px solid #ddd;">
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 20px 20px 10px; text-align:left;">
|
||||
<img src="{{.Props.SiteURL}}/static/images/logo-email.png" style="opacity: 0.5"
|
||||
width="130px" alt="">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="border-collapse: collapse; max-width: 443px">
|
||||
<tr style="width: 443px">
|
||||
<td>
|
||||
<table border="0" cellpadding="0" cellspacing="0"
|
||||
style="padding: 20px 0 0; text-align: center; margin: 0 auto">
|
||||
<tr>
|
||||
<td style="padding: 0 0 32px;">
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0"
|
||||
width="371px">
|
||||
<tr>
|
||||
<td>
|
||||
<h2
|
||||
style="margin-bottom: 0; text-align: center; color: #3D3C40; font-weight: bold; margin-top: 10px; line-height: 32px; font-size: 28px; font-family: Arial">
|
||||
{{ .Props.Title }}</h2>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p
|
||||
style="font-weight: normal; color: #3D3C40; font-size: 16px; line-height: 24px; font-family: Arial">
|
||||
{{ .Props.Info1 }}</p>
|
||||
<p style="margin-top: 16px; font-weight: bold; font-size: 16px; line-height: 24px; color: #3D3C40">{{.Props.Info2}}</p>
|
||||
<p style="margin: 31px 0 31px">
|
||||
<a href="{{.Props.SiteURL}}/admin_console/billing/subscription"
|
||||
target="_blank"
|
||||
style="font-weight: normal; border-radius: 4px; background: #166DE0; color: #fff;outline: none; min-width: 200px; padding: 11px 24px 11px 24px; font-size: 16px; font-family: Arial; cursor: pointer; -webkit-appearance: none;text-decoration: none; margin-top: 16px;">{{ .Props.Button }}</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="left" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="max-width: 443px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="font-family: Arial; padding: 20px 20px 24px 48px; text-align:left;">
|
||||
<h3 style="margin-bottom: 0;">Questions?</h3>
|
||||
<p style="font-weight: normal; margin-top: 0;">{{ .Props.EmailUs }} <a
|
||||
href="mailto:feedback@mattermost.com">feedback@mattermost.com</a></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspcing="0" width="612px">
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #E5E5E5"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
|
||||
style="font-family: Arial; line-height: 16px; font-size: 12px; background: #FFF;">
|
||||
<tr>
|
||||
<td
|
||||
style="font-weight: normal; text-align: center;color: #AAA;font-size: 11px;padding-bottom: 10px;padding: 0;line-height: 16px;padding-bottom: 48px;padding-top: 4px;">
|
||||
<p>
|
||||
{{.Props.Organization}}<br>
|
||||
{{.Props.Footer}}
|
||||
</p>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
{{end}}
|
||||
Ссылка в новой задаче
Block a user