Add metric warning support (announcement bar and DM) (#14483)

* Admin. Advisory: Add warning for number of active users metric status

Co-authored-by: Catalin Tomai <catalin.tomai@mattermost.com>
Этот коммит содержится в:
catalintomai
2020-07-22 20:32:21 -07:00
коммит произвёл GitHub
родитель 56fb31f06f
Коммит 549e5b57cd
21 изменённых файлов: 979 добавлений и 28 удалений

Просмотреть файл

@@ -54,6 +54,9 @@ func (api *API) InitSystem() {
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(setServerBusy)).Methods("POST")
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(getServerBusyExpires)).Methods("GET")
api.BaseRoutes.ApiRoot.Handle("/server_busy", api.ApiSessionRequired(clearServerBusy)).Methods("DELETE")
api.BaseRoutes.ApiRoot.Handle("/warn_metrics/status", api.ApiSessionRequired(getWarnMetricsStatus)).Methods("GET")
api.BaseRoutes.ApiRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(sendWarnMetricAckEmail)).Methods("POST")
}
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -544,3 +547,61 @@ func getServerBusyExpires(c *Context, w http.ResponseWriter, r *http.Request) {
}
w.Write([]byte(c.App.Srv().Busy.ToJson()))
}
func getWarnMetricsStatus(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
license := c.App.Srv().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
status, err := c.App.GetWarnMetricsStatus()
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.MapWarnMetricStatusToJson(status)))
}
func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request) {
auditRec := c.MakeAuditRecord("sendWarnMetricAckEmail", audit.Fail)
defer c.LogAuditRec(auditRec)
c.LogAudit("attempt")
if !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
license := c.App.Srv().License()
if license != nil {
mlog.Debug("License is present, skip.")
return
}
user, appErr := c.App.GetUser(c.App.Session().UserId)
if appErr != nil {
c.Err = appErr
return
}
ack := model.SendWarnMetricAckFromJson(r.Body)
if ack == nil {
c.SetInvalidParam("ack")
return
}
appErr = c.App.NotifyAndSetWarnMetricAck(c.Params.WarnMetricId, user, ack.ForceAck, false)
if appErr != nil {
c.Err = appErr
}
auditRec.Success()
ReturnStatusOK(w)
}

Просмотреть файл

