diff --git a/campaigns.go b/campaigns.go index 1fd0849..bf9240d 100644 --- a/campaigns.go +++ b/campaigns.go @@ -1,10 +1,13 @@ package main import ( + "bytes" "database/sql" "errors" "fmt" + "html/template" "net/http" + "net/url" "regexp" "strconv" "strings" @@ -33,6 +36,8 @@ type campaignReq struct { // This is only relevant to campaign test requests. SubscriberEmails pq.StringArray `json:"subscribers"` + + Type string `json:"type"` } type campaignStats struct { @@ -191,9 +196,20 @@ func handleCreateCampaign(c echo.Context) error { 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 err := validateCampaignFields(o, app); err != nil { + if c, err := validateCampaignFields(o, app); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } else { + o = c } if !app.Manager.HasMessenger(o.MessengerID) { @@ -205,6 +221,7 @@ func handleCreateCampaign(c echo.Context) error { var newID int if err := app.Queries.CreateCampaign.Get(&newID, uuid.NewV4(), + o.Type, o.Name, o.Subject, o.FromEmail, @@ -228,7 +245,6 @@ func handleCreateCampaign(c echo.Context) error { // Hand over to the GET handler to return the last insertion. c.SetParamNames("id") c.SetParamValues(fmt.Sprintf("%d", newID)) - return handleGetCampaigns(c) } @@ -265,8 +281,10 @@ func handleUpdateCampaign(c echo.Context) error { return err } - if err := validateCampaignFields(o, app); err != nil { + if c, err := validateCampaignFields(o, app); err != nil { return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } else { + o = c } res, err := app.Queries.UpdateCampaign.Exec(cm.ID, @@ -457,8 +475,10 @@ func handleTestCampaign(c echo.Context) error { return err } // Validate. - if err := validateCampaignFields(req, app); err != nil { + 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, "No subscribers to target.") @@ -524,37 +544,39 @@ func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) er } // validateCampaignFields validates incoming campaign field values. -func validateCampaignFields(c campaignReq, app *App) error { - if !regexFromAddress.Match([]byte(c.FromEmail)) { +func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) { + if c.FromEmail == "" { + c.FromEmail = app.Constants.FromEmail + } else if !regexFromAddress.Match([]byte(c.FromEmail)) { if !govalidator.IsEmail(c.FromEmail) { - return errors.New("invalid `from_email`") + return c, errors.New("invalid `from_email`") } } if !govalidator.IsByteLength(c.Name, 1, stdInputMaxLen) { - return errors.New("invalid length for `name`") + return c, errors.New("invalid length for `name`") } if !govalidator.IsByteLength(c.Subject, 1, stdInputMaxLen) { - return errors.New("invalid length for `subject`") + return c, errors.New("invalid length for `subject`") } // if !govalidator.IsByteLength(c.Body, 1, bodyMaxLen) { - // return errors.New("invalid length for `body`") + // 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 errors.New("`send_at` date should be in the future") + return c, errors.New("`send_at` date should be in the future") } } camp := models.Campaign{Body: c.Body, TemplateBody: tplTag} if err := c.CompileTemplate(app.Manager.TemplateFuncs(&camp)); err != nil { - return fmt.Errorf("Error compiling campaign body: %v", err) + return c, fmt.Errorf("Error compiling campaign body: %v", err) } - return nil + return c, nil } // isCampaignalMutable tells if a campaign's in a state where it's @@ -564,3 +586,53 @@ func isCampaignalMutable(status string) bool { 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, "Invalid list IDs.") + } + + // 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.Logger.Printf("error fetching lists for optin: %s", pqErrMsg(err)) + return o, echo.NewHTTPError(http.StatusInternalServerError, + "Error fetching optin lists.") + } + + // No opt-in lists. + if len(lists) == 0 { + return o, echo.NewHTTPError(http.StatusBadRequest, + "No opt-in lists found to create campaign.") + } + + // Construct the opt-in URL with list IDs. + var ( + listIDs = url.Values{} + listNames = make([]string, 0, len(lists)) + ) + for _, l := range lists { + listIDs.Add("l", l.UUID) + listNames = append(listNames, l.Name) + } + // 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.Logger.Printf("error compiling 'optin-campaign' template: %v", err) + return o, echo.NewHTTPError(http.StatusInternalServerError, + "Error compiling opt-in campaign template.") + } + + o.Name = "Opt-in campaign " + strings.Join(listNames, ", ") + o.Subject = "Confirm your subscription(s)" + o.Body = b.String() + return o, nil +} diff --git a/email-templates/subscriber-optin-campaign.html b/email-templates/subscriber-optin-campaign.html new file mode 100644 index 0000000..fbdaab6 --- /dev/null +++ b/email-templates/subscriber-optin-campaign.html @@ -0,0 +1,17 @@ +{{ define "optin-campaign" }} + +

Hi {{`{{ .Subscriber.FirstName }}`}},

+

You have been added to the following mailing lists:

+ +

+ Confirm subscription(s) +

+{{ end }} diff --git a/frontend/src/Campaign.js b/frontend/src/Campaign.js index 6b4eb58..244f5d3 100644 --- a/frontend/src/Campaign.js +++ b/frontend/src/Campaign.js @@ -259,6 +259,7 @@ class TheFormDef extends React.PureComponent { values.tags = [] } + values.type = cs.CampaignTypeRegular values.body = this.props.body values.content_type = this.props.contentType @@ -398,14 +399,14 @@ class TheFormDef extends React.PureComponent { } }); } else { - // eslint-disable-next-line radix - const id = parseInt(p.list_id) - if (id) { - subLists.push(id) + // eslint-disable-next-line radix + const id = parseInt(p.list_id) + if (id) { + subLists.push(id) + } } } } - } if (this.record) { this.props.pageTitle(record.name + " / Campaigns") @@ -469,7 +470,8 @@ class TheFormDef extends React.PureComponent { })(