listmonk/cmd/settings.go

327 lines
11 KiB
Go
Raw Normal View History

package main
import (
"encoding/json"
"net/http"
"regexp"
"strings"
"syscall"
"time"
"github.com/gofrs/uuid"
"github.com/jmoiron/sqlx/types"
"github.com/labstack/echo/v4"
)
type settings struct {
AppRootURL string `json:"app.root_url"`
AppLogoURL string `json:"app.logo_url"`
AppFaviconURL string `json:"app.favicon_url"`
AppFromEmail string `json:"app.from_email"`
AppNotifyEmails []string `json:"app.notify_emails"`
EnablePublicSubPage bool `json:"app.enable_public_subscription_page"`
SendOptinConfirmation bool `json:"app.send_optin_confirmation"`
CheckUpdates bool `json:"app.check_updates"`
AppLang string `json:"app.lang"`
2020-12-19 18:55:52 +08:00
AppBatchSize int `json:"app.batch_size"`
AppConcurrency int `json:"app.concurrency"`
AppMaxSendErrors int `json:"app.max_send_errors"`
AppMessageRate int `json:"app.message_rate"`
AppMessageSlidingWindow bool `json:"app.message_sliding_window"`
AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`
AppMessageSlidingWindowRate int `json:"app.message_sliding_window_rate"`
PrivacyIndividualTracking bool `json:"privacy.individual_tracking"`
PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
PrivacyAllowExport bool `json:"privacy.allow_export"`
PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
PrivacyExportable []string `json:"privacy.exportable"`
DomainBlocklist []string `json:"privacy.domain_blocklist"`
UploadProvider string `json:"upload.provider"`
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
UploadS3URL string `json:"upload.s3.url"`
UploadS3PublicURL string `json:"upload.s3.public_url"`
UploadS3AwsAccessKeyID string `json:"upload.s3.aws_access_key_id"`
UploadS3AwsDefaultRegion string `json:"upload.s3.aws_default_region"`
UploadS3AwsSecretAccessKey string `json:"upload.s3.aws_secret_access_key,omitempty"`
UploadS3Bucket string `json:"upload.s3.bucket"`
UploadS3BucketDomain string `json:"upload.s3.bucket_domain"`
UploadS3BucketPath string `json:"upload.s3.bucket_path"`
UploadS3BucketType string `json:"upload.s3.bucket_type"`
UploadS3Expiry string `json:"upload.s3.expiry"`
SMTP []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Host string `json:"host"`
HelloHostname string `json:"hello_hostname"`
Port int `json:"port"`
AuthProtocol string `json:"auth_protocol"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
EmailHeaders []map[string]string `json:"email_headers"`
MaxConns int `json:"max_conns"`
MaxMsgRetries int `json:"max_msg_retries"`
IdleTimeout string `json:"idle_timeout"`
WaitTimeout string `json:"wait_timeout"`
TLSType string `json:"tls_type"`
TLSSkipVerify bool `json:"tls_skip_verify"`
} `json:"smtp"`
Messengers []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Name string `json:"name"`
RootURL string `json:"root_url"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
MaxConns int `json:"max_conns"`
Timeout string `json:"timeout"`
MaxMsgRetries int `json:"max_msg_retries"`
} `json:"messengers"`
BounceEnabled bool `json:"bounce.enabled"`
BounceEnableWebhooks bool `json:"bounce.webhooks_enabled"`
BounceCount int `json:"bounce.count"`
BounceAction string `json:"bounce.action"`
SESEnabled bool `json:"bounce.ses_enabled"`
SendgridEnabled bool `json:"bounce.sendgrid_enabled"`
SendgridKey string `json:"bounce.sendgrid_key"`
BounceBoxes []struct {
UUID string `json:"uuid"`
Enabled bool `json:"enabled"`
Type string `json:"type"`
Host string `json:"host"`
Port int `json:"port"`
AuthProtocol string `json:"auth_protocol"`
ReturnPath string `json:"return_path"`
Username string `json:"username"`
Password string `json:"password,omitempty"`
TLSEnabled bool `json:"tls_enabled"`
TLSSkipVerify bool `json:"tls_skip_verify"`
ScanInterval string `json:"scan_interval"`
} `json:"bounce.mailboxes"`
2021-12-18 18:08:42 +08:00
AdminCustomCSS string `json:"appearance.admin.custom_css"`
AdminCustomJS string `json:"appearance.admin.custom_js"`
PublicCustomCSS string `json:"appearance.public.custom_css"`
PublicCustomJS string `json:"appearance.public.custom_js"`
}
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 := getSettings(app)
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 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 := getSettings(app)
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
}
2022-02-13 15:37:00 +08:00
// 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 {
2020-12-19 18:55:52 +08:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP"))
}
// Bounce boxes.
for i, s := range set.BounceBoxes {
2022-02-13 15:37:00 +08:00
// 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 {
2020-12-19 18:55:52 +08:00
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
// Marshal settings.
b, err := json.Marshal(set)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
}
// Update the settings in the DB.
if _, err := app.queries.UpdateSettings.Exec(b); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorUpdating",
"name", "{globals.terms.settings}", "error", pqErrMsg(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()})
}
func getSettings(app *App) (settings, error) {
var (
b types.JSONText
out settings
)
if err := app.queries.GetSettings.Get(&b); err != nil {
return out, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
"name", "{globals.terms.settings}", "error", pqErrMsg(err)))
}
2022-02-13 15:37:00 +08:00
// Unmarshal the settings and filter out sensitive fields.
if err := json.Unmarshal([]byte(b), &out); err != nil {
return out, echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
}
return out, nil
}