@@ -221,7 +221,7 @@ func (a *App) TestEmail(userId string, cfg *model.Config) *model.AppError {
T := utils.GetUserTranslations(user.Locale)
license := a.Srv().License()
if err := mailservice.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance); err != nil {
if err := mailservice.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg, license != nil && *license.Features.Compliance, ""); err != nil {
return model.NewAppError("testEmail", "app.admin.test_email.failure", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
}

Просмотреть файл

@@ -5,16 +5,20 @@ package app
import (
"context"
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
"github.com/mattermost/go-i18n/i18n"
goi18n "github.com/mattermost/go-i18n/i18n"
"github.com/mattermost/mattermost-server/v5/einterfaces"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/services/httpservice"
"github.com/mattermost/mattermost-server/v5/services/imageproxy"
"github.com/mattermost/mattermost-server/v5/services/mailservice"
"github.com/mattermost/mattermost-server/v5/services/searchengine"
"github.com/mattermost/mattermost-server/v5/services/timezones"
"github.com/mattermost/mattermost-server/v5/utils"
@@ -107,6 +111,7 @@ func (a *App) InitServer() {
if a.Srv().runjobs {
a.Srv().Go(func() {
runLicenseExpirationCheckJob(a)
runCheckNumberOfActiveUsersWarnMetricStatusJob(a)
})
}
a.srv.RunJobs()
@@ -184,6 +189,253 @@ func (s *Server) getFirstServerRunTimestamp() (int64, *model.AppError) {
return value, nil
}
func (a *App) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
systemDataList, appErr := a.Srv().Store.System().Get()
if appErr != nil {
return nil, appErr
}
result := map[string]*model.WarnMetricStatus{}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) {
if warnMetric, ok := model.WarnMetricsTable[key]; ok {
if !warnMetric.IsBotOnly && value == model.WARN_METRIC_STATUS_LIMIT_REACHED {
result[key], _ = a.getWarnMetricStatusAndDisplayTextsForId(key, nil)
}
}
}
}
return result, nil
}
func (a *App) getWarnMetricStatusAndDisplayTextsForId(warnMetricId string, T i18n.TranslateFunc) (*model.WarnMetricStatus, *model.WarnMetricDisplayTexts) {
var warnMetricStatus *model.WarnMetricStatus
var warnMetricDisplayTexts = &model.WarnMetricDisplayTexts{}
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
warnMetricStatus = &model.WarnMetricStatus{
Id: warnMetric.Id,
Limit: warnMetric.Limit,
Acked: false,
}
if T == nil {
mlog.Debug("No translation function")
return warnMetricStatus, nil
}
warnMetricDisplayTexts.BotMailToBody = T("api.server.warn_metric.bot_response.number_of_users.mailto_body", map[string]interface{}{"Limit": warnMetric.Limit})
warnMetricDisplayTexts.EmailBody = T("api.templates.warn_metric_ack.number_of_active_users.body", map[string]interface{}{"Limit": warnMetric.Limit})
switch warnMetricId {
case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_200.notification_title")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_200.notification_body")
case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_400.notification_title")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_400.notification_body")
case model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500:
warnMetricDisplayTexts.BotTitle = T("api.server.warn_metric.number_of_active_users_500.notification_title")
warnMetricDisplayTexts.BotMessageBody = T("api.server.warn_metric.number_of_active_users_500.notification_body")
default:
mlog.Error("Invalid metric id", mlog.String("id", warnMetricId))
return nil, nil
}
return warnMetricStatus, warnMetricDisplayTexts
}
return nil, nil
}
func (a *App) notifyAdminsOfWarnMetricStatus(warnMetricId string) *model.AppError {
perPage := 25
userOptions := &model.UserGetOptions{
Page: 0,
PerPage: perPage,
Role: model.SYSTEM_ADMIN_ROLE_ID,
Inactive: false,
}
// get sysadmins
var sysAdmins []*model.User
for {
sysAdminsList, err := a.GetUsers(userOptions)
if err != nil {
return err
}
if len(sysAdminsList) == 0 {
return model.NewAppError("NotifyAdminsOfWarnMetricStatus", "app.system.warn_metric.notification.empty_admin_list.app_error", nil, err.Error(), http.StatusInternalServerError)
}
sysAdmins = append(sysAdmins, sysAdminsList...)
if len(sysAdminsList) < perPage {
mlog.Debug("Number of system admins is less than page limit", mlog.Int("count", len(sysAdminsList)))
break
}
}
T := utils.GetUserTranslations(sysAdmins[0].Locale)
warnMetricsBot := &model.Bot{
Username: model.BOT_WARN_METRIC_BOT_USERNAME,
DisplayName: T("app.system.warn_metric.bot_displayname"),
Description: "",
OwnerId: sysAdmins[0].Id,
}
bot, err := a.getOrCreateWarnMetricsBot(warnMetricsBot)
if err != nil {
return err
}
for _, sysAdmin := range sysAdmins {
T := utils.GetUserTranslations(sysAdmin.Locale)
bot.DisplayName = T("app.system.warn_metric.bot_displayname")
bot.Description = T("app.system.warn_metric.bot_description")
channel, appErr := a.GetOrCreateDirectChannel(bot.UserId, sysAdmin.Id)
if appErr != nil {
mlog.Error("Cannot create channel for system bot notification!", mlog.String("Admin Id", sysAdmin.Id))
return appErr
}
warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
if warnMetricStatus == nil {
return model.NewAppError("NotifyAdminsOfWarnMetricStatus", "app.system.warn_metric.notification.invalid_metric.app_error", nil, "", http.StatusInternalServerError)
}
botPost := &model.Post{
UserId: bot.UserId,
ChannelId: channel.Id,
Type: model.POST_SYSTEM_WARN_METRIC_STATUS,
Message: "",
}
actions := []*model.PostAction{}
actions = append(actions,
&model.PostAction{
Id: "contactUs",
Name: T("api.server.warn_metric.contact_us"),
Type: model.POST_ACTION_TYPE_BUTTON,
Options: []*model.PostActionOptions{
{
Text: "TrackEventId",
Value: warnMetricId,
},
{
Text: "ActionExecutingMessage",
Value: T("api.server.warn_metric.contacting_us"),
},
},
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"bot_user_id": bot.UserId,
"force_ack": false,
},
URL: fmt.Sprintf("/warn_metrics/ack/%s", warnMetricId),
},
},
)
attachments := []*model.SlackAttachment{{
AuthorName: "",
Title: warnMetricDisplayTexts.BotTitle,
Text: warnMetricDisplayTexts.BotMessageBody,
Actions: actions,
}}
model.ParseSlackAttachment(botPost, attachments)
mlog.Debug("Send admin advisory for metric", mlog.String("warnMetricId", warnMetricId), mlog.String("userid", botPost.UserId))
if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil {
return err
}
}
return nil
}
func (a *App) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
if warnMetric, ok := model.WarnMetricsTable[warnMetricId]; ok {
data, err := a.Srv().Store.System().GetByName(warnMetric.Id)
if err == nil && data != nil && data.Value == model.WARN_METRIC_STATUS_ACK {
mlog.Debug("This metric warning has already been acknowledged")
return nil
}
if !forceAck {
if len(*a.Config().EmailSettings.SMTPServer) == 0 {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.missing_server.app_error", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}), http.StatusInternalServerError)
}
T := utils.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
//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.Error("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
}
bodyPage.Props["SiteURLHeader"] = T("api.templates.warn_metric_ack.body.site_url_header")
bodyPage.Props["SiteURL"] = a.GetSiteURL()
bodyPage.Props["DiagnosticIdHeader"] = T("api.templates.warn_metric_ack.body.diagnostic_id_header")
bodyPage.Props["DiagnosticIdValue"] = a.DiagnosticId()
bodyPage.Props["Footer"] = T("api.templates.warn_metric_ack.footer")
warnMetricStatus, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
if warnMetricStatus == nil {
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.invalid_warn_metric.app_error", nil, "", http.StatusInternalServerError)
}
subject := T("api.templates.warn_metric_ack.subject")
bodyPage.Props["Title"] = warnMetricDisplayTexts.EmailBody
if err = mailservice.SendMailUsingConfig(model.MM_SUPPORT_ADDRESS, subject, bodyPage.Render(), a.Config(), false, sender.Email); err != nil {
mlog.Error("Error while sending email", mlog.String("destination email", model.MM_SUPPORT_ADDRESS), mlog.Err(err))
return model.NewAppError("NotifyAndSetWarnMetricAck", "api.email.send_warn_metric_ack.failure.app_error", map[string]interface{}{"Error": err.Error()}, "", http.StatusInternalServerError)
}
}
mlog.Debug("Disable the monitoring of all warn metrics")
err = a.setWarnMetricsStatus(model.WARN_METRIC_STATUS_ACK)
if err != nil {
return err
}
if !warnMetric.IsBotOnly && !isBot {
message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_REMOVED, "", "", "", nil)
message.Add("warnMetricId", warnMetric.Id)
a.Publish(message)
}
}
return nil
}
func (a *App) setWarnMetricsStatus(status string) *model.AppError {
for _, warnMetric := range model.WarnMetricsTable {
a.setWarnMetricsStatusForId(warnMetric.Id, status)
}
return nil
}
func (a *App) setWarnMetricsStatusForId(warnMetricId string, status string) *model.AppError {
mlog.Info("Storing user acknowledgement for warn metric", mlog.String("warnMetricId", warnMetricId))
if err := a.Srv().Store.System().SaveOrUpdate(&model.System{
Name: warnMetricId,
Value: status,
}); err != nil {
mlog.Error("Unable to write to database.", mlog.Err(err))
return model.NewAppError("setWarnMetricsStatusForId", "app.system.warn_metric.store.app_error", map[string]interface{}{"WarnMetricName": warnMetricId}, "", http.StatusInternalServerError)
}
return nil
}
func (a *App) Srv() *Server {
return a.srv
}

