subscribers.go 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  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. r, err := app.importer.ValidateFields(req)
  248. if err != nil {
  249. return echo.NewHTTPError(http.StatusBadRequest, err.Error())
  250. } else {
  251. req = r
  252. }
  253. // Insert the subscriber into the DB.
  254. sub, isNew, _, err := insertSubscriber(req, app)
  255. if err != nil {
  256. return err
  257. }
  258. if !isNew {
  259. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
  260. }
  261. return c.JSON(http.StatusOK, okResp{sub})
  262. }
  263. // handleUpdateSubscriber handles modification of a subscriber.
  264. func handleUpdateSubscriber(c echo.Context) error {
  265. var (
  266. app = c.Get("app").(*App)
  267. id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
  268. req subUpdateReq
  269. )
  270. // Get and validate fields.
  271. if err := c.Bind(&req); err != nil {
  272. return err
  273. }
  274. if id < 1 {
  275. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  276. }
  277. if em, err := app.importer.SanitizeEmail(req.Email); err != nil {
  278. return echo.NewHTTPError(http.StatusBadRequest, err.Error())
  279. } else {
  280. req.Email = em
  281. }
  282. if req.Name != "" && !strHasLen(req.Name, 1, stdInputMaxLen) {
  283. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
  284. }
  285. // If there's an attribs value, validate it.
  286. if len(req.RawAttribs) > 0 {
  287. var a models.SubscriberAttribs
  288. if err := json.Unmarshal(req.RawAttribs, &a); err != nil {
  289. return echo.NewHTTPError(http.StatusInternalServerError,
  290. app.i18n.Ts("globals.messages.errorUpdating",
  291. "name", "{globals.terms.subscriber}", "error", err.Error()))
  292. }
  293. }
  294. subStatus := models.SubscriptionStatusUnconfirmed
  295. if req.PreconfirmSubs {
  296. subStatus = models.SubscriptionStatusConfirmed
  297. }
  298. _, err := app.queries.UpdateSubscriber.Exec(id,
  299. strings.ToLower(strings.TrimSpace(req.Email)),
  300. strings.TrimSpace(req.Name),
  301. req.Status,
  302. req.RawAttribs,
  303. req.Lists,
  304. subStatus)
  305. if err != nil {
  306. app.log.Printf("error updating subscriber: %v", err)
  307. return echo.NewHTTPError(http.StatusInternalServerError,
  308. app.i18n.Ts("globals.messages.errorUpdating",
  309. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  310. }
  311. // Send a confirmation e-mail (if there are any double opt-in lists).
  312. sub, err := getSubscriber(int(id), "", "", app)
  313. if err != nil {
  314. return err
  315. }
  316. if !req.PreconfirmSubs && app.constants.SendOptinConfirmation {
  317. _, _ = sendOptinConfirmation(sub, []int64(req.Lists), app)
  318. }
  319. return c.JSON(http.StatusOK, okResp{sub})
  320. }
  321. // handleGetSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
  322. func handleSubscriberSendOptin(c echo.Context) error {
  323. var (
  324. app = c.Get("app").(*App)
  325. id, _ = strconv.Atoi(c.Param("id"))
  326. )
  327. if id < 1 {
  328. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  329. }
  330. // Fetch the subscriber.
  331. out, err := getSubscriber(id, "", "", app)
  332. if err != nil {
  333. app.log.Printf("error fetching subscriber: %v", err)
  334. return echo.NewHTTPError(http.StatusInternalServerError,
  335. app.i18n.Ts("globals.messages.errorFetching",
  336. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  337. }
  338. if _, err := sendOptinConfirmation(out, nil, app); err != nil {
  339. return echo.NewHTTPError(http.StatusInternalServerError,
  340. app.i18n.T("subscribers.errorSendingOptin"))
  341. }
  342. return c.JSON(http.StatusOK, okResp{true})
  343. }
  344. // handleBlocklistSubscribers handles the blocklisting of one or more subscribers.
  345. // It takes either an ID in the URI, or a list of IDs in the request body.
  346. func handleBlocklistSubscribers(c echo.Context) error {
  347. var (
  348. app = c.Get("app").(*App)
  349. pID = c.Param("id")
  350. IDs pq.Int64Array
  351. )
  352. // Is it a /:id call?
  353. if pID != "" {
  354. id, _ := strconv.ParseInt(pID, 10, 64)
  355. if id < 1 {
  356. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  357. }
  358. IDs = append(IDs, id)
  359. } else {
  360. // Multiple IDs.
  361. var req subQueryReq
  362. if err := c.Bind(&req); err != nil {
  363. return echo.NewHTTPError(http.StatusBadRequest,
  364. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  365. }
  366. if len(req.SubscriberIDs) == 0 {
  367. return echo.NewHTTPError(http.StatusBadRequest,
  368. "No IDs given.")
  369. }
  370. IDs = req.SubscriberIDs
  371. }
  372. if _, err := app.queries.BlocklistSubscribers.Exec(IDs); err != nil {
  373. app.log.Printf("error blocklisting subscribers: %v", err)
  374. return echo.NewHTTPError(http.StatusInternalServerError,
  375. app.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
  376. }
  377. return c.JSON(http.StatusOK, okResp{true})
  378. }
  379. // handleManageSubscriberLists handles bulk addition or removal of subscribers
  380. // from or to one or more target lists.
  381. // It takes either an ID in the URI, or a list of IDs in the request body.
  382. func handleManageSubscriberLists(c echo.Context) error {
  383. var (
  384. app = c.Get("app").(*App)
  385. pID = c.Param("id")
  386. IDs pq.Int64Array
  387. )
  388. // Is it a /:id call?
  389. if pID != "" {
  390. id, _ := strconv.ParseInt(pID, 10, 64)
  391. if id < 1 {
  392. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  393. }
  394. IDs = append(IDs, id)
  395. }
  396. var req subQueryReq
  397. if err := c.Bind(&req); err != nil {
  398. return echo.NewHTTPError(http.StatusBadRequest,
  399. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  400. }
  401. if len(req.SubscriberIDs) == 0 {
  402. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
  403. }
  404. if len(IDs) == 0 {
  405. IDs = req.SubscriberIDs
  406. }
  407. if len(req.TargetListIDs) == 0 {
  408. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
  409. }
  410. // Action.
  411. var err error
  412. switch req.Action {
  413. case "add":
  414. _, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
  415. case "remove":
  416. _, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
  417. case "unsubscribe":
  418. _, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
  419. default:
  420. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
  421. }
  422. if err != nil {
  423. app.log.Printf("error updating subscriptions: %v", err)
  424. return echo.NewHTTPError(http.StatusInternalServerError,
  425. app.i18n.Ts("globals.messages.errorUpdating",
  426. "name", "{globals.terms.subscribers}", "error", err.Error()))
  427. }
  428. return c.JSON(http.StatusOK, okResp{true})
  429. }
  430. // handleDeleteSubscribers handles subscriber deletion.
  431. // It takes either an ID in the URI, or a list of IDs in the request body.
  432. func handleDeleteSubscribers(c echo.Context) error {
  433. var (
  434. app = c.Get("app").(*App)
  435. pID = c.Param("id")
  436. IDs pq.Int64Array
  437. )
  438. // Is it an /:id call?
  439. if pID != "" {
  440. id, _ := strconv.ParseInt(pID, 10, 64)
  441. if id < 1 {
  442. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  443. }
  444. IDs = append(IDs, id)
  445. } else {
  446. // Multiple IDs.
  447. i, err := parseStringIDs(c.Request().URL.Query()["id"])
  448. if err != nil {
  449. return echo.NewHTTPError(http.StatusBadRequest,
  450. app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
  451. }
  452. if len(i) == 0 {
  453. return echo.NewHTTPError(http.StatusBadRequest,
  454. app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
  455. }
  456. IDs = i
  457. }
  458. if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
  459. app.log.Printf("error deleting subscribers: %v", err)
  460. return echo.NewHTTPError(http.StatusInternalServerError,
  461. app.i18n.Ts("globals.messages.errorDeleting",
  462. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  463. }
  464. return c.JSON(http.StatusOK, okResp{true})
  465. }
  466. // handleDeleteSubscribersByQuery bulk deletes based on an
  467. // arbitrary SQL expression.
  468. func handleDeleteSubscribersByQuery(c echo.Context) error {
  469. var (
  470. app = c.Get("app").(*App)
  471. req subQueryReq
  472. )
  473. if err := c.Bind(&req); err != nil {
  474. return err
  475. }
  476. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  477. app.queries.DeleteSubscribersByQuery,
  478. req.ListIDs, app.db)
  479. if err != nil {
  480. app.log.Printf("error deleting subscribers: %v", err)
  481. return echo.NewHTTPError(http.StatusInternalServerError,
  482. app.i18n.Ts("globals.messages.errorDeleting",
  483. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  484. }
  485. return c.JSON(http.StatusOK, okResp{true})
  486. }
  487. // handleBlocklistSubscribersByQuery bulk blocklists subscribers
  488. // based on an arbitrary SQL expression.
  489. func handleBlocklistSubscribersByQuery(c echo.Context) error {
  490. var (
  491. app = c.Get("app").(*App)
  492. req subQueryReq
  493. )
  494. if err := c.Bind(&req); err != nil {
  495. return err
  496. }
  497. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  498. app.queries.BlocklistSubscribersByQuery,
  499. req.ListIDs, app.db)
  500. if err != nil {
  501. app.log.Printf("error blocklisting subscribers: %v", err)
  502. return echo.NewHTTPError(http.StatusInternalServerError,
  503. app.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
  504. }
  505. return c.JSON(http.StatusOK, okResp{true})
  506. }
  507. // handleManageSubscriberListsByQuery bulk adds/removes/unsubscribers subscribers
  508. // from one or more lists based on an arbitrary SQL expression.
  509. func handleManageSubscriberListsByQuery(c echo.Context) error {
  510. var (
  511. app = c.Get("app").(*App)
  512. req subQueryReq
  513. )
  514. if err := c.Bind(&req); err != nil {
  515. return err
  516. }
  517. if len(req.TargetListIDs) == 0 {
  518. return echo.NewHTTPError(http.StatusBadRequest,
  519. app.i18n.T("subscribers.errorNoListsGiven"))
  520. }
  521. // Action.
  522. var stmt string
  523. switch req.Action {
  524. case "add":
  525. stmt = app.queries.AddSubscribersToListsByQuery
  526. case "remove":
  527. stmt = app.queries.DeleteSubscriptionsByQuery
  528. case "unsubscribe":
  529. stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
  530. default:
  531. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
  532. }
  533. err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
  534. stmt, req.ListIDs, app.db, req.TargetListIDs)
  535. if err != nil {
  536. app.log.Printf("error updating subscriptions: %v", err)
  537. return echo.NewHTTPError(http.StatusInternalServerError,
  538. app.i18n.Ts("globals.messages.errorUpdating",
  539. "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
  540. }
  541. return c.JSON(http.StatusOK, okResp{true})
  542. }
  543. // handleDeleteSubscriberBounces deletes all the bounces on a subscriber.
  544. func handleDeleteSubscriberBounces(c echo.Context) error {
  545. var (
  546. app = c.Get("app").(*App)
  547. pID = c.Param("id")
  548. )
  549. id, _ := strconv.ParseInt(pID, 10, 64)
  550. if id < 1 {
  551. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  552. }
  553. if _, err := app.queries.DeleteBouncesBySubscriber.Exec(id, nil); err != nil {
  554. app.log.Printf("error deleting bounces: %v", err)
  555. return echo.NewHTTPError(http.StatusInternalServerError,
  556. app.i18n.Ts("globals.messages.errorDeleting",
  557. "name", "{globals.terms.bounces}", "error", pqErrMsg(err)))
  558. }
  559. return c.JSON(http.StatusOK, okResp{true})
  560. }
  561. // handleExportSubscriberData pulls the subscriber's profile,
  562. // list subscriptions, campaign views and clicks and produces
  563. // a JSON report. This is a privacy feature and depends on the
  564. // configuration in app.Constants.Privacy.
  565. func handleExportSubscriberData(c echo.Context) error {
  566. var (
  567. app = c.Get("app").(*App)
  568. pID = c.Param("id")
  569. )
  570. id, _ := strconv.ParseInt(pID, 10, 64)
  571. if id < 1 {
  572. return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
  573. }
  574. // Get the subscriber's data. A single query that gets the profile,
  575. // list subscriptions, campaign views, and link clicks. Names of
  576. // private lists are replaced with "Private list".
  577. _, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app)
  578. if err != nil {
  579. app.log.Printf("error exporting subscriber data: %s", err)
  580. return echo.NewHTTPError(http.StatusInternalServerError,
  581. app.i18n.Ts("globals.messages.errorFetching",
  582. "name", "{globals.terms.subscribers}", "error", err.Error()))
  583. }
  584. c.Response().Header().Set("Cache-Control", "no-cache")
  585. c.Response().Header().Set("Content-Disposition", `attachment; filename="data.json"`)
  586. return c.Blob(http.StatusOK, "application/json", b)
  587. }
  588. // insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
  589. // it was a new subscriber, and the second bool indicates if the subscriber was sent an optin confirmation.
  590. func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, bool, bool, error) {
  591. uu, err := uuid.NewV4()
  592. if err != nil {
  593. return req.Subscriber, false, false, err
  594. }
  595. req.UUID = uu.String()
  596. var (
  597. isNew = true
  598. subStatus = models.SubscriptionStatusUnconfirmed
  599. )
  600. if req.PreconfirmSubs {
  601. subStatus = models.SubscriptionStatusConfirmed
  602. }
  603. if err = app.queries.InsertSubscriber.Get(&req.ID,
  604. req.UUID,
  605. req.Email,
  606. strings.TrimSpace(req.Name),
  607. req.Status,
  608. req.Attribs,
  609. req.Lists,
  610. req.ListUUIDs,
  611. subStatus); err != nil {
  612. if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
  613. isNew = false
  614. } else {
  615. // return req.Subscriber, errSubscriberExists
  616. app.log.Printf("error inserting subscriber: %v", err)
  617. return req.Subscriber, false, false, echo.NewHTTPError(http.StatusInternalServerError,
  618. app.i18n.Ts("globals.messages.errorCreating",
  619. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  620. }
  621. }
  622. // Fetch the subscriber's full data. If the subscriber already existed and wasn't
  623. // created, the id will be empty. Fetch the details by e-mail then.
  624. sub, err := getSubscriber(req.ID, "", strings.ToLower(req.Email), app)
  625. if err != nil {
  626. return sub, false, false, err
  627. }
  628. hasOptin := false
  629. if !req.PreconfirmSubs && app.constants.SendOptinConfirmation {
  630. // Send a confirmation e-mail (if there are any double opt-in lists).
  631. num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
  632. hasOptin = num > 0
  633. }
  634. return sub, isNew, hasOptin, nil
  635. }
  636. // getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
  637. // Only one of these params should have a value.
  638. func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
  639. var out models.Subscribers
  640. if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
  641. app.log.Printf("error fetching subscriber: %v", err)
  642. return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
  643. app.i18n.Ts("globals.messages.errorFetching",
  644. "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
  645. }
  646. if len(out) == 0 {
  647. return models.Subscriber{}, echo.NewHTTPError(http.StatusBadRequest,
  648. app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
  649. }
  650. if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
  651. app.log.Printf("error loading subscriber lists: %v", err)
  652. return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
  653. app.i18n.Ts("globals.messages.errorFetching",
  654. "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
  655. }
  656. return out[0], nil
  657. }
  658. // exportSubscriberData collates the data of a subscriber including profile,
  659. // subscriptions, campaign_views, link_clicks (if they're enabled in the config)
  660. // and returns a formatted, indented JSON payload. Either takes a numeric id
  661. // and an empty subUUID or takes 0 and a string subUUID.
  662. func exportSubscriberData(id int64, subUUID string, exportables map[string]bool, app *App) (subProfileData, []byte, error) {
  663. // Get the subscriber's data. A single query that gets the profile,
  664. // list subscriptions, campaign views, and link clicks. Names of
  665. // private lists are replaced with "Private list".
  666. var (
  667. data subProfileData
  668. uu interface{}
  669. )
  670. // UUID should be a valid value or a nil.
  671. if subUUID != "" {
  672. uu = subUUID
  673. }
  674. if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
  675. app.log.Printf("error fetching subscriber export data: %v", err)
  676. return data, nil, err
  677. }
  678. // Filter out the non-exportable items.
  679. if _, ok := exportables["profile"]; !ok {
  680. data.Profile = nil
  681. }
  682. if _, ok := exportables["subscriptions"]; !ok {
  683. data.Subscriptions = nil
  684. }
  685. if _, ok := exportables["campaign_views"]; !ok {
  686. data.CampaignViews = nil
  687. }
  688. if _, ok := exportables["link_clicks"]; !ok {
  689. data.LinkClicks = nil
  690. }
  691. // Marshal the data into an indented payload.
  692. b, err := json.MarshalIndent(data, "", " ")
  693. if err != nil {
  694. app.log.Printf("error marshalling subscriber export data: %v", err)
  695. return data, nil, err
  696. }
  697. return data, b, nil
  698. }
  699. // sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
  700. // if at least one of the given listIDs is set to optin=double. It returns the number of
  701. // opt-in lists that were found.
  702. func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
  703. var lists []models.List
  704. // Fetch double opt-in lists from the given list IDs.
  705. // Get the list of subscription lists where the subscriber hasn't confirmed.
  706. if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
  707. pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
  708. app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
  709. return 0, err
  710. }
  711. // None.
  712. if len(lists) == 0 {
  713. return 0, nil
  714. }
  715. var (
  716. out = subOptin{Subscriber: &sub, Lists: lists}
  717. qListIDs = url.Values{}
  718. )
  719. // Construct the opt-in URL with list IDs.
  720. for _, l := range out.Lists {
  721. qListIDs.Add("l", l.UUID)
  722. }
  723. out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
  724. // Send the e-mail.
  725. if err := app.sendNotification([]string{sub.Email},
  726. app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
  727. app.log.Printf("error sending opt-in e-mail: %s", err)
  728. return 0, err
  729. }
  730. return len(lists), nil
  731. }
  732. // sanitizeSQLExp does basic sanitisation on arbitrary
  733. // SQL query expressions coming from the frontend.
  734. func sanitizeSQLExp(q string) string {
  735. if len(q) == 0 {
  736. return ""
  737. }
  738. q = strings.TrimSpace(q)
  739. // Remove semicolon suffix.
  740. if q[len(q)-1] == ';' {
  741. q = q[:len(q)-1]
  742. }
  743. return q
  744. }