123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- package core
- import (
- "context"
- "database/sql"
- "encoding/json"
- "fmt"
- "net/http"
- "strings"
- "github.com/gofrs/uuid"
- "github.com/knadh/listmonk/models"
- "github.com/labstack/echo/v4"
- "github.com/lib/pq"
- )
- var (
- subQuerySortFields = []string{"email", "name", "created_at", "updated_at"}
- )
- // GetSubscriber fetches a subscriber by one of the given params.
- func (c *Core) GetSubscriber(id int, uuid, email string) (models.Subscriber, error) {
- var uu interface{}
- if uuid != "" {
- uu = uuid
- }
- var out models.Subscribers
- if err := c.q.GetSubscriber.Select(&out, id, uu, email); err != nil {
- c.log.Printf("error fetching subscriber: %v", err)
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
- }
- if len(out) == 0 {
- return models.Subscriber{}, echo.NewHTTPError(http.StatusBadRequest,
- c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.subscriber}"))
- }
- if err := out.LoadLists(c.q.GetSubscriberListsLazy); err != nil {
- c.log.Printf("error loading subscriber lists: %v", err)
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
- }
- return out[0], nil
- }
- // GetSubscribersByEmail fetches a subscriber by one of the given params.
- func (c *Core) GetSubscribersByEmail(emails []string) (models.Subscribers, error) {
- var out models.Subscribers
- if err := c.q.GetSubscribersByEmails.Select(&out, pq.Array(emails)); err != nil {
- c.log.Printf("error fetching subscriber: %v", err)
- return nil, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
- }
- if len(out) == 0 {
- return nil, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("campaigns.noKnownSubsToTest"))
- }
- if err := out.LoadLists(c.q.GetSubscriberListsLazy); err != nil {
- c.log.Printf("error loading subscriber lists: %v", err)
- return nil, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
- }
- return out, nil
- }
- // QuerySubscribers queries and returns paginated subscrribers based on the given params including the total count.
- func (c *Core) QuerySubscribers(query string, listIDs []int, order, orderBy string, offset, limit int) (models.Subscribers, int, error) {
- // 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
- }
- // Required for pq.Array()
- if listIDs == nil {
- listIDs = []int{}
- }
- // 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(c.q.QuerySubscribersCount, cond)
- tx, err := c.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
- if err != nil {
- c.log.Printf("error preparing subscriber query: %v", err)
- return nil, 0, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
- }
- defer tx.Rollback()
- // Execute the readonly query and get the count of results.
- total := 0
- if err := tx.Get(&total, stmt, pq.Array(listIDs)); err != nil {
- return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- // No results.
- if total == 0 {
- return models.Subscribers{}, 0, nil
- }
- // Run the query again and fetch the actual data. stmt is the raw SQL query.
- var out models.Subscribers
- stmt = strings.ReplaceAll(c.q.QuerySubscribers, "%query%", cond)
- stmt = strings.ReplaceAll(stmt, "%order%", orderBy+" "+order)
- if err := tx.Select(&out, stmt, pq.Array(listIDs), offset, limit); err != nil {
- return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- // Lazy load lists for each subscriber.
- if err := out.LoadLists(c.q.GetSubscriberListsLazy); err != nil {
- c.log.Printf("error fetching subscriber lists: %v", err)
- return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- return out, total, nil
- }
- // GetSubscriberLists returns a subscriber's lists based on the given conditions.
- func (c *Core) GetSubscriberLists(subID int, uuid string, listIDs []int, listUUIDs []string, subStatus string, listType string) ([]models.List, error) {
- if listIDs == nil {
- listIDs = []int{}
- }
- if listUUIDs == nil {
- listUUIDs = []string{}
- }
- var uu interface{}
- if uuid != "" {
- uu = uuid
- }
- // Fetch double opt-in lists from the given list IDs.
- // Get the list of subscription lists where the subscriber hasn't confirmed.
- out := []models.List{}
- if err := c.q.GetSubscriberLists.Select(&out, subID, uu, pq.Array(listIDs), pq.Array(listUUIDs), subStatus, listType); err != nil {
- c.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
- return nil, err
- }
- return out, nil
- }
- // GetSubscriberProfileForExport returns the subscriber's profile data as a JSON exportable.
- // 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".
- func (c *Core) GetSubscriberProfileForExport(id int, uuid string) (models.SubscriberExportProfile, error) {
- var uu interface{}
- if uuid != "" {
- uu = uuid
- }
- var out models.SubscriberExportProfile
- if err := c.q.ExportSubscriberData.Get(&out, id, uu); err != nil {
- c.log.Printf("error fetching subscriber export data: %v", err)
- return models.SubscriberExportProfile{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", err.Error()))
- }
- return out, nil
- }
- // ExportSubscribers returns an iterator function that provides lists of subscribers based
- // on the given criteria in an exportable form. The iterator function returned can be called
- // repeatedly until there are nil subscribers. It's an iterator because exports can be extremely
- // large and may have to be fetched in batches from the DB and streamed somewhere.
- func (c *Core) ExportSubscribers(query string, subIDs, listIDs []int, batchSize int) (func() ([]models.SubscriberExport, error), error) {
- // There's an arbitrary query condition.
- cond := ""
- if query != "" {
- cond = " AND " + query
- }
- stmt := fmt.Sprintf(c.q.QuerySubscribersForExport, cond)
- stmt = strings.ReplaceAll(c.q.QuerySubscribersForExport, "%query%", cond)
- // Verify that the arbitrary SQL search expression is read only.
- if cond != "" {
- tx, err := c.db.Unsafe().BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
- if err != nil {
- c.log.Printf("error preparing subscriber query: %v", err)
- return nil, echo.NewHTTPError(http.StatusBadRequest,
- c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
- }
- defer tx.Rollback()
- if _, err := tx.Query(stmt, nil, 0, nil, 1); err != nil {
- return nil, echo.NewHTTPError(http.StatusBadRequest,
- c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
- }
- }
- if subIDs == nil {
- subIDs = []int{}
- }
- if listIDs == nil {
- listIDs = []int{}
- }
- // Prepare the actual query statement.
- tx, err := c.db.Preparex(stmt)
- if err != nil {
- c.log.Printf("error preparing subscriber query: %v", err)
- return nil, echo.NewHTTPError(http.StatusBadRequest,
- c.i18n.Ts("subscribers.errorPreparingQuery", "error", pqErrMsg(err)))
- }
- id := 0
- return func() ([]models.SubscriberExport, error) {
- var out []models.SubscriberExport
- if err := tx.Select(&out, pq.Array(listIDs), id, pq.Array(subIDs), batchSize); err != nil {
- c.log.Printf("error exporting subscribers by query: %v", err)
- return nil, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- if len(out) == 0 {
- return nil, nil
- }
- id = out[len(out)-1].ID
- return out, nil
- }, nil
- }
- // 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.
- // bool = optinSent?
- func (c *Core) InsertSubscriber(sub models.Subscriber, listIDs []int, listUUIDs []string, preconfirm bool) (models.Subscriber, bool, error) {
- uu, err := uuid.NewV4()
- if err != nil {
- c.log.Printf("error generating UUID: %v", err)
- return models.Subscriber{}, false, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUUID", "error", err.Error()))
- }
- sub.UUID = uu.String()
- subStatus := models.SubscriptionStatusUnconfirmed
- if preconfirm {
- subStatus = models.SubscriptionStatusConfirmed
- }
- if sub.Status == "" {
- sub.Status = models.UserStatusEnabled
- }
- // For pq.Array()
- if listIDs == nil {
- listIDs = []int{}
- }
- if listUUIDs == nil {
- listUUIDs = []string{}
- }
- if err = c.q.InsertSubscriber.Get(&sub.ID,
- sub.UUID,
- sub.Email,
- strings.TrimSpace(sub.Name),
- sub.Status,
- sub.Attribs,
- pq.Array(listIDs),
- pq.Array(listUUIDs),
- subStatus); err != nil {
- if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "subscribers_email_key" {
- return models.Subscriber{}, false, echo.NewHTTPError(http.StatusConflict,
- c.i18n.T("subscribers.emailExists"))
- } else {
- // return sub.Subscriber, errSubscriberExists
- c.log.Printf("error inserting subscriber: %v", err)
- return models.Subscriber{}, false, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorCreating",
- "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
- }
- }
- // Fetch the subscriber'out full data. If the subscriber already existed and wasn't
- // created, the id will be empty. Fetch the details by e-mail then.
- out, err := c.GetSubscriber(sub.ID, "", sub.Email)
- if err != nil {
- return models.Subscriber{}, false, err
- }
- hasOptin := false
- if !preconfirm && c.constants.SendOptinConfirmation {
- // Send a confirmation e-mail (if there are any double opt-in lists).
- num, _ := c.h.SendOptinConfirmation(out, listIDs)
- hasOptin = num > 0
- }
- return out, hasOptin, nil
- }
- // UpdateSubscriber updates a subscriber's properties.
- func (c *Core) UpdateSubscriber(id int, sub models.Subscriber) (models.Subscriber, error) {
- // Format raw JSON attributes.
- attribs := []byte("{}")
- if len(sub.Attribs) > 0 {
- if b, err := json.Marshal(sub.Attribs); err != nil {
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating",
- "name", "{globals.terms.subscriber}", "error", err.Error()))
- } else {
- attribs = b
- }
- }
- _, err := c.q.UpdateSubscriber.Exec(id,
- sub.Email,
- strings.TrimSpace(sub.Name),
- sub.Status,
- json.RawMessage(attribs),
- )
- if err != nil {
- c.log.Printf("error updating subscriber: %v", err)
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
- }
- out, err := c.GetSubscriber(sub.ID, "", sub.Email)
- if err != nil {
- return models.Subscriber{}, err
- }
- return out, nil
- }
- // UpdateSubscriberWithLists updates a subscriber's properties.
- // If deleteLists is set to true, all existing subscriptions are deleted and only
- // the ones provided are added or retained.
- func (c *Core) UpdateSubscriberWithLists(id int, sub models.Subscriber, listIDs []int, listUUIDs []string, preconfirm, deleteLists bool) (models.Subscriber, error) {
- subStatus := models.SubscriptionStatusUnconfirmed
- if preconfirm {
- subStatus = models.SubscriptionStatusConfirmed
- }
- // Format raw JSON attributes.
- attribs := []byte("{}")
- if len(sub.Attribs) > 0 {
- if b, err := json.Marshal(sub.Attribs); err != nil {
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating",
- "name", "{globals.terms.subscriber}", "error", err.Error()))
- } else {
- attribs = b
- }
- }
- _, err := c.q.UpdateSubscriberWithLists.Exec(id,
- sub.Email,
- strings.TrimSpace(sub.Name),
- sub.Status,
- json.RawMessage(attribs),
- pq.Array(listIDs),
- pq.Array(listUUIDs),
- subStatus,
- deleteLists)
- if err != nil {
- c.log.Printf("error updating subscriber: %v", err)
- return models.Subscriber{}, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
- }
- out, err := c.GetSubscriber(sub.ID, "", sub.Email)
- if err != nil {
- return models.Subscriber{}, err
- }
- return out, nil
- }
- // BlocklistSubscribers blocklists the given list of subscribers.
- func (c *Core) BlocklistSubscribers(subIDs []int) error {
- if _, err := c.q.BlocklistSubscribers.Exec(pq.Array(subIDs)); err != nil {
- c.log.Printf("error blocklisting subscribers: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("subscribers.errorBlocklisting", "error", err.Error()))
- }
- return nil
- }
- // BlocklistSubscribersByQuery blocklists the given list of subscribers.
- func (c *Core) BlocklistSubscribersByQuery(query string, listIDs []int) error {
- if err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.BlocklistSubscribersByQuery, listIDs, c.db); err != nil {
- c.log.Printf("error blocklisting subscribers: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("subscribers.errorBlocklisting", "error", pqErrMsg(err)))
- }
- return nil
- }
- // DeleteSubscribers deletes the given list of subscribers.
- func (c *Core) DeleteSubscribers(subIDs []int, subUUIDs []string) error {
- if subIDs == nil {
- subIDs = []int{}
- }
- if subUUIDs == nil {
- subUUIDs = []string{}
- }
- if _, err := c.q.DeleteSubscribers.Exec(pq.Array(subIDs), pq.Array(subUUIDs)); err != nil {
- c.log.Printf("error deleting subscribers: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- return nil
- }
- // DeleteSubscribersByQuery deletes subscribers by a given arbitrary query expression.
- func (c *Core) DeleteSubscribersByQuery(query string, listIDs []int) error {
- err := c.q.ExecSubQueryTpl(sanitizeSQLExp(query), c.q.DeleteSubscribersByQuery, listIDs, c.db)
- if err != nil {
- c.log.Printf("error deleting subscribers: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- return err
- }
- // UnsubscribeByCampaign unsubscibers a given subscriber from lists in a given campaign.
- func (c *Core) UnsubscribeByCampaign(subUUID, campUUID string, blocklist bool) error {
- if _, err := c.q.UnsubscribeByCampaign.Exec(campUUID, subUUID, blocklist); err != nil {
- c.log.Printf("error unsubscribing: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- return nil
- }
- // ConfirmOptionSubscription confirms a subscriber's optin subscription.
- func (c *Core) ConfirmOptionSubscription(subUUID string, listUUIDs []string) error {
- if _, err := c.q.ConfirmSubscriptionOptin.Exec(subUUID, pq.Array(listUUIDs)); err != nil {
- c.log.Printf("error confirming subscription: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- return nil
- }
- // DeleteSubscriberBounces deletes the given list of subscribers.
- func (c *Core) DeleteSubscriberBounces(id int, uuid string) error {
- var uu interface{}
- if uuid != "" {
- uu = uuid
- }
- if _, err := c.q.DeleteBouncesBySubscriber.Exec(id, uu); err != nil {
- c.log.Printf("error deleting bounces: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.bounces}", "error", pqErrMsg(err)))
- }
- return nil
- }
- // DeleteOrphanSubscribers deletes orphan subscriber records (subscribers without lists).
- func (c *Core) DeleteOrphanSubscribers() (int, error) {
- res, err := c.q.DeleteOrphanSubscribers.Exec()
- if err != nil {
- c.log.Printf("error deleting orphan subscribers: %v", err)
- return 0, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- n, _ := res.RowsAffected()
- return int(n), nil
- }
- // DeleteBlocklistedSubscribers deletes blocklisted subscribers.
- func (c *Core) DeleteBlocklistedSubscribers() (int, error) {
- res, err := c.q.DeleteBlocklistedSubscribers.Exec()
- if err != nil {
- c.log.Printf("error deleting blocklisted subscribers: %v", err)
- return 0, echo.NewHTTPError(http.StatusInternalServerError,
- c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- }
- n, _ := res.RowsAffected()
- return int(n), nil
- }
|