From 95b080985028794ef26e1dfc1b37c7234a326444 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20Espino?= Date: Fri, 12 Mar 2021 18:46:43 +0100 Subject: [PATCH] Extracting html templates into a library (#16946) * Extracting html templates into a library * Moving tests to the right place * Fixing tests * Addressing PR review comments * Addressing PR review comments * Replacing attomic with RWMutex * Returning errors as channel for Templates watcher * Address PR review comments * Other small fixes * Simplifying NewWithWatcher * Addressing PR review comments * Making error handling on rendering templates more robust * Fixing tests * Changing how we return errors * Fixing shadow variables * Addressing PR review comments * Logging errors from the outside of sendNotificationEmail * Fixing lock in shutdown * Fixing the resource copy for commands tests temporary directories * Removing unused import * A couple of tiny fixes --- app/app.go | 44 +- app/email.go | 687 ++++++++++++-------- app/email_batching.go | 54 +- app/email_batching_test.go | 6 +- app/notification.go | 5 +- app/notification_email.go | 63 +- app/notification_email_test.go | 54 +- app/server.go | 23 +- cmd/mattermost/commands/cmdtestlib.go | 4 +- cmd/mattermost/commands/config_flag_test.go | 9 +- shared/templates/templates.go | 143 ++++ shared/templates/templates_test.go | 115 ++++ testlib/resources.go | 1 + utils/html.go | 118 ---- utils/html_test.go | 86 --- web/unsupported_browser.go | 26 +- 16 files changed, 834 insertions(+), 604 deletions(-) create mode 100644 shared/templates/templates.go create mode 100644 shared/templates/templates_test.go delete mode 100644 utils/html.go delete mode 100644 utils/html_test.go diff --git a/app/app.go b/app/app.go index 804fa1c1fb..0b283b44f2 100644 --- a/app/app.go +++ b/app/app.go @@ -6,7 +6,6 @@ package app import ( "context" "fmt" - "html/template" "net/http" "strconv" "strings" @@ -21,6 +20,7 @@ import ( "github.com/mattermost/mattermost-server/v5/services/timezones" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mlog" + "github.com/mattermost/mattermost-server/v5/shared/templates" "github.com/mattermost/mattermost-server/v5/utils" ) @@ -148,12 +148,8 @@ func (a *App) TelemetryId() string { return a.Srv().TelemetryId() } -func (s *Server) HTMLTemplates() *template.Template { - if s.htmlTemplateWatcher != nil { - return s.htmlTemplateWatcher.Templates() - } - - return nil +func (s *Server) TemplatesContainer() *templates.Container { + return s.htmlTemplateWatcher } func (a *App) Handle404(w http.ResponseWriter, r *http.Request) { @@ -467,25 +463,25 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, i18n.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusInternalServerError) } T := i18n.GetUserTranslations(sender.Locale) - bodyPage := a.Srv().EmailService.newEmailTemplate("warn_metric_ack", sender.Locale) - bodyPage.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header") - bodyPage.Props["ContactNameValue"] = sender.GetFullName() - bodyPage.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header") - bodyPage.Props["ContactEmailValue"] = sender.Email + data := a.Srv().EmailService.newEmailTemplateData(sender.Locale) + data.Props["ContactNameHeader"] = T("api.templates.warn_metric_ack.body.contact_name_header") + data.Props["ContactNameValue"] = sender.GetFullName() + data.Props["ContactEmailHeader"] = T("api.templates.warn_metric_ack.body.contact_email_header") + data.Props["ContactEmailValue"] = sender.Email //same definition as the active users count metric displayed in the SystemConsole Analytics section registeredUsersCount, cerr := a.Srv().Store.User().Count(model.UserCountOptions{}) if cerr != nil { mlog.Warn("Error retrieving the number of registered users", mlog.Err(cerr)) } else { - bodyPage.Props["RegisteredUsersHeader"] = T("api.templates.warn_metric_ack.body.registered_users_header") - bodyPage.Props["RegisteredUsersValue"] = registeredUsersCount + data.Props["RegisteredUsersHeader"] = T("api.templates.warn_metric_ack.body.registered_users_header") + data.Props["RegisteredUsersValue"] = registeredUsersCount } - bodyPage.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header") - bodyPage.Props["SiteURL"] = a.GetSiteURL() - bodyPage.Props["TelemetryIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header") - bodyPage.Props["TelemetryIdValue"] = a.TelemetryId() - bodyPage.Props["Footer"] = T("api.templates.warn_metric_ack.footer") + data.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header") + data.Props["SiteURL"] = a.GetSiteURL() + data.Props["TelemetryIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header") + data.Props["TelemetryIdValue"] = a.TelemetryId() + data.Props["Footer"] = T("api.templates.warn_metric_ack.footer") warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T, false) if warnMetricStatus == nil { @@ -493,10 +489,16 @@ func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, } subject := T("api.templates.warn_metric_ack.subject") - bodyPage.Props["Title"] = warnMetricDisplayTexts.EmailBody + data.Props["Title"] = warnMetricDisplayTexts.EmailBody mailConfig := a.Srv().MailServiceConfig() - if err := mailservice.SendMailUsingConfig(model.MM_SUPPORT_ADVISOR_ADDRESS, subject, bodyPage.Render(), mailConfig, false, sender.Email); err != nil { + + body, err := a.Srv().TemplatesContainer().RenderToString("warn_metric_ack", data) + if err != nil { + return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError) + } + + if err := mailservice.SendMailUsingConfig(model.MM_SUPPORT_ADVISOR_ADDRESS, subject, body, mailConfig, false, sender.Email); err != nil { return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError) } } diff --git a/app/email.go b/app/email.go index 204463f0ae..6fdc2a9f25 100644 --- a/app/email.go +++ b/app/email.go @@ -7,6 +7,7 @@ import ( "bytes" "encoding/json" "fmt" + "html/template" "io" "net/http" "net/url" @@ -22,7 +23,7 @@ import ( "github.com/mattermost/mattermost-server/v5/services/mailservice" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mlog" - "github.com/mattermost/mattermost-server/v5/utils" + "github.com/mattermost/mattermost-server/v5/shared/templates" ) const ( @@ -94,14 +95,19 @@ func (es *EmailService) sendChangeUsernameEmail(newUsername, email, locale, site map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "TeamDisplayName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("email_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.username_change_body.title") - bodyPage.Props["Info"] = T("api.templates.username_change_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.username_change_body.title") + data.Props["Info"] = T("api.templates.username_change_body.info", map[string]interface{}{"TeamDisplayName": es.srv.Config().TeamSettings.SiteName, "NewUsername": newUsername}) - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("email_change_body", data) + if err != nil { + return model.NewAppError("sendChangeUsernameEmail", "api.user.send_email_change_username_and_forget.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("sendChangeUsernameEmail", "api.user.send_email_change_username_and_forget.error", nil, err.Error(), http.StatusInternalServerError) } @@ -117,15 +123,20 @@ func (es *EmailService) sendEmailChangeVerifyEmail(newUserEmail, locale, siteURL map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "TeamDisplayName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("email_change_verify_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.email_change_verify_body.title") - bodyPage.Props["Info"] = T("api.templates.email_change_verify_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.email_change_verify_body.title") + data.Props["Info"] = T("api.templates.email_change_verify_body.info", map[string]interface{}{"TeamDisplayName": es.srv.Config().TeamSettings.SiteName}) - bodyPage.Props["VerifyUrl"] = link - bodyPage.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button") + data.Props["VerifyUrl"] = link + data.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button") - if err := es.sendMail(newUserEmail, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("email_change_verify_body", data) + if err != nil { + return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(newUserEmail, subject, body); err != nil { return model.NewAppError("sendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) } @@ -139,14 +150,19 @@ func (es *EmailService) sendEmailChangeEmail(oldEmail, newEmail, locale, siteURL map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "TeamDisplayName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("email_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.email_change_body.title") - bodyPage.Props["Info"] = T("api.templates.email_change_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.email_change_body.title") + data.Props["Info"] = T("api.templates.email_change_body.info", map[string]interface{}{"TeamDisplayName": es.srv.Config().TeamSettings.SiteName, "NewEmail": newEmail}) - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(oldEmail, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("email_change_body", data) + if err != nil { + return model.NewAppError("sendEmailChangeEmail", "api.user.send_email_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(oldEmail, subject, body); err != nil { return model.NewAppError("sendEmailChangeEmail", "api.user.send_email_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) } @@ -166,20 +182,25 @@ func (es *EmailService) sendVerifyEmail(userEmail, locale, siteURL, token, redir subject := T("api.templates.verify_subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("verify_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.verify_body.title") - bodyPage.Props["SubTitle1"] = T("api.templates.verify_body.subTitle1") - bodyPage.Props["ServerURL"] = T("api.templates.verify_body.serverURL", map[string]interface{}{"ServerURL": serverURL}) - bodyPage.Props["SubTitle2"] = T("api.templates.verify_body.subTitle2") - bodyPage.Props["ButtonURL"] = link - bodyPage.Props["Button"] = T("api.templates.verify_body.button") - bodyPage.Props["Info"] = T("api.templates.verify_body.info") - bodyPage.Props["Info1"] = T("api.templates.verify_body.info1") - bodyPage.Props["QuestionTitle"] = T("api.templates.questions_footer.title") - bodyPage.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.verify_body.title") + data.Props["SubTitle1"] = T("api.templates.verify_body.subTitle1") + data.Props["ServerURL"] = T("api.templates.verify_body.serverURL", map[string]interface{}{"ServerURL": serverURL}) + data.Props["SubTitle2"] = T("api.templates.verify_body.subTitle2") + data.Props["ButtonURL"] = link + data.Props["Button"] = T("api.templates.verify_body.button") + data.Props["Info"] = T("api.templates.verify_body.info") + data.Props["Info1"] = T("api.templates.verify_body.info1") + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") - if err := es.sendMail(userEmail, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("verify_body", data) + if err != nil { + return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(userEmail, subject, body); err != nil { return model.NewAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) } @@ -192,14 +213,19 @@ func (es *EmailService) SendSignInChangeEmail(email, method, locale, siteURL str subject := T("api.templates.signin_change_email.subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("signin_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title") - bodyPage.Props["Info"] = T("api.templates.signin_change_email.body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.signin_change_email.body.title") + data.Props["Info"] = T("api.templates.signin_change_email.body.info", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "Method": method}) - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("signin_change_body", data) + if err != nil { + return model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) } @@ -222,22 +248,22 @@ func (es *EmailService) sendWelcomeEmail(userID string, email string, verified b map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "ServerURL": serverURL}) - bodyPage := es.newEmailTemplate("welcome_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.welcome_body.title") - bodyPage.Props["SubTitle1"] = T("api.templates.welcome_body.subTitle1") - bodyPage.Props["ServerURL"] = T("api.templates.welcome_body.serverURL", map[string]interface{}{"ServerURL": serverURL}) - bodyPage.Props["SubTitle2"] = T("api.templates.welcome_body.subTitle2") - bodyPage.Props["Button"] = T("api.templates.welcome_body.button") - bodyPage.Props["Info"] = T("api.templates.welcome_body.info") - bodyPage.Props["Info1"] = T("api.templates.welcome_body.info1") - bodyPage.Props["SiteURL"] = siteURL + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.welcome_body.title") + data.Props["SubTitle1"] = T("api.templates.welcome_body.subTitle1") + data.Props["ServerURL"] = T("api.templates.welcome_body.serverURL", map[string]interface{}{"ServerURL": serverURL}) + data.Props["SubTitle2"] = T("api.templates.welcome_body.subTitle2") + data.Props["Button"] = T("api.templates.welcome_body.button") + data.Props["Info"] = T("api.templates.welcome_body.info") + data.Props["Info1"] = T("api.templates.welcome_body.info1") + data.Props["SiteURL"] = siteURL if *es.srv.Config().NativeAppSettings.AppDownloadLink != "" { - bodyPage.Props["AppDownloadTitle"] = T("api.templates.welcome_body.app_download_title") - bodyPage.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info") - bodyPage.Props["AppDownloadButton"] = T("api.templates.welcome_body.app_download_button") - bodyPage.Props["AppDownloadLink"] = *es.srv.Config().NativeAppSettings.AppDownloadLink + data.Props["AppDownloadTitle"] = T("api.templates.welcome_body.app_download_title") + data.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info") + data.Props["AppDownloadButton"] = T("api.templates.welcome_body.app_download_button") + data.Props["AppDownloadLink"] = *es.srv.Config().NativeAppSettings.AppDownloadLink } if !verified && *es.srv.Config().EmailSettings.RequireEmailVerification { @@ -249,10 +275,15 @@ func (es *EmailService) sendWelcomeEmail(userID string, email string, verified b if redirect != "" { link += fmt.Sprintf("&redirect_to=%s", redirect) } - bodyPage.Props["ButtonURL"] = link + data.Props["ButtonURL"] = link } - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("welcome_body", data) + if err != nil { + return model.NewAppError("sendWelcomeEmail", "api.user.send_welcome_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("sendWelcomeEmail", "api.user.send_welcome_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) } @@ -266,30 +297,35 @@ func (es *EmailService) SendCloudWelcomeEmail(userEmail, locale, teamInviteID, w 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") + data := es.newEmailTemplateData(locale) + data.Props["Title"] = T("api.templates.cloud_welcome_email.title", map[string]interface{}{"WorkSpace": workSpaceName}) + data.Props["SubTitle"] = T("api.templates.cloud_welcome_email.subtitle") + data.Props["SubTitleInfo"] = T("api.templates.cloud_welcome_email.subtitle_info") + data.Props["Info"] = T("api.templates.cloud_welcome_email.info") + data.Props["Info2"] = T("api.templates.cloud_welcome_email.info2") + data.Props["WorkSpacePath"] = workSpacePath + data.Props["DNS"] = dns + data.Props["InviteInfo"] = T("api.templates.cloud_welcome_email.invite_info") + data.Props["InviteSubInfo"] = T("api.templates.cloud_welcome_email.invite_sub_info", map[string]interface{}{"WorkSpace": workSpaceName}) + data.Props["InviteSubInfoLink"] = fmt.Sprintf("%s/signup_user_complete/?id=%s", workSpacePath, teamInviteID) + data.Props["AddAppsInfo"] = T("api.templates.cloud_welcome_email.add_apps_info") + data.Props["AddAppsSubInfo"] = T("api.templates.cloud_welcome_email.add_apps_sub_info") + data.Props["AppMarketPlace"] = T("api.templates.cloud_welcome_email.app_market_place") + data.Props["AppMarketPlaceLink"] = "https://integrations.mattermost.com/" + data.Props["DownloadMMInfo"] = T("api.templates.cloud_welcome_email.download_mm_info") + data.Props["SignInSubInfo"] = T("api.templates.cloud_welcome_email.signin_sub_info") + data.Props["MMApps"] = T("api.templates.cloud_welcome_email.mm_apps") + data.Props["SignInSubInfo2"] = T("api.templates.cloud_welcome_email.signin_sub_info2") + data.Props["DownloadMMAppsLink"] = "https://mattermost.com/download/" + data.Props["Button"] = T("api.templates.cloud_welcome_email.button") + data.Props["GettingStartedQuestions"] = T("api.templates.cloud_welcome_email.start_questions") - if err := es.sendMail(userEmail, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("cloud_welcome_email", data) + if err != nil { + return model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(userEmail, subject, body); err != nil { return model.NewAppError("SendCloudWelcomeEmail", "api.user.send_cloud_welcome_email.error", nil, err.Error(), http.StatusInternalServerError) } @@ -303,14 +339,19 @@ func (es *EmailService) sendPasswordChangeEmail(email, method, locale, siteURL s map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "TeamDisplayName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("password_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.password_change_body.title") - bodyPage.Props["Info"] = T("api.templates.password_change_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.password_change_body.title") + data.Props["Info"] = T("api.templates.password_change_body.info", map[string]interface{}{"TeamDisplayName": es.srv.Config().TeamSettings.SiteName, "TeamURL": siteURL, "Method": method}) - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("password_change_body", data) + if err != nil { + return model.NewAppError("sendPasswordChangeEmail", "api.user.send_password_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("sendPasswordChangeEmail", "api.user.send_password_change_email_and_forget.error", nil, err.Error(), http.StatusInternalServerError) } @@ -323,14 +364,19 @@ func (es *EmailService) sendUserAccessTokenAddedEmail(email, locale, siteURL str subject := T("api.templates.user_access_token_subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("password_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.user_access_token_body.title") - bodyPage.Props["Info"] = T("api.templates.user_access_token_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.user_access_token_body.title") + data.Props["Info"] = T("api.templates.user_access_token_body.info", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "SiteURL": siteURL}) - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("password_change_body", data) + if err != nil { + return model.NewAppError("sendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("sendUserAccessTokenAddedEmail", "api.user.send_user_access_token.error", nil, err.Error(), http.StatusInternalServerError) } @@ -345,17 +391,22 @@ func (es *EmailService) SendPasswordResetEmail(email string, token *model.Token, subject := T("api.templates.reset_subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("reset_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.reset_body.title") - bodyPage.Props["SubTitle"] = T("api.templates.reset_body.subTitle") - bodyPage.Props["Info"] = T("api.templates.reset_body.info") - bodyPage.Props["ButtonURL"] = link - bodyPage.Props["Button"] = T("api.templates.reset_body.button") - bodyPage.Props["QuestionTitle"] = T("api.templates.questions_footer.title") - bodyPage.Props["QuestionInfo"] = T("api.templates.questions_footer.info") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.reset_body.title") + data.Props["SubTitle"] = T("api.templates.reset_body.subTitle") + data.Props["Info"] = T("api.templates.reset_body.info") + data.Props["ButtonURL"] = link + data.Props["Button"] = T("api.templates.reset_body.button") + data.Props["QuestionTitle"] = T("api.templates.questions_footer.title") + data.Props["QuestionInfo"] = T("api.templates.questions_footer.info") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("reset_body", data) + if err != nil { + return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -368,19 +419,24 @@ func (es *EmailService) sendMfaChangeEmail(email string, activated bool, locale, subject := T("api.templates.mfa_change_subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("mfa_change_body", locale) - bodyPage.Props["SiteURL"] = siteURL + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL if activated { - bodyPage.Props["Info"] = T("api.templates.mfa_activated_body.info", map[string]interface{}{"SiteURL": siteURL}) - bodyPage.Props["Title"] = T("api.templates.mfa_activated_body.title") + data.Props["Info"] = T("api.templates.mfa_activated_body.info", map[string]interface{}{"SiteURL": siteURL}) + data.Props["Title"] = T("api.templates.mfa_activated_body.title") } else { - bodyPage.Props["Info"] = T("api.templates.mfa_deactivated_body.info", map[string]interface{}{"SiteURL": siteURL}) - bodyPage.Props["Title"] = T("api.templates.mfa_deactivated_body.title") + data.Props["Info"] = T("api.templates.mfa_deactivated_body.info", map[string]interface{}{"SiteURL": siteURL}) + data.Props["Title"] = T("api.templates.mfa_deactivated_body.title") } - bodyPage.Props["Warning"] = T("api.templates.email_warning") + data.Props["Warning"] = T("api.templates.email_warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("mfa_change_body", data) + if err != nil { + return model.NewAppError("SendMfaChangeEmail", "api.user.send_mfa_change_email.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("SendMfaChangeEmail", "api.user.send_mfa_change_email.error", nil, err.Error(), http.StatusInternalServerError) } @@ -411,34 +467,39 @@ func (es *EmailService) SendInviteEmails(team *model.Team, senderName string, se "TeamDisplayName": team.DisplayName, "SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("invite_body", "") - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName}) - bodyPage.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle") - bodyPage.Props["Button"] = i18n.T("api.templates.invite_body.button") - bodyPage.Props["SenderName"] = senderName - bodyPage.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title") - bodyPage.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info") - bodyPage.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more") + data := es.newEmailTemplateData("") + data.Props["SiteURL"] = siteURL + data.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName}) + data.Props["SubTitle"] = i18n.T("api.templates.invite_body.subTitle") + data.Props["Button"] = i18n.T("api.templates.invite_body.button") + data.Props["SenderName"] = senderName + data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title") + data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info") + data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more") token := model.NewToken( TokenTypeTeamInvitation, model.MapToJson(map[string]string{"teamId": team.Id, "email": invite}), ) - props := make(map[string]string) - props["email"] = invite - props["display_name"] = team.DisplayName - props["name"] = team.Name - data := model.MapToJson(props) + tokenProps := make(map[string]string) + tokenProps["email"] = invite + tokenProps["display_name"] = team.DisplayName + tokenProps["name"] = team.Name + tokenData := model.MapToJson(tokenProps) if err := es.srv.Store.Token().Save(token); err != nil { mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) continue } - bodyPage.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(data), url.QueryEscape(token.Token)) + data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token)) - if err := es.sendMail(invite, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("invite_body", data) + if err != nil { + mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) + } + + if err := es.sendMail(invite, subject, body); err != nil { mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) } } @@ -470,19 +531,19 @@ func (es *EmailService) sendGuestInviteEmails(team *model.Team, channels []*mode "TeamDisplayName": team.DisplayName, "SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("invite_body", "") - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName}) - bodyPage.Props["SubTitle"] = i18n.T("api.templates.invite_body_guest.subTitle") - bodyPage.Props["Button"] = i18n.T("api.templates.invite_body.button") - bodyPage.Props["SenderName"] = senderName - bodyPage.Props["Message"] = "" + data := es.newEmailTemplateData("") + data.Props["SiteURL"] = siteURL + data.Props["Title"] = i18n.T("api.templates.invite_body.title", map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName}) + data.Props["SubTitle"] = i18n.T("api.templates.invite_body_guest.subTitle") + data.Props["Button"] = i18n.T("api.templates.invite_body.button") + data.Props["SenderName"] = senderName + data.Props["Message"] = "" if message != "" { - bodyPage.Props["Message"] = message + data.Props["Message"] = message } - bodyPage.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title") - bodyPage.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info") - bodyPage.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more") + data.Props["InviteFooterTitle"] = i18n.T("api.templates.invite_body_footer.title") + data.Props["InviteFooterInfo"] = i18n.T("api.templates.invite_body_footer.info") + data.Props["InviteFooterLearnMore"] = i18n.T("api.templates.invite_body_footer.learn_more") channelIDs := []string{} for _, channel := range channels { @@ -499,20 +560,20 @@ func (es *EmailService) sendGuestInviteEmails(team *model.Team, channels []*mode }), ) - props := make(map[string]string) - props["email"] = invite - props["display_name"] = team.DisplayName - props["name"] = team.Name - data := model.MapToJson(props) + tokenProps := make(map[string]string) + tokenProps["email"] = invite + tokenProps["display_name"] = team.DisplayName + tokenProps["name"] = team.Name + tokenData := model.MapToJson(tokenProps) if err := es.srv.Store.Token().Save(token); err != nil { mlog.Error("Failed to send invite email successfully ", mlog.Err(err)) continue } - bodyPage.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(data), url.QueryEscape(token.Token)) + data.Props["ButtonURL"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&t=%s", siteURL, url.QueryEscape(tokenData), url.QueryEscape(token.Token)) if !*es.srv.Config().EmailSettings.SendEmailNotifications { - mlog.Info("sending invitation ", mlog.String("to", invite), mlog.String("link", bodyPage.Props["Link"].(string))) + mlog.Info("sending invitation ", mlog.String("to", invite), mlog.String("link", data.Props["ButtomURL"].(string))) } embeddedFiles := make(map[string]io.Reader) @@ -524,7 +585,12 @@ func (es *EmailService) sendGuestInviteEmails(team *model.Team, channels []*mode } } - if nErr := es.sendMailWithEmbeddedFiles(invite, subject, bodyPage.Render(), embeddedFiles); nErr != nil { + body, err := es.srv.TemplatesContainer().RenderToString("invite_body", data) + if err != nil { + mlog.Error("Failed to send invite email successfully", mlog.Err(err)) + } + + if nErr := es.sendMailWithEmbeddedFiles(invite, subject, body, embeddedFiles); nErr != nil { mlog.Error("Failed to send invite email successfully", mlog.Err(nErr)) } } @@ -532,32 +598,32 @@ func (es *EmailService) sendGuestInviteEmails(team *model.Team, channels []*mode return nil } -func (es *EmailService) newEmailTemplate(name, locale string) *utils.HTMLTemplate { - t := utils.NewHTMLTemplate(es.srv.HTMLTemplates(), name) - +func (es *EmailService) newEmailTemplateData(locale string) templates.Data { var localT i18n.TranslateFunc if locale != "" { localT = i18n.GetUserTranslations(locale) } else { localT = i18n.T } - - t.Props["Footer"] = localT("api.templates.email_footer") - t.Props["FooterV2"] = localT("api.templates.email_footer_v2") + organization := "" if *es.srv.Config().EmailSettings.FeedbackOrganization != "" { - t.Props["Organization"] = localT("api.templates.email_organization") + *es.srv.Config().EmailSettings.FeedbackOrganization - } else { - t.Props["Organization"] = "" + organization = localT("api.templates.email_organization") + *es.srv.Config().EmailSettings.FeedbackOrganization } - t.Props["EmailInfo1"] = localT("api.templates.email_info1") - t.Props["EmailInfo2"] = localT("api.templates.email_info2") - t.Props["EmailInfo3"] = localT("api.templates.email_info3", - map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - t.Props["SupportEmail"] = *es.srv.Config().SupportSettings.SupportEmail - - return t + return templates.Data{ + Props: map[string]interface{}{ + "EmailInfo1": localT("api.templates.email_info1"), + "EmailInfo2": localT("api.templates.email_info2"), + "EmailInfo3": localT("api.templates.email_info3", + map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}), + "SupportEmail": *es.srv.Config().SupportSettings.SupportEmail, + "Footer": localT("api.templates.email_footer"), + "FooterV2": localT("api.templates.email_footer_v2"), + "Organization": organization, + }, + HTML: map[string]template.HTML{}, + } } func (es *EmailService) SendDeactivateAccountEmail(email string, locale, siteURL string) *model.AppError { @@ -569,14 +635,19 @@ func (es *EmailService) SendDeactivateAccountEmail(email string, locale, siteURL map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName, "ServerURL": serverURL}) - bodyPage := es.newEmailTemplate("deactivate_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.deactivate_body.title", map[string]interface{}{"ServerURL": serverURL}) - bodyPage.Props["Info"] = T("api.templates.deactivate_body.info", + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.deactivate_body.title", map[string]interface{}{"ServerURL": serverURL}) + data.Props["Info"] = T("api.templates.deactivate_body.info", map[string]interface{}{"SiteURL": siteURL}) - bodyPage.Props["Warning"] = T("api.templates.deactivate_body.warning") + data.Props["Warning"] = T("api.templates.deactivate_body.warning") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("deactivate_body", data) + if err != nil { + return model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("SendDeactivateEmail", "api.user.send_deactivate_email_and_forget.failed.error", nil, err.Error(), http.StatusInternalServerError) } @@ -595,13 +666,18 @@ func (es *EmailService) SendRemoveExpiredLicenseEmail(email string, locale, site subject := T("api.templates.remove_expired_license.subject", map[string]interface{}{"SiteName": es.srv.Config().TeamSettings.SiteName}) - bodyPage := es.newEmailTemplate("remove_expired_license", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.remove_expired_license.body.title") - bodyPage.Props["Link"] = renewalLink - bodyPage.Props["LinkButton"] = T("api.templates.remove_expired_license.body.renew_button") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.remove_expired_license.body.title") + data.Props["Link"] = renewalLink + data.Props["LinkButton"] = T("api.templates.remove_expired_license.body.renew_button") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, nErr := es.srv.TemplatesContainer().RenderToString("remove_expired_license", data) + if nErr != nil { + return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, nErr.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("SendRemoveExpiredLicenseEmail", "api.license.remove_expired_license.failed.error", nil, err.Error(), http.StatusInternalServerError) } @@ -667,17 +743,22 @@ func (es *EmailService) SendAtUserLimitWarningEmail(email string, locale string, subject := T("api.templates.at_limit_subject") - bodyPage := es.newEmailTemplate("reached_user_limit_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.at_limit_title") - bodyPage.Props["Info1"] = T("api.templates.at_limit_info1") - bodyPage.Props["Info2"] = T("api.templates.at_limit_info2") - bodyPage.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") - bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.at_limit_title") + data.Props["Info1"] = T("api.templates.at_limit_info1") + data.Props["Info2"] = T("api.templates.at_limit_info2") + data.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("reached_user_limit_body", data) + if err != nil { + return false, model.NewAppError("SendAtUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendAtUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -688,25 +769,30 @@ func (es *EmailService) SendAtUserLimitWarningEmail(email string, locale string, func (es *EmailService) SendUpgradeEmail(user, email, locale, siteURL, action string) (bool, *model.AppError) { T := i18n.GetUserTranslations(locale) - bodyPage := es.newEmailTemplate("cloud_upgrade_request_email", locale) + subject := T("api.templates.upgrade_request_subject") + + data := es.newEmailTemplateData(locale) + data.Props["Info5"] = T("api.templates.at_limit_info5") + data.Props["BillingPath"] = "admin_console/billing/subscription" + data.Props["SiteURL"] = siteURL + data.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data.Props["Footer"] = T("api.templates.copyright") if action == model.InviteLimitation { - bodyPage.Props["Title"] = T("api.templates.upgrade_request_title", map[string]interface{}{"UserName": user}) - bodyPage.Props["Info4"] = T("api.templates.upgrade_request_info4") + data.Props["Title"] = T("api.templates.upgrade_request_title", map[string]interface{}{"UserName": user}) + data.Props["Info4"] = T("api.templates.upgrade_request_info4") } else { - bodyPage.Props["Title"] = T("api.templates.upgrade_request_title2") - bodyPage.Props["Info4"] = T("api.templates.upgrade_request_info4_2") + data.Props["Title"] = T("api.templates.upgrade_request_title2") + data.Props["Info4"] = T("api.templates.upgrade_request_info4_2") } - subject := T("api.templates.upgrade_request_subject") - bodyPage.Props["Info5"] = T("api.templates.at_limit_info5") - bodyPage.Props["BillingPath"] = "admin_console/billing/subscription" - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") - bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + body, err := es.srv.TemplatesContainer().RenderToString("cloud_upgrade_request_email", data) + if err != nil { + return false, model.NewAppError("SendUpgradeEmail", "api.user.send_upgrade_request_email.error", nil, err.Error(), http.StatusInternalServerError) + } - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendUpgradeEmail", "api.user.send_upgrade_request_email.error", nil, err.Error(), http.StatusInternalServerError) } @@ -718,17 +804,22 @@ func (es *EmailService) SendOverUserLimitWarningEmail(email string, locale strin subject := T("api.templates.over_limit_subject") - bodyPage := es.newEmailTemplate("reached_user_limit_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.over_limit_title") - bodyPage.Props["Info1"] = T("api.templates.over_limit_info1") - bodyPage.Props["Info2"] = T("api.templates.over_limit_info2") - bodyPage.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") - bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_title") + data.Props["Info1"] = T("api.templates.over_limit_info1") + data.Props["Info2"] = T("api.templates.over_limit_info2") + data.Props["Button"] = T("api.templates.upgrade_mattermost_cloud") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("reached_user_limit_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -740,20 +831,25 @@ func (es *EmailService) SendOverUserLimitThirtyDayWarningEmail(email string, loc 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") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_30_days_title") + data.Props["Info1"] = T("api.templates.over_limit_30_days_info1") + data.Props["Info2"] = T("api.templates.over_limit_30_days_info2") + data.Props["Info2Item1"] = T("api.templates.over_limit_30_days_info2_item1") + data.Props["Info2Item2"] = T("api.templates.over_limit_30_days_info2_item2") + data.Props["Info2Item3"] = T("api.templates.over_limit_30_days_info2_item3") + data.Props["Button"] = T("api.templates.over_limit_fix_now") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_30_days_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -765,19 +861,24 @@ func (es *EmailService) SendOverUserLimitNinetyDayWarningEmail(email string, loc 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") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_90_days_title") + data.Props["Info1"] = T("api.templates.over_limit_90_days_info1", map[string]interface{}{"OverLimitDate": overLimitDate}) + data.Props["Info2"] = T("api.templates.over_limit_90_days_info2") + data.Props["Info3"] = T("api.templates.over_limit_90_days_info3") + data.Props["Info4"] = T("api.templates.over_limit_90_days_info4") + data.Props["Button"] = T("api.templates.over_limit_fix_now") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_90_days_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -789,17 +890,22 @@ func (es *EmailService) SendOverUserLimitWorkspaceSuspendedWarningEmail(email st 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") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_suspended_title") + data.Props["Info1"] = T("api.templates.over_limit_suspended_info1") + data.Props["Info2"] = T("api.templates.over_limit_suspended_info2") + data.Props["Button"] = T("api.templates.over_limit_suspended_contact_support") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_workspace_suspended_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -811,16 +917,21 @@ func (es *EmailService) SendOverUserFourteenDayWarningEmail(email string, 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") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_14_days_title") + data.Props["Info1"] = T("api.templates.over_limit_14_days_info1", map[string]interface{}{"OverLimitDate": overLimitDate}) + data.Props["Button"] = T("api.templates.over_limit_fix_now") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_7_days_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -832,16 +943,21 @@ func (es *EmailService) SendOverUserSevenDayWarningEmail(email string, locale st 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") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.over_limit_7_days_title") + data.Props["Info1"] = T("api.templates.over_limit_7_days_info1") + data.Props["Button"] = T("api.templates.over_limit_fix_now") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_7_days_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -852,15 +968,20 @@ func (es *EmailService) SendSuspensionEmailToSupport(email string, installationI // 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 + data := es.newEmailTemplateData("en") + data.Props["CustomerID"] = customerID + data.Props["SiteURL"] = siteURL + data.Props["SubscriptionID"] = subscriptionID + data.Props["InstallationID"] = installationID + data.Props["SuspensionDate"] = time.Now().AddDate(0, 0, 61).Format("2006-01-02") + data.Props["UserCount"] = userCount - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("over_user_limit_support_body", data) + if err != nil { + return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendOverUserLimitWarningEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -872,20 +993,25 @@ func (es *EmailService) SendPaymentFailedEmail(email string, locale string, fail subject := T("api.templates.payment_failed.subject") - bodyPage := es.newEmailTemplate("payment_failed_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.payment_failed.title") - bodyPage.Props["Info1"] = T("api.templates.payment_failed.info1", map[string]interface{}{"CardBrand": failedPayment.CardBrand, "LastFour": failedPayment.LastFour}) - bodyPage.Props["Info2"] = T("api.templates.payment_failed.info2") - bodyPage.Props["Info3"] = T("api.templates.payment_failed.info3") - bodyPage.Props["Button"] = T("api.templates.over_limit_fix_now") - bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.payment_failed.title") + data.Props["Info1"] = T("api.templates.payment_failed.info1", map[string]interface{}{"CardBrand": failedPayment.CardBrand, "LastFour": failedPayment.LastFour}) + data.Props["Info2"] = T("api.templates.payment_failed.info2") + data.Props["Info3"] = T("api.templates.payment_failed.info3") + data.Props["Button"] = T("api.templates.over_limit_fix_now") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - bodyPage.Props["FailedReason"] = failedPayment.FailureMessage + data.Props["FailedReason"] = failedPayment.FailureMessage - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("payment_failed_body", data) + if err != nil { + return false, model.NewAppError("SendPaymentFailedEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return false, model.NewAppError("SendPaymentFailedEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } @@ -897,17 +1023,22 @@ func (es *EmailService) SendNoCardPaymentFailedEmail(email string, locale string subject := T("api.templates.payment_failed_no_card.subject") - bodyPage := es.newEmailTemplate("payment_failed_no_card_body", locale) - bodyPage.Props["SiteURL"] = siteURL - bodyPage.Props["Title"] = T("api.templates.payment_failed_no_card.title") - bodyPage.Props["Info1"] = T("api.templates.payment_failed_no_card.info1") - bodyPage.Props["Info3"] = T("api.templates.payment_failed_no_card.info3") - bodyPage.Props["Button"] = T("api.templates.payment_failed_no_card.button") - bodyPage.Props["EmailUs"] = T("api.templates.email_us_anytime_at") + data := es.newEmailTemplateData(locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = T("api.templates.payment_failed_no_card.title") + data.Props["Info1"] = T("api.templates.payment_failed_no_card.info1") + data.Props["Info3"] = T("api.templates.payment_failed_no_card.info3") + data.Props["Button"] = T("api.templates.payment_failed_no_card.button") + data.Props["EmailUs"] = T("api.templates.email_us_anytime_at") - bodyPage.Props["Footer"] = T("api.templates.copyright") + data.Props["Footer"] = T("api.templates.copyright") - if err := es.sendMail(email, subject, bodyPage.Render()); err != nil { + body, err := es.srv.TemplatesContainer().RenderToString("payment_failed_no_card_body", data) + if err != nil { + return model.NewAppError("SendPaymentFailedEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + + if err := es.sendMail(email, subject, body); err != nil { return model.NewAppError("SendPaymentFailedEmail", "api.user.send_password_reset.send.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) } diff --git a/app/email_batching.go b/app/email_batching.go index 31f2054d90..514805c76e 100644 --- a/app/email_batching.go +++ b/app/email_batching.go @@ -15,7 +15,6 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mlog" - "github.com/mattermost/mattermost-server/v5/utils" ) const ( @@ -222,7 +221,13 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType } - contents += es.renderBatchedPost(notification, channel, sender, *es.srv.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType) + postContent, err := es.renderBatchedPost(notification, channel, sender, *es.srv.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType) + if err != nil { + mlog.Warn("Unable to render post for batched email notification template", mlog.Err(err)) + continue + } + + contents += postContent } tm := time.Unix(notifications[0].post.CreateAt/1000, 0) @@ -234,34 +239,39 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification "Day": tm.Day(), }) - body := es.newEmailTemplate("post_batched_body", user.Locale) - body.Props["SiteURL"] = *es.srv.Config().ServiceSettings.SiteURL - body.Props["Posts"] = template.HTML(contents) - body.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) + data := es.newEmailTemplateData(user.Locale) + data.Props["SiteURL"] = *es.srv.Config().ServiceSettings.SiteURL + data.Props["Posts"] = template.HTML(contents) + data.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) - if nErr := es.sendNotificationMail(user.Email, subject, body.Render()); nErr != nil { + body, err2 := es.srv.TemplatesContainer().RenderToString("post_batched_body", data) + if err2 != nil { + mlog.Warn("Unable build the batched email notification template", mlog.Err(err2)) + return + } + + if nErr := es.sendNotificationMail(user.Email, subject, body); nErr != nil { mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr)) } } -func (es *EmailService) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) string { +func (es *EmailService) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) (string, error) { // don't include message contents if email notification contents type is set to generic - var template *utils.HTMLTemplate + var templateName = "post_batched_post_generic" if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - template = es.newEmailTemplate("post_batched_post_full", userLocale) - } else { - template = es.newEmailTemplate("post_batched_post_generic", userLocale) + templateName = "post_batched_post_full" } - template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") - template.Props["PostMessage"] = es.srv.GetMessageForNotification(notification.post, translateFunc) - template.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id - template.Props["SenderName"] = sender.GetDisplayName(displayNameFormat) + data := es.newEmailTemplateData(userLocale) + data.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") + data.Props["PostMessage"] = es.srv.GetMessageForNotification(notification.post, translateFunc) + data.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id + data.Props["SenderName"] = sender.GetDisplayName(displayNameFormat) tm := time.Unix(notification.post.CreateAt/1000, 0) timezone, _ := tm.Zone() - template.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{ + data.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{ "Year": tm.Year(), "Month": translateFunc(tm.Month().String()), "Day": tm.Day(), @@ -271,17 +281,17 @@ func (es *EmailService) renderBatchedPost(notification *batchedNotification, cha }) if channel.Type == model.CHANNEL_DIRECT { - template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message") + data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message") } else if channel.Type == model.CHANNEL_GROUP { - template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message") + data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message") } else { // don't include channel name if email notification contents type is set to generic if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - template.Props["ChannelName"] = channel.DisplayName + data.Props["ChannelName"] = channel.DisplayName } else { - template.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification") + data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification") } } - return template.Render() + return es.srv.TemplatesContainer().RenderToString(templateName, data) } diff --git a/app/email_batching_test.go b/app/email_batching_test.go index 561af30333..6543200309 100644 --- a/app/email_batching_test.go +++ b/app/email_batching_test.go @@ -304,7 +304,8 @@ func TestRenderBatchedPostGeneric(t *testing.T) { return translationID } - var rendered = th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_GENERIC) + rendered, err := th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_GENERIC) + require.NoError(t, err) require.NotContains(t, rendered, post.Message, "Rendered email should not contain post contents when email notification contents type is set to Generic.") } @@ -329,6 +330,7 @@ func TestRenderBatchedPostFull(t *testing.T) { return translationID } - var rendered = th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_FULL) + rendered, err := th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_FULL) + require.NoError(t, err) require.Contains(t, rendered, post.Message, "Rendered email should contain post contents when email notification contents type is set to Full.") } diff --git a/app/notification.go b/app/notification.go index db311ef923..cdf55a18da 100644 --- a/app/notification.go +++ b/app/notification.go @@ -231,7 +231,10 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } if a.userAllowsEmail(profileMap[id], channelMemberNotifyPropsMap[id], post) { - a.sendNotificationEmail(notification, profileMap[id], team) + err := a.sendNotificationEmail(notification, profileMap[id], team) + if err != nil { + mlog.Warn("Unable to send notification email.", mlog.Err(err)) + } } } } diff --git a/app/notification_email.go b/app/notification_email.go index 81fc15e350..ef9f065e6f 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -7,7 +7,6 @@ import ( "fmt" "html" "html/template" - "net/http" "net/url" "path/filepath" "strings" @@ -16,17 +15,17 @@ import ( "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mlog" - "github.com/mattermost/mattermost-server/v5/utils" + "github.com/pkg/errors" ) -func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) *model.AppError { +func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) error { channel := notification.Channel post := notification.Post if channel.IsGroupOrDirect() { teams, err := a.Srv().Store.Team().GetTeamsByUserId(user.Id) if err != nil { - return model.NewAppError("sendNotificationEmail", "app.team.get_all.app_error", nil, err.Error(), http.StatusInternalServerError) + return errors.Wrap(err, "unable to get user teams") } // if the recipient isn't in the current user's team, just pick one @@ -97,7 +96,10 @@ func (a *App) sendNotificationEmail(notification *PostNotification, user *model. } landingURL := a.GetSiteURL() + "/landing#/" + team.Name - var bodyText = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc) + var bodyText, err = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc) + if err != nil { + return errors.Wrap(err, "unable to render the email notification template") + } a.Srv().Go(func() { if nErr := a.Srv().EmailService.sendNotificationMail(user.Email, html.UnescapeString(subjectText), bodyText); nErr != nil { @@ -163,11 +165,12 @@ func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, /** * Computes the email body for notification messages */ -func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc) string { +func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc) (string, error) { // only include message contents in notification email if email notification contents type is set to full - var bodyPage *utils.HTMLTemplate + var templateName = "post_body_generic" + data := a.Srv().EmailService.newEmailTemplateData(recipient.Locale) if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - bodyPage = a.Srv().EmailService.newEmailTemplate("post_body_full", recipient.Locale) + templateName = "post_body_full" postMessage := a.GetMessageForNotification(post, translateFunc) postMessage = html.EscapeString(postMessage) normalizedPostMessage, err := a.generateHyperlinkForChannels(postMessage, teamName, landingURL) @@ -175,16 +178,14 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err)) normalizedPostMessage = postMessage } - bodyPage.Props["PostMessage"] = template.HTML(normalizedPostMessage) - } else { - bodyPage = a.Srv().EmailService.newEmailTemplate("post_body_generic", recipient.Locale) + data.Props["PostMessage"] = template.HTML(normalizedPostMessage) } - bodyPage.Props["SiteURL"] = a.GetSiteURL() + data.Props["SiteURL"] = a.GetSiteURL() if teamName != "select_team" { - bodyPage.Props["TeamLink"] = landingURL + "/pl/" + post.Id + data.Props["TeamLink"] = landingURL + "/pl/" + post.Id } else { - bodyPage.Props["TeamLink"] = landingURL + data.Props["TeamLink"] = landingURL } t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) @@ -198,51 +199,51 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, } if channel.Type == model.CHANNEL_DIRECT { if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") - bodyPage.Props["Info1"] = "" + data.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") + data.Props["Info1"] = "" info["SenderName"] = senderName - bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", info) + data.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", info) } else { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ + data.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ "SenderName": senderName, }) - bodyPage.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", info) + data.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", info) } } else if channel.Type == model.CHANNEL_GROUP { if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.full") - bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.group_message.full", + data.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.full") + data.Props["Info1"] = translateFunc("app.notification.body.text.group_message.full", map[string]interface{}{ "ChannelName": channelName, }) info["SenderName"] = senderName - bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", info) + data.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", info) } else { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ + data.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ "SenderName": senderName, }) - bodyPage.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", info) + data.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", info) } } else { if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") - bodyPage.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", + data.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") + data.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", map[string]interface{}{ "ChannelName": channelName, }) info["SenderName"] = senderName - bodyPage.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", info) + data.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", info) } else { - bodyPage.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ + data.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ "SenderName": senderName, }) - bodyPage.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", info) + data.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", info) } } - bodyPage.Props["Button"] = translateFunc("api.templates.post_body.button") + data.Props["Button"] = translateFunc("api.templates.post_body.button") - return bodyPage.Render() + return a.Srv().TemplatesContainer().RenderToString(templateName, data) } type formattedPostTime struct { diff --git a/app/notification_email_test.go b/app/notification_email_test.go index cee9500e8e..bc24389899 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -91,7 +91,8 @@ func TestGetNotificationEmailBodyFullNotificationPublicChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new notification.", fmt.Sprintf("Expected email text 'You have a new notification. Got %s", body)) require.Contains(t, body, "Channel: "+channel.DisplayName, "Expected email text 'Channel: %s'. Got %s", channel.DisplayName, body) require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) @@ -123,7 +124,8 @@ func TestGetNotificationEmailBodyFullNotificationGroupChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new Group Message.", fmt.Sprintf("Expected email text 'You have a new Group Message. Got "+body)) require.Contains(t, body, "Channel: ChannelName", fmt.Sprintf("Expected email text 'Channel: ChannelName'. Got %s", body)) require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) @@ -155,7 +157,8 @@ func TestGetNotificationEmailBodyFullNotificationPrivateChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new notification.", fmt.Sprintf("Expected email text 'You have a new notification. Got "+body)) require.Contains(t, body, "Channel: "+channel.DisplayName, fmt.Sprintf("Expected email text 'Channel: "+channel.DisplayName+"'. Got "+body)) require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) @@ -187,7 +190,8 @@ func TestGetNotificationEmailBodyFullNotificationDirectChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new Direct Message.", fmt.Sprintf("Expected email text 'You have a new Direct Message. Got "+body)) require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) @@ -222,7 +226,8 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeWithTimezone(t *testi teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + require.NoError(t, err) r, _ := regexp.Compile("E([S|D]+)T") zone := r.FindString(body) require.Contains(t, body, "sender - 9:43 AM "+zone+", April 25", fmt.Sprintf("Expected email text 'sender - 9:43 AM %s, April 25'. Got %s", zone, body)) @@ -274,7 +279,8 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing err = tmp.Execute(&text, fmt.Sprintf("sender - %s:%s %s, %s %s", formattedTime.Hour, formattedTime.Minute, formattedTime.TimeZone, formattedTime.Month, formattedTime.Day)) require.NoError(t, err) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) postTimeLine := text.String() require.Contains(t, body, postTimeLine, fmt.Sprintf("Expected email text '%s'. Got %s", postTimeLine, body)) } @@ -307,7 +313,8 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime12Hour(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + require.NoError(t, err) require.Contains(t, body, "sender - 2:30 PM", fmt.Sprintf("Expected email text 'sender - 2:30 PM'. Got %s", body)) require.Contains(t, body, "April 25", fmt.Sprintf("Expected email text 'April 25'. Got %s", body)) } @@ -340,7 +347,8 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "sender - 14:30", fmt.Sprintf("Expected email text 'sender - 14:30'. Got %s", body)) require.Contains(t, body, "April 25", fmt.Sprintf("Expected email text 'April 25'. Got %s", body)) } @@ -370,7 +378,8 @@ func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new notification from "+senderName, fmt.Sprintf("Expected email text 'You have a new notification from %s'. Got %s", senderName, body)) require.False(t, strings.Contains(body, "Channel: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) @@ -401,7 +410,8 @@ func TestGetNotificationEmailBodyGenericNotificationGroupChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new Group Message from "+senderName, fmt.Sprintf("Expected email text 'You have a new Group Message from %s'. Got %s", senderName, body)) require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) @@ -432,7 +442,8 @@ func TestGetNotificationEmailBodyGenericNotificationPrivateChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new notification from "+senderName, fmt.Sprintf("Expected email text 'You have a new notification from %s'. Got %s", senderName, body)) require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) @@ -463,7 +474,8 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, "You have a new Direct Message from "+senderName, fmt.Sprintf("Expected email text 'You have a new Direct Message from "+senderName+"'. Got "+body)) require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) @@ -496,9 +508,10 @@ func TestGetNotificationEmailEscapingChars(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, ch, + body, err := th.App.getNotificationEmailBody(recipient, post, ch, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) assert.NotContains(t, body, message) } @@ -539,9 +552,10 @@ func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { channelStoreMock.On("GetByNames", "test", []string{ch.Name}, true).Return([]*model.Channel{ch}, nil) storeMock.On("Channel").Return(&channelStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, ch, + body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name assert.Contains(t, body, ""+mention+"") @@ -604,9 +618,10 @@ func TestGetNotificationEmailBodyMultiPublicChannelMention(t *testing.T) { channelStoreMock.On("GetByNames", "test", []string{ch.Name, ch2.Name, ch3.Name}, true).Return([]*model.Channel{ch, ch2, ch3}, nil) storeMock.On("Channel").Return(&channelStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, ch, + body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name channelURL2 := teamURL + "/channels/" + ch2.Name channelURL3 := teamURL + "/channels/" + ch3.Name @@ -652,9 +667,10 @@ func TestGetNotificationEmailBodyPrivateChannelMention(t *testing.T) { channelStoreMock.On("GetByNames", "test", []string{ch.Name}, true).Return([]*model.Channel{ch}, nil) storeMock.On("Channel").Return(&channelStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, ch, + body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name assert.NotContains(t, body, ""+mention+"") @@ -797,7 +813,8 @@ func TestLandingLink(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -826,6 +843,7 @@ func TestLandingLinkPermalink(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + require.NoError(t, err) require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } diff --git a/app/server.go b/app/server.go index f2520de07b..36f94e0694 100644 --- a/app/server.go +++ b/app/server.go @@ -58,6 +58,7 @@ import ( "github.com/mattermost/mattermost-server/v5/shared/filestore" "github.com/mattermost/mattermost-server/v5/shared/i18n" "github.com/mattermost/mattermost-server/v5/shared/mlog" + "github.com/mattermost/mattermost-server/v5/shared/templates" "github.com/mattermost/mattermost-server/v5/store" "github.com/mattermost/mattermost-server/v5/store/localcachelayer" "github.com/mattermost/mattermost-server/v5/store/retrylayer" @@ -65,6 +66,7 @@ import ( "github.com/mattermost/mattermost-server/v5/store/sqlstore" "github.com/mattermost/mattermost-server/v5/store/timerlayer" "github.com/mattermost/mattermost-server/v5/utils" + "github.com/mattermost/mattermost-server/v5/utils/fileutils" ) var MaxNotificationsPerChannelDefault int64 = 1000000 @@ -133,7 +135,7 @@ type Server struct { newStore func() (store.Store, error) - htmlTemplateWatcher *utils.HTMLTemplateWatcher + htmlTemplateWatcher *templates.Container sessionCache cache.Cache seenPendingPostIdsCache cache.Cache statusCache cache.Cache @@ -391,9 +393,19 @@ func NewServer(options ...Option) (*Server, error) { } } - if htmlTemplateWatcher, err2 := utils.NewHTMLTemplateWatcher("templates"); err2 != nil { - mlog.Error("Failed to parse server templates", mlog.Err(err2)) + templatesDir, ok := fileutils.FindDir("templates") + if !ok { + mlog.Error("Failed find server templates", mlog.String("directory", "templates")) } else { + htmlTemplateWatcher, errorsChan, err2 := templates.NewWithWatcher(templatesDir) + if err2 != nil { + return nil, errors.Wrap(err2, "cannot initialize server templates") + } + s.Go(func() { + for err2 := range errorsChan { + mlog.Warn("Server templates error", mlog.Err(err2)) + } + }) s.htmlTemplateWatcher = htmlTemplateWatcher } @@ -865,13 +877,10 @@ func (s *Server) Shutdown() { // Push notification hub needs to be shutdown after HTTP server // to prevent stray requests from generating a push notification after it's shut down. s.StopPushNotificationsHubWorkers() + s.htmlTemplateWatcher.Close() s.WaitForGoroutines() - if s.htmlTemplateWatcher != nil { - s.htmlTemplateWatcher.Close() - } - if s.advancedLogListenerCleanup != nil { s.advancedLogListenerCleanup() s.advancedLogListenerCleanup = nil diff --git a/cmd/mattermost/commands/cmdtestlib.go b/cmd/mattermost/commands/cmdtestlib.go index d936d1f473..3f202a1feb 100644 --- a/cmd/mattermost/commands/cmdtestlib.go +++ b/cmd/mattermost/commands/cmdtestlib.go @@ -38,7 +38,7 @@ type testHelper struct { // Setup creates an instance of testHelper. func Setup(t testing.TB) *testHelper { - dir, err := ioutil.TempDir("", "testHelper") + dir, err := testlib.SetupTestResources() if err != nil { panic("failed to create temporary directory: " + err.Error()) } @@ -60,7 +60,7 @@ func Setup(t testing.TB) *testHelper { // Setup creates an instance of testHelper. func SetupWithStoreMock(t testing.TB) *testHelper { - dir, err := ioutil.TempDir("", "testHelper") + dir, err := testlib.SetupTestResources() if err != nil { panic("failed to create temporary directory: " + err.Error()) } diff --git a/cmd/mattermost/commands/config_flag_test.go b/cmd/mattermost/commands/config_flag_test.go index 5104fa57c4..6c2d6a0427 100644 --- a/cmd/mattermost/commands/config_flag_test.go +++ b/cmd/mattermost/commands/config_flag_test.go @@ -9,9 +9,6 @@ import ( "testing" "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/v5/utils" - "github.com/mattermost/mattermost-server/v5/utils/fileutils" ) func TestConfigFlag(t *testing.T) { @@ -19,16 +16,14 @@ func TestConfigFlag(t *testing.T) { defer th.TearDown() dir := th.TemporaryDirectory() - i18n, ok := fileutils.FindDir("i18n") - require.True(t, ok) - require.NoError(t, utils.CopyDir(i18n, filepath.Join(dir, "i18n"))) - prevDir, err := os.Getwd() require.NoError(t, err) defer os.Chdir(prevDir) os.Chdir(dir) t.Run("version without a config file should fail", func(t *testing.T) { + err := os.RemoveAll("config") + require.NoError(t, err) th.SetAutoConfig(false) defer th.SetAutoConfig(true) require.Error(t, th.RunCommand(t, "version")) diff --git a/shared/templates/templates.go b/shared/templates/templates.go new file mode 100644 index 0000000000..12ec85b766 --- /dev/null +++ b/shared/templates/templates.go @@ -0,0 +1,143 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package templates + +import ( + "bytes" + "html/template" + "io" + "path/filepath" + "sync" + + "github.com/fsnotify/fsnotify" +) + +// Container represents a set of templates that can be render +type Container struct { + templates *template.Template + mutex sync.RWMutex + stop chan struct{} + stopped chan struct{} + watch bool +} + +// Data contains the data used to populate the template variables, it has Props +// that can be of any type and HTML that only can be `template.HTML` types. +type Data struct { + Props map[string]interface{} + HTML map[string]template.HTML +} + +// NewFromTemplates creates a new templates container using a +// `template.Template` object +func NewFromTemplate(templates *template.Template) *Container { + return &Container{templates: templates} +} + +// New creates a new templates container scanning a directory. +func New(directory string) (*Container, error) { + c := &Container{} + + htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")) + if err != nil { + return nil, err + } + c.templates = htmlTemplates + + return c, nil +} + +// NewWithWatcher creates a new templates container scanning a directory and +// watch the directory filesystem changes to apply them to the loaded +// templates. This function returns the container and an errors channel to pass +// all errors that can happen during the watch process, or an regular error if +// we fail to create the templates or the watcher. The caller must consume the +// returned errors channel to ensure not blocking the watch process. +func NewWithWatcher(directory string) (*Container, <-chan error, error) { + htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")) + if err != nil { + return nil, nil, err + } + + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, nil, err + } + + err = watcher.Add(directory) + if err != nil { + watcher.Close() + return nil, nil, err + } + + c := &Container{ + templates: htmlTemplates, + watch: true, + stop: make(chan struct{}), + stopped: make(chan struct{}), + } + errors := make(chan error) + + go func() { + defer close(errors) + defer close(c.stopped) + defer watcher.Close() + + for { + select { + case <-c.stop: + return + case event := <-watcher.Events: + if event.Op&fsnotify.Write == fsnotify.Write { + if htmlTemplates, err := template.ParseGlob(filepath.Join(directory, "*.html")); err != nil { + errors <- err + } else { + c.mutex.Lock() + c.templates = htmlTemplates + c.mutex.Unlock() + } + } + case err := <-watcher.Errors: + errors <- err + } + } + }() + + return c, errors, nil +} + +// Close stops the templates watcher of the container in case you have created +// it with watch parameter set to true +func (c *Container) Close() { + c.mutex.RLock() + defer c.mutex.RUnlock() + if c.watch { + close(c.stop) + <-c.stopped + } +} + +// RenderToString renders the template referenced with the template name using +// the data provided and return a string with the result +func (c *Container) RenderToString(templateName string, data Data) (string, error) { + var text bytes.Buffer + if err := c.Render(&text, templateName, data); err != nil { + return "", err + } + return text.String(), nil +} + +// RenderToString renders the template referenced with the template name using +// the data provided and write it to the writer provided +func (c *Container) Render(w io.Writer, templateName string, data Data) error { + c.mutex.RLock() + htmlTemplates := c.templates + c.mutex.RUnlock() + + if err := htmlTemplates.ExecuteTemplate(w, templateName, data); err != nil { + return err + } + + return nil +} diff --git a/shared/templates/templates_test.go b/shared/templates/templates_test.go new file mode 100644 index 0000000000..40940e1a03 --- /dev/null +++ b/shared/templates/templates_test.go @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package templates + +import ( + "bytes" + "html/template" + "io/ioutil" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHTMLTemplateWatcher(t *testing.T) { + dir, err := ioutil.TempDir("", "") + require.NoError(t, err) + defer os.RemoveAll(dir) + + require.NoError(t, os.Mkdir(filepath.Join(dir, "templates"), 0700)) + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}foo{{ end }}`), 0600)) + + prevDir, err := os.Getwd() + require.NoError(t, err) + defer os.Chdir(prevDir) + os.Chdir(dir) + + watcher, errChan, err := NewWithWatcher("templates") + require.NoError(t, err) + require.NotNil(t, watcher) + select { + case msg := <-errChan: + err = msg + default: + err = nil + } + require.NoError(t, err) + defer watcher.Close() + + text, err := watcher.RenderToString("foo", Data{}) + require.NoError(t, err) + assert.Equal(t, "foo", text) + + require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}bar{{ end }}`), 0600)) + + require.Eventually(t, func() bool { + text, err := watcher.RenderToString("foo", Data{}) + return text == "bar" && err == nil + }, time.Millisecond*1000, time.Millisecond*50) +} + +func TestNewWithWatcher_BadDirectory(t *testing.T) { + watcher, errChan, err := NewWithWatcher("notarealdirectory") + require.Error(t, err) + assert.Nil(t, watcher) + assert.Nil(t, errChan) +} + +func TestNew_BadDirectory(t *testing.T) { + watcher, err := New("notarealdirectory") + assert.Nil(t, watcher) + assert.Error(t, err) +} + +func TestRender(t *testing.T) { + tpl := template.New("test") + _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Props.Bar }}{{ end }}`) + require.NoError(t, err) + mt := NewFromTemplate(tpl) + + data := Data{ + Props: map[string]interface{}{ + "Bar": "bar", + }, + } + text, err := mt.RenderToString("foo", data) + require.NoError(t, err) + assert.Equal(t, "foobar", text) + + buf := &bytes.Buffer{} + require.NoError(t, mt.Render(buf, "foo", data)) + assert.Equal(t, "foobar", buf.String()) +} + +func TestRenderError(t *testing.T) { + tpl := template.New("test") + _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Foo.Bar }}bar{{ end }}`) + require.NoError(t, err) + mt := NewFromTemplate(tpl) + + text, err := mt.RenderToString("foo", Data{}) + require.Error(t, err) + assert.Equal(t, "", text) + + buf := &bytes.Buffer{} + assert.Error(t, mt.Render(buf, "foo", Data{})) + assert.Equal(t, "foo", buf.String()) +} + +func TestRenderUnknownTemplate(t *testing.T) { + tpl := template.New("") + mt := NewFromTemplate(tpl) + + text, err := mt.RenderToString("foo", Data{}) + require.Error(t, err) + assert.Equal(t, "", text) + + buf := &bytes.Buffer{} + assert.Error(t, mt.Render(buf, "foo", Data{})) + assert.Equal(t, "", buf.String()) +} diff --git a/testlib/resources.go b/testlib/resources.go index 63fd347d17..a0757f3a17 100644 --- a/testlib/resources.go +++ b/testlib/resources.go @@ -87,6 +87,7 @@ func getTestResourcesToSetup() []testResourceDetails { var testResourcesToSetup = []testResourceDetails{ {root, "mattermost-server", resourceTypeFolder, actionSymlink}, + {"go.mod", "go.mod", resourceTypeFile, actionSymlink}, {"i18n", "i18n", resourceTypeFolder, actionSymlink}, {"templates", "templates", resourceTypeFolder, actionSymlink}, {"tests", "tests", resourceTypeFolder, actionSymlink}, diff --git a/utils/html.go b/utils/html.go deleted file mode 100644 index 17ba46b7b2..0000000000 --- a/utils/html.go +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "bytes" - "errors" - "html/template" - "io" - "path/filepath" - "sync/atomic" - - "github.com/fsnotify/fsnotify" - - "github.com/mattermost/mattermost-server/v5/shared/mlog" - "github.com/mattermost/mattermost-server/v5/utils/fileutils" -) - -type HTMLTemplateWatcher struct { - templates atomic.Value - stop chan struct{} - stopped chan struct{} -} - -func NewHTMLTemplateWatcher(directory string) (*HTMLTemplateWatcher, error) { - templatesDir, _ := fileutils.FindDir(directory) - mlog.Debug("Parsing server templates", mlog.String("templates_directory", templatesDir)) - - ret := &HTMLTemplateWatcher{ - stop: make(chan struct{}), - stopped: make(chan struct{}), - } - - watcher, err := fsnotify.NewWatcher() - if err != nil { - return nil, err - } - - if err = watcher.Add(templatesDir); err != nil { - return nil, err - } - - htmlTemplates, err := template.ParseGlob(filepath.Join(templatesDir, "*.html")) - if err != nil { - return nil, err - } - ret.templates.Store(htmlTemplates) - - go func() { - defer close(ret.stopped) - defer watcher.Close() - - for { - select { - case <-ret.stop: - return - case event := <-watcher.Events: - if event.Op&fsnotify.Write == fsnotify.Write { - mlog.Info("Re-parsing templates because of modified file", mlog.String("file_name", event.Name)) - if htmlTemplates, err := template.ParseGlob(filepath.Join(templatesDir, "*.html")); err != nil { - mlog.Error("Failed to parse templates.", mlog.Err(err)) - } else { - ret.templates.Store(htmlTemplates) - } - } - case err := <-watcher.Errors: - mlog.Error("Failed in directory watcher", mlog.Err(err)) - } - } - }() - - return ret, nil -} - -func (w *HTMLTemplateWatcher) Templates() *template.Template { - return w.templates.Load().(*template.Template) -} - -func (w *HTMLTemplateWatcher) Close() { - close(w.stop) - <-w.stopped -} - -type HTMLTemplate struct { - Templates *template.Template - TemplateName string - Props map[string]interface{} - HTML map[string]template.HTML -} - -func NewHTMLTemplate(templates *template.Template, templateName string) *HTMLTemplate { - return &HTMLTemplate{ - Templates: templates, - TemplateName: templateName, - Props: make(map[string]interface{}), - HTML: make(map[string]template.HTML), - } -} - -func (t *HTMLTemplate) Render() string { - var text bytes.Buffer - t.RenderToWriter(&text) - return text.String() -} - -func (t *HTMLTemplate) RenderToWriter(w io.Writer) error { - if t.Templates == nil { - return errors.New("no html templates") - } - - if err := t.Templates.ExecuteTemplate(w, t.TemplateName, t); err != nil { - mlog.Warn("Error rendering template", mlog.String("template_name", t.TemplateName), mlog.Err(err)) - return err - } - - return nil -} diff --git a/utils/html_test.go b/utils/html_test.go deleted file mode 100644 index 07935797cb..0000000000 --- a/utils/html_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package utils - -import ( - "bytes" - "html/template" - "io/ioutil" - "os" - "path/filepath" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestHTMLTemplateWatcher(t *testing.T) { - TranslationsPreInit() - - dir, err := ioutil.TempDir("", "") - require.NoError(t, err) - defer os.RemoveAll(dir) - - require.NoError(t, os.Mkdir(filepath.Join(dir, "templates"), 0700)) - require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}foo{{ end }}`), 0600)) - - prevDir, err := os.Getwd() - require.NoError(t, err) - defer os.Chdir(prevDir) - os.Chdir(dir) - - watcher, err := NewHTMLTemplateWatcher("templates") - require.NotNil(t, watcher) - require.NoError(t, err) - defer watcher.Close() - - tpl := NewHTMLTemplate(watcher.Templates(), "foo") - assert.Equal(t, "foo", tpl.Render()) - - require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "templates", "foo.html"), []byte(`{{ define "foo" }}bar{{ end }}`), 0600)) - - for i := 0; i < 30; i++ { - tpl = NewHTMLTemplate(watcher.Templates(), "foo") - if tpl.Render() == "bar" { - break - } - time.Sleep(time.Millisecond * 50) - } - assert.Equal(t, "bar", tpl.Render()) -} - -func TestHTMLTemplateWatcher_BadDirectory(t *testing.T) { - TranslationsPreInit() - watcher, err := NewHTMLTemplateWatcher("notarealdirectory") - assert.Nil(t, watcher) - assert.Error(t, err) -} - -func TestHTMLTemplate(t *testing.T) { - tpl := template.New("test") - _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Props.Bar }}{{ end }}`) - require.NoError(t, err) - - htmlTemplate := NewHTMLTemplate(tpl, "foo") - htmlTemplate.Props["Bar"] = "bar" - assert.Equal(t, "foobar", htmlTemplate.Render()) - - buf := &bytes.Buffer{} - require.NoError(t, htmlTemplate.RenderToWriter(buf)) - assert.Equal(t, "foobar", buf.String()) -} - -func TestHTMLTemplate_RenderError(t *testing.T) { - tpl := template.New("test") - _, err := tpl.Parse(`{{ define "foo" }}foo{{ .Foo.Bar }}bar{{ end }}`) - require.NoError(t, err) - - htmlTemplate := NewHTMLTemplate(tpl, "foo") - assert.Equal(t, "foo", htmlTemplate.Render()) - - buf := &bytes.Buffer{} - assert.Error(t, htmlTemplate.RenderToWriter(buf)) - assert.Equal(t, "foo", buf.String()) -} diff --git a/web/unsupported_browser.go b/web/unsupported_browser.go index 695a2b2443..5463f17d26 100644 --- a/web/unsupported_browser.go +++ b/web/unsupported_browser.go @@ -9,7 +9,7 @@ import ( "github.com/avct/uasurfer" "github.com/mattermost/mattermost-server/v5/app" - "github.com/mattermost/mattermost-server/v5/utils" + "github.com/mattermost/mattermost-server/v5/shared/templates" ) // MattermostApp describes downloads for the Mattermost App @@ -46,7 +46,13 @@ type SystemBrowser struct { func renderUnsupportedBrowser(app app.AppIface, w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") - page := utils.NewHTMLTemplate(app.Srv().HTMLTemplates(), "unsupported_browser") + + data := templates.Data{ + Props: map[string]interface{}{ + "DownloadAppOrUpgradeBrowserString": app.T("web.error.unsupported_browser.download_app_or_upgrade_browser"), + "LearnMoreString": app.T("web.error.unsupported_browser.learn_more"), + }, + } // User Agent info ua := uasurfer.Parse(r.UserAgent()) @@ -57,18 +63,16 @@ func renderUnsupportedBrowser(app app.AppIface, w http.ResponseWriter, r *http.R // Basic heading translations if isSafari { - page.Props["NoLongerSupportString"] = app.T("web.error.unsupported_browser.no_longer_support_version") + data.Props["NoLongerSupportString"] = app.T("web.error.unsupported_browser.no_longer_support_version") } else { - page.Props["NoLongerSupportString"] = app.T("web.error.unsupported_browser.no_longer_support") + data.Props["NoLongerSupportString"] = app.T("web.error.unsupported_browser.no_longer_support") } - page.Props["DownloadAppOrUpgradeBrowserString"] = app.T("web.error.unsupported_browser.download_app_or_upgrade_browser") - page.Props["LearnMoreString"] = app.T("web.error.unsupported_browser.learn_more") // Mattermost app version if isWindows { - page.Props["App"] = renderMattermostAppWindows(app) + data.Props["App"] = renderMattermostAppWindows(app) } else if isMacOSX { - page.Props["App"] = renderMattermostAppMac(app) + data.Props["App"] = renderMattermostAppMac(app) } // Browsers to download @@ -78,14 +82,14 @@ func renderUnsupportedBrowser(app app.AppIface, w http.ResponseWriter, r *http.R if isSafari { browsers = append(browsers, renderBrowserSafari(app)) } - page.Props["Browsers"] = browsers + data.Props["Browsers"] = browsers // If on Windows 10, show link to Edge if isWindows10 { - page.Props["SystemBrowser"] = renderSystemBrowserEdge(app, r) + data.Props["SystemBrowser"] = renderSystemBrowserEdge(app, r) } - page.RenderToWriter(w) + app.Srv().TemplatesContainer().Render(w, "unsupported_browser", data) } func renderMattermostAppMac(app app.AppIface) MattermostApp {