listmonk/cmd/campaigns.go

731 lines
21 KiB
Go
Raw Permalink Normal View History

2018-10-25 15:51:47 +02:00
package main
import (
"bytes"
2018-10-25 15:51:47 +02:00
"database/sql"
"errors"
"fmt"
"html/template"
2018-10-25 15:51:47 +02:00
"net/http"
"net/url"
2018-10-25 15:51:47 +02:00
"regexp"
"strconv"
2018-10-29 10:50:49 +01:00
"strings"
2018-10-25 15:51:47 +02:00
"time"
2020-03-07 16:07:48 +01:00
"github.com/gofrs/uuid"
"github.com/knadh/listmonk/internal/messenger"
2020-03-08 08:33:38 +01:00
"github.com/knadh/listmonk/internal/subimporter"
2018-10-25 15:51:47 +02:00
"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.
2018-10-25 15:51:47 +02:00
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"`
2018-10-29 10:50:49 +01:00
// This is only relevant to campaign test requests.
SubscriberEmails pq.StringArray `json:"subscribers"`
Type string `json:"type"`
2018-10-25 15:51:47 +02:00
}
// 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"`
}
2018-10-25 15:51:47 +02:00
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"}
)
2018-10-25 15:51:47 +02:00
// handleGetCampaigns handles retrieval of campaigns.
func handleGetCampaigns(c echo.Context) error {
var (
app = c.Get("app").(*App)
2020-08-01 15:27:33 +02:00
pg = getPagination(c.QueryParams(), 20, 50)
out campsWrap
2018-10-25 15:51:47 +02:00
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")
2018-10-25 15:51:47 +02:00
noBody, _ = strconv.ParseBool(c.QueryParam("no_body"))
)
// Fetch one list.
single := false
2018-10-25 15:51:47 +02:00
if id > 0 {
single = true
}
if query != "" {
2020-08-01 16:16:47 +02:00
query = `%` +
string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&"))) + `%`
}
2018-10-25 15:51:47 +02:00
// Sort params.
if !strSliceContains(orderBy, campaignQuerySortFields) {
orderBy = "created_at"
}
if order != sortAsc && order != sortDesc {
order = sortDesc
}
stmt := fmt.Sprintf(app.queries.QueryCampaigns, orderBy, order)
// Unsafe to ignore scanning fields not present in models.Campaigns.
if err := db.Select(&out.Results, stmt, id, pq.StringArray(status), query, pg.Offset, pg.Limit); err != nil {
app.log.Printf("error fetching campaigns: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
}
if single && len(out.Results) == 0 {
2020-12-19 11:55:52 +01:00
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})
2018-10-25 15:51:47 +02:00
}
for i := 0; i < len(out.Results); i++ {
2018-10-25 15:51:47 +02:00
// Replace null tags.
if out.Results[i].Tags == nil {
out.Results[i].Tags = make(pq.StringArray, 0)
2018-10-25 15:51:47 +02:00
}
if noBody {
out.Results[i].Body = ""
2018-10-25 15:51:47 +02:00
}
}
// 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",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
}
2018-10-25 15:51:47 +02:00
if single {
return c.JSON(http.StatusOK, okResp{out.Results[0]})
2018-10-25 15:51:47 +02:00
}
// Meta.
out.Total = out.Results[0].Total
out.Page = pg.Page
out.PerPage = pg.PerPage
2018-10-25 15:51:47 +02:00
return c.JSON(http.StatusOK, okResp{out})
}
// handlePreviewCampaign renders the HTML preview of a campaign body.
2018-10-25 15:51:47 +02:00
func handlePreviewCampaign(c echo.Context) error {
var (
app = c.Get("app").(*App)
id, _ = strconv.Atoi(c.Param("id"))
)
if id < 1 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
2018-10-25 15:51:47 +02:00
}
var camp models.Campaign
err := app.queries.GetCampaignForPreview.Get(&camp, id)
2018-10-25 15:51:47 +02:00
if err != nil {
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
}
app.log.Printf("error fetching campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
// 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)
2018-10-29 10:50:49 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
2018-10-25 15:51:47 +02:00
}
// Render the message body.
m := app.manager.NewCampaignMessage(&camp, dummySubscriber)
if err := m.Render(); err != nil {
app.log.Printf("error rendering message: %v", err)
2018-10-29 10:50:49 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("templates.errorRendering", "error", err.Error()))
2018-10-25 15:51:47 +02:00
}
return c.HTML(http.StatusOK, string(m.Body()))
2018-10-25 15:51:47 +02:00
}
// 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})
}
2018-10-25 15:51:47 +02:00
// 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
}
2018-10-25 15:51:47 +02:00
// Validate.
if c, err := validateCampaignFields(o, app); err != nil {
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} else {
o = c
2018-10-25 15:51:47 +02:00
}
2020-03-07 16:07:48 +01:00
uu, err := uuid.NewV4()
if err != nil {
app.log.Printf("error generating UUID: %v", err)
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorUUID", "error", err.Error()))
2020-03-07 16:07:48 +01:00
}
2018-10-25 15:51:47 +02:00
// Insert and read ID.
var newID int
if err := app.queries.CreateCampaign.Get(&newID,
2020-03-07 16:07:48 +01:00
uu,
o.Type,
2018-10-25 15:51:47 +02:00
o.Name,
o.Subject,
o.FromEmail,
o.Body,
o.AltBody,
2018-10-25 15:51:47 +02:00
o.ContentType,
o.SendAt,
pq.StringArray(normalizeTags(o.Tags)),
o.Messenger,
2018-10-25 15:51:47 +02:00
o.TemplateID,
o.ListIDs,
2018-10-25 15:51:47 +02:00
); err != nil {
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noSubs"))
2018-10-25 15:51:47 +02:00
}
app.log.Printf("error creating campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorCreating",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
// Hand over to the GET handler to return the last insertion.
return handleGetCampaigns(copyEchoCtx(c, map[string]string{
"id": fmt.Sprintf("%d", newID),
}))
2018-10-25 15:51:47 +02:00
}
// 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 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
2018-10-25 15:51:47 +02:00
}
var cm models.Campaign
if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
2018-10-25 15:51:47 +02:00
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.campaign}"))
2018-10-25 15:51:47 +02:00
}
app.log.Printf("error fetching campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
if isCampaignalMutable(cm.Status) {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.cantUpdate"))
2018-10-25 15:51:47 +02:00
}
// 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}
2018-10-25 15:51:47 +02:00
if err := c.Bind(&o); err != nil {
return err
}
if c, err := validateCampaignFields(o, app); err != nil {
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} else {
o = c
2018-10-25 15:51:47 +02:00
}
_, err := app.queries.UpdateCampaign.Exec(cm.ID,
2018-10-25 15:51:47 +02:00
o.Name,
o.Subject,
o.FromEmail,
o.Body,
o.AltBody,
2018-10-25 15:51:47 +02:00
o.ContentType,
o.SendAt,
o.SendLater,
2018-10-25 15:51:47 +02:00
pq.StringArray(normalizeTags(o.Tags)),
o.Messenger,
2018-10-25 15:51:47 +02:00
o.TemplateID,
o.ListIDs)
2018-10-25 15:51:47 +02:00
if err != nil {
app.log.Printf("error updating campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorUpdating",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
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 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
2018-10-25 15:51:47 +02:00
}
var cm models.Campaign
if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
2018-10-25 15:51:47 +02:00
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.message.notFound", "name", "{globals.terms.campaign}"))
2018-10-25 15:51:47 +02:00
}
app.log.Printf("error fetching campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
// 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 {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.onlyScheduledAsDraft")
2018-10-25 15:51:47 +02:00
}
case models.CampaignStatusScheduled:
if cm.Status != models.CampaignStatusDraft {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.onlyDraftAsScheduled")
2018-10-25 15:51:47 +02:00
}
if !cm.SendAt.Valid {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.needsSendAt")
2018-10-25 15:51:47 +02:00
}
case models.CampaignStatusRunning:
if cm.Status != models.CampaignStatusPaused && cm.Status != models.CampaignStatusDraft {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.onlyPausedDraft")
2018-10-25 15:51:47 +02:00
}
case models.CampaignStatusPaused:
if cm.Status != models.CampaignStatusRunning {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.onlyActivePause")
2018-10-25 15:51:47 +02:00
}
case models.CampaignStatusCancelled:
if cm.Status != models.CampaignStatusRunning && cm.Status != models.CampaignStatusPaused {
2020-12-19 11:55:52 +01:00
errMsg = app.i18n.T("campaigns.onlyActiveCancel")
2018-10-25 15:51:47 +02:00
}
}
if len(errMsg) > 0 {
return echo.NewHTTPError(http.StatusBadRequest, errMsg)
}
res, err := app.queries.UpdateCampaignStatus.Exec(cm.ID, o.Status)
2018-10-25 15:51:47 +02:00
if err != nil {
app.log.Printf("error updating campaign status: %v", err)
2020-12-19 11:55:52 +01:00
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorUpdating",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
if n, _ := res.RowsAffected(); n == 0 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
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 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.invalidID"))
2018-10-25 15:51:47 +02:00
}
var cm models.Campaign
if err := app.queries.GetCampaign.Get(&cm, id, nil); err != nil {
2018-10-25 15:51:47 +02:00
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
app.log.Printf("error fetching campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil {
app.log.Printf("error deleting campaign: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorDeleting",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
}
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 {
2018-10-25 15:51:47 +02:00
if err == sql.ErrNoRows {
return c.JSON(http.StatusOK, okResp{[]struct{}{}})
}
app.log.Printf("error fetching campaign stats: %v", err)
2018-10-25 15:51:47 +02:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-25 15:51:47 +02:00
} 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 {
2018-11-27 07:14:54 +01:00
var (
sent = float64(c.Sent)
rate = sent / diff
)
if rate > sent || rate > float64(c.ToSend) {
rate = sent
2018-10-25 15:51:47 +02:00
}
2018-11-27 07:14:54 +01:00
out[i].Rate = rate
2018-10-25 15:51:47 +02:00
}
}
}
return c.JSON(http.StatusOK, okResp{out})
}
2018-10-29 10:50:49 +01:00
// 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 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("globals.messages.errorID"))
2018-10-29 10:50:49 +01:00
}
// Get and validate fields.
if err := c.Bind(&req); err != nil {
return err
}
2018-10-29 10:50:49 +01:00
// Validate.
if c, err := validateCampaignFields(req, app); err != nil {
2018-10-29 10:50:49 +01:00
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
} else {
req = c
2018-10-29 10:50:49 +01:00
}
if len(req.SubscriberEmails) == 0 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noSubsToTest"))
2018-10-29 10:50:49 +01:00
}
// 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)
2018-10-29 10:50:49 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.subscribers}", "error", pqErrMsg(err)))
2018-10-29 10:50:49 +01:00
} else if len(subs) == 0 {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest, app.i18n.T("campaigns.noKnownSubsToTest"))
2018-10-29 10:50:49 +01:00
}
// The campaign.
var camp models.Campaign
if err := app.queries.GetCampaignForPreview.Get(&camp, campID); err != nil {
2018-10-29 10:50:49 +01:00
if err == sql.ErrNoRows {
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("globals.messages.notFound",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}"))
2018-10-29 10:50:49 +01:00
}
2020-03-07 18:30:55 +01:00
app.log.Printf("error fetching campaign: %v", err)
2018-10-29 10:50:49 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("globals.messages.errorFetching",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.campaign}", "error", pqErrMsg(err)))
2018-10-29 10:50:49 +01:00
}
// Override certain values from the DB with incoming values.
2018-10-29 10:50:49 +01:00
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
2018-10-29 10:50:49 +01:00
// Send the test messages.
for _, s := range subs {
sub := s
2020-03-08 06:37:24 +01:00
if err := sendTestMessage(sub, &camp, app); err != nil {
2021-01-28 19:00:39 +01:00
app.log.Printf("error sending test message: %v", err)
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("campaigns.errorSendTest", "error", err.Error()))
2018-10-29 10:50:49 +01:00
}
}
return c.JSON(http.StatusOK, okResp{true})
}
// sendTestMessage takes a campaign and a subsriber and sends out a sample campaign message.
2020-03-08 06:37:24 +01:00
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)
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusInternalServerError,
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
2018-10-29 10:50:49 +01:00
}
// Render the message body.
m := app.manager.NewCampaignMessage(camp, sub)
if err := m.Render(); err != nil {
app.log.Printf("error rendering message: %v", err)
2020-12-19 11:55:52 +01:00
return echo.NewHTTPError(http.StatusNotFound,
app.i18n.Ts("templates.errorRendering", "error", err.Error()))
2018-10-29 10:50:49 +01:00
}
return app.messengers[camp.Messenger].Push(messenger.Message{
From: camp.FromEmail,
To: []string{sub.Email},
Subject: m.Subject(),
ContentType: camp.ContentType,
Body: m.Body(),
AltBody: m.AltBody(),
Subscriber: sub,
Campaign: camp,
})
2018-10-29 10:50:49 +01:00
}
2018-10-25 15:51:47 +02:00
// 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)) {
2020-03-08 08:33:38 +01:00
if !subimporter.IsEmail(c.FromEmail) {
2020-12-19 11:55:52 +01:00
return c, errors.New(app.i18n.T("campaigns.fieldInvalidFromEmail"))
2018-10-25 15:51:47 +02:00
}
}
2020-03-08 08:33:38 +01:00
if !strHasLen(c.Name, 1, stdInputMaxLen) {
2020-12-19 11:55:52 +01:00
return c, errors.New(app.i18n.T("campaigns.fieldInvalidName"))
2018-10-25 15:51:47 +02:00
}
2020-03-08 08:33:38 +01:00
if !strHasLen(c.Subject, 1, stdInputMaxLen) {
2020-12-19 11:55:52 +01:00
return c, errors.New(app.i18n.T("campaigns.fieldInvalidSubject"))
2018-10-25 15:51:47 +02:00
}
2020-03-08 08:33:38 +01:00
// if !hasLen(c.Body, 1, bodyMaxLen) {
// return c,errors.New("invalid length for `body`")
2018-10-25 15:51:47 +02:00
// }
// If there's a "send_at" date, it should be in the future.
if c.SendAt.Valid {
if c.SendAt.Time.Before(time.Now()) {
2020-12-19 11:55:52 +01:00
return c, errors.New(app.i18n.T("campaigns.fieldInvalidSendAt"))
2018-10-25 15:51:47 +02:00
}
}
if len(c.ListIDs) == 0 {
2020-12-19 11:55:52 +01:00
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()))
2018-10-29 10:50:49 +01:00
}
return c, nil
2018-10-25 15:51:47 +02:00
}
// 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 {
2020-12-19 11:55:52 +01:00
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",
2020-12-19 11:55:52 +01:00
"name", "{globals.terms.list}", "error", pqErrMsg(err)))
}
// No opt-in lists.
if len(lists) == 0 {
2020-12-19 11:55:52 +01:00
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)
2020-12-19 11:55:52 +01:00
return o, echo.NewHTTPError(http.StatusBadRequest,
app.i18n.Ts("templates.errorCompiling", "error", err.Error()))
}
o.Body = b.String()
return o, nil
}