Просмотреть файл

@@ -692,6 +692,7 @@ type AppIface interface {
GetUsersWithoutTeamPage(options *model.UserGetOptions, asAdmin bool) ([]*model.User, *model.AppError)
GetVerifyEmailToken(token string) (*model.Token, *model.AppError)
GetViewUsersRestrictions(userId string) (*model.ViewUsersRestrictions, *model.AppError)
GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError)
HTTPService() httpservice.HTTPService
Handle404(w http.ResponseWriter, r *http.Request)
HandleCommandResponse(command *model.Command, args *model.CommandArgs, response *model.CommandResponse, builtIn bool) (*model.CommandResponse, *model.AppError)
@@ -753,6 +754,7 @@ type AppIface interface {
NewPluginAPI(manifest *model.Manifest) plugin.API
Notification() einterfaces.NotificationInterface
NotificationsLog() *mlog.Logger
NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError
OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError
OriginChecker() func(*http.Request) bool
PatchChannel(channel *model.Channel, patch *model.ChannelPatch, userId string) (*model.Channel, *model.AppError)

Просмотреть файл

@@ -65,6 +65,51 @@ func (a *App) CreateBot(bot *model.Bot) (*model.Bot, *model.AppError) {
return savedBot, nil
}
func (a *App) getOrCreateWarnMetricsBot(botDef *model.Bot) (*model.Bot, *model.AppError) {
botUser, appErr := a.GetUserByUsername(botDef.Username)
if appErr != nil {
if appErr.StatusCode != http.StatusNotFound {
mlog.Error(appErr.Error())
return nil, appErr
}
// cannot find this bot user, save the user
user, err := a.Srv().Store.User().Save(model.UserFromBot(botDef))
if err != nil {
mlog.Error(err.Error())
return nil, err
}
botDef.UserId = user.Id
//save the bot
savedBot, nErr := a.Srv().Store.Bot().Save(botDef)
if nErr != nil {
a.Srv().Store.User().PermanentDelete(savedBot.UserId)
var nAppErr *model.AppError
switch {
case errors.As(nErr, &nAppErr): // in case we haven't converted to plain error.
return nil, nAppErr
default: // last fallback in case it doesn't map to an existing app error.
return nil, model.NewAppError("getOrCreateWarnMetricsBot", "app.bot.createbot.internal_error", nil, nErr.Error(), http.StatusInternalServerError)
}
}
return savedBot, nil
}
if botUser == nil {
return nil, model.NewAppError("getOrCreateWarnMetricsBot", "app.bot.createbot.internal_error", nil, "", http.StatusInternalServerError)
}
//return the bot for this user
savedBot, appErr := a.GetBot(botUser.Id, false)
if appErr != nil {
mlog.Error(appErr.Error())
return nil, appErr
}
return savedBot, nil
}
// PatchBot applies the given patch to the bot and corresponding user.
func (a *App) PatchBot(botUserId string, botPatch *model.BotPatch) (*model.Bot, *model.AppError) {
bot, err := a.GetBot(botUserId, true)

Просмотреть файл

@@ -57,6 +57,7 @@ const (
TRACK_ELASTICSEARCH = "elasticsearch"
TRACK_GROUPS = "groups"
TRACK_CHANNEL_MODERATION = "channel_moderation"
TRACK_WARN_METRICS = "warn_metrics"
TRACK_ACTIVITY = "activity"
TRACK_LICENSE = "license"
@@ -83,6 +84,7 @@ func (s *Server) sendDailyDiagnostics(override bool) {
s.trackElasticsearch()
s.trackGroups()
s.trackChannelModeration()
s.trackWarnMetrics()
}
}
@@ -1088,3 +1090,19 @@ func (s *Server) trackChannelModeration() {
"use_channel_mentions_guest_disabled_count": useChannelMentionsGuest,
})
}
func (s *Server) trackWarnMetrics() {
systemDataList, appErr := s.Store.System().Get()
if appErr != nil {
return
}
for key, value := range systemDataList {
if strings.HasPrefix(key, model.WARN_METRIC_STATUS_STORE_PREFIX) {
if _, ok := model.WarnMetricsTable[key]; ok {
s.SendDiagnostic(TRACK_WARN_METRICS, map[string]interface{}{
key: value != "false",
})
}
}
}
}

Просмотреть файл

