From 05720f627b91943807efee23d8ffa65030634590 Mon Sep 17 00:00:00 2001 From: Allan Guwatudde Date: Wed, 10 Mar 2021 20:39:21 +0300 Subject: [PATCH] [MM-33198] - Portal: Send admin welcome email after the installation is complete (#17043) * [MM-33198] - Portal: Send admin welcome email after the installation is complete * Send cloud welcome email * Feedback impl-2 * Fix template * Temp undo * Update * make i18n-extract * Translations * Feedback impl-3 * More template fixes Co-authored-by: Mattermod --- api4/cloud.go | 28 +++++++ app/email.go | 42 ++++++++++- app/user.go | 9 ++- i18n/en.json | 76 +++++++++++++++++++ model/cloud.go | 25 +++++-- model/user.go | 1 + templates/cloud_welcome_email.html | 116 +++++++++++++++++++++++++++++ 7 files changed, 287 insertions(+), 10 deletions(-) create mode 100644 templates/cloud_welcome_email.html diff --git a/api4/cloud.go b/api4/cloud.go index 62737f53f8..55e61129da 100644 --- a/api4/cloud.go +++ b/api4/cloud.go @@ -386,6 +386,34 @@ func handleCWSWebhook(c *Context, w http.ResponseWriter, r *http.Request) { c.Err = nErr return } + case model.EventTypeSendAdminWelcomeEmail: + user, appErr := c.App.GetUserByUsername(event.CloudWorkspaceOwner.UserName) + if appErr != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode) + return + } + + teams, appErr := c.App.GetAllTeams() + if appErr != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", appErr.Id, nil, appErr.Error(), appErr.StatusCode) + return + } + + team := teams[0] + + subscription, err := c.App.Cloud().GetSubscription(user.Id) + if err != nil { + c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError) + return + } + + if appErr := c.App.Srv().EmailService.SendCloudWelcomeEmail(user.Email, user.Locale, team.InviteId, subscription.GetWorkSpaceNameFromDNS(), subscription.DNS); appErr != nil { + c.Err = appErr + return + } + default: + c.Err = model.NewAppError("Api4.handleCWSWebhook", "api.cloud.cws_webhook_event_missing_error", nil, "", http.StatusNotFound) + return } ReturnStatusOK(w) diff --git a/app/email.go b/app/email.go index 9ca379dc29..204463f0ae 100644 --- a/app/email.go +++ b/app/email.go @@ -206,7 +206,10 @@ func (es *EmailService) SendSignInChangeEmail(email, method, locale, siteURL str return nil } -func (es *EmailService) sendWelcomeEmail(userID string, email string, verified bool, locale, siteURL, redirect string) *model.AppError { +func (es *EmailService) sendWelcomeEmail(userID string, email string, verified bool, disableWelcomeEmail bool, locale, siteURL, redirect string) *model.AppError { + if disableWelcomeEmail { + return nil + } if !*es.srv.Config().EmailSettings.SendEmailNotifications && !*es.srv.Config().EmailSettings.RequireEmailVerification { return model.NewAppError("SendWelcomeEmail", "api.user.send_welcome_email_and_forget.failed.error", nil, "Send Email Notifications and Require Email Verification is disabled in the system console", http.StatusInternalServerError) } @@ -256,6 +259,43 @@ func (es *EmailService) sendWelcomeEmail(userID string, email string, verified b return nil } +// SendCloudWelcomeEmail sends the cloud version of the welcome email +func (es *EmailService) SendCloudWelcomeEmail(userEmail, locale, teamInviteID, workSpaceName, dns string) *model.AppError { + T := i18n.GetUserTranslations(locale) + subject := T("api.templates.cloud_welcome_email.subject") + + workSpacePath := fmt.Sprintf("https://%s.cloud.mattermost.com", workSpaceName) + + bodyPage := es.newEmailTemplate("cloud_welcome_email", locale) + bodyPage.Props["Title"] = T("api.templates.cloud_welcome_email.title", map[string]interface{}{"WorkSpace": workSpaceName}) + bodyPage.Props["SubTitle"] = T("api.templates.cloud_welcome_email.subtitle") + bodyPage.Props["SubTitleInfo"] = T("api.templates.cloud_welcome_email.subtitle_info") + bodyPage.Props["Info"] = T("api.templates.cloud_welcome_email.info") + bodyPage.Props["Info2"] = T("api.templates.cloud_welcome_email.info2") + bodyPage.Props["WorkSpacePath"] = workSpacePath + bodyPage.Props["DNS"] = dns + bodyPage.Props["InviteInfo"] = T("api.templates.cloud_welcome_email.invite_info") + bodyPage.Props["InviteSubInfo"] = T("api.templates.cloud_welcome_email.invite_sub_info", map[string]interface{}{"WorkSpace": workSpaceName}) + bodyPage.Props["InviteSubInfoLink"] = fmt.Sprintf("%s/signup_user_complete/?id=%s", workSpacePath, teamInviteID) + bodyPage.Props["AddAppsInfo"] = T("api.templates.cloud_welcome_email.add_apps_info") + bodyPage.Props["AddAppsSubInfo"] = T("api.templates.cloud_welcome_email.add_apps_sub_info") + bodyPage.Props["AppMarketPlace"] = T("api.templates.cloud_welcome_email.app_market_place") + bodyPage.Props["AppMarketPlaceLink"] = "https://integrations.mattermost.com/" + bodyPage.Props["DownloadMMInfo"] = T("api.templates.cloud_welcome_email.download_mm_info") + bodyPage.Props["SignInSubInfo"] = T("api.templates.cloud_welcome_email.signin_sub_info") + bodyPage.Props["MMApps"] = T("api.templates.cloud_welcome_email.mm_apps") + bodyPage.Props["SignInSubInfo2"] = T("api.templates.cloud_welcome_email.signin_sub_info2") + bodyPage.Props["DownloadMMAppsLink"] = "https://mattermost.com/download/" + bodyPage.Props["Button"] = T("api.templates.cloud_welcome_email.button") + bodyPage.Props["GettingStartedQuestions"] = T("api.templates.cloud_welcome_email.start_questions") + + if err := es.sendMail(userEmail, subject, bodyPage.Render()); err != nil { + return model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError) + } + + return nil +} + func (es *EmailService) sendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppError { T := i18n.GetUserTranslations(locale) diff --git a/app/user.go b/app/user.go index 52cc519c28..ab123ad567 100644 --- a/app/user.go +++ b/app/user.go @@ -155,7 +155,7 @@ func (a *App) CreateUserWithInviteId(user *model.User, inviteId, redirect string a.AddDirectChannels(team.Id, ruser) - if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil { + if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil { mlog.Warn("Failed to send welcome email on create user with inviteId", mlog.Err(err)) } @@ -168,7 +168,7 @@ func (a *App) CreateUserAsAdmin(user *model.User, redirect string) (*model.User, return nil, err } - if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil { + if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil { mlog.Warn("Failed to send welcome email to the new user, created by system admin", mlog.Err(err)) } @@ -192,7 +192,7 @@ func (a *App) CreateUserFromSignup(user *model.User, redirect string) (*model.Us return nil, err } - if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, a.GetSiteURL(), redirect); err != nil { + if err := a.Srv().EmailService.sendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.DisableWelcomeEmail, ruser.Locale, a.GetSiteURL(), redirect); err != nil { mlog.Warn("Failed to send welcome email on create user from signup", mlog.Err(err)) } @@ -333,6 +333,9 @@ func (a *App) createUser(user *model.User) (*model.User, *model.AppError) { go a.UpdateViewedProductNoticesForNewUser(ruser.Id) ruser.Sanitize(map[string]bool{}) + + // Determine whether to send the created user a welcome email + ruser.DisableWelcomeEmail = user.DisableWelcomeEmail return ruser, nil } diff --git a/i18n/en.json b/i18n/en.json index 24b60595e8..28c3835fb7 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -479,6 +479,10 @@ "id": "api.cloud.app_error", "translation": "Internal error during cloud api request." }, + { + "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.get_admins_emails.error", "translation": "Error getting system admins email." @@ -2678,6 +2682,74 @@ "id": "api.templates.at_limit_title", "translation": "You’ve reached the user limit for the free tier " }, + { + "id": "api.templates.cloud_welcome_email.add_apps_info", + "translation": "Add apps to your workspace" + }, + { + "id": "api.templates.cloud_welcome_email.add_apps_sub_info", + "translation": "Streamline your work with tools like Github, Google Calendar and Chrome. Explore all of the integrations we have on our" + }, + { + "id": "api.templates.cloud_welcome_email.app_market_place", + "translation": "app marketplace." + }, + { + "id": "api.templates.cloud_welcome_email.button", + "translation": "Open Mattermost" + }, + { + "id": "api.templates.cloud_welcome_email.download_mm_info", + "translation": "Download the Mattermost App" + }, + { + "id": "api.templates.cloud_welcome_email.info", + "translation": "Thanks for creating " + }, + { + "id": "api.templates.cloud_welcome_email.info2", + "translation": "Make sure to save or bookmark your link for future use." + }, + { + "id": "api.templates.cloud_welcome_email.invite_info", + "translation": "Invite people to your workspace" + }, + { + "id": "api.templates.cloud_welcome_email.invite_sub_info", + "translation": "Share this link to invite your members to join {{.WorkSpace}}:" + }, + { + "id": "api.templates.cloud_welcome_email.mm_apps", + "translation": "mobile and desktop apps" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info", + "translation": "Sign into your workspace on our" + }, + { + "id": "api.templates.cloud_welcome_email.signin_sub_info2", + "translation": "for the best experience on PC, Mac, iOS and Android." + }, + { + "id": "api.templates.cloud_welcome_email.start_questions", + "translation": "Having questions about getting started? Email us at" + }, + { + "id": "api.templates.cloud_welcome_email.subject", + "translation": "Congratulations!" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle", + "translation": "Set up your workspace" + }, + { + "id": "api.templates.cloud_welcome_email.subtitle_info", + "translation": "Take the following steps to build out your teams and get the most out of your workspace." + }, + { + "id": "api.templates.cloud_welcome_email.title", + "translation": "Congratulations - your {{.WorkSpace}} workspace is ready to go!" + }, { "id": "api.templates.copyright", "translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301" @@ -3546,6 +3618,10 @@ "id": "api.user.saml.not_available.app_error", "translation": "SAML 2.0 is not configured or supported on this server." }, + { + "id": "api.user.send_cloud_welcome_email.error", + "translation": "Failed to send cloud welcome email" + }, { "id": "api.user.send_deactivate_email_and_forget.failed.error", "translation": "Failed to send the deactivate account email successfully" diff --git a/model/cloud.go b/model/cloud.go index da5b754a71..3a348d6dd6 100644 --- a/model/cloud.go +++ b/model/cloud.go @@ -3,11 +3,14 @@ package model +import "strings" + const ( - EventTypeFailedPayment = "failed-payment" - EventTypeFailedPaymentNoCard = "failed-payment-no-card" - JoinLimitation = "join" - InviteLimitation = "invite" + EventTypeFailedPayment = "failed-payment" + EventTypeFailedPaymentNoCard = "failed-payment-no-card" + EventTypeSendAdminWelcomeEmail = "send-admin-welcome-email" + JoinLimitation = "join" + InviteLimitation = "invite" ) // Product model represents a product on the cloud system. @@ -94,6 +97,11 @@ type Subscription struct { LastInvoice *Invoice `json:"last_invoice"` } +// GetWorkSpaceNameFromDNS returns the work space name. For example from test.mattermost.cloud.com, it returns test +func (s *Subscription) GetWorkSpaceNameFromDNS() string { + return strings.Split(s.DNS, ".")[0] +} + // Invoice model represents a cloud invoice type Invoice struct { ID string `json:"id"` @@ -121,8 +129,9 @@ type InvoiceLineItem struct { } type CWSWebhookPayload struct { - Event string `json:"event"` - FailedPayment *FailedPayment `json:"failed_payment"` + Event string `json:"event"` + FailedPayment *FailedPayment `json:"failed_payment"` + CloudWorkspaceOwner *CloudWorkspaceOwner `json:"cloud_workspace_owner"` } type FailedPayment struct { @@ -131,6 +140,10 @@ type FailedPayment struct { FailureMessage string `json:"failure_message"` } +// CloudWorkspaceOwner is part of the CWS Webhook payload that contains information about the user that created the workspace from the CWS +type CloudWorkspaceOwner struct { + UserName string `json:"username"` +} type SubscriptionStats struct { RemainingSeats int `json:"remaining_seats"` IsPaidTier string `json:"is_paid_tier"` diff --git a/model/user.go b/model/user.go index 2957bff68c..65ecfa0200 100644 --- a/model/user.go +++ b/model/user.go @@ -98,6 +98,7 @@ type User struct { BotLastIconUpdate int64 `db:"-" json:"bot_last_icon_update,omitempty"` TermsOfServiceId string `db:"-" json:"terms_of_service_id,omitempty"` TermsOfServiceCreateAt int64 `db:"-" json:"terms_of_service_create_at,omitempty"` + DisableWelcomeEmail bool `db:"-" json:"disable_welcome_email"` } //msgp UserMap diff --git a/templates/cloud_welcome_email.html b/templates/cloud_welcome_email.html new file mode 100644 index 0000000000..20c28cd666 --- /dev/null +++ b/templates/cloud_welcome_email.html @@ -0,0 +1,116 @@ +{{define "cloud_welcome_email"}} + + +
+ +
+
+ logo_email_blue +
+
+

+ {{.Props.Title}} +

+

+ {{.Props.Info}} {{.Props.DNS}}. {{.Props.Info2}} +

+

+ + {{.Props.Button }} + +

+
+ +
+

+ {{.Props.SubTitle}} +

+

+ {{.Props.SubTitleInfo}} +

+
+ +
+ + + + + + + {{.Props.InviteInfo}} + +

+ {{.Props.InviteSubInfo}} + {{.Props.InviteSubInfoLink}} +

+
+ +
+ + + + + + + {{.Props.AddAppsInfo}} + +

+ {{.Props.AddAppsSubInfo}} {{.Props.AppMarketPlace}} +

+
+ +
+ + + + + + + {{.Props.DownloadMMInfo}} + +

+ {{.Props.SignInSubInfo}} {{.Props.MMApps}} {{.Props.SignInSubInfo2}} +

+
+ +
+

+ {{.Props.GettingStartedQuestions}} support@mattermost.com. +

+
+
+ invite_illustration +
+
+

+ © 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301 +

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