|
@@ -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
|
|
|
+}
|