diff --git a/campaigns.go b/campaigns.go index f859c20..c05ed03 100644 --- a/campaigns.go +++ b/campaigns.go @@ -95,14 +95,14 @@ func handlePreviewCampaign(c echo.Context) error { id, _ = strconv.Atoi(c.Param("id")) body = c.FormValue("body") - camp models.Campaign + camp = &models.Campaign{} ) if id < 1 { return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.") } - err := app.Queries.GetCampaignForPreview.Get(&camp, id) + err := app.Queries.GetCampaignForPreview.Get(camp, id) if err != nil { if err == sql.ErrNoRows { return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.") @@ -130,25 +130,23 @@ func handlePreviewCampaign(c echo.Context) error { } // Compile the template. - if body == "" { - body = camp.Body + if body != "" { + camp.Body = body } - tpl, err := runner.CompileMessageTemplate(camp.TemplateBody, body) - if err != nil { + + if err := camp.CompileTemplate(app.Runner.TemplateFuncs(camp)); err != nil { return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("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 { + m := app.Runner.NewMessage(camp, &sub) + if err := m.Render(); err != nil { return echo.NewHTTPError(http.StatusBadRequest, - fmt.Sprintf("Error executing template: %v", err)) + fmt.Sprintf("Error rendering message: %v", err)) } - return c.HTML(http.StatusOK, out.String()) + return c.HTML(http.StatusOK, string(m.Body)) } // handleCreateCampaign handles campaign creation. @@ -479,14 +477,13 @@ func handleTestCampaign(c echo.Context) error { // 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 { + if err := camp.CompileTemplate(app.Runner.TemplateFuncs(camp)); err != nil { return fmt.Errorf("Error compiling template: %v", err) } // Render the message body. var out = bytes.Buffer{} - if err := tpl.ExecuteTemplate(&out, + if err := camp.Tpl.ExecuteTemplate(&out, runner.BaseTPL, runner.Message{Campaign: camp, Subscriber: sub, UnsubscribeURL: "#dummy"}); err != nil { return fmt.Errorf("Error executing template: %v", err) diff --git a/frontend/my/src/Import.js b/frontend/my/src/Import.js index 25f4336..fcf18eb 100644 --- a/frontend/my/src/Import.js +++ b/frontend/my/src/Import.js @@ -119,6 +119,7 @@ class TheFormDef extends React.PureComponent {
For existing subscribers, the names and attributes will be overwritten with the values in the CSV.
status
and attributes
are optional).
{" "}
attributes
should be a valid JSON string with double escaped quotes.
- Spreadsheet programs should automatically take care of this without having to manually
+ Spreadsheet programs should automatically take care of this without having you manually
escape quotes.
diff --git a/main.go b/main.go
index 5e90de2..f799af7 100644
--- a/main.go
+++ b/main.go
@@ -128,6 +128,7 @@ func registerHandlers(e *echo.Echo) {
// Subscriber facing views.
e.GET("/unsubscribe/:campUUID/:subUUID", handleUnsubscribePage)
e.POST("/unsubscribe/:campUUID/:subUUID", handleUnsubscribePage)
+ e.GET("/link/:linkUUID/:campUUID/:subUUID", handleLinkRedirect)
// Static views.
e.GET("/lists", handleIndexPage)
@@ -223,6 +224,8 @@ func main() {
// url.com/unsubscribe/{campaign_uuid}/{subscriber_uuid}
UnsubscribeURL: fmt.Sprintf("%s/unsubscribe/%%s/%%s", app.Constants.RootURL),
+ // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
+ LinkTrackURL: fmt.Sprintf("%s/link/%%s/%%s/%%s", app.Constants.RootURL),
}, newRunnerDB(q), logger)
app.Runner = r
diff --git a/models/models.go b/models/models.go
index 5862162..c7ae970 100644
--- a/models/models.go
+++ b/models/models.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"html/template"
+ "regexp"
"github.com/jmoiron/sqlx"
"github.com/jmoiron/sqlx/types"
@@ -37,6 +38,20 @@ const (
UserTypeUser = "user"
UserStatusEnabled = "enabled"
UserStatusDisabled = "disabled"
+
+ // BaseTpl is the name of the base template.
+ BaseTpl = "base"
+
+ // ContentTpl is the name of the compiled message.
+ ContentTpl = "content"
+)
+
+// Regular expression for matching {{ Track "http://link.com" }} in the template
+// and substituting it with {{ Track "http://link.com" .Campaign.UUID .Subscriber.UUID }}
+// before compilation. This string gimmick is to make linking easier for users.
+var (
+ regexpLinkTag = regexp.MustCompile(`{{(\s+)?Track\s+?"(.+?)"(\s+)?}}`)
+ regexpLinkTagReplace = `{{ Track "$2" .Campaign.UUID .Subscriber.UUID }}`
)
// Base holds common fields shared across models.
@@ -187,3 +202,29 @@ func (s SubscriberAttribs) Scan(src interface{}) error {
}
return fmt.Errorf("Could not not decode type %T -> %T", src, s)
}
+
+// CompileTemplate compiles a campaign body template into its base
+// template and sets the resultant template to Campaign.Tpl
+func (c *Campaign) CompileTemplate(f template.FuncMap) error {
+ // Compile the base template.
+ t := regexpLinkTag.ReplaceAllString(c.TemplateBody, regexpLinkTagReplace)
+ baseTPL, err := template.New(BaseTpl).Funcs(f).Parse(t)
+ if err != nil {
+ return fmt.Errorf("error compiling base template: %v", err)
+ }
+
+ // Compile the campaign message.
+ t = regexpLinkTag.ReplaceAllString(c.Body, regexpLinkTagReplace)
+ msgTpl, err := template.New(ContentTpl).Funcs(f).Parse(t)
+ if err != nil {
+ return fmt.Errorf("error compiling message: %v", err)
+ }
+
+ out, err := baseTPL.AddParseTree(ContentTpl, msgTpl.Tree)
+ if err != nil {
+ return fmt.Errorf("error inserting child template: %v", err)
+ }
+
+ c.Tpl = out
+ return nil
+}
diff --git a/public.go b/public.go
index c765e78..ef316e4 100644
--- a/public.go
+++ b/public.go
@@ -10,6 +10,7 @@ import (
"github.com/labstack/echo"
)
+// Template wraps a template.Template for echo.
type Template struct {
templates *template.Template
}
@@ -21,7 +22,8 @@ type publicTpl struct {
type unsubTpl struct {
publicTpl
- Blacklisted bool
+ Unsubscribe bool
+ Blacklist bool
}
type errorTpl struct {
@@ -33,8 +35,8 @@ type errorTpl struct {
var regexValidUUID = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
+// Render executes and renders a template for echo.
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
- // fmt.Println(t.templates.ExecuteTemplate(os.Stdout, name, nil))
return t.templates.ExecuteTemplate(w, name, data)
}
@@ -44,37 +46,66 @@ func handleUnsubscribePage(c echo.Context) error {
app = c.Get("app").(*App)
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
+ unsub, _ = strconv.ParseBool(c.FormValue("unsubscribe"))
blacklist, _ = strconv.ParseBool(c.FormValue("blacklist"))
out = unsubTpl{}
)
- out.Blacklisted = blacklist
+ out.Unsubscribe = unsub
+ out.Blacklist = blacklist
out.Title = "Unsubscribe from mailing list"
if !regexValidUUID.MatchString(campUUID) ||
!regexValidUUID.MatchString(subUUID) {
- err := errorTpl{}
- err.Title = "Invalid request"
- err.ErrorTitle = err.Title
- err.ErrorMessage = "The unsubscription request contains invalid IDs. Please make sure to follow the correct link."
- return c.Render(http.StatusBadRequest, "error", err)
+ return c.Render(http.StatusBadRequest, "error",
+ makeErrorTpl("Invalid request", "",
+ `The unsubscription request contains invalid IDs.
+ Please click on the correct link.`))
}
// Unsubscribe.
- res, err := app.Queries.Unsubscribe.Exec(campUUID, subUUID, blacklist)
- if err != nil {
- app.Logger.Printf("Error unsubscribing : %v", err)
- return echo.NewHTTPError(http.StatusBadRequest, "Subscription doesn't exist")
- }
+ if unsub {
+ res, err := app.Queries.Unsubscribe.Exec(campUUID, subUUID, blacklist)
+ if err != nil {
+ app.Logger.Printf("Error unsubscribing : %v", err)
+ return echo.NewHTTPError(http.StatusBadRequest, "There was an internal error while unsubscribing you.")
+ }
- num, err := res.RowsAffected()
- if num == 0 {
- err := errorTpl{}
- err.Title = "Invalid subscription"
- err.ErrorTitle = err.Title
- err.ErrorMessage = "Looks like you are not subscribed to this mailing list."
- return c.Render(http.StatusBadRequest, "error", err)
+ if !blacklist {
+ num, _ := res.RowsAffected()
+ if num == 0 {
+ return c.Render(http.StatusBadRequest, "error",
+ makeErrorTpl("Already unsubscribed", "",
+ `Looks like you are not subscribed to this mailing list.
+ You may have already unsubscribed.`))
+ }
+ }
}
return c.Render(http.StatusOK, "unsubscribe", out)
}
+
+// handleLinkRedirect handles link UUID to real link redirection.
+func handleLinkRedirect(c echo.Context) error {
+ var (
+ app = c.Get("app").(*App)
+ linkUUID = c.Param("linkUUID")
+ campUUID = c.Param("campUUID")
+ subUUID = c.Param("subUUID")
+ )
+ if !regexValidUUID.MatchString(linkUUID) ||
+ !regexValidUUID.MatchString(campUUID) ||
+ !regexValidUUID.MatchString(subUUID) {
+ return c.Render(http.StatusBadRequest, "error",
+ makeErrorTpl("Invalid link", "", "The link you clicked is invalid."))
+ }
+
+ var url string
+ if err := app.Queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil {
+ app.Logger.Printf("error fetching redirect link: %s", err)
+ return c.Render(http.StatusInternalServerError, "error",
+ makeErrorTpl("Error opening link", "", "There was an error opening the link. Please try later."))
+ }
+
+ return c.Redirect(http.StatusTemporaryRedirect, url)
+}
diff --git a/public/static/style.css b/public/static/style.css
index 533e89d..92d9fb1 100644
--- a/public/static/style.css
+++ b/public/static/style.css
@@ -18,18 +18,25 @@ h1, h2, h3, h4 {
}
.button {
- border: 0;
- background: transparent;
+ background: #7f2aff;
padding: 10px 30px;
border-radius: 3px;
- border: 1px solid #ddd;
+ border: 0;
cursor: pointer;
text-decoration: none;
- color: #111;
+ color: #ffff;
display: inline-block;
}
.button:hover {
- background: #f3f3f3;
+ background: #333;
+ }
+ .button .button-outline {
+ background: transparent;
+ border: 1px solid #ddd;
+ color: #444'
+ }
+ .button .button-outline:hover {
+ color: #111;
}
.wrap {
diff --git a/public/templates/index.html b/public/templates/index.html
index ffb1eeb..48e4b3d 100644
--- a/public/templates/index.html
+++ b/public/templates/index.html
@@ -27,20 +27,6 @@
Powered by listmonk
-
-
-