2020-07-08 19:00:14 +08:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"net/http"
|
2020-09-20 19:01:24 +08:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
2020-07-08 19:00:14 +08:00
|
|
|
"syscall"
|
|
|
|
"time"
|
|
|
|
|
2020-10-10 23:55:59 +08:00
|
|
|
"github.com/gofrs/uuid"
|
2020-07-08 19:00:14 +08:00
|
|
|
"github.com/jmoiron/sqlx/types"
|
2021-12-09 23:21:07 +08:00
|
|
|
"github.com/labstack/echo/v4"
|
2020-07-08 19:00:14 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
type settings struct {
|
2021-09-25 13:08:13 +08:00
|
|
|
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"`
|
2020-07-08 19:00:14 +08:00
|
|
|
|
2021-01-24 14:49:26 +08:00
|
|
|
AppMessageSlidingWindow bool `json:"app.message_sliding_window"`
|
|
|
|
AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`
|
|
|
|
AppMessageSlidingWindowRate int `json:"app.message_sliding_window_rate"`
|
|
|
|
|
2020-10-18 20:03:34 +08:00
|
|
|
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"`
|
2021-09-25 15:27:55 +08:00
|
|
|
DomainBlocklist []string `json:"privacy.domain_blocklist"`
|
2020-07-08 19:00:14 +08:00
|
|
|
|
2020-09-20 19:01:24 +08:00
|
|
|
UploadProvider string `json:"upload.provider"`
|
|
|
|
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
|
|
|
|
UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
|
2021-08-15 14:45:00 +08:00
|
|
|
UploadS3URL string `json:"upload.s3.url"`
|
2022-01-15 23:50:32 +08:00
|
|
|
UploadS3PublicURL string `json:"upload.s3.public_url"`
|
2020-09-20 19:01:24 +08:00
|
|
|
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"`
|
|
|
|
|
2020-07-08 19:00:14 +08:00
|
|
|
SMTP []struct {
|
2020-10-10 23:55:59 +08:00
|
|
|
UUID string `json:"uuid"`
|
2020-07-08 19:00:14 +08:00
|
|
|
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"`
|
2022-01-03 21:58:36 +08:00
|
|
|
TLSType string `json:"tls_type"`
|
2020-07-08 19:00:14 +08:00
|
|
|
TLSSkipVerify bool `json:"tls_skip_verify"`
|
|
|
|
} `json:"smtp"`
|
|
|
|
|
2020-09-20 19:01:24 +08:00
|
|
|
Messengers []struct {
|
2020-10-10 23:55:59 +08:00
|
|
|
UUID string `json:"uuid"`
|
2020-09-20 19:01:24 +08:00
|
|
|
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"`
|
2021-05-25 01:11:48 +08:00
|
|
|
|
|
|
|
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"`
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
2020-09-20 19:01:24 +08:00
|
|
|
var (
|
|
|
|
reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
|
|
|
|
)
|
|
|
|
|
2020-07-08 19:00:14 +08:00
|
|
|
// 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 = ""
|
|
|
|
}
|
2021-05-25 01:11:48 +08:00
|
|
|
for i := 0; i < len(s.BounceBoxes); i++ {
|
|
|
|
s.BounceBoxes[i].Password = ""
|
|
|
|
}
|
2020-09-20 19:01:24 +08:00
|
|
|
for i := 0; i < len(s.Messengers); i++ {
|
|
|
|
s.Messengers[i].Password = ""
|
|
|
|
}
|
2020-07-08 19:00:14 +08:00
|
|
|
s.UploadS3AwsSecretAccessKey = ""
|
2021-05-25 01:11:48 +08:00
|
|
|
s.SendgridKey = ""
|
2020-07-08 19:00:14 +08:00
|
|
|
|
|
|
|
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
|
2020-10-10 23:55:59 +08:00
|
|
|
// 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
|
|
|
|
}
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !has {
|
2020-12-19 18:55:52 +08:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP"))
|
2020-09-20 19:01:24 +08:00
|
|
|
}
|
|
|
|
|
2021-05-25 01:11:48 +08:00
|
|
|
// 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
|
2021-05-25 01:11:48 +08:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-20 19:01:24 +08:00
|
|
|
// Validate and sanitize postback Messenger names. Duplicates are disallowed
|
|
|
|
// and "email" is a reserved name.
|
|
|
|
names := map[string]bool{emailMsgr: true}
|
|
|
|
|
2020-10-10 23:55:59 +08:00
|
|
|
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
|
|
|
|
}
|
2020-09-20 19:01:24 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-10 23:55:59 +08:00
|
|
|
name := reAlphaNum.ReplaceAllString(strings.ToLower(m.Name), "")
|
2020-09-20 19:01:24 +08:00
|
|
|
if _, ok := names[name]; ok {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("settings.duplicateMessengerName", "name", name))
|
2020-09-20 19:01:24 +08:00
|
|
|
}
|
|
|
|
if len(name) == 0 {
|
2020-12-19 18:55:52 +08:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.invalidMessengerName"))
|
2020-09-20 19:01:24 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
set.Messengers[i].Name = name
|
|
|
|
names[name] = true
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// S3 password?
|
|
|
|
if set.UploadS3AwsSecretAccessKey == "" {
|
|
|
|
set.UploadS3AwsSecretAccessKey = cur.UploadS3AwsSecretAccessKey
|
|
|
|
}
|
2021-05-25 01:11:48 +08:00
|
|
|
if set.SendgridKey == "" {
|
|
|
|
set.SendgridKey = cur.SendgridKey
|
|
|
|
}
|
2020-07-08 19:00:14 +08:00
|
|
|
|
2021-09-25 15:27:55 +08:00
|
|
|
// 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
|
|
|
|
|
2020-07-08 19:00:14 +08:00
|
|
|
// Marshal settings.
|
|
|
|
b, err := json.Marshal(set)
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the settings in the DB.
|
|
|
|
if _, err := app.queries.UpdateSettings.Exec(b); err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("globals.messages.errorUpdating",
|
2021-01-23 21:18:10 +08:00
|
|
|
"name", "{globals.terms.settings}", "error", pqErrMsg(err)))
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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})
|
|
|
|
}
|
|
|
|
|
2020-10-11 02:24:03 +08:00
|
|
|
// 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()})
|
|
|
|
}
|
|
|
|
|
2020-07-08 19:00:14 +08:00
|
|
|
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,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 21:18:10 +08:00
|
|
|
"name", "{globals.terms.settings}", "error", pqErrMsg(err)))
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
2022-02-13 15:37:00 +08:00
|
|
|
// Unmarshal the settings and filter out sensitive fields.
|
2020-07-08 19:00:14 +08:00
|
|
|
if err := json.Unmarshal([]byte(b), &out); err != nil {
|
|
|
|
return out, echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 21:24:33 +08:00
|
|
|
app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
|
2020-07-08 19:00:14 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|