2018-10-25 13:51:47 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"database/sql"
|
2021-01-23 12:53:29 +00:00
|
|
|
"encoding/csv"
|
2019-07-21 13:48:41 +00:00
|
|
|
"encoding/json"
|
2021-01-31 08:08:02 +00:00
|
|
|
"errors"
|
2018-10-25 13:51:47 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2019-12-01 12:18:36 +00:00
|
|
|
"net/url"
|
2018-10-25 13:51:47 +00:00
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
|
2020-03-07 15:07:48 +00:00
|
|
|
"github.com/gofrs/uuid"
|
2020-03-07 18:47:54 +00:00
|
|
|
"github.com/knadh/listmonk/internal/subimporter"
|
2020-03-08 06:57:41 +00:00
|
|
|
"github.com/knadh/listmonk/models"
|
2021-12-09 15:21:07 +00:00
|
|
|
"github.com/labstack/echo/v4"
|
2018-10-25 13:51:47 +00:00
|
|
|
"github.com/lib/pq"
|
|
|
|
)
|
|
|
|
|
2020-03-08 09:40:51 +00:00
|
|
|
const (
|
|
|
|
dummyUUID = "00000000-0000-0000-0000-000000000000"
|
|
|
|
)
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// subQueryReq is a "catch all" struct for reading various
|
|
|
|
// subscriber related requests.
|
|
|
|
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"`
|
2022-02-04 18:33:28 +00:00
|
|
|
Status string `json:"status"`
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
|
2018-10-25 13:51:47 +00:00
|
|
|
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"`
|
|
|
|
}
|
|
|
|
|
2021-03-10 15:50:26 +00:00
|
|
|
type subUpdateReq struct {
|
|
|
|
models.Subscriber
|
2021-08-02 13:53:46 +00:00
|
|
|
RawAttribs json.RawMessage `json:"attribs"`
|
|
|
|
Lists pq.Int64Array `json:"lists"`
|
|
|
|
ListUUIDs pq.StringArray `json:"list_uuids"`
|
|
|
|
PreconfirmSubs bool `json:"preconfirm_subscriptions"`
|
2021-03-10 15:50:26 +00:00
|
|
|
}
|
|
|
|
|
2019-07-21 13:48:41 +00:00
|
|
|
// subProfileData represents a subscriber's collated data in JSON
|
|
|
|
// for export.
|
|
|
|
type subProfileData struct {
|
|
|
|
Email string `db:"email" json:"-"`
|
|
|
|
Profile json.RawMessage `db:"profile" json:"profile,omitempty"`
|
|
|
|
Subscriptions json.RawMessage `db:"subscriptions" json:"subscriptions,omitempty"`
|
|
|
|
CampaignViews json.RawMessage `db:"campaign_views" json:"campaign_views,omitempty"`
|
|
|
|
LinkClicks json.RawMessage `db:"link_clicks" json:"link_clicks,omitempty"`
|
|
|
|
}
|
|
|
|
|
2019-12-01 12:18:36 +00:00
|
|
|
// subOptin contains the data that's passed to the double opt-in e-mail template.
|
|
|
|
type subOptin struct {
|
|
|
|
*models.Subscriber
|
|
|
|
|
|
|
|
OptinURL string
|
2022-01-15 11:11:48 +00:00
|
|
|
UnsubURL string
|
2019-12-01 12:18:36 +00:00
|
|
|
Lists []models.List
|
|
|
|
}
|
|
|
|
|
2020-10-24 14:30:29 +00:00
|
|
|
var (
|
|
|
|
dummySubscriber = models.Subscriber{
|
2021-09-26 14:43:12 +00:00
|
|
|
Email: "demo@listmonk.app",
|
|
|
|
Name: "Demo Subscriber",
|
|
|
|
UUID: dummyUUID,
|
|
|
|
Attribs: models.SubscriberAttribs{"city": "Bengaluru"},
|
2020-10-24 14:30:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
subQuerySortFields = []string{"email", "name", "created_at", "updated_at"}
|
2021-01-31 08:08:02 +00:00
|
|
|
|
|
|
|
errSubscriberExists = errors.New("subscriber already exists")
|
2020-10-24 14:30:29 +00:00
|
|
|
)
|
2018-10-25 13:51:47 +00:00
|
|
|
|
|
|
|
// handleGetSubscriber handles the retrieval of a single subscriber by ID.
|
|
|
|
func handleGetSubscriber(c echo.Context) error {
|
|
|
|
var (
|
2018-10-29 09:50:49 +00:00
|
|
|
app = c.Get("app").(*App)
|
2018-10-25 13:51:47 +00:00
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
)
|
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
if id < 1 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
|
|
|
|
sub, err := getSubscriber(id, "", "", app)
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-06-24 15:00:44 +00:00
|
|
|
return err
|
2019-10-25 05:41:47 +00:00
|
|
|
}
|
2018-10-25 13:51:47 +00:00
|
|
|
|
2020-07-09 17:59:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{sub})
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// handleQuerySubscribers handles querying subscribers based on an arbitrary SQL expression.
|
2018-10-25 13:51:47 +00:00
|
|
|
func handleQuerySubscribers(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
2021-06-05 06:31:33 +00:00
|
|
|
pg = getPagination(c.QueryParams(), 30)
|
2018-10-25 13:51:47 +00:00
|
|
|
|
|
|
|
// The "WHERE ?" bit.
|
2020-10-24 14:30:29 +00:00
|
|
|
query = sanitizeSQLExp(c.FormValue("query"))
|
|
|
|
orderBy = c.FormValue("order_by")
|
|
|
|
order = c.FormValue("order")
|
2021-09-18 10:16:22 +00:00
|
|
|
out = subsWrap{Results: make([]models.Subscriber, 0, 1)}
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
|
|
|
|
2021-11-29 15:08:57 +00:00
|
|
|
// Limit the subscribers to sepcific lists?
|
2022-03-19 08:14:23 +00:00
|
|
|
listIDs, err := getQueryInts("list_id", c.QueryParams())
|
2021-11-29 15:08:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-10-24 14:30:29 +00:00
|
|
|
// There's an arbitrary query condition.
|
|
|
|
cond := ""
|
2018-10-25 13:51:47 +00:00
|
|
|
if query != "" {
|
|
|
|
cond = " AND " + query
|
|
|
|
}
|
|
|
|
|
2020-10-24 14:30:29 +00:00
|
|
|
// Sort params.
|
|
|
|
if !strSliceContains(orderBy, subQuerySortFields) {
|
2021-09-18 10:16:22 +00:00
|
|
|
orderBy = "subscribers.id"
|
2020-10-24 14:30:29 +00:00
|
|
|
}
|
|
|
|
if order != sortAsc && order != sortDesc {
|
2021-09-18 10:16:22 +00:00
|
|
|
order = sortDesc
|
2020-10-24 14:30:29 +00:00
|
|
|
}
|
|
|
|
|
2021-09-18 10:16:22 +00:00
|
|
|
// 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)
|
2020-03-07 18:33:22 +00:00
|
|
|
tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error preparing subscriber query: %v", err)
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2019-12-05 15:57:31 +00:00
|
|
|
defer tx.Rollback()
|
2018-10-25 13:51:47 +00:00
|
|
|
|
2021-09-18 10:16:22 +00:00
|
|
|
// 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)
|
2020-07-05 13:53:45 +00:00
|
|
|
if err := tx.Select(&out.Results, stmt, listIDs, pg.Offset, pg.Limit); err != nil {
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Lazy load lists for each subscriber.
|
2020-03-07 18:33:22 +00:00
|
|
|
if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
|
|
|
|
app.log.Printf("error fetching subscriber lists: %v", err)
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-11-30 08:19:13 +00:00
|
|
|
out.Query = query
|
2018-10-25 13:51:47 +00:00
|
|
|
if len(out.Results) == 0 {
|
|
|
|
out.Results = make(models.Subscribers, 0)
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
|
|
|
// Meta.
|
2021-09-18 10:16:22 +00:00
|
|
|
out.Total = total
|
2018-10-25 13:51:47 +00:00
|
|
|
out.Page = pg.Page
|
|
|
|
out.PerPage = pg.PerPage
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{out})
|
|
|
|
}
|
|
|
|
|
2021-01-23 12:53:29 +00:00
|
|
|
// handleExportSubscribers handles querying subscribers based on an arbitrary SQL expression.
|
|
|
|
func handleExportSubscribers(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
|
|
|
|
// The "WHERE ?" bit.
|
|
|
|
query = sanitizeSQLExp(c.FormValue("query"))
|
|
|
|
)
|
|
|
|
|
2021-11-29 15:08:57 +00:00
|
|
|
// Limit the subscribers to sepcific lists?
|
2022-03-19 08:14:23 +00:00
|
|
|
listIDs, err := getQueryInts("list_id", c.QueryParams())
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
|
|
|
}
|
|
|
|
|
|
|
|
// Export only specific subscriber IDs?
|
|
|
|
subIDs, err := getQueryInts("id", c.QueryParams())
|
2021-11-29 15:08:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2021-01-23 12:53:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
2021-01-23 12:53:29 +00:00
|
|
|
}
|
|
|
|
defer tx.Rollback()
|
|
|
|
|
2022-03-19 08:14:23 +00:00
|
|
|
if _, err := tx.Query(stmt, nil, 0, nil, 1); err != nil {
|
2021-01-23 12:53:29 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
2021-01-23 12:53:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare the actual query statement.
|
|
|
|
tx, err := db.Preparex(stmt)
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
|
2021-01-23 12:53:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Run the query until all rows are exhausted.
|
|
|
|
var (
|
|
|
|
id = 0
|
|
|
|
|
|
|
|
h = c.Response().Header()
|
|
|
|
wr = csv.NewWriter(c.Response())
|
|
|
|
)
|
|
|
|
|
|
|
|
h.Set(echo.HeaderContentType, echo.MIMEOctetStream)
|
|
|
|
h.Set("Content-type", "text/csv")
|
|
|
|
h.Set(echo.HeaderContentDisposition, "attachment; filename="+"subscribers.csv")
|
|
|
|
h.Set("Content-Transfer-Encoding", "binary")
|
|
|
|
h.Set("Cache-Control", "no-cache")
|
|
|
|
wr.Write([]string{"uuid", "email", "name", "attributes", "status", "created_at", "updated_at"})
|
|
|
|
|
|
|
|
loop:
|
|
|
|
for {
|
|
|
|
var out []models.SubscriberExport
|
2022-03-19 08:14:23 +00:00
|
|
|
if err := tx.Select(&out, listIDs, id, subIDs, app.constants.DBBatchSize); err != nil {
|
2021-01-23 12:53:29 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2021-01-23 12:53:29 +00:00
|
|
|
}
|
|
|
|
if len(out) == 0 {
|
|
|
|
break loop
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, r := range out {
|
|
|
|
if err = wr.Write([]string{r.UUID, r.Email, r.Name, r.Attribs, r.Status,
|
|
|
|
r.CreatedAt.Time.String(), r.UpdatedAt.Time.String()}); err != nil {
|
|
|
|
app.log.Printf("error streaming CSV export: %v", err)
|
|
|
|
break loop
|
|
|
|
}
|
|
|
|
}
|
|
|
|
wr.Flush()
|
|
|
|
|
|
|
|
id = out[len(out)-1].ID
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// handleCreateSubscriber handles the creation of a new subscriber.
|
2018-10-25 13:51:47 +00:00
|
|
|
func handleCreateSubscriber(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
req subimporter.SubReq
|
|
|
|
)
|
|
|
|
|
|
|
|
// Get and validate fields.
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return err
|
2020-03-07 14:49:22 +00:00
|
|
|
}
|
2021-09-25 07:27:55 +00:00
|
|
|
|
|
|
|
r, err := app.importer.ValidateFields(req)
|
|
|
|
if err != nil {
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
2021-09-25 07:27:55 +00:00
|
|
|
} else {
|
|
|
|
req = r
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 14:49:22 +00:00
|
|
|
// Insert the subscriber into the DB.
|
2021-02-13 12:25:10 +00:00
|
|
|
sub, isNew, _, err := insertSubscriber(req, app)
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-03-07 14:49:22 +00:00
|
|
|
return err
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2021-02-13 12:25:10 +00:00
|
|
|
if !isNew {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.emailExists"))
|
|
|
|
}
|
2018-10-25 13:51:47 +00:00
|
|
|
|
2020-06-24 15:00:44 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{sub})
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// handleUpdateSubscriber handles modification of a subscriber.
|
2018-10-25 13:51:47 +00:00
|
|
|
func handleUpdateSubscriber(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
id, _ = strconv.ParseInt(c.Param("id"), 10, 64)
|
2021-03-10 15:50:26 +00:00
|
|
|
req subUpdateReq
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
|
|
|
// Get and validate fields.
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2021-09-25 07:27:55 +00:00
|
|
|
|
|
|
|
if em, err := app.importer.SanitizeEmail(req.Email); err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
|
|
|
} else {
|
|
|
|
req.Email = em
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2021-09-25 07:27:55 +00:00
|
|
|
|
2020-03-08 07:33:38 +00:00
|
|
|
if req.Name != "" && !strHasLen(req.Name, 1, stdInputMaxLen) {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidName"))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2021-03-10 15:50:26 +00:00
|
|
|
// 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()))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-02 13:53:46 +00:00
|
|
|
subStatus := models.SubscriptionStatusUnconfirmed
|
|
|
|
if req.PreconfirmSubs {
|
|
|
|
subStatus = models.SubscriptionStatusConfirmed
|
|
|
|
}
|
|
|
|
|
2021-03-09 12:24:07 +00:00
|
|
|
_, err := app.queries.UpdateSubscriber.Exec(id,
|
2018-11-01 16:29:38 +00:00
|
|
|
strings.ToLower(strings.TrimSpace(req.Email)),
|
|
|
|
strings.TrimSpace(req.Name),
|
2018-10-25 13:51:47 +00:00
|
|
|
req.Status,
|
2021-03-10 15:50:26 +00:00
|
|
|
req.RawAttribs,
|
2021-08-02 13:53:46 +00:00
|
|
|
req.Lists,
|
|
|
|
subStatus)
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error updating subscriber: %v", err)
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorUpdating",
|
2020-12-19 10:55:52 +00:00
|
|
|
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-06-24 15:00:44 +00:00
|
|
|
// Send a confirmation e-mail (if there are any double opt-in lists).
|
2021-02-13 12:25:10 +00:00
|
|
|
sub, err := getSubscriber(int(id), "", "", app)
|
2020-06-24 15:00:44 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2021-08-02 13:53:46 +00:00
|
|
|
|
2020-07-09 17:59:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{sub})
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-02-09 11:47:58 +00:00
|
|
|
// handleGetSubscriberSendOptin sends an optin confirmation e-mail to a subscriber.
|
2020-06-24 15:00:44 +00:00
|
|
|
func handleSubscriberSendOptin(c echo.Context) error {
|
2020-02-09 11:47:58 +00:00
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
id, _ = strconv.Atoi(c.Param("id"))
|
|
|
|
)
|
|
|
|
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2020-02-09 11:47:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch the subscriber.
|
2021-02-15 12:57:14 +00:00
|
|
|
out, err := getSubscriber(id, "", "", app)
|
2020-02-09 11:47:58 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error fetching subscriber: %v", err)
|
2020-02-09 11:47:58 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2020-02-09 11:47:58 +00:00
|
|
|
}
|
|
|
|
|
2021-02-15 12:57:14 +00:00
|
|
|
if _, err := sendOptinConfirmation(out, nil, app); err != nil {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
|
|
|
app.i18n.T("subscribers.errorSendingOptin"))
|
2020-02-09 11:47:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
2020-08-01 11:15:29 +00:00
|
|
|
// handleBlocklistSubscribers handles the blocklisting of one or more subscribers.
|
2018-12-18 05:24:55 +00:00
|
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
2020-08-01 11:15:29 +00:00
|
|
|
func handleBlocklistSubscribers(c echo.Context) error {
|
2018-10-25 13:51:47 +00:00
|
|
|
var (
|
2018-12-18 05:24:55 +00:00
|
|
|
app = c.Get("app").(*App)
|
|
|
|
pID = c.Param("id")
|
|
|
|
IDs pq.Int64Array
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// Is it a /:id call?
|
|
|
|
if pID != "" {
|
|
|
|
id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
IDs = append(IDs, id)
|
|
|
|
} else {
|
|
|
|
// Multiple IDs.
|
|
|
|
var req subQueryReq
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-09-11 07:27:55 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
if len(req.SubscriberIDs) == 0 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
"No IDs given.")
|
|
|
|
}
|
|
|
|
IDs = req.SubscriberIDs
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-08-01 11:15:29 +00:00
|
|
|
if _, err := app.queries.BlocklistSubscribers.Exec(IDs); err != nil {
|
|
|
|
app.log.Printf("error blocklisting subscribers: %v", err)
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// handleManageSubscriberLists handles bulk addition or removal of subscribers
|
|
|
|
// from or to one or more target lists.
|
|
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
|
|
func handleManageSubscriberLists(c echo.Context) error {
|
2018-10-25 13:51:47 +00:00
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
2018-12-18 05:24:55 +00:00
|
|
|
pID = c.Param("id")
|
|
|
|
IDs pq.Int64Array
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
|
|
|
|
2022-03-20 05:32:43 +00:00
|
|
|
// Is it an /:id call?
|
2018-12-18 05:24:55 +00:00
|
|
|
if pID != "" {
|
|
|
|
id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
IDs = append(IDs, id)
|
|
|
|
}
|
|
|
|
|
|
|
|
var req subQueryReq
|
2018-10-25 13:51:47 +00:00
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-09-11 07:27:55 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2018-12-18 05:24:55 +00:00
|
|
|
if len(req.SubscriberIDs) == 0 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoIDs"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
if len(IDs) == 0 {
|
|
|
|
IDs = req.SubscriberIDs
|
|
|
|
}
|
|
|
|
if len(req.TargetListIDs) == 0 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.errorNoListsGiven"))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
// Action.
|
|
|
|
var err error
|
|
|
|
switch req.Action {
|
|
|
|
case "add":
|
2022-02-04 18:33:28 +00:00
|
|
|
_, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs, req.Status)
|
2018-12-18 05:24:55 +00:00
|
|
|
case "remove":
|
2020-03-07 18:33:22 +00:00
|
|
|
_, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
|
2018-12-18 05:24:55 +00:00
|
|
|
case "unsubscribe":
|
2020-03-07 18:33:22 +00:00
|
|
|
_, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
|
2018-12-18 05:24:55 +00:00
|
|
|
default:
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error updating subscriptions: %v", err)
|
2018-12-18 05:24:55 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorUpdating",
|
2020-12-19 10:55:52 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", err.Error()))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleDeleteSubscribers handles subscriber deletion.
|
|
|
|
// It takes either an ID in the URI, or a list of IDs in the request body.
|
|
|
|
func handleDeleteSubscribers(c echo.Context) error {
|
2018-10-25 13:51:47 +00:00
|
|
|
var (
|
2018-12-18 05:24:55 +00:00
|
|
|
app = c.Get("app").(*App)
|
|
|
|
pID = c.Param("id")
|
|
|
|
IDs pq.Int64Array
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
2018-12-18 05:24:55 +00:00
|
|
|
|
|
|
|
// Is it an /:id call?
|
|
|
|
if pID != "" {
|
|
|
|
id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
IDs = append(IDs, id)
|
2018-10-25 13:51:47 +00:00
|
|
|
} else {
|
2018-12-18 05:24:55 +00:00
|
|
|
// Multiple IDs.
|
|
|
|
i, err := parseStringIDs(c.Request().URL.Query()["id"])
|
|
|
|
if err != nil {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-09-11 07:27:55 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
if len(i) == 0 {
|
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorNoIDs", "error", err.Error()))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
IDs = i
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 18:33:22 +00:00
|
|
|
if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
|
|
|
|
app.log.Printf("error deleting subscribers: %v", err)
|
2018-10-25 13:51:47 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorDeleting",
|
2020-12-19 10:55:52 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleDeleteSubscribersByQuery bulk deletes based on an
|
|
|
|
// arbitrary SQL expression.
|
|
|
|
func handleDeleteSubscribersByQuery(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
req subQueryReq
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return err
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2018-12-18 05:24:55 +00:00
|
|
|
|
2020-03-07 18:33:22 +00:00
|
|
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
|
|
|
app.queries.DeleteSubscribersByQuery,
|
|
|
|
req.ListIDs, app.db)
|
2018-12-18 05:24:55 +00:00
|
|
|
if err != nil {
|
2020-12-19 10:55:52 +00:00
|
|
|
app.log.Printf("error deleting subscribers: %v", err)
|
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorDeleting",
|
2020-12-19 10:55:52 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
2020-08-01 11:15:29 +00:00
|
|
|
// handleBlocklistSubscribersByQuery bulk blocklists subscribers
|
2018-12-18 05:24:55 +00:00
|
|
|
// based on an arbitrary SQL expression.
|
2020-08-01 11:15:29 +00:00
|
|
|
func handleBlocklistSubscribersByQuery(c echo.Context) error {
|
2018-12-18 05:24:55 +00:00
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
req subQueryReq
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return err
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 18:33:22 +00:00
|
|
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
2020-08-01 11:15:29 +00:00
|
|
|
app.queries.BlocklistSubscribersByQuery,
|
2020-03-07 18:33:22 +00:00
|
|
|
req.ListIDs, app.db)
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-08-01 11:15:29 +00:00
|
|
|
app.log.Printf("error blocklisting subscribers: %v", err)
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
2020-08-01 11:15:29 +00:00
|
|
|
// handleManageSubscriberListsByQuery bulk adds/removes/unsubscribers subscribers
|
2018-12-18 05:24:55 +00:00
|
|
|
// from one or more lists based on an arbitrary SQL expression.
|
|
|
|
func handleManageSubscriberListsByQuery(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
req subQueryReq
|
|
|
|
)
|
|
|
|
|
|
|
|
if err := c.Bind(&req); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if len(req.TargetListIDs) == 0 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest,
|
|
|
|
app.i18n.T("subscribers.errorNoListsGiven"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Action.
|
|
|
|
var stmt string
|
|
|
|
switch req.Action {
|
|
|
|
case "add":
|
2020-03-07 18:33:22 +00:00
|
|
|
stmt = app.queries.AddSubscribersToListsByQuery
|
2018-12-18 05:24:55 +00:00
|
|
|
case "remove":
|
2020-03-07 18:33:22 +00:00
|
|
|
stmt = app.queries.DeleteSubscriptionsByQuery
|
2018-12-18 05:24:55 +00:00
|
|
|
case "unsubscribe":
|
2020-03-07 18:33:22 +00:00
|
|
|
stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
|
2018-12-18 05:24:55 +00:00
|
|
|
default:
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("subscribers.invalidAction"))
|
2018-12-18 05:24:55 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 18:33:22 +00:00
|
|
|
err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
|
|
|
|
stmt, req.ListIDs, app.db, req.TargetListIDs)
|
2018-10-25 13:51:47 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error updating subscriptions: %v", err)
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorUpdating",
|
2020-12-19 10:55:52 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2021-05-24 17:11:48 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
|
|
|
}
|
|
|
|
|
|
|
|
// handleDeleteSubscriberBounces deletes all the bounces on a subscriber.
|
|
|
|
func handleDeleteSubscriberBounces(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
pID = c.Param("id")
|
|
|
|
)
|
|
|
|
|
|
|
|
id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
if id < 1 {
|
|
|
|
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)))
|
|
|
|
}
|
|
|
|
|
2018-12-18 05:24:55 +00:00
|
|
|
return c.JSON(http.StatusOK, okResp{true})
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2019-07-04 12:10:55 +00:00
|
|
|
|
2019-07-21 13:48:41 +00:00
|
|
|
// handleExportSubscriberData pulls the subscriber's profile,
|
|
|
|
// list subscriptions, campaign views and clicks and produces
|
|
|
|
// a JSON report. This is a privacy feature and depends on the
|
|
|
|
// configuration in app.Constants.Privacy.
|
|
|
|
func handleExportSubscriberData(c echo.Context) error {
|
|
|
|
var (
|
|
|
|
app = c.Get("app").(*App)
|
|
|
|
pID = c.Param("id")
|
|
|
|
)
|
|
|
|
id, _ := strconv.ParseInt(pID, 10, 64)
|
|
|
|
if id < 1 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
|
2019-07-21 13:48:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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".
|
2020-03-07 18:33:22 +00:00
|
|
|
_, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app)
|
2019-07-21 13:48:41 +00:00
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error exporting subscriber data: %s", err)
|
2020-12-19 10:55:52 +00:00
|
|
|
return echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscribers}", "error", err.Error()))
|
2019-07-21 13:48:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
c.Response().Header().Set("Cache-Control", "no-cache")
|
2020-08-01 12:24:51 +00:00
|
|
|
c.Response().Header().Set("Content-Disposition", `attachment; filename="data.json"`)
|
2019-07-21 13:48:41 +00:00
|
|
|
return c.Blob(http.StatusOK, "application/json", b)
|
|
|
|
}
|
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
// 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) {
|
2020-03-07 15:07:48 +00:00
|
|
|
uu, err := uuid.NewV4()
|
|
|
|
if err != nil {
|
2021-02-13 12:25:10 +00:00
|
|
|
return req.Subscriber, false, false, err
|
2020-03-07 15:07:48 +00:00
|
|
|
}
|
|
|
|
req.UUID = uu.String()
|
|
|
|
|
2021-04-17 08:04:37 +00:00
|
|
|
var (
|
|
|
|
isNew = true
|
|
|
|
subStatus = models.SubscriptionStatusUnconfirmed
|
|
|
|
)
|
|
|
|
if req.PreconfirmSubs {
|
|
|
|
subStatus = models.SubscriptionStatusConfirmed
|
|
|
|
}
|
|
|
|
|
2021-09-26 11:13:10 +00:00
|
|
|
if req.Status == "" {
|
|
|
|
req.Status = models.UserStatusEnabled
|
|
|
|
}
|
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
if err = app.queries.InsertSubscriber.Get(&req.ID,
|
2020-03-07 14:49:22 +00:00
|
|
|
req.UUID,
|
|
|
|
req.Email,
|
|
|
|
strings.TrimSpace(req.Name),
|
|
|
|
req.Status,
|
|
|
|
req.Attribs,
|
|
|
|
req.Lists,
|
2021-04-17 08:04:37 +00:00
|
|
|
req.ListUUIDs,
|
|
|
|
subStatus); err != nil {
|
2020-03-07 14:49:22 +00:00
|
|
|
if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
|
2021-02-13 12:25:10 +00:00
|
|
|
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)))
|
2020-03-07 14:49:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
// 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)
|
2020-06-24 15:00:44 +00:00
|
|
|
if err != nil {
|
2021-02-13 12:25:10 +00:00
|
|
|
return sub, false, false, err
|
2020-06-24 15:00:44 +00:00
|
|
|
}
|
|
|
|
|
2021-04-17 08:04:37 +00:00
|
|
|
hasOptin := false
|
2021-09-25 05:08:13 +00:00
|
|
|
if !req.PreconfirmSubs && app.constants.SendOptinConfirmation {
|
2021-04-17 08:04:37 +00:00
|
|
|
// 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
|
2020-06-24 15:00:44 +00:00
|
|
|
}
|
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
// 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
|
2020-06-24 15:00:44 +00:00
|
|
|
|
2021-02-13 12:25:10 +00:00
|
|
|
if err := app.queries.GetSubscriber.Select(&out, id, uuid, email); err != nil {
|
2020-06-24 15:00:44 +00:00
|
|
|
app.log.Printf("error fetching subscriber: %v", err)
|
|
|
|
return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
|
2020-06-24 15:00:44 +00:00
|
|
|
}
|
|
|
|
if len(out) == 0 {
|
2020-12-19 10:55:52 +00:00
|
|
|
return models.Subscriber{}, echo.NewHTTPError(http.StatusBadRequest,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
|
2020-06-24 15:00:44 +00:00
|
|
|
}
|
|
|
|
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,
|
2021-01-23 13:24:33 +00:00
|
|
|
app.i18n.Ts("globals.messages.errorFetching",
|
2021-01-23 13:18:10 +00:00
|
|
|
"name", "{globals.terms.lists}", "error", pqErrMsg(err)))
|
2020-06-24 15:00:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return out[0], nil
|
2020-03-07 14:49:22 +00:00
|
|
|
}
|
|
|
|
|
2019-07-21 13:48:41 +00:00
|
|
|
// exportSubscriberData collates the data of a subscriber including profile,
|
|
|
|
// 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 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
|
|
|
|
}
|
2020-03-07 18:33:22 +00:00
|
|
|
if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
|
|
|
|
app.log.Printf("error fetching subscriber export data: %v", err)
|
2019-07-21 13:48:41 +00:00
|
|
|
return data, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Filter out the non-exportable items.
|
|
|
|
if _, ok := exportables["profile"]; !ok {
|
|
|
|
data.Profile = nil
|
|
|
|
}
|
|
|
|
if _, ok := exportables["subscriptions"]; !ok {
|
|
|
|
data.Subscriptions = nil
|
|
|
|
}
|
|
|
|
if _, ok := exportables["campaign_views"]; !ok {
|
|
|
|
data.CampaignViews = nil
|
|
|
|
}
|
|
|
|
if _, ok := exportables["link_clicks"]; !ok {
|
|
|
|
data.LinkClicks = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Marshal the data into an indented payload.
|
|
|
|
b, err := json.MarshalIndent(data, "", " ")
|
|
|
|
if err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error marshalling subscriber export data: %v", err)
|
2019-07-21 13:48:41 +00:00
|
|
|
return data, nil, err
|
|
|
|
}
|
|
|
|
return data, b, nil
|
|
|
|
}
|
|
|
|
|
2020-06-24 15:00:44 +00:00
|
|
|
// sendOptinConfirmation sends a double opt-in confirmation e-mail to a subscriber
|
2021-02-13 12:25:10 +00:00
|
|
|
// 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) {
|
2019-12-01 12:18:36 +00:00
|
|
|
var lists []models.List
|
|
|
|
|
|
|
|
// Fetch double opt-in lists from the given list IDs.
|
2020-02-09 10:55:19 +00:00
|
|
|
// Get the list of subscription lists where the subscriber hasn't confirmed.
|
2020-03-07 18:33:22 +00:00
|
|
|
if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
|
2020-05-23 07:24:25 +00:00
|
|
|
pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
|
2021-02-13 12:25:10 +00:00
|
|
|
return 0, err
|
2019-12-01 12:18:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// None.
|
|
|
|
if len(lists) == 0 {
|
2021-02-13 12:25:10 +00:00
|
|
|
return 0, nil
|
2019-12-01 12:18:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2020-03-07 18:33:22 +00:00
|
|
|
out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
|
2022-01-15 11:11:48 +00:00
|
|
|
out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
|
2019-12-01 12:18:36 +00:00
|
|
|
|
|
|
|
// Send the e-mail.
|
2020-03-08 06:57:41 +00:00
|
|
|
if err := app.sendNotification([]string{sub.Email},
|
2020-12-19 10:55:52 +00:00
|
|
|
app.i18n.T("subscribers.optinSubject"), notifSubscriberOptin, out); err != nil {
|
|
|
|
app.log.Printf("error sending opt-in e-mail: %s", err)
|
2021-02-13 12:25:10 +00:00
|
|
|
return 0, err
|
2019-12-01 12:18:36 +00:00
|
|
|
}
|
2021-02-13 12:25:10 +00:00
|
|
|
return len(lists), nil
|
2019-12-01 12:18:36 +00:00
|
|
|
}
|
|
|
|
|
2019-07-04 12:10:55 +00:00
|
|
|
// sanitizeSQLExp does basic sanitisation on arbitrary
|
|
|
|
// SQL query expressions coming from the frontend.
|
|
|
|
func sanitizeSQLExp(q string) string {
|
|
|
|
if len(q) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
q = strings.TrimSpace(q)
|
|
|
|
|
|
|
|
// Remove semicolon suffix.
|
|
|
|
if q[len(q)-1] == ';' {
|
|
|
|
q = q[:len(q)-1]
|
|
|
|
}
|
|
|
|
return q
|
|
|
|
}
|
2021-11-29 15:08:57 +00:00
|
|
|
|
2022-03-19 08:14:23 +00:00
|
|
|
func getQueryInts(param string, qp url.Values) (pq.Int64Array, error) {
|
2021-11-29 15:08:57 +00:00
|
|
|
out := pq.Int64Array{}
|
2022-03-19 08:14:23 +00:00
|
|
|
if vals, ok := qp[param]; ok {
|
2021-11-29 15:08:57 +00:00
|
|
|
for _, v := range vals {
|
|
|
|
if v == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
listID, err := strconv.Atoi(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
out = append(out, int64(listID))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return out, nil
|
|
|
|
}
|