123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320 |
- package main
- import (
- "encoding/json"
- "net/http"
- "regexp"
- "strings"
- "syscall"
- "time"
- "github.com/gofrs/uuid"
- "github.com/jmoiron/sqlx/types"
- "github.com/labstack/echo"
- )
- 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"`
- 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"`
- 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"`
- TLSEnabled bool `json:"tls_enabled"`
- 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"`
- }
- 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
- }
- // Assign a UUID. The frontend only sends a password when the user explictly
- // 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 explictly
- // 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
- // 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)))
- }
- // Unmarshall 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
- }
|