|
@@ -1,8 +1,6 @@
|
|
package main
|
|
package main
|
|
|
|
|
|
import (
|
|
import (
|
|
- "context"
|
|
|
|
- "database/sql"
|
|
|
|
"encoding/csv"
|
|
"encoding/csv"
|
|
"encoding/json"
|
|
"encoding/json"
|
|
"errors"
|
|
"errors"
|
|
@@ -12,11 +10,8 @@ import (
|
|
"strconv"
|
|
"strconv"
|
|
"strings"
|
|
"strings"
|
|
|
|
|
|
- "github.com/gofrs/uuid"
|
|
|
|
- "github.com/knadh/listmonk/internal/subimporter"
|
|
|
|
"github.com/knadh/listmonk/models"
|
|
"github.com/knadh/listmonk/models"
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/labstack/echo/v4"
|
|
- "github.com/lib/pq"
|
|
|
|
)
|
|
)
|
|
|
|
|
|
const (
|
|
const (
|
|
@@ -26,29 +21,12 @@ const (
|
|
// subQueryReq is a "catch all" struct for reading various
|
|
// subQueryReq is a "catch all" struct for reading various
|
|
// subscriber related requests.
|
|
// subscriber related requests.
|
|
type subQueryReq struct {
|
|
type subQueryReq struct {
|
|
- Query string `json:"query"`
|
|
|
|
- ListIDs pq.Int64Array `json:"list_ids"`
|
|
|
|
- TargetListIDs pq.Int64Array `json:"target_list_ids"`
|
|
|
|
- SubscriberIDs pq.Int64Array `json:"ids"`
|
|
|
|
- Action string `json:"action"`
|
|
|
|
- Status string `json:"status"`
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-type subsWrap struct {
|
|
|
|
- Results models.Subscribers `json:"results"`
|
|
|
|
-
|
|
|
|
- Query string `json:"query"`
|
|
|
|
- Total int `json:"total"`
|
|
|
|
- PerPage int `json:"per_page"`
|
|
|
|
- Page int `json:"page"`
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-type subUpdateReq struct {
|
|
|
|
- models.Subscriber
|
|
|
|
- RawAttribs json.RawMessage `json:"attribs"`
|
|
|
|
- Lists pq.Int64Array `json:"lists"`
|
|
|
|
- ListUUIDs pq.StringArray `json:"list_uuids"`
|
|
|
|
- PreconfirmSubs bool `json:"preconfirm_subscriptions"`
|
|
|
|
|
|
+ Query string `json:"query"`
|
|
|
|
+ ListIDs []int `json:"list_ids"`
|
|
|
|
+ TargetListIDs []int `json:"target_list_ids"`
|
|
|
|
+ SubscriberIDs []int `json:"ids"`
|
|
|
|
+ Action string `json:"action"`
|
|
|
|
+ Status string `json:"status"`
|
|
}
|
|
}
|
|
|
|
|
|
// subProfileData represents a subscriber's collated data in JSON
|
|
// subProfileData represents a subscriber's collated data in JSON
|
|
@@ -63,7 +41,7 @@ type subProfileData struct {
|
|
|
|
|
|
// subOptin contains the data that's passed to the double opt-in e-mail template.
|
|
// subOptin contains the data that's passed to the double opt-in e-mail template.
|
|
type subOptin struct {
|
|
type subOptin struct {
|
|
- *models.Subscriber
|
|
|
|
|
|
+ models.Subscriber
|
|
|
|
|
|
OptinURL string
|
|
OptinURL string
|
|
UnsubURL string
|
|
UnsubURL string
|
|
@@ -94,12 +72,12 @@ func handleGetSubscriber(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
|
|
|
|
- sub, err := getSubscriber(id, "", "", app)
|
|
|
|
|
|
+ out, err := app.core.GetSubscriber(id, "", "")
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
|
|
- return c.JSON(http.StatusOK, okResp{sub})
|
|
|
|
|
|
+ return c.JSON(http.StatusOK, okResp{out})
|
|
}
|
|
}
|
|
|
|
|
|
// handleQuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
|
|
// handleQuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
|
|
@@ -112,7 +90,7 @@ func handleQuerySubscribers(c echo.Context) error {
|
|
query = sanitizeSQLExp(c.FormValue("query"))
|
|
query = sanitizeSQLExp(c.FormValue("query"))
|
|
orderBy = c.FormValue("order_by")
|
|
orderBy = c.FormValue("order_by")
|
|
order = c.FormValue("order")
|
|
order = c.FormValue("order")
|
|
- out = subsWrap{Results: make([]models.Subscriber, 0, 1)}
|
|
|
|
|
|
+ out models.PageResults
|
|
)
|
|
)
|
|
|
|
|
|
// Limit the subscribers to specific lists?
|
|
// Limit the subscribers to specific lists?
|
|
@@ -121,67 +99,13 @@ func handleQuerySubscribers(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
|
|
|
|
- // There's an arbitrary query condition.
|
|
|
|
- cond := ""
|
|
|
|
- if query != "" {
|
|
|
|
- cond = " AND " + query
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Sort params.
|
|
|
|
- if !strSliceContains(orderBy, subQuerySortFields) {
|
|
|
|
- orderBy = "subscribers.id"
|
|
|
|
- }
|
|
|
|
- if order != sortAsc && order != sortDesc {
|
|
|
|
- order = sortDesc
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Create a readonly transaction that just does COUNT() to obtain the count of results
|
|
|
|
- // and to ensure that the arbitrary query is indeed readonly.
|
|
|
|
- stmt := fmt.Sprintf(app.queries.QuerySubscribersCount, cond)
|
|
|
|
- tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
|
|
|
|
|
+ res, total, err := app.core.QuerySubscribers(query, listIDs, order, orderBy, pg.Offset, pg.Limit)
|
|
if err != nil {
|
|
if err != nil {
|
|
- app.log.Printf("error preparing subscriber query: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
- app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
- defer tx.Rollback()
|
|
|
|
-
|
|
|
|
- // Execute the readonly query and get the count of results.
|
|
|
|
- var total = 0
|
|
|
|
- if err := tx.Get(&total, stmt, listIDs); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // No results.
|
|
|
|
- if total == 0 {
|
|
|
|
- return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Run the query again and fetch the actual data. stmt is the raw SQL query.
|
|
|
|
- stmt = fmt.Sprintf(app.queries.QuerySubscribers, cond, orderBy, order)
|
|
|
|
- if err := tx.Select(&out.Results, stmt, listIDs, pg.Offset, pg.Limit); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Lazy load lists for each subscriber.
|
|
|
|
- if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
|
|
|
|
- app.log.Printf("error fetching subscriber lists: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
out.Query = query
|
|
out.Query = query
|
|
- if len(out.Results) == 0 {
|
|
|
|
- out.Results = make(models.Subscribers, 0)
|
|
|
|
- return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Meta.
|
|
|
|
|
|
+ out.Results = res
|
|
out.Total = total
|
|
out.Total = total
|
|
out.Page = pg.Page
|
|
out.Page = pg.Page
|
|
out.PerPage = pg.PerPage
|
|
out.PerPage = pg.PerPage
|
|
@@ -210,41 +134,13 @@ func handleExportSubscribers(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
|
|
|
|
- // There's an arbitrary query condition.
|
|
|
|
- cond := ""
|
|
|
|
- if query != "" {
|
|
|
|
- cond = " AND " + query
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- stmt := fmt.Sprintf(app.queries.QuerySubscribersForExport, cond)
|
|
|
|
-
|
|
|
|
- // Verify that the arbitrary SQL search expression is read only.
|
|
|
|
- if cond != "" {
|
|
|
|
- tx, err := app.db.Unsafe().BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
|
|
|
- if err != nil {
|
|
|
|
- app.log.Printf("error preparing subscriber query: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
- app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
- defer tx.Rollback()
|
|
|
|
-
|
|
|
|
- if _, err := tx.Query(stmt, nil, 0, nil, 1); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
- app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Prepare the actual query statement.
|
|
|
|
- tx, err := db.Preparex(stmt)
|
|
|
|
|
|
+ // Get the batched export iterator.
|
|
|
|
+ exp, err := app.core.ExportSubscribers(query, subIDs, listIDs, app.constants.DBBatchSize)
|
|
if err != nil {
|
|
if err != nil {
|
|
- return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
- app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
- // Run the query until all rows are exhausted.
|
|
|
|
var (
|
|
var (
|
|
- id = 0
|
|
|
|
-
|
|
|
|
h = c.Response().Header()
|
|
h = c.Response().Header()
|
|
wr = csv.NewWriter(c.Response())
|
|
wr = csv.NewWriter(c.Response())
|
|
)
|
|
)
|
|
@@ -257,15 +153,14 @@ func handleExportSubscribers(c echo.Context) error {
|
|
wr.Write([]string{"uuid", "email", "name", "attributes", "status", "created_at", "updated_at"})
|
|
wr.Write([]string{"uuid", "email", "name", "attributes", "status", "created_at", "updated_at"})
|
|
|
|
|
|
loop:
|
|
loop:
|
|
|
|
+ // Iterate in batches until there are no more subscribers to export.
|
|
for {
|
|
for {
|
|
- var out []models.SubscriberExport
|
|
|
|
- if err := tx.Select(&out, listIDs, id, subIDs, app.constants.DBBatchSize); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ out, err := exp()
|
|
|
|
+ if err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
- if len(out) == 0 {
|
|
|
|
- break loop
|
|
|
|
|
|
+ if out == nil || len(out) == 0 {
|
|
|
|
+ break
|
|
}
|
|
}
|
|
|
|
|
|
for _, r := range out {
|
|
for _, r := range out {
|
|
@@ -275,9 +170,9 @@ loop:
|
|
break loop
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
- wr.Flush()
|
|
|
|
|
|
|
|
- id = out[len(out)-1].ID
|
|
|
|
|
|
+ // Flush CSV to stream after each batch.
|
|
|
|
+ wr.Flush()
|
|
}
|
|
}
|
|
|
|
|
|
return nil
|
|
return nil
|
|
@@ -287,7 +182,12 @@ loop:
|
|
func handleCreateSubscriber(c echo.Context) error {
|
|
func handleCreateSubscriber(c echo.Context) error {
|
|
var (
|
|
var (
|
|
app = c.Get("app").(*App)
|
|
app = c.Get("app").(*App)
|
|
- req subimporter.SubReq
|
|
|
|
|
|
+ req struct {
|
|
|
|
+ models.Subscriber
|
|
|
|
+ Lists []int `json:"lists"`
|
|
|
|
+ ListUUIDs []string `json:"list_uuids"`
|
|
|
|
+ PreconfirmSubs bool `json:"preconfirm_subscriptions"`
|
|
|
|
+ }
|
|
)
|
|
)
|
|
|
|
|
|
// Get and validate fields.
|
|
// Get and validate fields.
|
|
@@ -295,15 +195,24 @@ func handleCreateSubscriber(c echo.Context) error {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
|
|
- r, err := app.importer.ValidateFields(req)
|
|
|
|
|
|
+ // Validate fields.
|
|
|
|
+ if len(req.Email) > 1000 {
|
|
|
|
+ return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidEmail"))
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ em, err := app.importer.SanitizeEmail(req.Email)
|
|
if err != nil {
|
|
if err != nil {
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
- } else {
|
|
|
|
- req = r
|
|
|
|
|
|
+ }
|
|
|
|
+ req.Email = em
|
|
|
|
+
|
|
|
|
+ req.Name = strings.TrimSpace(req.Name)
|
|
|
|
+ if len(req.Name) == 0 || len(req.Name) > stdInputMaxLen {
|
|
|
|
+ return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
|
}
|
|
}
|
|
|
|
|
|
// Insert the subscriber into the DB.
|
|
// Insert the subscriber into the DB.
|
|
- sub, isNew, _, err := insertSubscriber(req, app)
|
|
|
|
|
|
+ sub, isNew, _, err := app.core.CreateSubscriber(req.Subscriber, req.Lists, req.ListUUIDs, req.PreconfirmSubs)
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
@@ -318,9 +227,14 @@ func handleCreateSubscriber(c echo.Context) error {
|
|
func handleUpdateSubscriber(c echo.Context) error {
|
|
func handleUpdateSubscriber(c echo.Context) error {
|
|
var (
|
|
var (
|
|
app = c.Get("app").(*App)
|
|
app = c.Get("app").(*App)
|
|
- id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
|
|
|
- req subUpdateReq
|
|
|
|
|
|
+ id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
+ req struct {
|
|
|
|
+ models.Subscriber
|
|
|
|
+ Lists []int `json:"lists"`
|
|
|
|
+ PreconfirmSubs bool `json:"preconfirm_subscriptions"`
|
|
|
|
+ }
|
|
)
|
|
)
|
|
|
|
+
|
|
// Get and validate fields.
|
|
// Get and validate fields.
|
|
if err := c.Bind(&req); err != nil {
|
|
if err := c.Bind(&req); err != nil {
|
|
return err
|
|
return err
|
|
@@ -340,42 +254,12 @@ func handleUpdateSubscriber(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
|
}
|
|
}
|
|
|
|
|
|
- // If there's an attribs value, validate it.
|
|
|
|
- if len(req.RawAttribs) > 0 {
|
|
|
|
- var a models.SubscriberAttribs
|
|
|
|
- if err := json.Unmarshal(req.RawAttribs, &a); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorUpdating",
|
|
|
|
- "name", "{globals.terms.subscriber}", "error", err.Error()))
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- subStatus := models.SubscriptionStatusUnconfirmed
|
|
|
|
- if req.PreconfirmSubs {
|
|
|
|
- subStatus = models.SubscriptionStatusConfirmed
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- _, err := app.queries.UpdateSubscriber.Exec(id,
|
|
|
|
- strings.ToLower(strings.TrimSpace(req.Email)),
|
|
|
|
- strings.TrimSpace(req.Name),
|
|
|
|
- req.Status,
|
|
|
|
- req.RawAttribs,
|
|
|
|
- req.Lists,
|
|
|
|
- subStatus)
|
|
|
|
- if err != nil {
|
|
|
|
- app.log.Printf("error updating subscriber: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorUpdating",
|
|
|
|
- "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Send a confirmation e-mail (if there are any double opt-in lists).
|
|
|
|
- sub, err := getSubscriber(int(id), "", "", app)
|
|
|
|
|
|
+ out, err := app.core.UpdateSubscriber(id, req.Subscriber, req.Lists, req.PreconfirmSubs)
|
|
if err != nil {
|
|
if err != nil {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
|
|
- return c.JSON(http.StatusOK, okResp{sub})
|
|
|
|
|
|
+ return c.JSON(http.StatusOK, okResp{out})
|
|
}
|
|
}
|
|
|
|
|
|
// handleGetSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
|
// handleGetSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
|
@@ -390,17 +274,13 @@ func handleSubscriberSendOptin(c echo.Context) error {
|
|
}
|
|
}
|
|
|
|
|
|
// Fetch the subscriber.
|
|
// Fetch the subscriber.
|
|
- out, err := getSubscriber(id, "", "", app)
|
|
|
|
|
|
+ out, err := app.core.GetSubscriber(id, "", "")
|
|
if err != nil {
|
|
if err != nil {
|
|
- app.log.Printf("error fetching subscriber: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
- if _, err := sendOptinConfirmation(out, nil, app); err != nil {
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.T("subscribers.errorSendingOptin"))
|
|
|
|
|
|
+ if _, err := sendOptinConfirmationHook(app)(out, nil); err != nil {
|
|
|
|
+ return echo.NewHTTPError(http.StatusInternalServerError, app.i18n.T("subscribers.errorSendingOptin"))
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -410,18 +290,19 @@ func handleSubscriberSendOptin(c echo.Context) error {
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
func handleBlocklistSubscribers(c echo.Context) error {
|
|
func handleBlocklistSubscribers(c echo.Context) error {
|
|
var (
|
|
var (
|
|
- app = c.Get("app").(*App)
|
|
|
|
- pID = c.Param("id")
|
|
|
|
- IDs pq.Int64Array
|
|
|
|
|
|
+ app = c.Get("app").(*App)
|
|
|
|
+ pID = c.Param("id")
|
|
|
|
+ subIDs []int
|
|
)
|
|
)
|
|
|
|
|
|
// Is it a /:id call?
|
|
// Is it a /:id call?
|
|
if pID != "" {
|
|
if pID != "" {
|
|
- id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
|
|
+ id, _ := strconv.Atoi(pID)
|
|
if id < 1 {
|
|
if id < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
- IDs = append(IDs, id)
|
|
|
|
|
|
+
|
|
|
|
+ subIDs = append(subIDs, id)
|
|
} else {
|
|
} else {
|
|
// Multiple IDs.
|
|
// Multiple IDs.
|
|
var req subQueryReq
|
|
var req subQueryReq
|
|
@@ -433,13 +314,12 @@ func handleBlocklistSubscribers(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
"No IDs given.")
|
|
"No IDs given.")
|
|
}
|
|
}
|
|
- IDs = req.SubscriberIDs
|
|
|
|
|
|
+
|
|
|
|
+ subIDs = req.SubscriberIDs
|
|
}
|
|
}
|
|
|
|
|
|
- if _, err := app.queries.BlocklistSubscribers.Exec(IDs); err != nil {
|
|
|
|
- app.log.Printf("error blocklisting subscribers: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
|
|
|
|
|
|
+ if err := app.core.BlocklistSubscribers(subIDs); err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -450,18 +330,18 @@ func handleBlocklistSubscribers(c echo.Context) error {
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
func handleManageSubscriberLists(c echo.Context) error {
|
|
func handleManageSubscriberLists(c echo.Context) error {
|
|
var (
|
|
var (
|
|
- app = c.Get("app").(*App)
|
|
|
|
- pID = c.Param("id")
|
|
|
|
- IDs pq.Int64Array
|
|
|
|
|
|
+ app = c.Get("app").(*App)
|
|
|
|
+ pID = c.Param("id")
|
|
|
|
+ subIDs []int
|
|
)
|
|
)
|
|
|
|
|
|
// Is it an /:id call?
|
|
// Is it an /:id call?
|
|
if pID != "" {
|
|
if pID != "" {
|
|
- id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
|
|
+ id, _ := strconv.Atoi(pID)
|
|
if id < 1 {
|
|
if id < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
- IDs = append(IDs, id)
|
|
|
|
|
|
+ subIDs = append(subIDs, id)
|
|
}
|
|
}
|
|
|
|
|
|
var req subQueryReq
|
|
var req subQueryReq
|
|
@@ -472,8 +352,8 @@ func handleManageSubscriberLists(c echo.Context) error {
|
|
if len(req.SubscriberIDs) == 0 {
|
|
if len(req.SubscriberIDs) == 0 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
|
|
}
|
|
}
|
|
- if len(IDs) == 0 {
|
|
|
|
- IDs = req.SubscriberIDs
|
|
|
|
|
|
+ if len(subIDs) == 0 {
|
|
|
|
+ subIDs = req.SubscriberIDs
|
|
}
|
|
}
|
|
if len(req.TargetListIDs) == 0 {
|
|
if len(req.TargetListIDs) == 0 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
|
|
@@ -483,20 +363,17 @@ func handleManageSubscriberLists(c echo.Context) error {
|
|
var err error
|
|
var err error
|
|
switch req.Action {
|
|
switch req.Action {
|
|
case "add":
|
|
case "add":
|
|
- _, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs, req.Status)
|
|
|
|
|
|
+ err = app.core.AddSubscriptions(subIDs, req.TargetListIDs, req.Status)
|
|
case "remove":
|
|
case "remove":
|
|
- _, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
|
|
|
|
|
|
+ err = app.core.DeleteSubscriptions(subIDs, req.TargetListIDs)
|
|
case "unsubscribe":
|
|
case "unsubscribe":
|
|
- _, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
|
|
|
|
|
|
+ err = app.core.UnsubscribeLists(subIDs, req.TargetListIDs)
|
|
default:
|
|
default:
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
|
}
|
|
}
|
|
|
|
|
|
if err != nil {
|
|
if err != nil {
|
|
- app.log.Printf("error updating subscriptions: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorUpdating",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", err.Error()))
|
|
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -506,18 +383,18 @@ func handleManageSubscriberLists(c echo.Context) error {
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
func handleDeleteSubscribers(c echo.Context) error {
|
|
func handleDeleteSubscribers(c echo.Context) error {
|
|
var (
|
|
var (
|
|
- app = c.Get("app").(*App)
|
|
|
|
- pID = c.Param("id")
|
|
|
|
- IDs pq.Int64Array
|
|
|
|
|
|
+ app = c.Get("app").(*App)
|
|
|
|
+ pID = c.Param("id")
|
|
|
|
+ subIDs []int
|
|
)
|
|
)
|
|
|
|
|
|
// Is it an /:id call?
|
|
// Is it an /:id call?
|
|
if pID != "" {
|
|
if pID != "" {
|
|
- id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
|
|
+ id, _ := strconv.Atoi(pID)
|
|
if id < 1 {
|
|
if id < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
- IDs = append(IDs, id)
|
|
|
|
|
|
+ subIDs = append(subIDs, id)
|
|
} else {
|
|
} else {
|
|
// Multiple IDs.
|
|
// Multiple IDs.
|
|
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
|
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
|
@@ -529,14 +406,11 @@ func handleDeleteSubscribers(c echo.Context) error {
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
|
|
app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
|
|
}
|
|
}
|
|
- IDs = i
|
|
|
|
|
|
+ subIDs = i
|
|
}
|
|
}
|
|
|
|
|
|
- if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
|
|
|
|
- app.log.Printf("error deleting subscribers: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorDeleting",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ if err := app.core.DeleteSubscribers(subIDs, nil); err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -554,14 +428,8 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
|
|
- err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
|
|
|
- app.queries.DeleteSubscribersByQuery,
|
|
|
|
- req.ListIDs, app.db)
|
|
|
|
- if err != nil {
|
|
|
|
- app.log.Printf("error deleting subscribers: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorDeleting",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ if err := app.core.DeleteSubscribersByQuery(req.Query, req.ListIDs); err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -579,13 +447,8 @@ func handleBlocklistSubscribersByQuery(c echo.Context) error {
|
|
return err
|
|
return err
|
|
}
|
|
}
|
|
|
|
|
|
- err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
|
|
|
- app.queries.BlocklistSubscribersByQuery,
|
|
|
|
- req.ListIDs, app.db)
|
|
|
|
- if err != nil {
|
|
|
|
- app.log.Printf("error blocklisting subscribers: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ if err := app.core.BlocklistSubscribersByQuery(req.Query, req.ListIDs); err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -608,25 +471,20 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
|
|
}
|
|
}
|
|
|
|
|
|
// Action.
|
|
// Action.
|
|
- var stmt string
|
|
|
|
|
|
+ var err error
|
|
switch req.Action {
|
|
switch req.Action {
|
|
case "add":
|
|
case "add":
|
|
- stmt = app.queries.AddSubscribersToListsByQuery
|
|
|
|
|
|
+ err = app.core.AddSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs)
|
|
case "remove":
|
|
case "remove":
|
|
- stmt = app.queries.DeleteSubscriptionsByQuery
|
|
|
|
|
|
+ err = app.core.DeleteSubscriptionsByQuery(req.Query, req.ListIDs, req.TargetListIDs)
|
|
case "unsubscribe":
|
|
case "unsubscribe":
|
|
- stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
|
|
|
|
|
|
+ err = app.core.UnsubscribeListsByQuery(req.Query, req.ListIDs, req.TargetListIDs)
|
|
default:
|
|
default:
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
|
}
|
|
}
|
|
|
|
|
|
- err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
|
|
|
- stmt, req.ListIDs, app.db, req.TargetListIDs)
|
|
|
|
if err != nil {
|
|
if err != nil {
|
|
- app.log.Printf("error updating subscriptions: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorUpdating",
|
|
|
|
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -639,16 +497,13 @@ func handleDeleteSubscriberBounces(c echo.Context) error {
|
|
pID = c.Param("id")
|
|
pID = c.Param("id")
|
|
)
|
|
)
|
|
|
|
|
|
- id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
|
|
+ id, _ := strconv.Atoi(pID)
|
|
if id < 1 {
|
|
if id < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
|
|
|
|
- if _, err := app.queries.DeleteBouncesBySubscriber.Exec(id, nil); err != nil {
|
|
|
|
- app.log.Printf("error deleting bounces: %v", err)
|
|
|
|
- return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorDeleting",
|
|
|
|
- "name", "{globals.terms.bounces}", "error", pqErrMsg(err)))
|
|
|
|
|
|
+ if err := app.core.DeleteSubscriberBounces(id, ""); err != nil {
|
|
|
|
+ return err
|
|
}
|
|
}
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
@@ -663,7 +518,8 @@ func handleExportSubscriberData(c echo.Context) error {
|
|
app = c.Get("app").(*App)
|
|
app = c.Get("app").(*App)
|
|
pID = c.Param("id")
|
|
pID = c.Param("id")
|
|
)
|
|
)
|
|
- id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
|
|
+
|
|
|
|
+ id, _ := strconv.Atoi(pID)
|
|
if id < 1 {
|
|
if id < 1 {
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
}
|
|
}
|
|
@@ -684,106 +540,13 @@ func handleExportSubscriberData(c echo.Context) error {
|
|
return c.Blob(http.StatusOK, "application/json", b)
|
|
return c.Blob(http.StatusOK, "application/json", b)
|
|
}
|
|
}
|
|
|
|
|
|
-// insertSubscriber inserts a subscriber and returns the ID. The first bool indicates if
|
|
|
|
-// it was a new subscriber, and the second bool indicates if the subscriber was sent an optin confirmation.
|
|
|
|
-func insertSubscriber(req subimporter.SubReq, app *App) (models.Subscriber, bool, bool, error) {
|
|
|
|
- uu, err := uuid.NewV4()
|
|
|
|
- if err != nil {
|
|
|
|
- return req.Subscriber, false, false, err
|
|
|
|
- }
|
|
|
|
- req.UUID = uu.String()
|
|
|
|
-
|
|
|
|
- var (
|
|
|
|
- isNew = true
|
|
|
|
- subStatus = models.SubscriptionStatusUnconfirmed
|
|
|
|
- )
|
|
|
|
- if req.PreconfirmSubs {
|
|
|
|
- subStatus = models.SubscriptionStatusConfirmed
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if req.Status == "" {
|
|
|
|
- req.Status = models.UserStatusEnabled
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- if err = app.queries.InsertSubscriber.Get(&req.ID,
|
|
|
|
- req.UUID,
|
|
|
|
- req.Email,
|
|
|
|
- strings.TrimSpace(req.Name),
|
|
|
|
- req.Status,
|
|
|
|
- req.Attribs,
|
|
|
|
- req.Lists,
|
|
|
|
- req.ListUUIDs,
|
|
|
|
- subStatus); err != nil {
|
|
|
|
- if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
|
|
|
|
- isNew = false
|
|
|
|
- } else {
|
|
|
|
- // return req.Subscriber, errSubscriberExists
|
|
|
|
- app.log.Printf("error inserting subscriber: %v", err)
|
|
|
|
- return req.Subscriber, false, false, echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorCreating",
|
|
|
|
- "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // Fetch the subscriber's full data. If the subscriber already existed and wasn't
|
|
|
|
- // created, the id will be empty. Fetch the details by e-mail then.
|
|
|
|
- sub, err := getSubscriber(req.ID, "", strings.ToLower(req.Email), app)
|
|
|
|
- if err != nil {
|
|
|
|
- return sub, false, false, err
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- hasOptin := false
|
|
|
|
- if !req.PreconfirmSubs && app.constants.SendOptinConfirmation {
|
|
|
|
- // Send a confirmation e-mail (if there are any double opt-in lists).
|
|
|
|
- num, _ := sendOptinConfirmation(sub, []int64(req.Lists), app)
|
|
|
|
- hasOptin = num > 0
|
|
|
|
- }
|
|
|
|
- return sub, isNew, hasOptin, nil
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-// getSubscriber gets a single subscriber by ID, uuid, or e-mail in that order.
|
|
|
|
-// Only one of these params should have a value.
|
|
|
|
-func getSubscriber(id int, uuid, email string, app *App) (models.Subscriber, error) {
|
|
|
|
- var out models.Subscribers
|
|
|
|
-
|
|
|
|
- if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
|
|
|
|
- app.log.Printf("error fetching subscriber: %v", err)
|
|
|
|
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
- if len(out) == 0 {
|
|
|
|
- return models.Subscriber{}, echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
- app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
|
|
|
|
- }
|
|
|
|
- if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
|
|
|
|
- app.log.Printf("error loading subscriber lists: %v", err)
|
|
|
|
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
- app.i18n.Ts("globals.messages.errorFetching",
|
|
|
|
- "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return out[0], nil
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
// exportSubscriberData collates the data of a subscriber including profile,
|
|
// exportSubscriberData collates the data of a subscriber including profile,
|
|
// subscriptions, campaign_views, link_clicks (if they're enabled in the config)
|
|
// subscriptions, campaign_views, link_clicks (if they're enabled in the config)
|
|
// and returns a formatted, indented JSON payload. Either takes a numeric id
|
|
// and returns a formatted, indented JSON payload. Either takes a numeric id
|
|
// and an empty subUUID or takes 0 and a string subUUID.
|
|
// and an empty subUUID or takes 0 and a string subUUID.
|
|
-func exportSubscriberData(id int64, subUUID string, exportables map[string]bool, app *App) (subProfileData, []byte, error) {
|
|
|
|
- // Get the subscriber's data. A single query that gets the profile,
|
|
|
|
- // list subscriptions, campaign views, and link clicks. Names of
|
|
|
|
- // private lists are replaced with "Private list".
|
|
|
|
- var (
|
|
|
|
- data subProfileData
|
|
|
|
- uu interface{}
|
|
|
|
- )
|
|
|
|
- // UUID should be a valid value or a nil.
|
|
|
|
- if subUUID != "" {
|
|
|
|
- uu = subUUID
|
|
|
|
- }
|
|
|
|
- if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
|
|
|
|
- app.log.Printf("error fetching subscriber export data: %v", err)
|
|
|
|
|
|
+func exportSubscriberData(id int, subUUID string, exportables map[string]bool, app *App) (models.SubscriberExportProfile, []byte, error) {
|
|
|
|
+ data, err := app.core.GetSubscriberProfileForExport(id, subUUID)
|
|
|
|
+ if err != nil {
|
|
return data, nil, err
|
|
return data, nil, err
|
|
}
|
|
}
|
|
|
|
|
|
@@ -807,46 +570,8 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
|
|
app.log.Printf("error marshalling subscriber export data: %v", err)
|
|
app.log.Printf("error marshalling subscriber export data: %v", err)
|
|
return data, nil, err
|
|
return data, nil, err
|
|
}
|
|
}
|
|
- return data, b, nil
|
|
|
|
-}
|
|
|
|
-
|
|
|
|
-// sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
|
|
|
|
-// if at least one of the given listIDs is set to optin=double. It returns the number of
|
|
|
|
-// opt-in lists that were found.
|
|
|
|
-func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) (int, error) {
|
|
|
|
- var lists []models.List
|
|
|
|
-
|
|
|
|
- // Fetch double opt-in lists from the given list IDs.
|
|
|
|
- // Get the list of subscription lists where the subscriber hasn't confirmed.
|
|
|
|
- if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
|
|
|
|
- pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
|
|
|
|
- app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
|
|
|
|
- return 0, err
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- // None.
|
|
|
|
- if len(lists) == 0 {
|
|
|
|
- return 0, nil
|
|
|
|
- }
|
|
|
|
|
|
|
|
- var (
|
|
|
|
- out = subOptin{Subscriber: &sub, Lists: lists}
|
|
|
|
- qListIDs = url.Values{}
|
|
|
|
- )
|
|
|
|
- // Construct the opt-in URL with list IDs.
|
|
|
|
- for _, l := range out.Lists {
|
|
|
|
- qListIDs.Add("l", l.UUID)
|
|
|
|
- }
|
|
|
|
- out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
|
|
|
|
- out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
|
|
|
-
|
|
|
|
- // Send the e-mail.
|
|
|
|
- if err := app.sendNotification([]string{sub.Email},
|
|
|
|
- app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
|
|
|
|
- app.log.Printf("error sending opt-in e-mail: %s", err)
|
|
|
|
- return 0, err
|
|
|
|
- }
|
|
|
|
- return len(lists), nil
|
|
|
|
|
|
+ return data, b, nil
|
|
}
|
|
}
|
|
|
|
|
|
// sanitizeSQLExp does basic sanitisation on arbitrary
|
|
// sanitizeSQLExp does basic sanitisation on arbitrary
|
|
@@ -864,8 +589,8 @@ func sanitizeSQLExp(q string) string {
|
|
return q
|
|
return q
|
|
}
|
|
}
|
|
|
|
|
|
-func getQueryInts(param string, qp url.Values) (pq.Int64Array, error) {
|
|
|
|
- out := pq.Int64Array{}
|
|
|
|
|
|
+func getQueryInts(param string, qp url.Values) ([]int, error) {
|
|
|
|
+ var out []int
|
|
if vals, ok := qp[param]; ok {
|
|
if vals, ok := qp[param]; ok {
|
|
for _, v := range vals {
|
|
for _, v := range vals {
|
|
if v == "" {
|
|
if v == "" {
|
|
@@ -876,9 +601,46 @@ func getQueryInts(param string, qp url.Values) (pq.Int64Array, error) {
|
|
if err != nil {
|
|
if err != nil {
|
|
return nil, err
|
|
return nil, err
|
|
}
|
|
}
|
|
- out = append(out, int64(listID))
|
|
|
|
|
|
+ out = append(out, listID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
return out, nil
|
|
return out, nil
|
|
}
|
|
}
|
|
|
|
+
|
|
|
|
+// sendOptinConfirmationHook returns an enclosed callback that sends optin confirmation e-mails.
|
|
|
|
+// This is plugged into the 'core' package to send optin confirmations when a new subscriber is
|
|
|
|
+// created via `core.CreateSubscriber()`.
|
|
|
|
+func sendOptinConfirmationHook(app *App) func(sub models.Subscriber, listIDs []int) (int, error) {
|
|
|
|
+ return func(sub models.Subscriber, listIDs []int) (int, error) {
|
|
|
|
+ lists, err := app.core.GetSubscriberLists(sub.ID, "", listIDs, nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble)
|
|
|
|
+ if err != nil {
|
|
|
|
+ return 0, err
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // None.
|
|
|
|
+ if len(lists) == 0 {
|
|
|
|
+ return 0, nil
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ var (
|
|
|
|
+ out = subOptin{Subscriber: sub, Lists: lists}
|
|
|
|
+ qListIDs = url.Values{}
|
|
|
|
+ )
|
|
|
|
+
|
|
|
|
+ // Construct the opt-in URL with list IDs.
|
|
|
|
+ for _, l := range out.Lists {
|
|
|
|
+ qListIDs.Add("l", l.UUID)
|
|
|
|
+ }
|
|
|
|
+ out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
|
|
|
|
+ out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
|
|
|
+
|
|
|
|
+ // Send the e-mail.
|
|
|
|
+ if err := app.sendNotification([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
|
|
|
|
+ app.log.Printf("error sending opt-in e-mail: %s", err)
|
|
|
|
+ return 0, err
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return len(lists), nil
|
|
|
|
+ }
|
|
|
|
+}
|