diff --git a/campaigns.go b/campaigns.go index cd17ea7..f859c20 100644 --- a/campaigns.go +++ b/campaigns.go @@ -8,6 +8,7 @@ import ( "net/http" "regexp" "strconv" + "strings" "time" "github.com/asaskevich/govalidator" @@ -24,6 +25,9 @@ type campaignReq struct { models.Campaign MessengerID string `json:"messenger"` Lists pq.Int64Array `json:"lists"` + + // This is only relevant to campaign test requests. + SubscriberEmails pq.StringArray `json:"subscribers"` } type campaignStats struct { @@ -131,7 +135,8 @@ func handlePreviewCampaign(c echo.Context) error { } tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, body) if err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error compiling template: %v", err)) + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("Error compiling template: %v", err)) } // Render the message body. @@ -139,7 +144,8 @@ func handlePreviewCampaign(c echo.Context) error { if err := tpl.ExecuteTemplate(&out, runner.BaseTPL, runner.Message{Campaign: &camp, Subscriber: &sub, UnsubscribeURL: "#dummy"}); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error executing template: %v", err)) + return echo.NewHTTPError(http.StatusBadRequest, + fmt.Sprintf("Error executing template: %v", err)) } return c.HTML(http.StatusOK, out.String()) @@ -408,6 +414,91 @@ func handleGetCampaignMessengers(c echo.Context) error { return c.JSON(http.StatusOK, okResp{app.Runner.GetMessengerNames()}) } +// 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, "Invalid campaign ID.") + } + + // Get and validate fields. + if err := c.Bind(&req); err != nil { + return err + } + // Validate. + if err := validateCampaignFields(req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, err.Error()) + } + if len(req.SubscriberEmails) == 0 { + return echo.NewHTTPError(http.StatusBadRequest, "No subscribers to target.") + } + + // 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 { + return echo.NewHTTPError(http.StatusInternalServerError, + fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err))) + } else if len(subs) == 0 { + return echo.NewHTTPError(http.StatusBadRequest, "No known subscribers given.") + } + + // 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, "Campaign not found.") + } + return echo.NewHTTPError(http.StatusInternalServerError, + fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err))) + } + + // Override certain values in the DB with incoming values. + camp.Name = req.Name + camp.Subject = req.Subject + camp.FromEmail = req.FromEmail + camp.Body = req.Body + + // Send the test messages. + for _, s := range subs { + if err := sendTestMessage(&s, &camp, app); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Error sending test: %v", err)) + } + } + + return c.JSON(http.StatusOK, okResp{true}) +} + +// sendTestMessage takes a campaign and a subsriber and sends out a sample campain message. +func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error { + tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, camp.Body) + if err != nil { + return fmt.Errorf("Error compiling template: %v", err) + } + + // Render the message body. + var out = bytes.Buffer{} + if err := tpl.ExecuteTemplate(&out, + runner.BaseTPL, + runner.Message{Campaign: camp, Subscriber: sub, UnsubscribeURL: "#dummy"}); err != nil { + return fmt.Errorf("Error executing template: %v", err) + } + + if err := app.Messenger.Push(camp.FromEmail, sub.Email, camp.Subject, []byte(out.Bytes())); err != nil { + return err + } + + return nil +} + // validateCampaignFields validates incoming campaign field values. func validateCampaignFields(c campaignReq) error { if !regexFromAddress.Match([]byte(c.FromEmail)) { @@ -434,6 +525,11 @@ func validateCampaignFields(c campaignReq) error { } } + _, err := runner.CompileMessageTemplate(tplTag, c.Body) + if err != nil { + return fmt.Errorf("Error compiling campaign body: %v", err) + } + return nil } diff --git a/frontend/my/src/Campaign.js b/frontend/my/src/Campaign.js index e43ef9c..1d8c005 100644 --- a/frontend/my/src/Campaign.js +++ b/frontend/my/src/Campaign.js @@ -25,7 +25,7 @@ class Editor extends React.PureComponent { rawInput: null, selContentType: "richtext", contentType: "richtext", - body: "", + body: "" } quillModules = { @@ -129,7 +129,9 @@ class Editor extends React.PureComponent { modules={ this.quillModules } defaultValue={ this.props.record.body } ref={ (o) => { - this.setState({ quill: o }) + if(o) { + this.setState({ quill: o }) + } }} onChange={ () => { if(!this.state.quill) { @@ -167,7 +169,8 @@ class Editor extends React.PureComponent { class TheFormDef extends React.PureComponent { state = { editorVisible: false, - sendLater: false + sendLater: false, + loading: false } componentWillReceiveProps(nextProps) { @@ -200,15 +203,16 @@ class TheFormDef extends React.PureComponent { if (err) { return } - + if(!values.tags) { values.tags = [] } - + values.body = this.props.body values.content_type = this.props.contentType - + // Create a new campaign. + this.setState({ loading: true }) if(!this.props.isSingle) { this.props.modelRequest(cs.ModelCampaigns, cs.Routes.CreateCampaign, cs.MethodPost, values).then((resp) => { notification["success"]({ placement: "topRight", @@ -218,21 +222,52 @@ class TheFormDef extends React.PureComponent { this.props.route.history.push(cs.Routes.ViewCampaign.replace(":id", resp.data.data.id)) this.props.fetchRecord(resp.data.data.id) this.props.setCurrentTab("content") + this.setState({ loading: false }) }).catch(e => { notification["error"]({ message: "Error", description: e.message }) }) } else { this.props.modelRequest(cs.ModelCampaigns, cs.Routes.UpdateCampaign, cs.MethodPut, { ...values, id: this.props.record.id }).then((resp) => { notification["success"]({ placement: "topRight", - message: "Campaign updated", - description: `"${values["name"]}" updated` }) + message: "Campaign updated", + description: `"${values["name"]}" updated` }) }).catch(e => { notification["error"]({ message: "Error", description: e.message }) + this.setState({ loading: false }) }) } }) } + + handleTestCampaign = (e) => { + e.preventDefault() + this.props.form.validateFields((err, values) => { + if (err) { + return + } + + if(!values.tags) { + values.tags = [] + } + + values.id = this.props.record.id + values.body = this.props.body + values.content_type = this.props.contentType + + this.setState({ loading: true }) + this.props.request(cs.Routes.TestCampaign, cs.MethodPost, values).then((resp) => { + this.setState({ loading: false }) + notification["success"]({ placement: "topRight", + message: "Test sent", + description: `Test messages sent` }) + }).catch(e => { + this.setState({ loading: false }) + notification["error"]({ message: "Error", description: e.message }) + }) + }) + } + render() { const { record } = this.props; const { getFieldDecorator } = this.props.form @@ -244,101 +279,107 @@ class TheFormDef extends React.PureComponent { return (
Hi there
-Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et elit ac elit sollicitudin condimentum non a magna. Sed tempor mauris in facilisis vehicula. Aenean nisl urna, accumsan ac tincidunt vitae, interdum cursus massa. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam varius turpis et turpis lacinia placerat. Aenean id ligula a orci lacinia blandit at eu felis. Phasellus vel lobortis lacus. Suspendisse leo elit, luctus sed erat ut, venenatis fermentum ipsum. Donec bibendum neque quis.
+Hi there
+Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et elit ac elit sollicitudin condimentum non a magna. Sed tempor mauris in facilisis vehicula. Aenean nisl urna, accumsan ac tincidunt vitae, interdum cursus massa. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam varius turpis et turpis lacinia placerat. Aenean id ligula a orci lacinia blandit at eu felis. Phasellus vel lobortis lacus. Suspendisse leo elit, luctus sed erat ut, venenatis fermentum ipsum. Donec bibendum neque quis.
-Nam luctus dui non placerat mattis. Morbi non accumsan orci, vel interdum urna. Duis faucibus id nunc ut euismod. Curabitur et eros id erat feugiat fringilla in eget neque. Aliquam accumsan cursus eros sed faucibus.
+Nam luctus dui non placerat mattis. Morbi non accumsan orci, vel interdum urna. Duis faucibus id nunc ut euismod. Curabitur et eros id erat feugiat fringilla in eget neque. Aliquam accumsan cursus eros sed faucibus.
Here is a link to listmonk.
` )