123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796 |
- package main
- import (
- "bytes"
- "database/sql"
- "errors"
- "fmt"
- "html/template"
- "net/http"
- "net/url"
- "regexp"
- "strconv"
- "strings"
- "time"
- "github.com/gofrs/uuid"
- "github.com/jmoiron/sqlx"
- "github.com/knadh/listmonk/models"
- "github.com/labstack/echo"
- "github.com/lib/pq"
- null "gopkg.in/volatiletech/null.v6"
- )
- // campaignReq is a wrapper over the Campaign model for receiving
- // campaign creation and updation data from APIs.
- type campaignReq struct {
- models.Campaign
- // Indicates if the "send_at" date should be written or set to null.
- SendLater bool `db:"-" json:"send_later"`
- // This overrides Campaign.Lists to receive and
- // write a list of int IDs during creation and updation.
- // Campaign.Lists is JSONText for sending lists children
- // to the outside world.
- ListIDs pq.Int64Array `db:"-" json:"lists"`
- // This is only relevant to campaign test requests.
- SubscriberEmails pq.StringArray `json:"subscribers"`
- Type string `json:"type"`
- }
- // campaignContentReq wraps params coming from API requests for converting
- // campaign content formats.
- type campaignContentReq struct {
- models.Campaign
- From string `json:"from"`
- To string `json:"to"`
- }
- type campCountStats struct {
- CampaignID int `db:"campaign_id" json:"campaign_id"`
- Count int `db:"count" json:"count"`
- Timestamp time.Time `db:"timestamp" json:"timestamp"`
- }
- type campTopLinks struct {
- URL string `db:"url" json:"url"`
- Count int `db:"count" json:"count"`
- }
- type campaignStats struct {
- ID int `db:"id" json:"id"`
- Status string `db:"status" json:"status"`
- ToSend int `db:"to_send" json:"to_send"`
- Sent int `db:"sent" json:"sent"`
- Started null.Time `db:"started_at" json:"started_at"`
- UpdatedAt null.Time `db:"updated_at" json:"updated_at"`
- Rate float64 `json:"rate"`
- }
- type campsWrap struct {
- Results models.Campaigns `json:"results"`
- Query string `json:"query"`
- Total int `json:"total"`
- PerPage int `json:"per_page"`
- Page int `json:"page"`
- }
- var (
- regexFromAddress = regexp.MustCompile(`(.+?)\s<(.+?)@(.+?)>`)
- regexFullTextQuery = regexp.MustCompile(`\s+`)
- campaignQuerySortFields = []string{"name", "status", "created_at", "updated_at"}
- bounceQuerySortFields = []string{"email", "campaign_name", "source", "created_at"}
- )
- // handleGetCampaigns handles retrieval of campaigns.
- func handleGetCampaigns(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- pg = getPagination(c.QueryParams(), 20)
- out campsWrap
- id, _ = strconv.Atoi(c.Param("id"))
- status = c.QueryParams()["status"]
- query = strings.TrimSpace(c.FormValue("query"))
- orderBy = c.FormValue("order_by")
- order = c.FormValue("order")
- noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
- )
- // Fetch one list.
- single := false
- if id > 0 {
- single = true
- }
- queryStr, stmt := makeCampaignQuery(query, orderBy, order, app.queries.QueryCampaigns)
- // Unsafe to ignore scanning fields not present in models.Campaigns.
- if err := db.Select(&out.Results, stmt, id, pq.StringArray(status), queryStr, pg.Offset, pg.Limit); err != nil {
- app.log.Printf("error fetching campaigns: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- if single && len(out.Results) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("campaigns.notFound", "name", "{globals.terms.campaign}"))
- }
- if len(out.Results) == 0 {
- out.Results = []models.Campaign{}
- return c.JSON(http.StatusOK, okResp{out})
- }
- for i := 0; i < len(out.Results); i++ {
- // Replace null tags.
- if out.Results[i].Tags == nil {
- out.Results[i].Tags = make(pq.StringArray, 0)
- }
- if noBody {
- out.Results[i].Body = ""
- }
- }
- // Lazy load stats.
- if err := out.Results.LoadStats(app.queries.GetCampaignStats); err != nil {
- app.log.Printf("error fetching campaign stats: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- if single {
- return c.JSON(http.StatusOK, okResp{out.Results[0]})
- }
- // Meta.
- out.Total = out.Results[0].Total
- out.Page = pg.Page
- out.PerPage = pg.PerPage
- return c.JSON(http.StatusOK, okResp{out})
- }
- // handlePreviewCampaign renders the HTML preview of a campaign body.
- func handlePreviewCampaign(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- id, _ = strconv.Atoi(c.Param("id"))
- )
- if id < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
- }
- var camp models.Campaign
- err := app.queries.GetCampaignForPreview.Get(&camp, id)
- if err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
- }
- app.log.Printf("error fetching campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- // There's a body in the request to preview instead of the body in the DB.
- if c.Request().Method == http.MethodPost {
- camp.ContentType = c.FormValue("content_type")
- camp.Body = c.FormValue("body")
- }
- // Use a dummy campaign ID to prevent views and clicks from {{ TrackView }}
- // and {{ TrackLink }} being registered on preview.
- camp.UUID = dummySubscriber.UUID
- if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
- app.log.Printf("error compiling template: %v", err)
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
- }
- // Render the message body.
- msg, err := app.manager.NewCampaignMessage(&camp, dummySubscriber)
- if err != nil {
- app.log.Printf("error rendering message: %v", err)
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("templates.errorRendering", "error", err.Error()))
- }
- return c.HTML(http.StatusOK, string(msg.Body()))
- }
- // handleCampaignContent handles campaign content (body) format conversions.
- func handleCampaignContent(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- id, _ = strconv.Atoi(c.Param("id"))
- )
- if id < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
- }
- var camp campaignContentReq
- if err := c.Bind(&camp); err != nil {
- return err
- }
- out, err := camp.ConvertContent(camp.From, camp.To)
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err.Error())
- }
- return c.JSON(http.StatusOK, okResp{out})
- }
- // handleCreateCampaign handles campaign creation.
- // Newly created campaigns are always drafts.
- func handleCreateCampaign(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- o campaignReq
- )
- if err := c.Bind(&o); err != nil {
- return err
- }
- // If the campaign's 'opt-in', prepare a default message.
- if o.Type == models.CampaignTypeOptin {
- op, err := makeOptinCampaignMessage(o, app)
- if err != nil {
- return err
- }
- o = op
- }
- // Validate.
- if c, err := validateCampaignFields(o, app); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err.Error())
- } else {
- o = c
- }
- uu, err := uuid.NewV4()
- if err != nil {
- app.log.Printf("error generating UUID: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorUUID", "error", err.Error()))
- }
- // Insert and read ID.
- var newID int
- if err := app.queries.CreateCampaign.Get(&newID,
- uu,
- o.Type,
- o.Name,
- o.Subject,
- o.FromEmail,
- o.Body,
- o.AltBody,
- o.ContentType,
- o.SendAt,
- pq.StringArray(normalizeTags(o.Tags)),
- o.Messenger,
- o.TemplateID,
- o.ListIDs,
- ); err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noSubs"))
- }
- app.log.Printf("error creating campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorCreating",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- // Hand over to the GET handler to return the last insertion.
- return handleGetCampaigns(copyEchoCtx(c, map[string]string{
- "id": fmt.Sprintf("%d", newID),
- }))
- }
- // handleUpdateCampaign handles campaign modification.
- // Campaigns that are done cannot be modified.
- func handleUpdateCampaign(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- id, _ = strconv.Atoi(c.Param("id"))
- )
- if id < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
- }
- var cm models.Campaign
- if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
- }
- app.log.Printf("error fetching campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- if isCampaignalMutable(cm.Status) {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.cantUpdate"))
- }
- // Read the incoming params into the existing campaign fields from the DB.
- // This allows updating of values that have been sent where as fields
- // that are not in the request retain the old values.
- o := campaignReq{Campaign: cm}
- if err := c.Bind(&o); err != nil {
- return err
- }
- if c, err := validateCampaignFields(o, app); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err.Error())
- } else {
- o = c
- }
- _, err := app.queries.UpdateCampaign.Exec(cm.ID,
- o.Name,
- o.Subject,
- o.FromEmail,
- o.Body,
- o.AltBody,
- o.ContentType,
- o.SendAt,
- o.SendLater,
- pq.StringArray(normalizeTags(o.Tags)),
- o.Messenger,
- o.TemplateID,
- o.ListIDs)
- if err != nil {
- app.log.Printf("error updating campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorUpdating",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- return handleGetCampaigns(c)
- }
- // handleUpdateCampaignStatus handles campaign status modification.
- func handleUpdateCampaignStatus(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- id, _ = strconv.Atoi(c.Param("id"))
- )
- if id < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
- }
- var cm models.Campaign
- if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.message.notFound", "name", "{globals.terms.campaign}"))
- }
- app.log.Printf("error fetching campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- // Incoming params.
- var o campaignReq
- if err := c.Bind(&o); err != nil {
- return err
- }
- errMsg := ""
- switch o.Status {
- case models.CampaignStatusDraft:
- if cm.Status != models.CampaignStatusScheduled {
- errMsg = app.i18n.T("campaigns.onlyScheduledAsDraft")
- }
- case models.CampaignStatusScheduled:
- if cm.Status != models.CampaignStatusDraft {
- errMsg = app.i18n.T("campaigns.onlyDraftAsScheduled")
- }
- if !cm.SendAt.Valid {
- errMsg = app.i18n.T("campaigns.needsSendAt")
- }
- case models.CampaignStatusRunning:
- if cm.Status != models.CampaignStatusPaused && cm.Status != models.CampaignStatusDraft {
- errMsg = app.i18n.T("campaigns.onlyPausedDraft")
- }
- case models.CampaignStatusPaused:
- if cm.Status != models.CampaignStatusRunning {
- errMsg = app.i18n.T("campaigns.onlyActivePause")
- }
- case models.CampaignStatusCancelled:
- if cm.Status != models.CampaignStatusRunning && cm.Status != models.CampaignStatusPaused {
- errMsg = app.i18n.T("campaigns.onlyActiveCancel")
- }
- }
- if len(errMsg) > 0 {
- return echo.NewHTTPError(http.StatusBadRequest, errMsg)
- }
- res, err := app.queries.UpdateCampaignStatus.Exec(cm.ID, o.Status)
- if err != nil {
- app.log.Printf("error updating campaign status: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorUpdating",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- if n, _ := res.RowsAffected(); n == 0 {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.notFound",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- return handleGetCampaigns(c)
- }
- // handleDeleteCampaign handles campaign deletion.
- // Only scheduled campaigns that have not started yet can be deleted.
- func handleDeleteCampaign(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- id, _ = strconv.Atoi(c.Param("id"))
- )
- if id < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
- }
- var cm models.Campaign
- if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.notFound",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- app.log.Printf("error fetching campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil {
- app.log.Printf("error deleting campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorDeleting",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- return c.JSON(http.StatusOK, okResp{true})
- }
- // handleGetRunningCampaignStats returns stats of a given set of campaign IDs.
- func handleGetRunningCampaignStats(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- out []campaignStats
- )
- if err := app.queries.GetCampaignStatus.Select(&out, models.CampaignStatusRunning); err != nil {
- if err == sql.ErrNoRows {
- return c.JSON(http.StatusOK, okResp{[]struct{}{}})
- }
- app.log.Printf("error fetching campaign stats: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- } else if len(out) == 0 {
- return c.JSON(http.StatusOK, okResp{[]struct{}{}})
- }
- // Compute rate.
- for i, c := range out {
- if c.Started.Valid && c.UpdatedAt.Valid {
- diff := c.UpdatedAt.Time.Sub(c.Started.Time).Minutes()
- if diff > 0 {
- var (
- sent = float64(c.Sent)
- rate = sent / diff
- )
- if rate > sent || rate > float64(c.ToSend) {
- rate = sent
- }
- out[i].Rate = rate
- }
- }
- }
- return c.JSON(http.StatusOK, okResp{out})
- }
- // handleTestCampaign handles the sending of a campaign message to
- // arbitrary subscribers for testing.
- func handleTestCampaign(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- campID, _ = strconv.Atoi(c.Param("id"))
- req campaignReq
- )
- if campID < 1 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
- }
- // Get and validate fields.
- if err := c.Bind(&req); err != nil {
- return err
- }
- // Validate.
- if c, err := validateCampaignFields(req, app); err != nil {
- return echo.NewHTTPError(http.StatusBadRequest, err.Error())
- } else {
- req = c
- }
- if len(req.SubscriberEmails) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noSubsToTest"))
- }
- // Get the subscribers.
- for i := 0; i < len(req.SubscriberEmails); i++ {
- req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i]))
- }
- var subs models.Subscribers
- if err := app.queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
- app.log.Printf("error fetching subscribers: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
- } else if len(subs) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noKnownSubsToTest"))
- }
- // The campaign.
- var camp models.Campaign
- if err := app.queries.GetCampaignForPreview.Get(&camp, campID); err != nil {
- if err == sql.ErrNoRows {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.notFound",
- "name", "{globals.terms.campaign}"))
- }
- app.log.Printf("error fetching campaign: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
- }
- // Override certain values from the DB with incoming values.
- camp.Name = req.Name
- camp.Subject = req.Subject
- camp.FromEmail = req.FromEmail
- camp.Body = req.Body
- camp.AltBody = req.AltBody
- camp.Messenger = req.Messenger
- camp.ContentType = req.ContentType
- camp.TemplateID = req.TemplateID
- // Send the test messages.
- for _, s := range subs {
- sub := s
- if err := sendTestMessage(sub, &camp, app); err != nil {
- app.log.Printf("error sending test message: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("campaigns.errorSendTest", "error", err.Error()))
- }
- }
- return c.JSON(http.StatusOK, okResp{true})
- }
- // handleGetCampaignViewAnalytics retrieves view counts for a campaign.
- func handleGetCampaignViewAnalytics(c echo.Context) error {
- var (
- app = c.Get("app").(*App)
- typ = c.Param("type")
- from = c.QueryParams().Get("from")
- to = c.QueryParams().Get("to")
- )
- ids, err := parseStringIDs(c.Request().URL.Query()["id"])
- if err != nil {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.errorInvalidIDs", "error", err.Error()))
- }
- if len(ids) == 0 {
- return echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("globals.messages.missingFields", "name", "`id`"))
- }
- // Pick campaign view counts or click counts.
- var stmt *sqlx.Stmt
- switch typ {
- case "views":
- stmt = app.queries.GetCampaignViewCounts
- case "clicks":
- stmt = app.queries.GetCampaignClickCounts
- case "bounces":
- stmt = app.queries.GetCampaignBounceCounts
- case "links":
- out := make([]campTopLinks, 0)
- if err := app.queries.GetCampaignLinkCounts.Select(&out, pq.Int64Array(ids), from, to); err != nil {
- app.log.Printf("error fetching campaign %s: %v", typ, err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.analytics}", "error", pqErrMsg(err)))
- }
- return c.JSON(http.StatusOK, okResp{out})
- default:
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidData"))
- }
- if !strHasLen(from, 10, 30) || !strHasLen(to, 10, 30) {
- return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("analytics.invalidDates"))
- }
- out := make([]campCountStats, 0)
- if err := stmt.Select(&out, pq.Int64Array(ids), from, to); err != nil {
- app.log.Printf("error fetching campaign %s: %v", typ, err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.analytics}", "error", pqErrMsg(err)))
- }
- return c.JSON(http.StatusOK, okResp{out})
- }
- // sendTestMessage takes a campaign and a subsriber and sends out a sample campaign message.
- func sendTestMessage(sub models.Subscriber, camp *models.Campaign, app *App) error {
- if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
- app.log.Printf("error compiling template: %v", err)
- return echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
- }
- // Create a sample campaign message.
- msg, err := app.manager.NewCampaignMessage(camp, sub)
- if err != nil {
- app.log.Printf("error rendering message: %v", err)
- return echo.NewHTTPError(http.StatusNotFound,
- app.i18n.Ts("templates.errorRendering", "error", err.Error()))
- }
- return app.manager.PushCampaignMessage(msg)
- }
- // validateCampaignFields validates incoming campaign field values.
- func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
- if c.FromEmail == "" {
- c.FromEmail = app.constants.FromEmail
- } else if !regexFromAddress.Match([]byte(c.FromEmail)) {
- if _, err := app.importer.SanitizeEmail(c.FromEmail); err != nil {
- return c, errors.New(app.i18n.T("campaigns.fieldInvalidFromEmail"))
- }
- }
- if !strHasLen(c.Name, 1, stdInputMaxLen) {
- return c, errors.New(app.i18n.T("campaigns.fieldInvalidName"))
- }
- if !strHasLen(c.Subject, 1, stdInputMaxLen) {
- return c, errors.New(app.i18n.T("campaigns.fieldInvalidSubject"))
- }
- // if !hasLen(c.Body, 1, bodyMaxLen) {
- // return c,errors.New("invalid length for `body`")
- // }
- // If there's a "send_at" date, it should be in the future.
- if c.SendAt.Valid {
- if c.SendAt.Time.Before(time.Now()) {
- return c, errors.New(app.i18n.T("campaigns.fieldInvalidSendAt"))
- }
- }
- if len(c.ListIDs) == 0 {
- return c, errors.New(app.i18n.T("campaigns.fieldInvalidListIDs"))
- }
- if !app.manager.HasMessenger(c.Messenger) {
- return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidMessenger", "name", c.Messenger))
- }
- camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
- if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
- return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
- }
- return c, nil
- }
- // isCampaignalMutable tells if a campaign's in a state where it's
- // properties can be mutated.
- func isCampaignalMutable(status string) bool {
- return status == models.CampaignStatusRunning ||
- status == models.CampaignStatusCancelled ||
- status == models.CampaignStatusFinished
- }
- // makeOptinCampaignMessage makes a default opt-in campaign message body.
- func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
- if len(o.ListIDs) == 0 {
- return o, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.fieldInvalidListIDs"))
- }
- // Fetch double opt-in lists from the given list IDs.
- var lists []models.List
- err := app.queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil)
- if err != nil {
- app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
- return o, echo.NewHTTPError(http.StatusInternalServerError,
- app.i18n.Ts("globals.messages.errorFetching",
- "name", "{globals.terms.list}", "error", pqErrMsg(err)))
- }
- // No opt-in lists.
- if len(lists) == 0 {
- return o, echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noOptinLists"))
- }
- // Construct the opt-in URL with list IDs.
- listIDs := url.Values{}
- for _, l := range lists {
- listIDs.Add("l", l.UUID)
- }
- // optinURLFunc := template.URL("{{ OptinURL }}?" + listIDs.Encode())
- optinURLAttr := template.HTMLAttr(fmt.Sprintf(`href="{{ OptinURL }}%s"`, listIDs.Encode()))
- // Prepare sample opt-in message for the campaign.
- var b bytes.Buffer
- if err := app.notifTpls.ExecuteTemplate(&b, "optin-campaign", struct {
- Lists []models.List
- OptinURLAttr template.HTMLAttr
- }{lists, optinURLAttr}); err != nil {
- app.log.Printf("error compiling 'optin-campaign' template: %v", err)
- return o, echo.NewHTTPError(http.StatusBadRequest,
- app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
- }
- o.Body = b.String()
- return o, nil
- }
- // makeCampaignQuery cleans an optional campaign search string and prepares the
- // campaign SQL statement (string) and returns them.
- func makeCampaignQuery(q, orderBy, order, query string) (string, string) {
- if q != "" {
- q = `%` + string(regexFullTextQuery.ReplaceAll([]byte(q), []byte("&"))) + `%`
- }
- // Sort params.
- if !strSliceContains(orderBy, campaignQuerySortFields) {
- orderBy = "created_at"
- }
- if order != sortAsc && order != sortDesc {
- order = sortDesc
- }
- return q, fmt.Sprintf(query, orderBy, order)
- }
|