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:
+
+ {{ range $i, $l := .Lists }}
+ {{ if eq .Type "public" }}
+ - {{ .Name }}
+ {{ else }}
+ - Private list
+ {{ end }}
+ {{ end }}
+
+
+ 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 {
})(