@@ -550,15 +550,19 @@ func (es *EmailService) sendNotificationMail(to, subject, htmlBody string) *mode
}
func (es *EmailService) sendMail(to, subject, htmlBody string) *model.AppError {
return es.sendMailWithCC(to, subject, htmlBody, "")
}
func (es *EmailService) sendMailWithCC(to, subject, htmlBody string, ccMail string) *model.AppError {
license := es.srv.License()
return mailservice.SendMailUsingConfig(to, subject, htmlBody, es.srv.Config(), license != nil && *license.Features.Compliance)
return mailservice.SendMailUsingConfig(to, subject, htmlBody, es.srv.Config(), license != nil && *license.Features.Compliance, ccMail)
}
func (es *EmailService) sendMailWithEmbeddedFiles(to, subject, htmlBody string, embeddedFiles map[string]io.Reader) *model.AppError {
license := es.srv.License()
config := es.srv.Config()
return mailservice.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, config, license != nil && *license.Features.Compliance)
return mailservice.SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, embeddedFiles, config, license != nil && *license.Features.Compliance, "")
}
func (es *EmailService) CreateVerifyEmailToken(userId string, newEmail string) (*model.Token, *model.AppError) {

Просмотреть файл

@@ -26,10 +26,12 @@ import (
"net/http"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/gorilla/mux"
"github.com/mattermost/mattermost-server/v5/mlog"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/mattermost/mattermost-server/v5/store"
"github.com/mattermost/mattermost-server/v5/utils"
@@ -211,11 +213,20 @@ func (a *App) DoPostActionWithCookie(postId, actionId, userId, selectedOption st
return "", appErr
}
resp, appErr := a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
if appErr != nil {
return "", appErr
var resp *http.Response
if strings.HasPrefix(upstreamURL, "/warn_metrics/") {
appErr = a.doLocalWarnMetricsRequest(upstreamURL, upstreamRequest)
if appErr != nil {
return "", appErr
}
return "", nil
} else {
resp, appErr = a.DoActionRequest(upstreamURL, upstreamRequest.ToJson())
if appErr != nil {
return "", appErr
}
defer resp.Body.Close()
}
defer resp.Body.Close()
var response model.PostActionIntegrationResponse
if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
@@ -398,6 +409,129 @@ func (a *App) doPluginRequest(method, rawURL string, values url.Values, body []b
return resp, nil
}
func (a *App) doLocalWarnMetricsRequest(rawURL string, upstreamRequest *model.PostActionIntegrationRequest) *model.AppError {
_, err := url.Parse(rawURL)
if err != nil {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, err.Error(), http.StatusBadRequest)
}
warnMetricId := filepath.Base(rawURL)
if warnMetricId == "" {
return model.NewAppError("doLocalWarnMetricsRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest)
}
user, appErr := a.GetUser(a.Session().UserId)
if appErr != nil {
return appErr
}
botPost := &model.Post{
UserId: upstreamRequest.Context["bot_user_id"].(string),
ChannelId: upstreamRequest.ChannelId,
HasReactions: true,
}
forceAck := upstreamRequest.Context["force_ack"].(bool)
if appErr = a.NotifyAndSetWarnMetricAck(warnMetricId, user, forceAck, true); appErr != nil {
if forceAck {
return appErr
}
mailtoLinkText := a.buildWarnMetricMailtoLink(warnMetricId, user)
botPost.Message = ":warning: " + utils.T("api.server.warn_metric.bot_response.notification_failure.message")
actions := []*model.PostAction{}
actions = append(actions,
&model.PostAction{
Id: "emailUs",
Name: utils.T("api.server.warn_metric.email_us"),
Type: model.POST_ACTION_TYPE_BUTTON,
Options: []*model.PostActionOptions{
{
Text: "WarnMetricMailtoUrl",
Value: mailtoLinkText,
},
{
Text: "TrackEventId",
Value: warnMetricId,
},
},
Integration: &model.PostActionIntegration{
Context: model.StringInterface{
"bot_user_id": botPost.UserId,
"force_ack": true,
},
URL: fmt.Sprintf("/warn_metrics/ack/%s", model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500),
},
},
)
attachements := []*model.SlackAttachment{{
AuthorName: "",
Title: "",
Actions: actions,
Text: utils.T("api.server.warn_metric.bot_response.notification_failure.body"),
}}
model.ParseSlackAttachment(botPost, attachements)
} else {
botPost.Message = ":white_check_mark: " + utils.T("api.server.warn_metric.bot_response.notification_success.message")
}
if _, err := a.CreatePostAsUser(botPost, a.Session().Id, true); err != nil {
return err
}
return nil
}
type MailToLinkContent struct {
MetricId string `json:"metric_id"`
MailRecipient string `json:"mail_recipient"`
MailCC string `json:"mail_cc"`
MailSubject string `json:"mail_subject"`
MailBody string `json:"mail_body"`
}
func (mlc *MailToLinkContent) ToJson() string {
b, _ := json.Marshal(mlc)
return string(b)
}
func (a *App) buildWarnMetricMailtoLink(warnMetricId string, user *model.User) string {
T := utils.GetUserTranslations(user.Locale)
_, warnMetricDisplayTexts := a.getWarnMetricStatusAndDisplayTextsForId(warnMetricId, T)
mailBody := warnMetricDisplayTexts.BotMailToBody
mailBody += T("api.server.warn_metric.bot_response.mailto_contact_header", map[string]interface{}{"Contact": user.GetFullName()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_email_header", map[string]interface{}{"Email": user.Email})
mailBody += "\r\n"
registeredUsersCount, err := a.Srv().Store.User().Count(model.UserCountOptions{})
if err != nil {
mlog.Error("Error retrieving the number of registered users", mlog.Err(err))
} else {
mailBody += utils.T("api.server.warn_metric.bot_response.mailto_registered_users_header", map[string]interface{}{"NoRegisteredUsers": registeredUsersCount})
mailBody += "\r\n"
}
mailBody += T("api.server.warn_metric.bot_response.mailto_site_url_header", map[string]interface{}{"SiteUrl": a.GetSiteURL()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_diagnostic_id_header", map[string]interface{}{"DiagnosticId": a.DiagnosticId()})
mailBody += "\r\n"
mailBody += T("api.server.warn_metric.bot_response.mailto_footer")
mailToLinkContent := &MailToLinkContent{
MetricId: warnMetricId,
MailRecipient: "support@mattermost.com",
MailCC: user.Email,
MailSubject: T("api.server.warn_metric.bot_response.mailto_subject"),
MailBody: mailBody,
}
return mailToLinkContent.ToJson()
}
func (a *App) DoLocalRequest(rawURL string, body []byte) (*http.Response, *model.AppError) {
return a.doPluginRequest("POST", rawURL, nil, body)
}

Просмотреть файл

@@ -8940,6 +8940,28 @@ func (a *OpenTracingAppLayer) GetViewUsersRestrictions(userId string) (*model.Vi
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) GetWarnMetricsStatus() (map[string]*model.WarnMetricStatus, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetWarnMetricsStatus")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0, resultVar1 := a.app.GetWarnMetricsStatus()
if resultVar1 != nil {
span.LogFields(spanlog.Error(resultVar1))
ext.Error.Set(span, true)
}
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) Handle404(w http.ResponseWriter, r *http.Request) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.Handle404")
@@ -10255,6 +10277,28 @@ func (a *OpenTracingAppLayer) NewWebHub() *app.Hub {
return resultVar0
}
func (a *OpenTracingAppLayer) NotifyAndSetWarnMetricAck(warnMetricId string, sender *model.User, forceAck bool, isBot bool) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifyAndSetWarnMetricAck")
a.ctx = newCtx
a.app.Srv().Store.SetContext(newCtx)
defer func() {
a.app.Srv().Store.SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.NotifyAndSetWarnMetricAck(warnMetricId, sender, forceAck, isBot)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) NotifySessionsExpired() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.NotifySessionsExpired")

Просмотреть файл

@@ -115,7 +115,7 @@ func (s *Server) DoSecurityUpdateCheck() {
for _, user := range users {
mlog.Info("Sending security bulletin", mlog.String("bulletin_id", bulletin.Id), mlog.String("user_email", user.Email))
license := s.License()
mailservice.SendMailUsingConfig(user.Email, utils.T("mattermost.bulletin.subject"), string(body), s.Config(), license != nil && *license.Features.Compliance)
mailservice.SendMailUsingConfig(user.Email, utils.T("mattermost.bulletin.subject"), string(body), s.Config(), license != nil && *license.Features.Compliance, "")
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}

Просмотреть файл

@@ -1090,6 +1090,13 @@ func runLicenseExpirationCheckJob(a *App) {
}, time.Hour*24)
}
func runCheckNumberOfActiveUsersWarnMetricStatusJob(a *App) {
doCheckNumberOfActiveUsersWarnMetricStatus(a)
model.CreateRecurringTask("Check Number Of Active Users Warn Metric Status", func() {
doCheckNumberOfActiveUsersWarnMetricStatus(a)
}, time.Hour*24)
}
func doSecurity(s *Server) {
s.DoSecurityUpdateCheck()
}
@@ -1117,6 +1124,58 @@ func doSessionCleanup(s *Server) {
s.Store.Session().Cleanup(model.GetMillis(), SESSIONS_CLEANUP_BATCH_SIZE)
}
func doCheckNumberOfActiveUsersWarnMetricStatus(a *App) {
license := a.Srv().License()
if license != nil {
mlog.Debug("License is present, skip this check")
return
}
numberOfActiveUsers, err := a.Srv().Store.User().Count(model.UserCountOptions{})
if err != nil {
mlog.Error("Error to get active registered users.", mlog.Err(err))
}
warnMetrics := []model.WarnMetric{}
if numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit {
return
} else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit {
warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200])
} else if numberOfActiveUsers >= model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400].Limit && numberOfActiveUsers < model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500].Limit {
warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400])
} else {
warnMetrics = append(warnMetrics, model.WarnMetricsTable[model.SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500])
}
for _, warnMetric := range warnMetrics {
data, err := a.Srv().Store.System().GetByName(warnMetric.Id)
if err == nil && data != nil && (data.Value == model.WARN_METRIC_STATUS_ACK || data.Value == model.WARN_METRIC_STATUS_RUNONCE) {
mlog.Debug("This metric warning has already been acked or should only run once")
continue
}
if err = a.Srv().Store.System().SaveOrUpdate(&model.System{Name: warnMetric.Id, Value: model.WARN_METRIC_STATUS_LIMIT_REACHED}); err != nil {
mlog.Error("Unable to write to database.", mlog.String("id", warnMetric.Id), mlog.Err(err))
continue
}
warnMetricStatus, _ := a.getWarnMetricStatusAndDisplayTextsForId(warnMetric.Id, nil)
if !warnMetric.IsBotOnly {
message := model.NewWebSocketEvent(model.WEBSOCKET_WARN_METRIC_STATUS_RECEIVED, "", "", "", nil)
message.Add("warnMetricStatus", warnMetricStatus.ToJson())
a.Publish(message)
}
if err = a.notifyAdminsOfWarnMetricStatus(warnMetric.Id); err != nil {
mlog.Error("Failed to send notifications to admin users.", mlog.Err(err))
}
if warnMetric.IsRunOnce {
a.setWarnMetricsStatusForId(warnMetric.Id, model.WARN_METRIC_STATUS_RUNONCE)
}
}
}
func doLicenseExpirationCheck(a *App) {
a.Srv().LoadLicense()
license := a.Srv().License()

Просмотреть файл

@@ -1178,6 +1178,18 @@
"id": "api.create_terms_of_service.empty_text.app_error",
"translation": "Please enter text for your Custom Terms of Service."
},
{
"id": "api.email.send_warn_metric_ack.failure.app_error",
"translation": "Failure to send admin acknowledgment email"
},
{
"id": "api.email.send_warn_metric_ack.invalid_warn_metric.app_error",
"translation": "Could not find warn metric."
},
{
"id": "api.email.send_warn_metric_ack.missing_server.app_error",
"translation": "SMTP Server is required"
},
{
"id": "api.email_batching.add_notification_email_to_batch.channel_full.app_error",
"translation": "Email batching job's receiving channel was full. Please increase the EmailBatchingBufferSize."
@@ -1926,6 +1938,86 @@
"id": "api.server.start_server.starting.critical",
"translation": "Error starting server, err:%v"
},
{
"id": "api.server.warn_metric.bot_response.mailto_contact_header",
"translation": "Contact: {{.Contact}}"
},
{
"id": "api.server.warn_metric.bot_response.mailto_diagnostic_id_header",
"translation": "Diagnostic Id: {{.DiagnosticId}}"
},
{
"id": "api.server.warn_metric.bot_response.mailto_email_header",
"translation": "Email: {{.Email}}"
},
{
"id": "api.server.warn_metric.bot_response.mailto_footer",
"translation": "If you have any additional inquiries, please contact support@mattermost.com"
},
{
"id": "api.server.warn_metric.bot_response.mailto_registered_users_header",
"translation": "Total Active Users: {{.NoRegisteredUsers}}"
},
{
"id": "api.server.warn_metric.bot_response.mailto_site_url_header",
"translation": "Site URL: {{.SiteUrl}}"
},
{
"id": "api.server.warn_metric.bot_response.mailto_subject",
"translation": "Mattermost Contact Us request"
},
{
"id": "api.server.warn_metric.bot_response.notification_failure.body",
"translation": "Please email us."
},
{
"id": "api.server.warn_metric.bot_response.notification_failure.message",
"translation": "Message could not be sent."
},
{
"id": "api.server.warn_metric.bot_response.notification_success.message",
"translation": "Thank you for contacting Mattermost. We will follow up with you soon."
},
{
"id": "api.server.warn_metric.bot_response.number_of_users.mailto_body",
"translation": "Mattermost Contact Us request. My team has now {{.Limit}} users and I am considering Mattermost Enterprise Edition.\r\n"
},
{
"id": "api.server.warn_metric.contact_us",
"translation": "Contact us"
},
{
"id": "api.server.warn_metric.contacting_us",
"translation": "Contacting us"
},
{
"id": "api.server.warn_metric.email_us",
"translation": "Email us"
},
{
"id": "api.server.warn_metric.number_of_active_users_200.notification_body",
"translation": "Your Mattermost system now has 200 users. As your user base grows, provisioning new accounts can become time-consuming. We recommend that you upgrade to Mattermost Enterprise E10 and integrate your organizations Active Directory/LDAP, which will allow anyone with an account to access Mattermost. Users can log in without having to create new usernames and passwords, and administrators save time provisioning and managing accounts.\n[Learn more about integrating with AD/LDAP](https://docs.mattermost.com/deployment/sso-ldap.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=adldap)"
},
{
"id": "api.server.warn_metric.number_of_active_users_200.notification_title",
"translation": "Integrate AD/LDAP"
},
{
"id": "api.server.warn_metric.number_of_active_users_400.notification_body",
"translation": "Your Mattermost system now has 400 users. When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials. Contact support to learn more about integrating with SAML 2.0, available in Mattermost Enterprise E20.\n[Learn more about integrating with SAML 2.0](https://docs.mattermost.com/deployment/sso-saml.html?utm_medium=product&utm_source=mattermost-advisor-bot&utm_content=saml)"
},
{
"id": "api.server.warn_metric.number_of_active_users_400.notification_title",
"translation": "Integrate SAML 2.0"
},
{
"id": "api.server.warn_metric.number_of_active_users_500.notification_body",
"translation": "Mattermost strongly recommends that deployments of over 500 users upgrade to Mattermost Enterprise E20, which offers features such as user management, server clustering, and performance monitoring."
},
{
"id": "api.server.warn_metric.number_of_active_users_500.notification_title",
"translation": "Upgrade to Mattermost Enterprise edition"
},
{
"id": "api.slackimport.slack_add_bot_user.email_pwd",
"translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n"
@@ -2462,6 +2554,38 @@
"id": "api.templates.verify_subject",
"translation": "[{{ .SiteName }}] Email Verification"
},
{
"id": "api.templates.warn_metric_ack.body.contact_email_header",
"translation": "Email: "
},
{
"id": "api.templates.warn_metric_ack.body.contact_name_header",
"translation": "Contact: "
},
{
"id": "api.templates.warn_metric_ack.body.diagnostic_id_header",
"translation": "Diagnostic Id: "
},
{
"id": "api.templates.warn_metric_ack.body.registered_users_header",
"translation": "Total Active Users: "
},
{
"id": "api.templates.warn_metric_ack.body.site_url_header",
"translation": "Site URL: "
},
{
"id": "api.templates.warn_metric_ack.footer",
"translation": "If you have any additional inquiries, please contact support@mattermost.com"
},
{
"id": "api.templates.warn_metric_ack.number_of_active_users.body",
"translation": "Mattermost Contact Us request. My team has now {{ .Limit }} users and I am considering Mattermost Enterprise Edition."
},
{
"id": "api.templates.warn_metric_ack.subject",
"translation": "Mattermost Contact Us request"
},
{
"id": "api.templates.welcome_body.app_download_info",
"translation": "For the best experience, download the apps for PC, Mac, iOS and Android from:"
@@ -4202,6 +4326,26 @@
"id": "app.submit_interactive_dialog.json_error",
"translation": "Encountered an error encoding JSON for the interactive dialog."
},
{
"id": "app.system.warn_metric.bot_description",
"translation": "[Learn more about the Mattermost Advisor](https://about.mattermost.com/default-channel-handle-documentation)"
},
{
"id": "app.system.warn_metric.bot_displayname",
"translation": "Mattermost Advisor"
},
{
"id": "app.system.warn_metric.notification.empty_admin_list.app_error",
"translation": "List of admins is empty."
},
{
"id": "app.system.warn_metric.notification.invalid_metric.app_error",
"translation": "Could not find metric."
},
{
"id": "app.system.warn_metric.store.app_error",
"translation": "Failed to store value for {{.WarnMetricName}}"
},
{
"id": "app.system_install_date.parse_int.app_error",
"translation": "Failed to parse installation date."

Просмотреть файл

@@ -13,9 +13,10 @@ import (
)
const (
BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES
BOT_DESCRIPTION_MAX_RUNES = 1024
BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId
BOT_DISPLAY_NAME_MAX_RUNES = USER_FIRST_NAME_MAX_RUNES
BOT_DESCRIPTION_MAX_RUNES = 1024
BOT_CREATOR_ID_MAX_RUNES = KEY_VALUE_PLUGIN_ID_MAX_RUNES // UserId or PluginId
BOT_WARN_METRIC_BOT_USERNAME = "mattermost-advisor"
)
// Bot is a special type of User meant for programmatic interactions.

Просмотреть файл

@@ -47,6 +47,7 @@ const (
GENERIC_NO_CHANNEL_NOTIFICATION = "generic_no_channel"
GENERIC_NOTIFICATION = "generic"
GENERIC_NOTIFICATION_SERVER = "https://push-test.mattermost.com"
MM_SUPPORT_ADDRESS = "support@mattermost.com"
FULL_NOTIFICATION = "full"
ID_LOADED_NOTIFICATION = "id_loaded"

Просмотреть файл

@@ -64,6 +64,7 @@ const (
POST_PROPS_MENTION_HIGHLIGHT_DISABLED = "mentionHighlightDisabled"
POST_PROPS_GROUP_HIGHLIGHT_DISABLED = "disable_group_highlight"
POST_SYSTEM_WARN_METRIC_STATUS = "warn_metric_status"
)
var AT_MENTION_PATTEN = regexp.MustCompile(`\B@`)
@@ -312,7 +313,8 @@ func (o *Post) IsValid(maxPostSize int) *AppError {
POST_CHANNEL_RESTORED,
POST_CHANGE_CHANNEL_PRIVACY,
POST_ME,
POST_ADD_BOT_TEAMS_CHANNELS:
POST_ADD_BOT_TEAMS_CHANNELS,
POST_SYSTEM_WARN_METRIC_STATUS:
default:
if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)

Просмотреть файл

@@ -10,16 +10,26 @@ import (
)
const (
SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret"
SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp"
SYSTEM_CLUSTER_ENCRYPTION_KEY = "ClusterEncryptionKey"
SYSTEM_DIAGNOSTIC_ID = "DiagnosticId"
SYSTEM_RAN_UNIT_TESTS = "RanUnitTests"
SYSTEM_LAST_SECURITY_TIME = "LastSecurityTime"
SYSTEM_ACTIVE_LICENSE_ID = "ActiveLicenseId"
SYSTEM_LAST_COMPLIANCE_TIME = "LastComplianceTime"
SYSTEM_ASYMMETRIC_SIGNING_KEY = "AsymmetricSigningKey"
SYSTEM_POST_ACTION_COOKIE_SECRET = "PostActionCookieSecret"
SYSTEM_INSTALLATION_DATE_KEY = "InstallationDate"
SYSTEM_FIRST_SERVER_RUN_TIMESTAMP_KEY = "FirstServerRunTimestamp"
SYSTEM_CLUSTER_ENCRYPTION_KEY = "ClusterEncryptionKey"
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200 = "warn_metric_number_of_active_users_200"
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400 = "warn_metric_number_of_active_users_400"
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500 = "warn_metric_number_of_active_users_500"
)
const (
WARN_METRIC_STATUS_LIMIT_REACHED = "true"
WARN_METRIC_STATUS_RUNONCE = "runonce"
WARN_METRIC_STATUS_ACK = "ack"
WARN_METRIC_STATUS_STORE_PREFIX = "warn_metric_"
)
type System struct {
@@ -70,3 +80,78 @@ func ServerBusyStateFromJson(r io.Reader) *ServerBusyState {
json.NewDecoder(r).Decode(&sbs)
return sbs
}
var WarnMetricsTable = map[string]WarnMetric{
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200: {
Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_200,
Limit: 200,
IsBotOnly: true,
IsRunOnce: true,
},
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400: {
Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_400,
Limit: 400,
IsBotOnly: true,
IsRunOnce: true,
},
SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500: {
Id: SYSTEM_WARN_METRIC_NUMBER_OF_ACTIVE_USERS_500,
Limit: 500,
IsBotOnly: false,
IsRunOnce: false,
},
}
type WarnMetric struct {
Id string
Limit int64
IsBotOnly bool
IsRunOnce bool
}
type WarnMetricDisplayTexts struct {
BotTitle string
BotMessageBody string
BotMailToBody string
EmailBody string
}
type WarnMetricStatus struct {
Id string `json:"id"`
Limit int64 `json:"limit"`
Acked bool `json:"acked"`
StoreStatus string `json:"store_status,omitempty"`
}
func (wms *WarnMetricStatus) ToJson() string {
b, _ := json.Marshal(wms)
return string(b)
}
func WarnMetricStatusFromJson(data io.Reader) *WarnMetricStatus {
var o WarnMetricStatus
if err := json.NewDecoder(data).Decode(&o); err != nil {
return nil
} else {
return &o
}
}
func MapWarnMetricStatusToJson(o map[string]*WarnMetricStatus) string {
b, _ := json.Marshal(o)
return string(b)
}
type SendWarnMetricAck struct {
ForceAck bool `json:"forceAck"`
}
func (swma *SendWarnMetricAck) ToJson() string {
b, _ := json.Marshal(swma)
return string(b)
}
func SendWarnMetricAckFromJson(r io.Reader) *SendWarnMetricAck {
var swma *SendWarnMetricAck
json.NewDecoder(r).Decode(&swma)
return swma
}

Просмотреть файл

@@ -66,6 +66,8 @@ const (
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_UPDATED = "sidebar_category_updated"
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_DELETED = "sidebar_category_deleted"
WEBSOCKET_EVENT_SIDEBAR_CATEGORY_ORDER_UPDATED = "sidebar_category_order_updated"
WEBSOCKET_WARN_METRIC_STATUS_RECEIVED = "warn_metric_status_received"
WEBSOCKET_WARN_METRIC_STATUS_REMOVED = "warn_metric_status_removed"
)
type WebSocketMessage interface {

Просмотреть файл

@@ -29,6 +29,7 @@ type mailData struct {
mimeTo string
smtpTo string
from mail.Address
cc string
replyTo mail.Address
subject string
htmlBody string
@@ -250,7 +251,7 @@ func TestConnection(config *model.Config) *model.AppError {
return nil
}
func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *model.Config, enableComplianceFeatures bool) *model.AppError {
func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embeddedFiles map[string]io.Reader, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
fromMail := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.FeedbackEmail}
replyTo := mail.Address{Name: *config.EmailSettings.FeedbackName, Address: *config.EmailSettings.ReplyToAddress}
@@ -258,6 +259,7 @@ func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embedded
mimeTo: to,
smtpTo: to,
from: fromMail,
cc: ccMail,
replyTo: replyTo,
subject: subject,
htmlBody: htmlBody,
@@ -267,8 +269,8 @@ func SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody string, embedded
return sendMailUsingConfigAdvanced(mail, config, enableComplianceFeatures)
}
func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool) *model.AppError {
return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures)
func SendMailUsingConfig(to, subject, htmlBody string, config *model.Config, enableComplianceFeatures bool, ccMail string) *model.AppError {
return SendMailWithEmbeddedFilesUsingConfig(to, subject, htmlBody, nil, config, enableComplianceFeatures, ccMail)
}
// allows for sending an email with attachments and differing MIME/SMTP recipients
@@ -328,6 +330,10 @@ func SendMail(c smtpClient, mail mailData, fileBackend filesstore.FileBackend, d
headers["Reply-To"] = []string{mail.replyTo.String()}
}
if len(mail.cc) > 0 {
headers["CC"] = []string{mail.cc}
}
for k, v := range mail.mimeHeaders {
headers[k] = []string{encodeRFC2047Word(v)}
}

Просмотреть файл

@@ -138,11 +138,12 @@ func TestSendMailUsingConfig(t *testing.T) {
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
err2 := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true)
err2 := SendMailUsingConfig(emailTo, emailSubject, emailBody, cfg, true, emailCC)
require.Nil(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
@@ -176,6 +177,7 @@ func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) {
var emailTo = "test@example.com"
var emailSubject = "Testing this email"
var emailBody = "This is a test from autobot"
var emailCC = "test@example.com"
//Delete all the messages before check the sample email
DeleteMailBox(emailTo)
@@ -184,7 +186,7 @@ func TestSendMailWithEmbeddedFilesUsingConfig(t *testing.T) {
"test1.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
"test2.png": bytes.NewReader([]byte("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx")),
}
err2 := SendMailWithEmbeddedFilesUsingConfig(emailTo, emailSubject, emailBody, embeddedFiles, cfg, true)
err2 := SendMailWithEmbeddedFilesUsingConfig(emailTo, emailSubject, emailBody, embeddedFiles, cfg, true, emailCC)
require.Nil(t, err2, "Should connect to the SMTP Server")
//Check if the email was send to the right email address
@@ -416,7 +418,7 @@ func TestSendMail(t *testing.T) {
for testName, tc := range testCases {
t.Run(testName, func(t *testing.T) {
mail := mailData{"", "", mail.Address{}, tc.replyTo, "", "", nil, nil, nil}
mail := mailData{"", "", mail.Address{}, "", tc.replyTo, "", "", nil, nil, nil}
appErr = SendMail(mocm, mail, mockBackend, time.Now())
require.Nil(t, appErr)
if len(tc.contains) > 0 {

84
templates/warn_metric_ack.html Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
{{define "warn_metric_ack"}}
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
style="margin-top: 20px; line-height: 1.7; color: #555;">
<tr>
<td>
<table align="center" border="0" cellpadding="0" cellspacing="0" width="100%"
style="max-width: 660px; font-family: Helvetica, Arial, sans-serif; font-size: 14px; background: #FFF;">
<tr>
<td style="border: 1px solid #ddd;">
<table border="0" cellpadding="0" cellspacing="0"
style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<tr>
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
<h2 style="font-weight: normal; margin-top: 10px;">{{.Props.Title}}</h2>
<p>{{.Props.Info}}<br>{{.Props.Warning}}</p>
</td>
</tr>
<tr>
<td>
<table border="0" cellpadding="0" cellspacing="0"
style="padding: 20px 50px 0; text-align: center; margin: 0 auto">
<!-- {{if .Props.ContactNameValue}} -->
<tr>
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
<p style="font-weight: bold; margin-top: 0;">
{{.Props.ContactNameHeader}}
{{.Props.ContactNameValue}}</p>
</td>
</tr>
<!-- {{end}} -->
{{if .Props.ContactEmailValue}}
<tr>
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
<p style="font-weight: bold; margin-top: 0;">
{{.Props.ContactEmailHeader}}
{{.Props.ContactEmailValue}}</p>
</td>
</tr>
{{end}}
{{if .Props.RegisteredUsersValue}}
<tr>
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
<p style="font-weight: bold; margin-top: 0;">
{{.Props.RegisteredUsersHeader}}
{{.Props.RegisteredUsersValue}}</p>
</td>
</tr>
{{end}}
{{if .Props.SiteURL}}
<tr>
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
<p style="font-weight: bold; margin-top: 0;">{{.Props.SiteURLHeader}}
{{.Props.SiteURL}}</p>
</td>
</tr>
{{end}}
{{if .Props.DiagnosticIdValue}}
<tr>
<td style="border: 0; padding: 0 0 0 20px; text-align: left;">
<p style="font-weight: bold; margin-top: 0;">
{{.Props.DiagnosticIdHeader}}
{{.Props.DiagnosticIdValue}}</p>
</td>
</tr>
{{end}}
</table>
</td>
</tr>
</table>
</td>
</tr>
<tr>
{{template "email_footer" . }}
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
{{end}}

Просмотреть файл

@@ -78,6 +78,7 @@ type Params struct {
FilterAllowReference bool
FilterParentTeamPermitted bool
CategoryId string
WarnMetricId string
}
func ParamsFromRequest(r *http.Request) *Params {
@@ -316,5 +317,9 @@ func ParamsFromRequest(r *http.Request) *Params {
params.IncludeDeleted = val
}
if val, ok := props["warn_metric_id"]; ok {
params.WarnMetricId = val
}
return params
}