subscribers.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856
  1. package main
  2. import (
  3. "context"
  4. "database/sql"
  5. "encoding/csv"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "net/http"
  10. "net/url"
  11. "strconv"
  12. "strings"
  13. "github.com/gofrs/uuid"
  14. "github.com/knadh/listmonk/internal/subimporter"
  15. "github.com/knadh/listmonk/models"
  16. "github.com/labstack/echo"
  17. "github.com/lib/pq"
  18. )
  19. const (
  20. dummyUUID = "00000000-0000-0000-0000-000000000000"
  21. )
  22. // subQueryReq is a "catch all" struct for reading various
  23. // subscriber related requests.
  24. type subQueryReq struct {
  25. Query string `json:"query"`
  26. ListIDs pq.Int64Array `json:"list_ids"`
  27. TargetListIDs pq.Int64Array `json:"target_list_ids"`
  28. SubscriberIDs pq.Int64Array `json:"ids"`
  29. Action string `json:"action"`
  30. }
  31. type subsWrap struct {
  32. Results models.Subscribers `json:"results"`
  33. Query string `json:"query"`
  34. Total int `json:"total"`
  35. PerPage int `json:"per_page"`
  36. Page int `json:"page"`
  37. }
  38. type subUpdateReq struct {
  39. models.Subscriber
  40. RawAttribs json.RawMessage `json:"attribs"`
  41. Lists pq.Int64Array `json:"lists"`
  42. ListUUIDs pq.StringArray `json:"list_uuids"`
  43. PreconfirmSubs bool `json:"preconfirm_subscriptions"`
  44. }
  45. // subProfileData represents a subscriber's collated data in JSON
  46. // for export.
  47. type subProfileData struct {
  48. Email string `db:"email" json:"-"`
  49. Profile json.RawMessage `db:"profile" json:"profile,omitempty"`
  50. Subscriptions json.RawMessage `db:"subscriptions" json:"subscriptions,omitempty"`
  51. CampaignViews json.RawMessage `db:"campaign_views" json:"campaign_views,omitempty"`
  52. LinkClicks json.RawMessage `db:"link_clicks" json:"link_clicks,omitempty"`
  53. }
  54. // subOptin contains the data that's passed to the double opt-in e-mail template.
  55. type subOptin struct {
  56. *models.Subscriber
  57. OptinURL string
  58. Lists []models.List
  59. }
  60. var (
  61. dummySubscriber = models.Subscriber{
  62. Email: "demo@listmonk.app",
  63. Name: "Demo Subscriber",
  64. UUID: dummyUUID,
  65. }
  66. subQuerySortFields = []string{"email", "name", "created_at", "updated_at"}
  67. errSubscriberExists = errors.New("subscriber already exists")
  68. )
  69. // handleGetSubscriber handles the retrieval of a single subscriber by ID.
  70. func handleGetSubscriber(c echo.Context) error {
  71. var (
  72. app = c.Get("app").(*App)
  73. id, _ = strconv.Atoi(c.Param("id"))
  74. )
  75. if id < 1 {
  76. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  77. }
  78. sub, err := getSubscriber(id, "", "", app)
  79. if err != nil {
  80. return err
  81. }
  82. return c.JSON(http.StatusOK, okResp{sub})
  83. }
  84. // handleQuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
  85. func handleQuerySubscribers(c echo.Context) error {
  86. var (
  87. app = c.Get("app").(*App)
  88. pg = getPagination(c.QueryParams(), 30)
  89. // Limit the subscribers to a particular list?
  90. listID, _ = strconv.Atoi(c.FormValue("list_id"))
  91. // The "WHERE ?" bit.
  92. query = sanitizeSQLExp(c.FormValue("query"))
  93. orderBy = c.FormValue("order_by")
  94. order = c.FormValue("order")
  95. out = subsWrap{Results: make([]models.Subscriber, 0, 1)}
  96. )
  97. listIDs := pq.Int64Array{}
  98. if listID < 0 {
  99. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
  100. } else if listID > 0 {
  101. listIDs = append(listIDs, int64(listID))
  102. }
  103. // There's an arbitrary query condition.
  104. cond := ""
  105. if query != "" {
  106. cond = " AND " + query
  107. }
  108. // Sort params.
  109. if !strSliceContains(orderBy, subQuerySortFields) {
  110. orderBy = "subscribers.id"
  111. }
  112. if order != sortAsc && order != sortDesc {
  113. order = sortDesc
  114. }
  115. // Create a readonly transaction that just does COUNT() to obtain the count of results
  116. // and to ensure that the arbitrary query is indeed readonly.
  117. stmt := fmt.Sprintf(app.queries.QuerySubscribersCount, cond)
  118. tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
  119. if err != nil {
  120. app.log.Printf("error preparing subscriber query: %v", err)
  121. return echo.NewHTTPError(http.StatusBadRequest,
  122. app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
  123. }
  124. defer tx.Rollback()
  125. // Execute the readonly query and get the count of results.
  126. var total = 0
  127. if err := tx.Get(&total, stmt, listIDs); err != nil {
  128. return echo.NewHTTPError(http.StatusInternalServerError,
  129. app.i18n.Ts("globals.messages.errorFetching",
  130. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  131. }
  132. // No results.
  133. if total == 0 {
  134. return c.JSON(http.StatusOK, okResp{out})
  135. }
  136. // Run the query again and fetch the actual data. stmt is the raw SQL query.
  137. stmt = fmt.Sprintf(app.queries.QuerySubscribers, cond, orderBy, order)
  138. if err := tx.Select(&out.Results, stmt, listIDs, pg.Offset, pg.Limit); err != nil {
  139. return echo.NewHTTPError(http.StatusInternalServerError,
  140. app.i18n.Ts("globals.messages.errorFetching",
  141. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  142. }
  143. // Lazy load lists for each subscriber.
  144. if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
  145. app.log.Printf("error fetching subscriber lists: %v", err)
  146. return echo.NewHTTPError(http.StatusInternalServerError,
  147. app.i18n.Ts("globals.messages.errorFetching",
  148. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  149. }
  150. out.Query = query
  151. if len(out.Results) == 0 {
  152. out.Results = make(models.Subscribers, 0)
  153. return c.JSON(http.StatusOK, okResp{out})
  154. }
  155. // Meta.
  156. out.Total = total
  157. out.Page = pg.Page
  158. out.PerPage = pg.PerPage
  159. return c.JSON(http.StatusOK, okResp{out})
  160. }
  161. // handleExportSubscribers handles querying subscribers based on an arbitrary SQL expression.
  162. func handleExportSubscribers(c echo.Context) error {
  163. var (
  164. app = c.Get("app").(*App)
  165. // Limit the subscribers to a particular list?
  166. listID, _ = strconv.Atoi(c.FormValue("list_id"))
  167. // The "WHERE ?" bit.
  168. query = sanitizeSQLExp(c.FormValue("query"))
  169. )
  170. listIDs := pq.Int64Array{}
  171. if listID < 0 {
  172. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
  173. } else if listID > 0 {
  174. listIDs = append(listIDs, int64(listID))
  175. }
  176. // There's an arbitrary query condition.
  177. cond := ""
  178. if query != "" {
  179. cond = " AND " + query
  180. }
  181. stmt := fmt.Sprintf(app.queries.QuerySubscribersForExport, cond)
  182. // Verify that the arbitrary SQL search expression is read only.
  183. if cond != "" {
  184. tx, err := app.db.Unsafe().BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
  185. if err != nil {
  186. app.log.Printf("error preparing subscriber query: %v", err)
  187. return echo.NewHTTPError(http.StatusBadRequest,
  188. app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
  189. }
  190. defer tx.Rollback()
  191. if _, err := tx.Query(stmt, nil, 0, 1); err != nil {
  192. return echo.NewHTTPError(http.StatusBadRequest,
  193. app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
  194. }
  195. }
  196. // Prepare the actual query statement.
  197. tx, err := db.Preparex(stmt)
  198. if err != nil {
  199. return echo.NewHTTPError(http.StatusBadRequest,
  200. app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
  201. }
  202. // Run the query until all rows are exhausted.
  203. var (
  204. id = 0
  205. h = c.Response().Header()
  206. wr = csv.NewWriter(c.Response())
  207. )
  208. h.Set(echo.HeaderContentType, echo.MIMEOctetStream)
  209. h.Set("Content-type", "text/csv")
  210. h.Set(echo.HeaderContentDisposition, "attachment; filename="+"subscribers.csv")
  211. h.Set("Content-Transfer-Encoding", "binary")
  212. h.Set("Cache-Control", "no-cache")
  213. wr.Write([]string{"uuid", "email", "name", "attributes", "status", "created_at", "updated_at"})
  214. loop:
  215. for {
  216. var out []models.SubscriberExport
  217. if err := tx.Select(&out, listIDs, id, app.constants.DBBatchSize); err != nil {
  218. return echo.NewHTTPError(http.StatusInternalServerError,
  219. app.i18n.Ts("globals.messages.errorFetching",
  220. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  221. }
  222. if len(out) == 0 {
  223. break loop
  224. }
  225. for _, r := range out {
  226. if err = wr.Write([]string{r.UUID, r.Email, r.Name, r.Attribs, r.Status,
  227. r.CreatedAt.Time.String(), r.UpdatedAt.Time.String()}); err != nil {
  228. app.log.Printf("error streaming CSV export: %v", err)
  229. break loop
  230. }
  231. }
  232. wr.Flush()
  233. id = out[len(out)-1].ID
  234. }
  235. return nil
  236. }
  237. // handleCreateSubscriber handles the creation of a new subscriber.
  238. func handleCreateSubscriber(c echo.Context) error {
  239. var (
  240. app = c.Get("app").(*App)
  241. req subimporter.SubReq
  242. )
  243. // Get and validate fields.
  244. if err := c.Bind(&req); err != nil {
  245. return err
  246. }
  247. req.Email = strings.ToLower(strings.TrimSpace(req.Email))
  248. if err := subimporter.ValidateFields(req); err != nil {
  249. return echo.NewHTTPError(http.StatusBadRequest, err.Error())
  250. }
  251. // Insert the subscriber into the DB.
  252. sub, isNew, _, err := insertSubscriber(req, app)
  253. if err != nil {
  254. return err
  255. }
  256. if !isNew {
  257. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
  258. }
  259. return c.JSON(http.StatusOK, okResp{sub})
  260. }
  261. // handleUpdateSubscriber handles modification of a subscriber.
  262. func handleUpdateSubscriber(c echo.Context) error {
  263. var (
  264. app = c.Get("app").(*App)
  265. id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
  266. req subUpdateReq
  267. )
  268. // Get and validate fields.
  269. if err := c.Bind(&req); err != nil {
  270. return err
  271. }
  272. if id < 1 {
  273. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  274. }
  275. if req.Email != "" && !subimporter.IsEmail(req.Email) {
  276. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail"))
  277. }
  278. if req.Name != "" && !strHasLen(req.Name, 1, stdInputMaxLen) {
  279. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
  280. }
  281. // If there's an attribs value, validate it.
  282. if len(req.RawAttribs) > 0 {
  283. var a models.SubscriberAttribs
  284. if err := json.Unmarshal(req.RawAttribs, &a); err != nil {
  285. return echo.NewHTTPError(http.StatusInternalServerError,
  286. app.i18n.Ts("globals.messages.errorUpdating",
  287. "name", "{globals.terms.subscriber}", "error", err.Error()))
  288. }
  289. }
  290. subStatus := models.SubscriptionStatusUnconfirmed
  291. if req.PreconfirmSubs {
  292. subStatus = models.SubscriptionStatusConfirmed
  293. }
  294. _, err := app.queries.UpdateSubscriber.Exec(id,
  295. strings.ToLower(strings.TrimSpace(req.Email)),
  296. strings.TrimSpace(req.Name),
  297. req.Status,
  298. req.RawAttribs,
  299. req.Lists,
  300. subStatus)
  301. if err != nil {
  302. app.log.Printf("error updating subscriber: %v", err)
  303. return echo.NewHTTPError(http.StatusInternalServerError,
  304. app.i18n.Ts("globals.messages.errorUpdating",
  305. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  306. }
  307. // Send a confirmation e-mail (if there are any double opt-in lists).
  308. sub, err := getSubscriber(int(id), "", "", app)
  309. if err != nil {
  310. return err
  311. }
  312. if !req.PreconfirmSubs {
  313. _, _ = sendOptinConfirmation(sub, []int64(req.Lists), app)
  314. }
  315. return c.JSON(http.StatusOK, okResp{sub})
  316. }
  317. // handleGetSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
  318. func handleSubscriberSendOptin(c echo.Context) error {
  319. var (
  320. app = c.Get("app").(*App)
  321. id, _ = strconv.Atoi(c.Param("id"))
  322. )
  323. if id < 1 {
  324. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  325. }
  326. // Fetch the subscriber.
  327. out, err := getSubscriber(id, "", "", app)
  328. if err != nil {
  329. app.log.Printf("error fetching subscriber: %v", err)
  330. return echo.NewHTTPError(http.StatusInternalServerError,
  331. app.i18n.Ts("globals.messages.errorFetching",
  332. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  333. }
  334. if _, err := sendOptinConfirmation(out, nil, app); err != nil {
  335. return echo.NewHTTPError(http.StatusInternalServerError,
  336. app.i18n.T("subscribers.errorSendingOptin"))
  337. }
  338. return c.JSON(http.StatusOK, okResp{true})
  339. }
  340. // handleBlocklistSubscribers handles the blocklisting of one or more subscribers.
  341. // It takes either an ID in the URI, or a list of IDs in the request body.
  342. func handleBlocklistSubscribers(c echo.Context) error {
  343. var (
  344. app = c.Get("app").(*App)
  345. pID = c.Param("id")
  346. IDs pq.Int64Array
  347. )
  348. // Is it a /:id call?
  349. if pID != "" {
  350. id, _ := strconv.ParseInt(pID, 10, 64)
  351. if id < 1 {
  352. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  353. }
  354. IDs = append(IDs, id)
  355. } else {
  356. // Multiple IDs.
  357. var req subQueryReq
  358. if err := c.Bind(&req); err != nil {
  359. return echo.NewHTTPError(http.StatusBadRequest,
  360. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  361. }
  362. if len(req.SubscriberIDs) == 0 {
  363. return echo.NewHTTPError(http.StatusBadRequest,
  364. "No IDs given.")
  365. }
  366. IDs = req.SubscriberIDs
  367. }
  368. if _, err := app.queries.BlocklistSubscribers.Exec(IDs); err != nil {
  369. app.log.Printf("error blocklisting subscribers: %v", err)
  370. return echo.NewHTTPError(http.StatusInternalServerError,
  371. app.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
  372. }
  373. return c.JSON(http.StatusOK, okResp{true})
  374. }
  375. // handleManageSubscriberLists handles bulk addition or removal of subscribers
  376. // from or to one or more target lists.
  377. // It takes either an ID in the URI, or a list of IDs in the request body.
  378. func handleManageSubscriberLists(c echo.Context) error {
  379. var (
  380. app = c.Get("app").(*App)
  381. pID = c.Param("id")
  382. IDs pq.Int64Array
  383. )
  384. // Is it a /:id call?
  385. if pID != "" {
  386. id, _ := strconv.ParseInt(pID, 10, 64)
  387. if id < 1 {
  388. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  389. }
  390. IDs = append(IDs, id)
  391. }
  392. var req subQueryReq
  393. if err := c.Bind(&req); err != nil {
  394. return echo.NewHTTPError(http.StatusBadRequest,
  395. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  396. }
  397. if len(req.SubscriberIDs) == 0 {
  398. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
  399. }
  400. if len(IDs) == 0 {
  401. IDs = req.SubscriberIDs
  402. }
  403. if len(req.TargetListIDs) == 0 {
  404. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
  405. }
  406. // Action.
  407. var err error
  408. switch req.Action {
  409. case "add":
  410. _, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
  411. case "remove":
  412. _, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
  413. case "unsubscribe":
  414. _, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
  415. default:
  416. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
  417. }
  418. if err != nil {
  419. app.log.Printf("error updating subscriptions: %v", err)
  420. return echo.NewHTTPError(http.StatusInternalServerError,
  421. app.i18n.Ts("globals.messages.errorUpdating",
  422. "name", "{globals.terms.subscribers}", "error", err.Error()))
  423. }
  424. return c.JSON(http.StatusOK, okResp{true})
  425. }
  426. // handleDeleteSubscribers handles subscriber deletion.
  427. // It takes either an ID in the URI, or a list of IDs in the request body.
  428. func handleDeleteSubscribers(c echo.Context) error {
  429. var (
  430. app = c.Get("app").(*App)
  431. pID = c.Param("id")
  432. IDs pq.Int64Array
  433. )
  434. // Is it an /:id call?
  435. if pID != "" {
  436. id, _ := strconv.ParseInt(pID, 10, 64)
  437. if id < 1 {
  438. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  439. }
  440. IDs = append(IDs, id)
  441. } else {
  442. // Multiple IDs.
  443. i, err := parseStringIDs(c.Request().URL.Query()["id"])
  444. if err != nil {
  445. return echo.NewHTTPError(http.StatusBadRequest,
  446. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  447. }
  448. if len(i) == 0 {
  449. return echo.NewHTTPError(http.StatusBadRequest,
  450. app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
  451. }
  452. IDs = i
  453. }
  454. if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
  455. app.log.Printf("error deleting subscribers: %v", err)
  456. return echo.NewHTTPError(http.StatusInternalServerError,
  457. app.i18n.Ts("globals.messages.errorDeleting",
  458. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  459. }
  460. return c.JSON(http.StatusOK, okResp{true})
  461. }
  462. // handleDeleteSubscribersByQuery bulk deletes based on an
  463. // arbitrary SQL expression.
  464. func handleDeleteSubscribersByQuery(c echo.Context) error {
  465. var (
  466. app = c.Get("app").(*App)
  467. req subQueryReq
  468. )
  469. if err := c.Bind(&req); err != nil {
  470. return err
  471. }
  472. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  473. app.queries.DeleteSubscribersByQuery,
  474. req.ListIDs, app.db)
  475. if err != nil {
  476. app.log.Printf("error deleting subscribers: %v", err)
  477. return echo.NewHTTPError(http.StatusInternalServerError,
  478. app.i18n.Ts("globals.messages.errorDeleting",
  479. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  480. }
  481. return c.JSON(http.StatusOK, okResp{true})
  482. }
  483. // handleBlocklistSubscribersByQuery bulk blocklists subscribers
  484. // based on an arbitrary SQL expression.
  485. func handleBlocklistSubscribersByQuery(c echo.Context) error {
  486. var (
  487. app = c.Get("app").(*App)
  488. req subQueryReq
  489. )
  490. if err := c.Bind(&req); err != nil {
  491. return err
  492. }
  493. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  494. app.queries.BlocklistSubscribersByQuery,
  495. req.ListIDs, app.db)
  496. if err != nil {
  497. app.log.Printf("error blocklisting subscribers: %v", err)
  498. return echo.NewHTTPError(http.StatusInternalServerError,
  499. app.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
  500. }
  501. return c.JSON(http.StatusOK, okResp{true})
  502. }
  503. // handleManageSubscriberListsByQuery bulk adds/removes/unsubscribers subscribers
  504. // from one or more lists based on an arbitrary SQL expression.
  505. func handleManageSubscriberListsByQuery(c echo.Context) error {
  506. var (
  507. app = c.Get("app").(*App)
  508. req subQueryReq
  509. )
  510. if err := c.Bind(&req); err != nil {
  511. return err
  512. }
  513. if len(req.TargetListIDs) == 0 {
  514. return echo.NewHTTPError(http.StatusBadRequest,
  515. app.i18n.T("subscribers.errorNoListsGiven"))
  516. }
  517. // Action.
  518. var stmt string
  519. switch req.Action {
  520. case "add":
  521. stmt = app.queries.AddSubscribersToListsByQuery
  522. case "remove":
  523. stmt = app.queries.DeleteSubscriptionsByQuery
  524. case "unsubscribe":
  525. stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
  526. default:
  527. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
  528. }
  529. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  530. stmt, req.ListIDs, app.db, req.TargetListIDs)
  531. if err != nil {
  532. app.log.Printf("error updating subscriptions: %v", err)
  533. return echo.NewHTTPError(http.StatusInternalServerError,
  534. app.i18n.Ts("globals.messages.errorUpdating",
  535. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  536. }
  537. return c.JSON(http.StatusOK, okResp{true})
  538. }
  539. // handleDeleteSubscriberBounces deletes all the bounces on a subscriber.
  540. func handleDeleteSubscriberBounces(c echo.Context) error {
  541. var (
  542. app = c.Get("app").(*App)
  543. pID = c.Param("id")
  544. )
  545. id, _ := strconv.ParseInt(pID, 10, 64)
  546. if id < 1 {
  547. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  548. }
  549. if _, err := app.queries.DeleteBouncesBySubscriber.Exec(id, nil); err != nil {
  550. app.log.Printf("error deleting bounces: %v", err)
  551. return echo.NewHTTPError(http.StatusInternalServerError,
  552. app.i18n.Ts("globals.messages.errorDeleting",
  553. "name", "{globals.terms.bounces}", "error", pqErrMsg(err)))
  554. }
  555. return c.JSON(http.StatusOK, okResp{true})
  556. }
  557. // handleExportSubscriberData pulls the subscriber's profile,
  558. // list subscriptions, campaign views and clicks and produces
  559. // a JSON report. This is a privacy feature and depends on the
  560. // configuration in app.Constants.Privacy.
  561. func handleExportSubscriberData(c echo.Context) error {
  562. var (
  563. app = c.Get("app").(*App)
  564. pID = c.Param("id")
  565. )
  566. id, _ := strconv.ParseInt(pID, 10, 64)
  567. if id < 1 {
  568. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  569. }
  570. // Get the subscriber's data. A single query that gets the profile,
  571. // list subscriptions, campaign views, and link clicks. Names of
  572. // private lists are replaced with "Private list".
  573. _, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app)
  574. if err != nil {
  575. app.log.Printf("error exporting subscriber data: %s", err)
  576. return echo.NewHTTPError(http.StatusInternalServerError,
  577. app.i18n.Ts("globals.messages.errorFetching",
  578. "name", "{globals.terms.subscribers}", "error", err.Error()))
  579. }
  580. c.Response().Header().Set("Cache-Control", "no-cache")
  581. c.Response().Header().Set("Content-Disposition", `attachment; filename="data.json"`)
  582. return c.Blob(http.StatusOK, "application/json", b)
  583. }
  584. // insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
  585. // it was a new subscriber, and the second bool indicates if the subscriber was sent an optin confirmation.
  586. func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, bool, bool, error) {
  587. uu, err := uuid.NewV4()
  588. if err != nil {
  589. return req.Subscriber, false, false, err
  590. }
  591. req.UUID = uu.String()
  592. var (
  593. isNew = true
  594. subStatus = models.SubscriptionStatusUnconfirmed
  595. )
  596. if req.PreconfirmSubs {
  597. subStatus = models.SubscriptionStatusConfirmed
  598. }
  599. if err = app.queries.InsertSubscriber.Get(&req.ID,
  600. req.UUID,
  601. req.Email,
  602. strings.TrimSpace(req.Name),
  603. req.Status,
  604. req.Attribs,
  605. req.Lists,
  606. req.ListUUIDs,
  607. subStatus); err != nil {
  608. if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
  609. isNew = false
  610. } else {
  611. // return req.Subscriber, errSubscriberExists
  612. app.log.Printf("error inserting subscriber: %v", err)
  613. return req.Subscriber, false, false, echo.NewHTTPError(http.StatusInternalServerError,
  614. app.i18n.Ts("globals.messages.errorCreating",
  615. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  616. }
  617. }
  618. // Fetch the subscriber's full data. If the subscriber already existed and wasn't
  619. // created, the id will be empty. Fetch the details by e-mail then.
  620. sub, err := getSubscriber(req.ID, "", strings.ToLower(req.Email), app)
  621. if err != nil {
  622. return sub, false, false, err
  623. }
  624. hasOptin := false
  625. if !req.PreconfirmSubs {
  626. // Send a confirmation e-mail (if there are any double opt-in lists).
  627. num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
  628. hasOptin = num > 0
  629. }
  630. return sub, isNew, hasOptin, nil
  631. }
  632. // getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
  633. // Only one of these params should have a value.
  634. func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
  635. var out models.Subscribers
  636. if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
  637. app.log.Printf("error fetching subscriber: %v", err)
  638. return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
  639. app.i18n.Ts("globals.messages.errorFetching",
  640. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  641. }
  642. if len(out) == 0 {
  643. return models.Subscriber{}, echo.NewHTTPError(http.StatusBadRequest,
  644. app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
  645. }
  646. if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
  647. app.log.Printf("error loading subscriber lists: %v", err)
  648. return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
  649. app.i18n.Ts("globals.messages.errorFetching",
  650. "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
  651. }
  652. return out[0], nil
  653. }
  654. // exportSubscriberData collates the data of a subscriber including profile,
  655. // subscriptions, campaign_views, link_clicks (if they're enabled in the config)
  656. // and returns a formatted, indented JSON payload. Either takes a numeric id
  657. // and an empty subUUID or takes 0 and a string subUUID.
  658. func exportSubscriberData(id int64, subUUID string, exportables map[string]bool, app *App) (subProfileData, []byte, error) {
  659. // Get the subscriber's data. A single query that gets the profile,
  660. // list subscriptions, campaign views, and link clicks. Names of
  661. // private lists are replaced with "Private list".
  662. var (
  663. data subProfileData
  664. uu interface{}
  665. )
  666. // UUID should be a valid value or a nil.
  667. if subUUID != "" {
  668. uu = subUUID
  669. }
  670. if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
  671. app.log.Printf("error fetching subscriber export data: %v", err)
  672. return data, nil, err
  673. }
  674. // Filter out the non-exportable items.
  675. if _, ok := exportables["profile"]; !ok {
  676. data.Profile = nil
  677. }
  678. if _, ok := exportables["subscriptions"]; !ok {
  679. data.Subscriptions = nil
  680. }
  681. if _, ok := exportables["campaign_views"]; !ok {
  682. data.CampaignViews = nil
  683. }
  684. if _, ok := exportables["link_clicks"]; !ok {
  685. data.LinkClicks = nil
  686. }
  687. // Marshal the data into an indented payload.
  688. b, err := json.MarshalIndent(data, "", " ")
  689. if err != nil {
  690. app.log.Printf("error marshalling subscriber export data: %v", err)
  691. return data, nil, err
  692. }
  693. return data, b, nil
  694. }
  695. // sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
  696. // if at least one of the given listIDs is set to optin=double. It returns the number of
  697. // opt-in lists that were found.
  698. func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
  699. var lists []models.List
  700. // Fetch double opt-in lists from the given list IDs.
  701. // Get the list of subscription lists where the subscriber hasn't confirmed.
  702. if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
  703. pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
  704. app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
  705. return 0, err
  706. }
  707. // None.
  708. if len(lists) == 0 {
  709. return 0, nil
  710. }
  711. var (
  712. out = subOptin{Subscriber: &sub, Lists: lists}
  713. qListIDs = url.Values{}
  714. )
  715. // Construct the opt-in URL with list IDs.
  716. for _, l := range out.Lists {
  717. qListIDs.Add("l", l.UUID)
  718. }
  719. out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
  720. // Send the e-mail.
  721. if err := app.sendNotification([]string{sub.Email},
  722. app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
  723. app.log.Printf("error sending opt-in e-mail: %s", err)
  724. return 0, err
  725. }
  726. return len(lists), nil
  727. }
  728. // sanitizeSQLExp does basic sanitisation on arbitrary
  729. // SQL query expressions coming from the frontend.
  730. func sanitizeSQLExp(q string) string {
  731. if len(q) == 0 {
  732. return ""
  733. }
  734. q = strings.TrimSpace(q)
  735. // Remove semicolon suffix.
  736. if q[len(q)-1] == ';' {
  737. q = q[:len(q)-1]
  738. }
  739. return q
  740. }