package main import ( "net/http" "regexp" "strings" "syscall" "time" "github.com/gofrs/uuid" "github.com/knadh/listmonk/models" "github.com/labstack/echo/v4" ) var ( reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`) ) // handleGetSettings returns settings from the DB. func handleGetSettings(c echo.Context) error { app := c.Get("app").(*App) s, err := app.core.GetSettings() if err != nil { return err } // Empty out passwords. for i := 0; i < len(s.SMTP); i++ { s.SMTP[i].Password = "" } for i := 0; i < len(s.BounceBoxes); i++ { s.BounceBoxes[i].Password = "" } for i := 0; i < len(s.Messengers); i++ { s.Messengers[i].Password = "" } s.UploadS3AwsSecretAccessKey = "" s.SendgridKey = "" return c.JSON(http.StatusOK, okResp{s}) } // handleUpdateSettings returns settings from the DB. func handleUpdateSettings(c echo.Context) error { var ( app = c.Get("app").(*App) set models.Settings ) // Unmarshal and marshal the fields once to sanitize the settings blob. if err := c.Bind(&set); err != nil { return err } // Get the existing settings. cur, err := app.core.GetSettings() if err != nil { return err } // There should be at least one SMTP block that's enabled. has := false for i, s := range set.SMTP { if s.Enabled { has = true } // Assign a UUID. The frontend only sends a password when the user explicitly // changes the password. In other cases, the existing password in the DB // is copied while updating the settings and the UUID is used to match // the incoming array of SMTP blocks with the array in the DB. if s.UUID == "" { set.SMTP[i].UUID = uuid.Must(uuid.NewV4()).String() } // If there's no password coming in from the frontend, copy the existing // password by matching the UUID. if s.Password == "" { for _, c := range cur.SMTP { if s.UUID == c.UUID { set.SMTP[i].Password = c.Password } } } } if !has { return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP")) } // Bounce boxes. for i, s := range set.BounceBoxes { // Assign a UUID. The frontend only sends a password when the user explicitly // changes the password. In other cases, the existing password in the DB // is copied while updating the settings and the UUID is used to match // the incoming array of blocks with the array in the DB. if s.UUID == "" { set.BounceBoxes[i].UUID = uuid.Must(uuid.NewV4()).String() } if d, _ := time.ParseDuration(s.ScanInterval); d.Minutes() < 1 { return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.bounces.invalidScanInterval")) } // If there's no password coming in from the frontend, copy the existing // password by matching the UUID. if s.Password == "" { for _, c := range cur.BounceBoxes { if s.UUID == c.UUID { set.BounceBoxes[i].Password = c.Password } } } } // Validate and sanitize postback Messenger names. Duplicates are disallowed // and "email" is a reserved name. names := map[string]bool{emailMsgr: true} for i, m := range set.Messengers { // UUID to keep track of password changes similar to the SMTP logic above. if m.UUID == "" { set.Messengers[i].UUID = uuid.Must(uuid.NewV4()).String() } if m.Password == "" { for _, c := range cur.Messengers { if m.UUID == c.UUID { set.Messengers[i].Password = c.Password } } } name := reAlphaNum.ReplaceAllString(strings.ToLower(m.Name), "") if _, ok := names[name]; ok { return echo.NewHTTPError(http.StatusBadRequest, app.i18n.Ts("settings.duplicateMessengerName", "name", name)) } if len(name) == 0 { return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.invalidMessengerName")) } set.Messengers[i].Name = name names[name] = true } // S3 password? if set.UploadS3AwsSecretAccessKey == "" { set.UploadS3AwsSecretAccessKey = cur.UploadS3AwsSecretAccessKey } if set.SendgridKey == "" { set.SendgridKey = cur.SendgridKey } // Domain blocklist. doms := make([]string, 0) for _, d := range set.DomainBlocklist { d = strings.TrimSpace(strings.ToLower(d)) if d != "" { doms = append(doms, d) } } set.DomainBlocklist = doms // Update the settings in the DB. if err := app.core.UpdateSettings(set); err != nil { return err } // If there are any active campaigns, don't do an auto reload and // warn the user on the frontend. if app.manager.HasRunningCampaigns() { app.Lock() app.needsRestart = true app.Unlock() return c.JSON(http.StatusOK, okResp{struct { NeedsRestart bool `json:"needs_restart"` }{true}}) } // No running campaigns. Reload the app. go func() { <-time.After(time.Millisecond * 500) app.sigChan <- syscall.SIGHUP }() return c.JSON(http.StatusOK, okResp{true}) } // handleGetLogs returns the log entries stored in the log buffer. func handleGetLogs(c echo.Context) error { app := c.Get("app").(*App) return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()}) }