settings.go 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. package main
  2. import (
  3. "encoding/json"
  4. "net/http"
  5. "regexp"
  6. "strings"
  7. "syscall"
  8. "time"
  9. "github.com/gofrs/uuid"
  10. "github.com/jmoiron/sqlx/types"
  11. "github.com/labstack/echo"
  12. )
  13. type settings struct {
  14. AppRootURL string `json:"app.root_url"`
  15. AppLogoURL string `json:"app.logo_url"`
  16. AppFaviconURL string `json:"app.favicon_url"`
  17. AppFromEmail string `json:"app.from_email"`
  18. AppNotifyEmails []string `json:"app.notify_emails"`
  19. EnablePublicSubPage bool `json:"app.enable_public_subscription_page"`
  20. CheckUpdates bool `json:"app.check_updates"`
  21. AppLang string `json:"app.lang"`
  22. AppBatchSize int `json:"app.batch_size"`
  23. AppConcurrency int `json:"app.concurrency"`
  24. AppMaxSendErrors int `json:"app.max_send_errors"`
  25. AppMessageRate int `json:"app.message_rate"`
  26. AppMessageSlidingWindow bool `json:"app.message_sliding_window"`
  27. AppMessageSlidingWindowDuration string `json:"app.message_sliding_window_duration"`
  28. AppMessageSlidingWindowRate int `json:"app.message_sliding_window_rate"`
  29. PrivacyIndividualTracking bool `json:"privacy.individual_tracking"`
  30. PrivacyUnsubHeader bool `json:"privacy.unsubscribe_header"`
  31. PrivacyAllowBlocklist bool `json:"privacy.allow_blocklist"`
  32. PrivacyAllowExport bool `json:"privacy.allow_export"`
  33. PrivacyAllowWipe bool `json:"privacy.allow_wipe"`
  34. PrivacyExportable []string `json:"privacy.exportable"`
  35. UploadProvider string `json:"upload.provider"`
  36. UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
  37. UploadFilesystemUploadURI string `json:"upload.filesystem.upload_uri"`
  38. UploadS3URL string `json:"upload.s3.url"`
  39. UploadS3AwsAccessKeyID string `json:"upload.s3.aws_access_key_id"`
  40. UploadS3AwsDefaultRegion string `json:"upload.s3.aws_default_region"`
  41. UploadS3AwsSecretAccessKey string `json:"upload.s3.aws_secret_access_key,omitempty"`
  42. UploadS3Bucket string `json:"upload.s3.bucket"`
  43. UploadS3BucketDomain string `json:"upload.s3.bucket_domain"`
  44. UploadS3BucketPath string `json:"upload.s3.bucket_path"`
  45. UploadS3BucketType string `json:"upload.s3.bucket_type"`
  46. UploadS3Expiry string `json:"upload.s3.expiry"`
  47. SMTP []struct {
  48. UUID string `json:"uuid"`
  49. Enabled bool `json:"enabled"`
  50. Host string `json:"host"`
  51. HelloHostname string `json:"hello_hostname"`
  52. Port int `json:"port"`
  53. AuthProtocol string `json:"auth_protocol"`
  54. Username string `json:"username"`
  55. Password string `json:"password,omitempty"`
  56. EmailHeaders []map[string]string `json:"email_headers"`
  57. MaxConns int `json:"max_conns"`
  58. MaxMsgRetries int `json:"max_msg_retries"`
  59. IdleTimeout string `json:"idle_timeout"`
  60. WaitTimeout string `json:"wait_timeout"`
  61. TLSEnabled bool `json:"tls_enabled"`
  62. TLSSkipVerify bool `json:"tls_skip_verify"`
  63. } `json:"smtp"`
  64. Messengers []struct {
  65. UUID string `json:"uuid"`
  66. Enabled bool `json:"enabled"`
  67. Name string `json:"name"`
  68. RootURL string `json:"root_url"`
  69. Username string `json:"username"`
  70. Password string `json:"password,omitempty"`
  71. MaxConns int `json:"max_conns"`
  72. Timeout string `json:"timeout"`
  73. MaxMsgRetries int `json:"max_msg_retries"`
  74. } `json:"messengers"`
  75. BounceEnabled bool `json:"bounce.enabled"`
  76. BounceEnableWebhooks bool `json:"bounce.webhooks_enabled"`
  77. BounceCount int `json:"bounce.count"`
  78. BounceAction string `json:"bounce.action"`
  79. SESEnabled bool `json:"bounce.ses_enabled"`
  80. SendgridEnabled bool `json:"bounce.sendgrid_enabled"`
  81. SendgridKey string `json:"bounce.sendgrid_key"`
  82. BounceBoxes []struct {
  83. UUID string `json:"uuid"`
  84. Enabled bool `json:"enabled"`
  85. Type string `json:"type"`
  86. Host string `json:"host"`
  87. Port int `json:"port"`
  88. AuthProtocol string `json:"auth_protocol"`
  89. ReturnPath string `json:"return_path"`
  90. Username string `json:"username"`
  91. Password string `json:"password,omitempty"`
  92. TLSEnabled bool `json:"tls_enabled"`
  93. TLSSkipVerify bool `json:"tls_skip_verify"`
  94. ScanInterval string `json:"scan_interval"`
  95. } `json:"bounce.mailboxes"`
  96. }
  97. var (
  98. reAlphaNum = regexp.MustCompile(`[^a-z0-9\-]`)
  99. )
  100. // handleGetSettings returns settings from the DB.
  101. func handleGetSettings(c echo.Context) error {
  102. app := c.Get("app").(*App)
  103. s, err := getSettings(app)
  104. if err != nil {
  105. return err
  106. }
  107. // Empty out passwords.
  108. for i := 0; i < len(s.SMTP); i++ {
  109. s.SMTP[i].Password = ""
  110. }
  111. for i := 0; i < len(s.BounceBoxes); i++ {
  112. s.BounceBoxes[i].Password = ""
  113. }
  114. for i := 0; i < len(s.Messengers); i++ {
  115. s.Messengers[i].Password = ""
  116. }
  117. s.UploadS3AwsSecretAccessKey = ""
  118. s.SendgridKey = ""
  119. return c.JSON(http.StatusOK, okResp{s})
  120. }
  121. // handleUpdateSettings returns settings from the DB.
  122. func handleUpdateSettings(c echo.Context) error {
  123. var (
  124. app = c.Get("app").(*App)
  125. set settings
  126. )
  127. // Unmarshal and marshal the fields once to sanitize the settings blob.
  128. if err := c.Bind(&set); err != nil {
  129. return err
  130. }
  131. // Get the existing settings.
  132. cur, err := getSettings(app)
  133. if err != nil {
  134. return err
  135. }
  136. // There should be at least one SMTP block that's enabled.
  137. has := false
  138. for i, s := range set.SMTP {
  139. if s.Enabled {
  140. has = true
  141. }
  142. // Assign a UUID. The frontend only sends a password when the user explictly
  143. // changes the password. In other cases, the existing password in the DB
  144. // is copied while updating the settings and the UUID is used to match
  145. // the incoming array of SMTP blocks with the array in the DB.
  146. if s.UUID == "" {
  147. set.SMTP[i].UUID = uuid.Must(uuid.NewV4()).String()
  148. }
  149. // If there's no password coming in from the frontend, copy the existing
  150. // password by matching the UUID.
  151. if s.Password == "" {
  152. for _, c := range cur.SMTP {
  153. if s.UUID == c.UUID {
  154. set.SMTP[i].Password = c.Password
  155. }
  156. }
  157. }
  158. }
  159. if !has {
  160. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.errorNoSMTP"))
  161. }
  162. // Bounce boxes.
  163. for i, s := range set.BounceBoxes {
  164. // Assign a UUID. The frontend only sends a password when the user explictly
  165. // changes the password. In other cases, the existing password in the DB
  166. // is copied while updating the settings and the UUID is used to match
  167. // the incoming array of blocks with the array in the DB.
  168. if s.UUID == "" {
  169. set.BounceBoxes[i].UUID = uuid.Must(uuid.NewV4()).String()
  170. }
  171. if d, _ := time.ParseDuration(s.ScanInterval); d.Minutes() < 1 {
  172. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.bounces.invalidScanInterval"))
  173. }
  174. // If there's no password coming in from the frontend, copy the existing
  175. // password by matching the UUID.
  176. if s.Password == "" {
  177. for _, c := range cur.BounceBoxes {
  178. if s.UUID == c.UUID {
  179. set.BounceBoxes[i].Password = c.Password
  180. }
  181. }
  182. }
  183. }
  184. // Validate and sanitize postback Messenger names. Duplicates are disallowed
  185. // and "email" is a reserved name.
  186. names := map[string]bool{emailMsgr: true}
  187. for i, m := range set.Messengers {
  188. // UUID to keep track of password changes similar to the SMTP logic above.
  189. if m.UUID == "" {
  190. set.Messengers[i].UUID = uuid.Must(uuid.NewV4()).String()
  191. }
  192. if m.Password == "" {
  193. for _, c := range cur.Messengers {
  194. if m.UUID == c.UUID {
  195. set.Messengers[i].Password = c.Password
  196. }
  197. }
  198. }
  199. name := reAlphaNum.ReplaceAllString(strings.ToLower(m.Name), "")
  200. if _, ok := names[name]; ok {
  201. return echo.NewHTTPError(http.StatusBadRequest,
  202. app.i18n.Ts("settings.duplicateMessengerName", "name", name))
  203. }
  204. if len(name) == 0 {
  205. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("settings.invalidMessengerName"))
  206. }
  207. set.Messengers[i].Name = name
  208. names[name] = true
  209. }
  210. // S3 password?
  211. if set.UploadS3AwsSecretAccessKey == "" {
  212. set.UploadS3AwsSecretAccessKey = cur.UploadS3AwsSecretAccessKey
  213. }
  214. if set.SendgridKey == "" {
  215. set.SendgridKey = cur.SendgridKey
  216. }
  217. // Marshal settings.
  218. b, err := json.Marshal(set)
  219. if err != nil {
  220. return echo.NewHTTPError(http.StatusInternalServerError,
  221. app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
  222. }
  223. // Update the settings in the DB.
  224. if _, err := app.queries.UpdateSettings.Exec(b); err != nil {
  225. return echo.NewHTTPError(http.StatusInternalServerError,
  226. app.i18n.Ts("globals.messages.errorUpdating",
  227. "name", "{globals.terms.settings}", "error", pqErrMsg(err)))
  228. }
  229. // If there are any active campaigns, don't do an auto reload and
  230. // warn the user on the frontend.
  231. if app.manager.HasRunningCampaigns() {
  232. app.Lock()
  233. app.needsRestart = true
  234. app.Unlock()
  235. return c.JSON(http.StatusOK, okResp{struct {
  236. NeedsRestart bool `json:"needs_restart"`
  237. }{true}})
  238. }
  239. // No running campaigns. Reload the app.
  240. go func() {
  241. <-time.After(time.Millisecond * 500)
  242. app.sigChan <- syscall.SIGHUP
  243. }()
  244. return c.JSON(http.StatusOK, okResp{true})
  245. }
  246. // handleGetLogs returns the log entries stored in the log buffer.
  247. func handleGetLogs(c echo.Context) error {
  248. app := c.Get("app").(*App)
  249. return c.JSON(http.StatusOK, okResp{app.bufLog.Lines()})
  250. }
  251. func getSettings(app *App) (settings, error) {
  252. var (
  253. b types.JSONText
  254. out settings
  255. )
  256. if err := app.queries.GetSettings.Get(&b); err != nil {
  257. return out, echo.NewHTTPError(http.StatusInternalServerError,
  258. app.i18n.Ts("globals.messages.errorFetching",
  259. "name", "{globals.terms.settings}", "error", pqErrMsg(err)))
  260. }
  261. // Unmarshall the settings and filter out sensitive fields.
  262. if err := json.Unmarshal([]byte(b), &out); err != nil {
  263. return out, echo.NewHTTPError(http.StatusInternalServerError,
  264. app.i18n.Ts("settings.errorEncoding", "error", err.Error()))
  265. }
  266. return out, nil
  267. }