Pārlūkot izejas kodu

Refactor and cleanup initialization.

- Clean up main.go (by moving init to init.go) and improve
  composition comprehension.
- Refactor app context and init struct and field names.
- Update package dependencies in initialisation.
Kailash Nadh 5 gadi atpakaļ
vecāks
revīzija
8853809713
16 mainītis faili ar 574 papildinājumiem un 567 dzēšanām
  1. 4 4
      admin.go
  2. 45 45
      campaigns.go
  3. 3 6
      go.mod
  4. 21 0
      go.sum
  5. 4 4
      handlers.go
  6. 7 7
      import.go
  7. 310 0
      init.go
  8. 22 23
      install.go
  9. 9 9
      lists.go
  10. 51 284
      main.go
  11. 16 16
      media.go
  12. 4 27
      notifications.go
  13. 24 24
      public.go
  14. 46 46
      subscribers.go
  15. 8 8
      templates.go
  16. 0 64
      utils.go

+ 4 - 4
admin.go

@@ -27,9 +27,9 @@ func handleGetConfigScript(c echo.Context) error {
 	var (
 	var (
 		app = c.Get("app").(*App)
 		app = c.Get("app").(*App)
 		out = configScript{
 		out = configScript{
-			RootURL:    app.Constants.RootURL,
-			FromEmail:  app.Constants.FromEmail,
-			Messengers: app.Manager.GetMessengerNames(),
+			RootURL:    app.constants.RootURL,
+			FromEmail:  app.constants.FromEmail,
+			Messengers: app.manager.GetMessengerNames(),
 		}
 		}
 
 
 		b = bytes.Buffer{}
 		b = bytes.Buffer{}
@@ -48,7 +48,7 @@ func handleGetDashboardStats(c echo.Context) error {
 		out dashboardStats
 		out dashboardStats
 	)
 	)
 
 
-	if err := app.Queries.GetDashboardStats.Get(&out); err != nil {
+	if err := app.queries.GetDashboardStats.Get(&out); err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching dashboard stats: %s", pqErrMsg(err)))
 	}
 	}

+ 45 - 45
campaigns.go

@@ -86,9 +86,9 @@ func handleGetCampaigns(c echo.Context) error {
 		query = string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&")))
 		query = string(regexFullTextQuery.ReplaceAll([]byte(query), []byte("&")))
 	}
 	}
 
 
-	err := app.Queries.QueryCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit)
+	err := app.queries.QueryCampaigns.Select(&out.Results, id, pq.StringArray(status), query, pg.Offset, pg.Limit)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error fetching campaigns: %v", err)
+		app.log.Printf("error fetching campaigns: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
 	}
 	}
@@ -112,8 +112,8 @@ func handleGetCampaigns(c echo.Context) error {
 	}
 	}
 
 
 	// Lazy load stats.
 	// Lazy load stats.
-	if err := out.Results.LoadStats(app.Queries.GetCampaignStats); err != nil {
-		app.Logger.Printf("error fetching campaign stats: %v", err)
+	if err := out.Results.LoadStats(app.queries.GetCampaignStats); err != nil {
+		app.log.Printf("error fetching campaign stats: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign stats: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign stats: %v", pqErrMsg(err)))
 	}
 	}
@@ -144,25 +144,25 @@ func handlePreviewCampaign(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
 		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 != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 	}
 
 
 	var sub models.Subscriber
 	var sub models.Subscriber
 	// Get a random subscriber from the campaign.
 	// Get a random subscriber from the campaign.
-	if err := app.Queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil {
+	if err := app.queries.GetOneCampaignSubscriber.Get(&sub, camp.ID); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			// There's no subscriber. Mock one.
 			// There's no subscriber. Mock one.
 			sub = dummySubscriber
 			sub = dummySubscriber
 		} else {
 		} else {
-			app.Logger.Printf("error fetching subscriber: %v", err)
+			app.log.Printf("error fetching subscriber: %v", err)
 			return echo.NewHTTPError(http.StatusInternalServerError,
 			return echo.NewHTTPError(http.StatusInternalServerError,
 				fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 				fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 		}
 		}
@@ -173,16 +173,16 @@ func handlePreviewCampaign(c echo.Context) error {
 		camp.Body = body
 		camp.Body = body
 	}
 	}
 
 
-	if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil {
-		app.Logger.Printf("error compiling template: %v", err)
+	if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
+		app.log.Printf("error compiling template: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error compiling template: %v", err))
 			fmt.Sprintf("Error compiling template: %v", err))
 	}
 	}
 
 
 	// Render the message body.
 	// Render the message body.
-	m := app.Manager.NewMessage(camp, &sub)
+	m := app.manager.NewMessage(camp, &sub)
 	if err := m.Render(); err != nil {
 	if err := m.Render(); err != nil {
-		app.Logger.Printf("error rendering message: %v", err)
+		app.log.Printf("error rendering message: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error rendering message: %v", err))
 			fmt.Sprintf("Error rendering message: %v", err))
 	}
 	}
@@ -218,20 +218,20 @@ func handleCreateCampaign(c echo.Context) error {
 		o = c
 		o = c
 	}
 	}
 
 
-	if !app.Manager.HasMessenger(o.MessengerID) {
+	if !app.manager.HasMessenger(o.MessengerID) {
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Unknown messenger %s", o.MessengerID))
 			fmt.Sprintf("Unknown messenger %s", o.MessengerID))
 	}
 	}
 
 
 	uu, err := uuid.NewV4()
 	uu, err := uuid.NewV4()
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error generating UUID: %v", err)
+		app.log.Printf("error generating UUID: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 	}
 	}
 
 
 	// Insert and read ID.
 	// Insert and read ID.
 	var newID int
 	var newID int
-	if err := app.Queries.CreateCampaign.Get(&newID,
+	if err := app.queries.CreateCampaign.Get(&newID,
 		uu,
 		uu,
 		o.Type,
 		o.Type,
 		o.Name,
 		o.Name,
@@ -250,7 +250,7 @@ func handleCreateCampaign(c echo.Context) error {
 				"There aren't any subscribers in the target lists to create the campaign.")
 				"There aren't any subscribers in the target lists to create the campaign.")
 		}
 		}
 
 
-		app.Logger.Printf("error creating campaign: %v", err)
+		app.log.Printf("error creating campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err)))
 	}
 	}
@@ -274,12 +274,12 @@ func handleUpdateCampaign(c echo.Context) error {
 	}
 	}
 
 
 	var cm models.Campaign
 	var cm models.Campaign
-	if err := app.Queries.GetCampaign.Get(&cm, id); err != nil {
+	if err := app.queries.GetCampaign.Get(&cm, id); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 	}
@@ -301,7 +301,7 @@ func handleUpdateCampaign(c echo.Context) error {
 		o = c
 		o = c
 	}
 	}
 
 
-	res, err := app.Queries.UpdateCampaign.Exec(cm.ID,
+	res, err := app.queries.UpdateCampaign.Exec(cm.ID,
 		o.Name,
 		o.Name,
 		o.Subject,
 		o.Subject,
 		o.FromEmail,
 		o.FromEmail,
@@ -313,7 +313,7 @@ func handleUpdateCampaign(c echo.Context) error {
 		o.TemplateID,
 		o.TemplateID,
 		o.ListIDs)
 		o.ListIDs)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating campaign: %v", err)
+		app.log.Printf("error updating campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
 	}
 	}
@@ -337,12 +337,12 @@ func handleUpdateCampaignStatus(c echo.Context) error {
 	}
 	}
 
 
 	var cm models.Campaign
 	var cm models.Campaign
-	if err := app.Queries.GetCampaign.Get(&cm, id); err != nil {
+	if err := app.queries.GetCampaign.Get(&cm, id); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 	}
@@ -385,9 +385,9 @@ func handleUpdateCampaignStatus(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusBadRequest, errMsg)
 		return echo.NewHTTPError(http.StatusBadRequest, errMsg)
 	}
 	}
 
 
-	res, err := app.Queries.UpdateCampaignStatus.Exec(cm.ID, o.Status)
+	res, err := app.queries.UpdateCampaignStatus.Exec(cm.ID, o.Status)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating campaign status: %v", err)
+		app.log.Printf("error updating campaign status: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err)))
 	}
 	}
@@ -412,12 +412,12 @@ func handleDeleteCampaign(c echo.Context) error {
 	}
 	}
 
 
 	var cm models.Campaign
 	var cm models.Campaign
-	if err := app.Queries.GetCampaign.Get(&cm, id); err != nil {
+	if err := app.queries.GetCampaign.Get(&cm, id); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 	}
@@ -429,8 +429,8 @@ func handleDeleteCampaign(c echo.Context) error {
 			"Only campaigns that haven't been started can be deleted.")
 			"Only campaigns that haven't been started can be deleted.")
 	}
 	}
 
 
-	if _, err := app.Queries.DeleteCampaign.Exec(cm.ID); err != nil {
-		app.Logger.Printf("error deleting campaign: %v", err)
+	if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil {
+		app.log.Printf("error deleting campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err)))
 	}
 	}
@@ -445,12 +445,12 @@ func handleGetRunningCampaignStats(c echo.Context) error {
 		out []campaignStats
 		out []campaignStats
 	)
 	)
 
 
-	if err := app.Queries.GetCampaignStatus.Select(&out, models.CampaignStatusRunning); err != nil {
+	if err := app.queries.GetCampaignStatus.Select(&out, models.CampaignStatusRunning); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return c.JSON(http.StatusOK, okResp{[]struct{}{}})
 			return c.JSON(http.StatusOK, okResp{[]struct{}{}})
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign stats: %v", err)
+		app.log.Printf("error fetching campaign stats: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err)))
 	} else if len(out) == 0 {
 	} else if len(out) == 0 {
@@ -509,8 +509,8 @@ func handleTestCampaign(c echo.Context) error {
 		req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i]))
 		req.SubscriberEmails[i] = strings.ToLower(strings.TrimSpace(req.SubscriberEmails[i]))
 	}
 	}
 	var subs models.Subscribers
 	var subs models.Subscribers
-	if err := app.Queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
-		app.Logger.Printf("error fetching subscribers: %v", err)
+	if err := app.queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
+		app.log.Printf("error fetching subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
 	} else if len(subs) == 0 {
 	} else if len(subs) == 0 {
@@ -519,12 +519,12 @@ func handleTestCampaign(c echo.Context) error {
 
 
 	// The campaign.
 	// The campaign.
 	var camp models.Campaign
 	var camp models.Campaign
-	if err := app.Queries.GetCampaignForPreview.Get(&camp, campID); err != nil {
+	if err := app.queries.GetCampaignForPreview.Get(&camp, campID); err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 		}
 
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 	}
@@ -549,20 +549,20 @@ func handleTestCampaign(c echo.Context) error {
 
 
 // sendTestMessage takes a campaign and a subsriber and sends out a sample campaign message.
 // sendTestMessage takes a campaign and a subsriber and sends out a sample campaign message.
 func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error {
 func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) error {
-	if err := camp.CompileTemplate(app.Manager.TemplateFuncs(camp)); err != nil {
-		app.Logger.Printf("error compiling template: %v", err)
+	if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
+		app.log.Printf("error compiling template: %v", err)
 		return fmt.Errorf("Error compiling template: %v", err)
 		return fmt.Errorf("Error compiling template: %v", err)
 	}
 	}
 
 
 	// Render the message body.
 	// Render the message body.
-	m := app.Manager.NewMessage(camp, sub)
+	m := app.manager.NewMessage(camp, sub)
 	if err := m.Render(); err != nil {
 	if err := m.Render(); err != nil {
-		app.Logger.Printf("error rendering message: %v", err)
+		app.log.Printf("error rendering message: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error rendering message: %v", err))
 			fmt.Sprintf("Error rendering message: %v", err))
 	}
 	}
 
 
-	if err := app.Messenger.Push(camp.FromEmail, []string{sub.Email}, camp.Subject, m.Body, nil); err != nil {
+	if err := app.messenger.Push(camp.FromEmail, []string{sub.Email}, camp.Subject, m.Body, nil); err != nil {
 		return err
 		return err
 	}
 	}
 
 
@@ -572,7 +572,7 @@ func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) er
 // validateCampaignFields validates incoming campaign field values.
 // validateCampaignFields validates incoming campaign field values.
 func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
 func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
 	if c.FromEmail == "" {
 	if c.FromEmail == "" {
-		c.FromEmail = app.Constants.FromEmail
+		c.FromEmail = app.constants.FromEmail
 	} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
 	} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
 		if !govalidator.IsEmail(c.FromEmail) {
 		if !govalidator.IsEmail(c.FromEmail) {
 			return c, errors.New("invalid `from_email`")
 			return c, errors.New("invalid `from_email`")
@@ -598,7 +598,7 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
 	}
 	}
 
 
 	camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
 	camp := models.Campaign{Body: c.Body, TemplateBody: tplTag}
-	if err := c.CompileTemplate(app.Manager.TemplateFuncs(&camp)); err != nil {
+	if err := c.CompileTemplate(app.manager.TemplateFuncs(&camp)); err != nil {
 		return c, fmt.Errorf("Error compiling campaign body: %v", err)
 		return c, fmt.Errorf("Error compiling campaign body: %v", err)
 	}
 	}
 
 
@@ -621,9 +621,9 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
 
 
 	// Fetch double opt-in lists from the given list IDs.
 	// Fetch double opt-in lists from the given list IDs.
 	var lists []models.List
 	var lists []models.List
-	err := app.Queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil)
+	err := app.queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
+		app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
 		return o, echo.NewHTTPError(http.StatusInternalServerError,
 		return o, echo.NewHTTPError(http.StatusInternalServerError,
 			"Error fetching opt-in lists.")
 			"Error fetching opt-in lists.")
 	}
 	}
@@ -648,11 +648,11 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
 
 
 	// Prepare sample opt-in message for the campaign.
 	// Prepare sample opt-in message for the campaign.
 	var b bytes.Buffer
 	var b bytes.Buffer
-	if err := app.NotifTpls.ExecuteTemplate(&b, "optin-campaign", struct {
+	if err := app.notifTpls.ExecuteTemplate(&b, "optin-campaign", struct {
 		Lists        []models.List
 		Lists        []models.List
 		OptinURLAttr template.HTMLAttr
 		OptinURLAttr template.HTMLAttr
 	}{lists, optinURLAttr}); err != nil {
 	}{lists, optinURLAttr}); err != nil {
-		app.Logger.Printf("error compiling 'optin-campaign' template: %v", err)
+		app.log.Printf("error compiling 'optin-campaign' template: %v", err)
 		return o, echo.NewHTTPError(http.StatusInternalServerError,
 		return o, echo.NewHTTPError(http.StatusInternalServerError,
 			"Error compiling opt-in campaign template.")
 			"Error compiling opt-in campaign template.")
 	}
 	}

+ 3 - 6
go.mod

@@ -7,8 +7,8 @@ require (
 	github.com/jinzhu/gorm v1.9.1
 	github.com/jinzhu/gorm v1.9.1
 	github.com/jmoiron/sqlx v1.2.0
 	github.com/jmoiron/sqlx v1.2.0
 	github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b
 	github.com/jordan-wright/email v0.0.0-20181027021455-480bedc4908b
-	github.com/knadh/goyesql v2.0.0+incompatible
-	github.com/knadh/koanf v0.4.4
+	github.com/knadh/goyesql/v2 v2.1.1
+	github.com/knadh/koanf v0.8.1
 	github.com/knadh/stuffbin v1.0.0
 	github.com/knadh/stuffbin v1.0.0
 	github.com/kr/pretty v0.1.0 // indirect
 	github.com/kr/pretty v0.1.0 // indirect
 	github.com/labstack/echo v3.3.10+incompatible
 	github.com/labstack/echo v3.3.10+incompatible
@@ -16,14 +16,11 @@ require (
 	github.com/lib/pq v1.0.0
 	github.com/lib/pq v1.0.0
 	github.com/mattn/go-colorable v0.0.9 // indirect
 	github.com/mattn/go-colorable v0.0.9 // indirect
 	github.com/mattn/go-isatty v0.0.4 // indirect
 	github.com/mattn/go-isatty v0.0.4 // indirect
-	github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727
+	github.com/rhnvrm/simples3 v0.5.0
 	github.com/spf13/pflag v1.0.3
 	github.com/spf13/pflag v1.0.3
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
 	github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 // indirect
-	golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd // indirect
 	golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
 	golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b // indirect
-	golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 // indirect
-	google.golang.org/appengine v1.4.0 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
 	gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b
 	gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b
 )
 )

+ 21 - 0
go.sum

@@ -7,6 +7,9 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0=
 github.com/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0=
 github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU=
 github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU=
+github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
 github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
 github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
@@ -14,6 +17,7 @@ github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaL
 github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
 github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
 github.com/jinzhu/gorm v1.9.1 h1:lDSDtsCt5AGGSKTs8AHlSDbbgif4G4+CKJ8ETBDVHTA=
@@ -24,8 +28,12 @@ github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8 h1:HVq7nA5uWjpo93WsWjv
 github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
 github.com/knadh/email v0.0.0-20200206100304-6d2c7064c2e8/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
 github.com/knadh/goyesql v2.0.0+incompatible h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk=
 github.com/knadh/goyesql v2.0.0+incompatible h1:hJFJrU8kaiLmvYt9I/1k1AB7q+qRhHs/afzTfQ3eGqk=
 github.com/knadh/goyesql v2.0.0+incompatible/go.mod h1:W0tSzU8l7lYH1Fihj+bdQzkzOwvirrsMNHwkuY22qoY=
 github.com/knadh/goyesql v2.0.0+incompatible/go.mod h1:W0tSzU8l7lYH1Fihj+bdQzkzOwvirrsMNHwkuY22qoY=
+github.com/knadh/goyesql/v2 v2.1.1 h1:Orp5ldaxPM4ozKHfu1m7p6iolJFXDGOpF3/jyOgO6ls=
+github.com/knadh/goyesql/v2 v2.1.1/go.mod h1:pMzCA130/ZhEIoMmSmbEFXor3A2dxl5L+JllAc/l64s=
 github.com/knadh/koanf v0.4.4 h1:Pg+eR7wuJtCGHLeip31K20eJojjZ3lXE8ILQQGj2PTM=
 github.com/knadh/koanf v0.4.4 h1:Pg+eR7wuJtCGHLeip31K20eJojjZ3lXE8ILQQGj2PTM=
 github.com/knadh/koanf v0.4.4/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
 github.com/knadh/koanf v0.4.4/go.mod h1:Qd5yvXN39ZzjoRJdXMKN2QqHzQKhSx/K8fU5gyn4LPs=
+github.com/knadh/koanf v0.8.1 h1:4VLACWqrkWRQIup3ooq6lOnaSbOJSNO+YVXnJn/NPZ8=
+github.com/knadh/koanf v0.8.1/go.mod h1:kVvmDbXnBtW49Czi4c1M+nnOWF0YSNZ8BaKvE/bCO1w=
 github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
 github.com/knadh/stuffbin v1.0.0 h1:NQon6PTpLXies4bRFhS3VpLCf6y+jn6YVXU3i2wPQ+M=
 github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
 github.com/knadh/stuffbin v1.0.0/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
 github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
@@ -53,6 +61,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727 h1:2josYcx2gm3CT0WMqi0jBagvg50V3UMWlYN/CnBEbSI=
 github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727 h1:2josYcx2gm3CT0WMqi0jBagvg50V3UMWlYN/CnBEbSI=
 github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727/go.mod h1:iphavgjkW1uvoIiqLUX6D42XuuI9Cr+B/63xw3gb9qA=
 github.com/rhnvrm/simples3 v0.2.4-0.20191018074503-3d5b071ef727/go.mod h1:iphavgjkW1uvoIiqLUX6D42XuuI9Cr+B/63xw3gb9qA=
+github.com/rhnvrm/simples3 v0.5.0 h1:X+WX0hqoKScdoJAw/G3GArfZ6Ygsn8q+6MdocTMKXOw=
+github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -64,16 +74,27 @@ github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QI
 github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
 github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
 golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
 golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd h1:VtIkGDhk0ph3t+THbvXHfMZ8QHgsBO39Nh52+74pq7w=
 golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM=
 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225 h1:kNX+jCowfMYzvlSvJu5pQWEmyWFrBXJ3PBy10xKMXK8=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
 golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
 golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24 h1:R8bzl0244nw47n1xKs1MUMAaTNgjavKcN/aX2Ss3+Fo=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=

+ 4 - 4
handlers.go

@@ -35,7 +35,7 @@ type pagination struct {
 var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
 var reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
 
 
 // registerHandlers registers HTTP handlers.
 // registerHandlers registers HTTP handlers.
-func registerHandlers(e *echo.Echo) {
+func registerHTTPHandlers(e *echo.Echo) {
 	e.GET("/", handleIndexPage)
 	e.GET("/", handleIndexPage)
 	e.GET("/api/config.js", handleGetConfigScript)
 	e.GET("/api/config.js", handleGetConfigScript)
 	e.GET("/api/dashboard/stats", handleGetDashboardStats)
 	e.GET("/api/dashboard/stats", handleGetDashboardStats)
@@ -128,7 +128,7 @@ func registerHandlers(e *echo.Echo) {
 func handleIndexPage(c echo.Context) error {
 func handleIndexPage(c echo.Context) error {
 	app := c.Get("app").(*App)
 	app := c.Get("app").(*App)
 
 
-	b, err := app.FS.Read("/frontend/index.html")
+	b, err := app.fs.Read("/frontend/index.html")
 	if err != nil {
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 	}
 	}
@@ -161,8 +161,8 @@ func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc
 		)
 		)
 
 
 		var exists bool
 		var exists bool
-		if err := app.Queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
-			app.Logger.Printf("error checking subscriber existence: %v", err)
+		if err := app.queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
+			app.log.Printf("error checking subscriber existence: %v", err)
 			return c.Render(http.StatusInternalServerError, tplMessage,
 			return c.Render(http.StatusInternalServerError, tplMessage,
 				makeMsgTpl("Error", "",
 				makeMsgTpl("Error", "",
 					`Error processing request. Please retry.`))
 					`Error processing request. Please retry.`))

+ 7 - 7
import.go

@@ -25,7 +25,7 @@ func handleImportSubscribers(c echo.Context) error {
 	app := c.Get("app").(*App)
 	app := c.Get("app").(*App)
 
 
 	// Is an import already running?
 	// Is an import already running?
-	if app.Importer.GetStats().Status == subimporter.StatusImporting {
+	if app.importer.GetStats().Status == subimporter.StatusImporting {
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			"An import is already running. Wait for it to finish or stop it before trying again.")
 			"An import is already running. Wait for it to finish or stop it before trying again.")
 	}
 	}
@@ -71,7 +71,7 @@ func handleImportSubscribers(c echo.Context) error {
 	}
 	}
 
 
 	// Start the importer session.
 	// Start the importer session.
-	impSess, err := app.Importer.NewSession(file.Filename, r.Mode, r.ListIDs)
+	impSess, err := app.importer.NewSession(file.Filename, r.Mode, r.ListIDs)
 	if err != nil {
 	if err != nil {
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error starting import session: %v", err))
 			fmt.Sprintf("Error starting import session: %v", err))
@@ -95,14 +95,14 @@ func handleImportSubscribers(c echo.Context) error {
 		go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0]))
 		go impSess.LoadCSV(dir+"/"+files[0], rune(r.Delim[0]))
 	}
 	}
 
 
-	return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()})
+	return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
 }
 }
 
 
 // handleGetImportSubscribers returns import statistics.
 // handleGetImportSubscribers returns import statistics.
 func handleGetImportSubscribers(c echo.Context) error {
 func handleGetImportSubscribers(c echo.Context) error {
 	var (
 	var (
 		app = c.Get("app").(*App)
 		app = c.Get("app").(*App)
-		s   = app.Importer.GetStats()
+		s   = app.importer.GetStats()
 	)
 	)
 	return c.JSON(http.StatusOK, okResp{s})
 	return c.JSON(http.StatusOK, okResp{s})
 }
 }
@@ -110,7 +110,7 @@ func handleGetImportSubscribers(c echo.Context) error {
 // handleGetImportSubscriberStats returns import statistics.
 // handleGetImportSubscriberStats returns import statistics.
 func handleGetImportSubscriberStats(c echo.Context) error {
 func handleGetImportSubscriberStats(c echo.Context) error {
 	app := c.Get("app").(*App)
 	app := c.Get("app").(*App)
-	return c.JSON(http.StatusOK, okResp{string(app.Importer.GetLogs())})
+	return c.JSON(http.StatusOK, okResp{string(app.importer.GetLogs())})
 }
 }
 
 
 // handleStopImportSubscribers sends a stop signal to the importer.
 // handleStopImportSubscribers sends a stop signal to the importer.
@@ -118,6 +118,6 @@ func handleGetImportSubscriberStats(c echo.Context) error {
 // is finished, it's state is cleared.
 // is finished, it's state is cleared.
 func handleStopImportSubscribers(c echo.Context) error {
 func handleStopImportSubscribers(c echo.Context) error {
 	app := c.Get("app").(*App)
 	app := c.Get("app").(*App)
-	app.Importer.Stop()
-	return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()})
+	app.importer.Stop()
+	return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
 }
 }

+ 310 - 0
init.go

@@ -0,0 +1,310 @@
+package main
+
+import (
+	"fmt"
+	"html/template"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	_ "github.com/jinzhu/gorm/dialects/postgres"
+	"github.com/jmoiron/sqlx"
+	"github.com/knadh/goyesql/v2"
+	goyesqlx "github.com/knadh/goyesql/v2/sqlx"
+	"github.com/knadh/koanf/maps"
+	"github.com/knadh/listmonk/manager"
+	"github.com/knadh/listmonk/media"
+	"github.com/knadh/listmonk/media/providers/filesystem"
+	"github.com/knadh/listmonk/media/providers/s3"
+	"github.com/knadh/listmonk/messenger"
+	"github.com/knadh/listmonk/subimporter"
+	"github.com/knadh/stuffbin"
+	"github.com/labstack/echo"
+)
+
+const (
+	queryFilePath = "queries.sql"
+)
+
+// initFileSystem initializes the stuffbin FileSystem to provide
+// access to bunded static assets to the app.
+func initFS() stuffbin.FileSystem {
+	// Get the executable's path.
+	path, err := os.Executable()
+	if err != nil {
+		log.Fatalf("error getting executable path: %v", err)
+	}
+
+	fs, err := stuffbin.UnStuff(path)
+	if err == nil {
+		return fs
+	}
+
+	// Running in local mode. Load the required static assets into
+	// the in-memory stuffbin.FileSystem.
+	lo.Printf("unable to initialize embedded filesystem: %v", err)
+	lo.Printf("using local filesystem for static assets")
+	files := []string{
+		"config.toml.sample",
+		"queries.sql",
+		"schema.sql",
+		"email-templates",
+		"public",
+
+		// The frontend app's static assets are aliased to /frontend
+		// so that they are accessible at localhost:port/frontend/static/ ...
+		"frontend/build:/frontend",
+	}
+
+	fs, err = stuffbin.NewLocalFS("/", files...)
+	if err != nil {
+		lo.Fatalf("failed to initialize local file for assets: %v", err)
+	}
+
+	return fs
+}
+
+// initDB initializes the main DB connection pool and parse and loads the app's
+// SQL queries into a prepared query map.
+func initDB() *sqlx.DB {
+	var dbCfg dbConf
+	if err := ko.Unmarshal("db", &dbCfg); err != nil {
+		log.Fatalf("error loading db config: %v", err)
+	}
+	db, err := connectDB(dbCfg)
+	if err != nil {
+		lo.Fatalf("error connecting to DB: %v", err)
+	}
+
+	return db
+}
+
+// initQueries loads named SQL queries from the queries file and optionally
+// prepares them.
+func initQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem, prepareQueries bool) (goyesql.Queries, *Queries) {
+	// Load SQL queries.
+	qB, err := fs.Read(sqlFile)
+	if err != nil {
+		lo.Fatalf("error reading SQL file %s: %v", sqlFile, err)
+	}
+	qMap, err := goyesql.ParseBytes(qB)
+	if err != nil {
+		lo.Fatalf("error parsing SQL queries: %v", err)
+	}
+
+	if !prepareQueries {
+		return qMap, nil
+	}
+
+	// Prepare queries.
+	var q Queries
+	if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
+		lo.Fatalf("error preparing SQL queries: %v", err)
+	}
+	return qMap, &q
+}
+
+// constants contains static, constant config values required by the app.
+type constants struct {
+	RootURL      string   `koanf:"root"`
+	LogoURL      string   `koanf:"logo_url"`
+	FaviconURL   string   `koanf:"favicon_url"`
+	FromEmail    string   `koanf:"from_email"`
+	NotifyEmails []string `koanf:"notify_emails"`
+	Privacy      struct {
+		AllowBlacklist bool            `koanf:"allow_blacklist"`
+		AllowExport    bool            `koanf:"allow_export"`
+		AllowWipe      bool            `koanf:"allow_wipe"`
+		Exportable     map[string]bool `koanf:"-"`
+	} `koanf:"privacy"`
+
+	UnsubURL     string
+	LinkTrackURL string
+	ViewTrackURL string
+	OptinURL     string
+}
+
+func initConstants() *constants {
+	// Read constants.
+	var c constants
+	if err := ko.Unmarshal("app", &c); err != nil {
+		log.Fatalf("error loading app config: %v", err)
+	}
+	if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
+		log.Fatalf("error loading app config: %v", err)
+	}
+	c.RootURL = strings.TrimRight(c.RootURL, "/")
+	c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
+
+	// Static URLS.
+	// url.com/subscription/{campaign_uuid}/{subscriber_uuid}
+	c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", c.RootURL)
+
+	// url.com/subscription/optin/{subscriber_uuid}
+	c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", c.RootURL)
+
+	// url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
+	c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", c.RootURL)
+
+	// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
+	c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
+
+	return &c
+}
+
+// initCampaignManager initializes the campaign manager.
+func initCampaignManager(app *App) *manager.Manager {
+	campNotifCB := func(subject string, data interface{}) error {
+		return sendNotification(app.constants.NotifyEmails, subject, notifTplCampaign, data, app)
+	}
+	return manager.New(manager.Config{
+		Concurrency:   ko.Int("app.concurrency"),
+		MaxSendErrors: ko.Int("app.max_send_errors"),
+		FromEmail:     app.constants.FromEmail,
+		UnsubURL:      app.constants.UnsubURL,
+		OptinURL:      app.constants.OptinURL,
+		LinkTrackURL:  app.constants.LinkTrackURL,
+		ViewTrackURL:  app.constants.ViewTrackURL,
+	}, newManagerDB(app.queries), campNotifCB, lo)
+
+}
+
+// initImporter initializes the bulk subscriber importer.
+func initImporter(app *App) *subimporter.Importer {
+	return subimporter.New(app.queries.UpsertSubscriber.Stmt,
+		app.queries.UpsertBlacklistSubscriber.Stmt,
+		app.queries.UpdateListsDate.Stmt,
+		app.db.DB,
+		func(subject string, data interface{}) error {
+			go sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data, app)
+			return nil
+		})
+}
+
+// initMessengers initializes various messaging backends.
+func initMessengers(r *manager.Manager) messenger.Messenger {
+	// Load SMTP configurations for the default e-mail Messenger.
+	var (
+		mapKeys = ko.MapKeys("smtp")
+		srv     = make([]messenger.Server, 0, len(mapKeys))
+	)
+
+	for _, name := range mapKeys {
+		if !ko.Bool(fmt.Sprintf("smtp.%s.enabled", name)) {
+			lo.Printf("skipped SMTP: %s", name)
+			continue
+		}
+
+		var s messenger.Server
+		if err := ko.Unmarshal("smtp."+name, &s); err != nil {
+			lo.Fatalf("error loading SMTP: %v", err)
+		}
+		s.Name = name
+		s.SendTimeout *= time.Millisecond
+		srv = append(srv, s)
+
+		lo.Printf("loaded SMTP: %s (%s@%s)", s.Name, s.Username, s.Host)
+	}
+
+	// Initialize the default e-mail messenger.
+	msgr, err := messenger.NewEmailer(srv...)
+	if err != nil {
+		lo.Fatalf("error loading e-mail messenger: %v", err)
+	}
+	if err := r.AddMessenger(msgr); err != nil {
+		lo.Printf("error registering messenger %s", err)
+	}
+
+	return msgr
+}
+
+// initMediaStore initializes Upload manager with a custom backend.
+func initMediaStore() media.Store {
+	switch provider := ko.String("upload.provider"); provider {
+	case "s3":
+		var opts s3.Opts
+		ko.Unmarshal("upload.s3", &opts)
+		uplder, err := s3.NewS3Store(opts)
+		if err != nil {
+			lo.Fatalf("error initializing s3 upload provider %s", err)
+		}
+		return uplder
+
+	case "filesystem":
+		var opts filesystem.Opts
+		ko.Unmarshal("upload.filesystem", &opts)
+		opts.UploadPath = filepath.Clean(opts.UploadPath)
+		opts.UploadURI = filepath.Clean(opts.UploadURI)
+		uplder, err := filesystem.NewDiskStore(opts)
+		if err != nil {
+			lo.Fatalf("error initializing filesystem upload provider %s", err)
+		}
+		return uplder
+
+	default:
+		lo.Fatalf("unknown provider. please select one of either filesystem or s3")
+	}
+	return nil
+}
+
+// initNotifTemplates compiles and returns e-mail notification templates that are
+// used for sending ad-hoc notifications to admins and subscribers.
+func initNotifTemplates(path string, fs stuffbin.FileSystem, cs *constants) *template.Template {
+	// Register utility functions that the e-mail templates can use.
+	funcs := template.FuncMap{
+		"RootURL": func() string {
+			return cs.RootURL
+		},
+		"LogoURL": func() string {
+			return cs.LogoURL
+		}}
+
+	tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/email-templates/*.html")
+	if err != nil {
+		lo.Fatalf("error parsing e-mail notif templates: %v", err)
+	}
+	return tpl
+}
+
+// initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
+func initHTTPServer(app *App) {
+	// Initialize the HTTP server.
+	var srv = echo.New()
+	srv.HideBanner = true
+
+	// Register app (*App) to be injected into all HTTP handlers.
+	srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+		return func(c echo.Context) error {
+			c.Set("app", app)
+			return next(c)
+		}
+	})
+
+	// Parse and load user facing templates.
+	tpl, err := stuffbin.ParseTemplatesGlob(nil, app.fs, "/public/templates/*.html")
+	if err != nil {
+		lo.Fatalf("error parsing public templates: %v", err)
+	}
+	srv.Renderer = &tplRenderer{
+		templates:  tpl,
+		RootURL:    app.constants.RootURL,
+		LogoURL:    app.constants.LogoURL,
+		FaviconURL: app.constants.FaviconURL}
+
+	// Initialize the static file server.
+	fSrv := app.fs.FileServer()
+	srv.GET("/public/*", echo.WrapHandler(fSrv))
+	srv.GET("/frontend/*", echo.WrapHandler(fSrv))
+	if ko.String("upload.provider") == "filesystem" {
+		srv.Static(ko.String("upload.filesystem.upload_uri"),
+			ko.String("upload.filesystem.upload_path"))
+	}
+
+	// Register all HTTP handlers.
+	registerHTTPHandlers(srv)
+
+	// Start the server.
+	srv.Logger.Fatal(srv.Start(ko.String("app.address")))
+}

+ 22 - 23
install.go

@@ -10,14 +10,17 @@ import (
 
 
 	"github.com/gofrs/uuid"
 	"github.com/gofrs/uuid"
 	"github.com/jmoiron/sqlx"
 	"github.com/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
+	goyesqlx "github.com/knadh/goyesql/v2/sqlx"
 	"github.com/knadh/listmonk/models"
 	"github.com/knadh/listmonk/models"
+	"github.com/knadh/stuffbin"
 	"github.com/lib/pq"
 	"github.com/lib/pq"
 )
 )
 
 
 // install runs the first time setup of creating and
 // install runs the first time setup of creating and
 // migrating the database and creating the super user.
 // migrating the database and creating the super user.
-func install(app *App, qMap goyesql.Queries, prompt bool) {
+func install(db *sqlx.DB, fs stuffbin.FileSystem, prompt bool) {
+	qMap, _ := initQueries(queryFilePath, db, fs, false)
+
 	fmt.Println("")
 	fmt.Println("")
 	fmt.Println("** First time installation **")
 	fmt.Println("** First time installation **")
 	fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
 	fmt.Printf("** IMPORTANT: This will wipe existing listmonk tables and types in the DB '%s' **",
@@ -28,7 +31,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		var ok string
 		var ok string
 		fmt.Print("Continue (y/n)?  ")
 		fmt.Print("Continue (y/n)?  ")
 		if _, err := fmt.Scanf("%s", &ok); err != nil {
 		if _, err := fmt.Scanf("%s", &ok); err != nil {
-			logger.Fatalf("Error reading value from terminal: %v", err)
+			lo.Fatalf("Error reading value from terminal: %v", err)
 		}
 		}
 		if strings.ToLower(ok) != "y" {
 		if strings.ToLower(ok) != "y" {
 			fmt.Println("Installation cancelled.")
 			fmt.Println("Installation cancelled.")
@@ -37,15 +40,15 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 	}
 	}
 
 
 	// Migrate the tables.
 	// Migrate the tables.
-	err := installMigrate(app.DB, app)
+	err := installMigrate(db, fs)
 	if err != nil {
 	if err != nil {
-		logger.Fatalf("Error migrating DB schema: %v", err)
+		lo.Fatalf("Error migrating DB schema: %v", err)
 	}
 	}
 
 
 	// Load the queries.
 	// Load the queries.
 	var q Queries
 	var q Queries
-	if err := scanQueriesToStruct(&q, qMap, app.DB.Unsafe()); err != nil {
-		logger.Fatalf("error loading SQL queries: %v", err)
+	if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
+		lo.Fatalf("error loading SQL queries: %v", err)
 	}
 	}
 
 
 	// Sample list.
 	// Sample list.
@@ -60,7 +63,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		models.ListOptinSingle,
 		models.ListOptinSingle,
 		pq.StringArray{"test"},
 		pq.StringArray{"test"},
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("Error creating list: %v", err)
+		lo.Fatalf("Error creating list: %v", err)
 	}
 	}
 
 
 	if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()),
 	if err := q.CreateList.Get(&optinList, uuid.Must(uuid.NewV4()),
@@ -69,7 +72,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		models.ListOptinDouble,
 		models.ListOptinDouble,
 		pq.StringArray{"test"},
 		pq.StringArray{"test"},
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("Error creating list: %v", err)
+		lo.Fatalf("Error creating list: %v", err)
 	}
 	}
 
 
 	// Sample subscriber.
 	// Sample subscriber.
@@ -80,7 +83,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		`{"type": "known", "good": true, "city": "Bengaluru"}`,
 		`{"type": "known", "good": true, "city": "Bengaluru"}`,
 		pq.Int64Array{int64(defList)},
 		pq.Int64Array{int64(defList)},
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("Error creating subscriber: %v", err)
+		lo.Fatalf("Error creating subscriber: %v", err)
 	}
 	}
 	if _, err := q.UpsertSubscriber.Exec(
 	if _, err := q.UpsertSubscriber.Exec(
 		uuid.Must(uuid.NewV4()),
 		uuid.Must(uuid.NewV4()),
@@ -89,7 +92,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
 		`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
 		pq.Int64Array{int64(optinList)},
 		pq.Int64Array{int64(optinList)},
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("Error creating subscriber: %v", err)
+		lo.Fatalf("Error creating subscriber: %v", err)
 	}
 	}
 
 
 	// Default template.
 	// Default template.
@@ -103,10 +106,10 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		"Default template",
 		"Default template",
 		string(tplBody),
 		string(tplBody),
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("error creating default template: %v", err)
+		lo.Fatalf("error creating default template: %v", err)
 	}
 	}
 	if _, err := q.SetDefaultTemplate.Exec(tplID); err != nil {
 	if _, err := q.SetDefaultTemplate.Exec(tplID); err != nil {
-		logger.Fatalf("error setting default template: %v", err)
+		lo.Fatalf("error setting default template: %v", err)
 	}
 	}
 
 
 	// Sample campaign.
 	// Sample campaign.
@@ -126,17 +129,17 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		1,
 		1,
 		pq.Int64Array{1},
 		pq.Int64Array{1},
 	); err != nil {
 	); err != nil {
-		logger.Fatalf("error creating sample campaign: %v", err)
+		lo.Fatalf("error creating sample campaign: %v", err)
 	}
 	}
 
 
-	logger.Printf("Setup complete")
-	logger.Printf(`Run the program and access the dashboard at %s`, ko.String("app.address"))
+	lo.Printf("Setup complete")
+	lo.Printf(`Run the program and access the dashboard at %s`, ko.MustString("app.address"))
 
 
 }
 }
 
 
 // installMigrate executes the SQL schema and creates the necessary tables and types.
 // installMigrate executes the SQL schema and creates the necessary tables and types.
-func installMigrate(db *sqlx.DB, app *App) error {
-	q, err := app.FS.Read("/schema.sql")
+func installMigrate(db *sqlx.DB, fs stuffbin.FileSystem) error {
+	q, err := fs.Read("/schema.sql")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -156,11 +159,7 @@ func newConfigFile() error {
 
 
 	// Initialize the static file system into which all
 	// Initialize the static file system into which all
 	// required static assets (.sql, .js files etc.) are loaded.
 	// required static assets (.sql, .js files etc.) are loaded.
-	fs, err := initFileSystem(os.Args[0])
-	if err != nil {
-		return err
-	}
-
+	fs := initFS()
 	b, err := fs.Read("config.toml.sample")
 	b, err := fs.Read("config.toml.sample")
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)
 		return fmt.Errorf("error reading sample config (is binary stuffed?): %v", err)

+ 9 - 9
lists.go

@@ -37,9 +37,9 @@ func handleGetLists(c echo.Context) error {
 		single = true
 		single = true
 	}
 	}
 
 
-	err := app.Queries.GetLists.Select(&out.Results, listID, pg.Offset, pg.Limit)
+	err := app.queries.GetLists.Select(&out.Results, listID, pg.Offset, pg.Limit)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error fetching lists: %v", err)
+		app.log.Printf("error fetching lists: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
 	}
 	}
@@ -87,20 +87,20 @@ func handleCreateList(c echo.Context) error {
 
 
 	uu, err := uuid.NewV4()
 	uu, err := uuid.NewV4()
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error generating UUID: %v", err)
+		app.log.Printf("error generating UUID: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 	}
 	}
 
 
 	// Insert and read ID.
 	// Insert and read ID.
 	var newID int
 	var newID int
 	o.UUID = uu.String()
 	o.UUID = uu.String()
-	if err := app.Queries.CreateList.Get(&newID,
+	if err := app.queries.CreateList.Get(&newID,
 		o.UUID,
 		o.UUID,
 		o.Name,
 		o.Name,
 		o.Type,
 		o.Type,
 		o.Optin,
 		o.Optin,
 		pq.StringArray(normalizeTags(o.Tags))); err != nil {
 		pq.StringArray(normalizeTags(o.Tags))); err != nil {
-		app.Logger.Printf("error creating list: %v", err)
+		app.log.Printf("error creating list: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error creating list: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error creating list: %s", pqErrMsg(err)))
 	}
 	}
@@ -128,10 +128,10 @@ func handleUpdateList(c echo.Context) error {
 		return err
 		return err
 	}
 	}
 
 
-	res, err := app.Queries.UpdateList.Exec(id,
+	res, err := app.queries.UpdateList.Exec(id,
 		o.Name, o.Type, o.Optin, pq.StringArray(normalizeTags(o.Tags)))
 		o.Name, o.Type, o.Optin, pq.StringArray(normalizeTags(o.Tags)))
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating list: %v", err)
+		app.log.Printf("error updating list: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error updating list: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating list: %s", pqErrMsg(err)))
 	}
 	}
@@ -165,8 +165,8 @@ func handleDeleteLists(c echo.Context) error {
 		ids = append(ids, id)
 		ids = append(ids, id)
 	}
 	}
 
 
-	if _, err := app.Queries.DeleteLists.Exec(ids); err != nil {
-		app.Logger.Printf("error deleting lists: %v", err)
+	if _, err := app.queries.DeleteLists.Exec(ids); err != nil {
+		app.log.Printf("error deleting lists: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error deleting: %v", err))
 			fmt.Sprintf("Error deleting: %v", err))
 	}
 	}

+ 51 - 284
main.go

@@ -5,68 +5,41 @@ import (
 	"html/template"
 	"html/template"
 	"log"
 	"log"
 	"os"
 	"os"
-	"path/filepath"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
-	_ "github.com/jinzhu/gorm/dialects/postgres"
 	"github.com/jmoiron/sqlx"
 	"github.com/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
 	"github.com/knadh/koanf"
 	"github.com/knadh/koanf"
-	"github.com/knadh/koanf/maps"
 	"github.com/knadh/koanf/parsers/toml"
 	"github.com/knadh/koanf/parsers/toml"
 	"github.com/knadh/koanf/providers/env"
 	"github.com/knadh/koanf/providers/env"
 	"github.com/knadh/koanf/providers/file"
 	"github.com/knadh/koanf/providers/file"
 	"github.com/knadh/koanf/providers/posflag"
 	"github.com/knadh/koanf/providers/posflag"
 	"github.com/knadh/listmonk/manager"
 	"github.com/knadh/listmonk/manager"
 	"github.com/knadh/listmonk/media"
 	"github.com/knadh/listmonk/media"
-	"github.com/knadh/listmonk/media/providers/filesystem"
-	"github.com/knadh/listmonk/media/providers/s3"
 	"github.com/knadh/listmonk/messenger"
 	"github.com/knadh/listmonk/messenger"
 	"github.com/knadh/listmonk/subimporter"
 	"github.com/knadh/listmonk/subimporter"
 	"github.com/knadh/stuffbin"
 	"github.com/knadh/stuffbin"
-	"github.com/labstack/echo"
 	flag "github.com/spf13/pflag"
 	flag "github.com/spf13/pflag"
 )
 )
 
 
-type constants struct {
-	RootURL      string `koanf:"root"`
-	LogoURL      string `koanf:"logo_url"`
-	FaviconURL   string `koanf:"favicon_url"`
-	UnsubURL     string
-	LinkTrackURL string
-	ViewTrackURL string
-	OptinURL     string
-	FromEmail    string         `koanf:"from_email"`
-	NotifyEmails []string       `koanf:"notify_emails"`
-	Privacy      privacyOptions `koanf:"privacy"`
-}
-
-type privacyOptions struct {
-	AllowBlacklist bool            `koanf:"allow_blacklist"`
-	AllowExport    bool            `koanf:"allow_export"`
-	AllowWipe      bool            `koanf:"allow_wipe"`
-	Exportable     map[string]bool `koanf:"-"`
-}
-
 // App contains the "global" components that are
 // App contains the "global" components that are
 // passed around, especially through HTTP handlers.
 // passed around, especially through HTTP handlers.
 type App struct {
 type App struct {
-	Constants *constants
-	DB        *sqlx.DB
-	Queries   *Queries
-	Importer  *subimporter.Importer
-	Manager   *manager.Manager
-	FS        stuffbin.FileSystem
-	Logger    *log.Logger
-	NotifTpls *template.Template
-	Messenger messenger.Messenger
-	Media     media.Store
+	fs        stuffbin.FileSystem
+	db        *sqlx.DB
+	queries   *Queries
+	constants *constants
+	manager   *manager.Manager
+	importer  *subimporter.Importer
+	messenger messenger.Messenger
+	media     media.Store
+	notifTpls *template.Template
+	log       *log.Logger
 }
 }
 
 
 var (
 var (
 	// Global logger.
 	// Global logger.
-	logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
+	lo = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile)
 
 
 	// Global configuration reader.
 	// Global configuration reader.
 	ko = koanf.New(".")
 	ko = koanf.New(".")
@@ -75,14 +48,14 @@ var (
 )
 )
 
 
 func init() {
 func init() {
-	// Register --help handler.
 	f := flag.NewFlagSet("config", flag.ContinueOnError)
 	f := flag.NewFlagSet("config", flag.ContinueOnError)
 	f.Usage = func() {
 	f.Usage = func() {
+		// Register --help handler.
 		fmt.Println(f.FlagUsages())
 		fmt.Println(f.FlagUsages())
 		os.Exit(0)
 		os.Exit(0)
 	}
 	}
 
 
-	// Setup the default configuration.
+	// Register the commandline flags.
 	f.StringSlice("config", []string{"config.toml"},
 	f.StringSlice("config", []string{"config.toml"},
 		"Path to one or more config files (will be merged in order)")
 		"Path to one or more config files (will be merged in order)")
 	f.Bool("install", false, "Run first time installation")
 	f.Bool("install", false, "Run first time installation")
@@ -90,9 +63,8 @@ func init() {
 	f.Bool("new-config", false, "Generate sample config file")
 	f.Bool("new-config", false, "Generate sample config file")
 	f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install")
 	f.Bool("yes", false, "Assume 'yes' to prompts, eg: during --install")
 
 
-	// Process flags.
 	if err := f.Parse(os.Args[1:]); err != nil {
 	if err := f.Parse(os.Args[1:]); err != nil {
-		logger.Fatalf("error loading flags: %v", err)
+		lo.Fatalf("error loading flags: %v", err)
 	}
 	}
 
 
 	// Display version.
 	// Display version.
@@ -104,277 +76,72 @@ func init() {
 	// Generate new config.
 	// Generate new config.
 	if ok, _ := f.GetBool("new-config"); ok {
 	if ok, _ := f.GetBool("new-config"); ok {
 		if err := newConfigFile(); err != nil {
 		if err := newConfigFile(); err != nil {
-			logger.Println(err)
+			lo.Println(err)
 			os.Exit(1)
 			os.Exit(1)
 		}
 		}
-		logger.Println("generated config.toml. Edit and run --install")
+		lo.Println("generated config.toml. Edit and run --install")
 		os.Exit(0)
 		os.Exit(0)
 	}
 	}
 
 
 	// Load config files.
 	// Load config files.
 	cFiles, _ := f.GetStringSlice("config")
 	cFiles, _ := f.GetStringSlice("config")
 	for _, f := range cFiles {
 	for _, f := range cFiles {
-		logger.Printf("reading config: %s", f)
+		lo.Printf("reading config: %s", f)
 		if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
 		if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
 			if os.IsNotExist(err) {
 			if os.IsNotExist(err) {
-				logger.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.")
+				lo.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.")
 			}
 			}
-			logger.Fatalf("error loadng config from file: %v.", err)
+			lo.Fatalf("error loadng config from file: %v.", err)
 		}
 		}
 	}
 	}
+
 	// Load environment variables and merge into the loaded config.
 	// Load environment variables and merge into the loaded config.
 	if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
 	if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
 		return strings.Replace(strings.ToLower(
 		return strings.Replace(strings.ToLower(
 			strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
 			strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
 	}), nil); err != nil {
 	}), nil); err != nil {
-		logger.Fatalf("error loading config from env: %v", err)
+		lo.Fatalf("error loading config from env: %v", err)
 	}
 	}
 	if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
 	if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
-		logger.Fatalf("error loading config: %v", err)
+		lo.Fatalf("error loading config: %v", err)
 	}
 	}
 }
 }
 
 
-// initFileSystem initializes the stuffbin FileSystem to provide
-// access to bunded static assets to the app.
-func initFileSystem(binPath string) (stuffbin.FileSystem, error) {
-	fs, err := stuffbin.UnStuff(binPath)
-	if err == nil {
-		return fs, nil
-	}
-
-	// Running in local mode. Load the required static assets into
-	// the in-memory stuffbin.FileSystem.
-	logger.Printf("unable to initialize embedded filesystem: %v", err)
-	logger.Printf("using local filesystem for static assets")
-	files := []string{
-		"config.toml.sample",
-		"queries.sql",
-		"schema.sql",
-		"email-templates",
-		"public",
-
-		// The frontend app's static assets are aliased to /frontend
-		// so that they are accessible at localhost:port/frontend/static/ ...
-		"frontend/build:/frontend",
-	}
-
-	fs, err = stuffbin.NewLocalFS("/", files...)
-	if err != nil {
-		return nil, fmt.Errorf("failed to initialize local file for assets: %v", err)
-	}
-
-	return fs, nil
-}
-
-// initMessengers initializes various messaging backends.
-func initMessengers(r *manager.Manager) messenger.Messenger {
-	// Load SMTP configurations for the default e-mail Messenger.
+func main() {
+	// Initialize the DB and the filesystem that are required by the installer
+	// and the app.
 	var (
 	var (
-		mapKeys = ko.MapKeys("smtp")
-		srv     = make([]messenger.Server, 0, len(mapKeys))
+		fs = initFS()
+		db = initDB()
 	)
 	)
-
-	for _, name := range mapKeys {
-		if !ko.Bool(fmt.Sprintf("smtp.%s.enabled", name)) {
-			logger.Printf("skipped SMTP: %s", name)
-			continue
-		}
-
-		var s messenger.Server
-		if err := ko.Unmarshal("smtp."+name, &s); err != nil {
-			logger.Fatalf("error loading SMTP: %v", err)
-		}
-		s.Name = name
-		s.SendTimeout *= time.Millisecond
-		srv = append(srv, s)
-
-		logger.Printf("loaded SMTP: %s (%s@%s)", s.Name, s.Username, s.Host)
-	}
-
-	msgr, err := messenger.NewEmailer(srv...)
-	if err != nil {
-		logger.Fatalf("error loading e-mail messenger: %v", err)
-	}
-	if err := r.AddMessenger(msgr); err != nil {
-		logger.Printf("error registering messenger %s", err)
-	}
-
-	return msgr
-}
-
-// initMediaStore initializes Upload manager with a custom backend.
-func initMediaStore() media.Store {
-	switch provider := ko.String("upload.provider"); provider {
-	case "s3":
-		var opts s3.Opts
-		ko.Unmarshal("upload.s3", &opts)
-		uplder, err := s3.NewS3Store(opts)
-		if err != nil {
-			logger.Fatalf("error initializing s3 upload provider %s", err)
-		}
-		return uplder
-	case "filesystem":
-		var opts filesystem.Opts
-		ko.Unmarshal("upload.filesystem", &opts)
-		opts.UploadPath = filepath.Clean(opts.UploadPath)
-		opts.UploadURI = filepath.Clean(opts.UploadURI)
-		uplder, err := filesystem.NewDiskStore(opts)
-		if err != nil {
-			logger.Fatalf("error initializing filesystem upload provider %s", err)
-		}
-		return uplder
-	default:
-		logger.Fatalf("unknown provider. please select one of either filesystem or s3")
-	}
-	return nil
-}
-
-func main() {
-	// Connect to the DB.
-	var dbCfg dbConf
-	if err := ko.Unmarshal("db", &dbCfg); err != nil {
-		log.Fatalf("error loading db config: %v", err)
-	}
-	db, err := connectDB(dbCfg)
-	if err != nil {
-		logger.Fatalf("error connecting to DB: %v", err)
-	}
 	defer db.Close()
 	defer db.Close()
 
 
-	var c constants
-	if err := ko.Unmarshal("app", &c); err != nil {
-		log.Fatalf("error loading app config: %v", err)
-	}
-	if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
-		log.Fatalf("error loading app config: %v", err)
-	}
-	c.RootURL = strings.TrimRight(c.RootURL, "/")
-	c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
-
-	// Initialize the static file system into which all
-	// required static assets (.sql, .js files etc.) are loaded.
-	fs, err := initFileSystem(os.Args[0])
-	if err != nil {
-		logger.Fatal(err)
-	}
-
-	// Initialize the app context that's passed around.
-	app := &App{
-		Constants: &c,
-		DB:        db,
-		Logger:    logger,
-		FS:        fs,
-	}
-
-	// Load SQL queries.
-	qB, err := fs.Read("/queries.sql")
-	if err != nil {
-		logger.Fatalf("error reading queries.sql: %v", err)
-	}
-	qMap, err := goyesql.ParseBytes(qB)
-	if err != nil {
-		logger.Fatalf("error parsing SQL queries: %v", err)
-	}
-
-	// Run the first time installation.
+	// Installer mode? This runs before the SQL queries are loaded and prepared
+	// as the installer needs to work on an empty DB.
 	if ko.Bool("install") {
 	if ko.Bool("install") {
-		install(app, qMap, !ko.Bool("yes"))
+		install(db, fs, !ko.Bool("yes"))
 		return
 		return
 	}
 	}
 
 
-	// Map queries to the query container.
-	q := &Queries{}
-	if err := scanQueriesToStruct(q, qMap, db.Unsafe()); err != nil {
-		logger.Fatalf("no SQL queries loaded: %v", err)
-	}
-	app.Queries = q
-
-	// Initialize the bulk subscriber importer.
-	importNotifCB := func(subject string, data interface{}) error {
-		go sendNotification(app.Constants.NotifyEmails, subject, notifTplImport, data, app)
-		return nil
-	}
-	app.Importer = subimporter.New(q.UpsertSubscriber.Stmt,
-		q.UpsertBlacklistSubscriber.Stmt,
-		q.UpdateListsDate.Stmt,
-		db.DB,
-		importNotifCB)
-
-	// Prepare notification e-mail templates.
-	notifTpls, err := compileNotifTpls("/email-templates/*.html", fs, app)
-	if err != nil {
-		logger.Fatalf("error loading e-mail notification templates: %v", err)
-	}
-	app.NotifTpls = notifTpls
-
-	// Static URLS.
-	// url.com/subscription/{campaign_uuid}/{subscriber_uuid}
-	c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", app.Constants.RootURL)
-
-	// url.com/subscription/optin/{subscriber_uuid}
-	c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", app.Constants.RootURL)
-
-	// url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
-	c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", app.Constants.RootURL)
-
-	// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
-	c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", app.Constants.RootURL)
-
-	// Initialize the campaign manager.
-	campNotifCB := func(subject string, data interface{}) error {
-		return sendNotification(app.Constants.NotifyEmails, subject, notifTplCampaign, data, app)
-	}
-	m := manager.New(manager.Config{
-		Concurrency:   ko.Int("app.concurrency"),
-		MaxSendErrors: ko.Int("app.max_send_errors"),
-		FromEmail:     app.Constants.FromEmail,
-		UnsubURL:      c.UnsubURL,
-		OptinURL:      c.OptinURL,
-		LinkTrackURL:  c.LinkTrackURL,
-		ViewTrackURL:  c.ViewTrackURL,
-	}, newManagerDB(q), campNotifCB, logger)
-	app.Manager = m
-
-	// Add messengers.
-	app.Messenger = initMessengers(app.Manager)
-
-	// Add uploader
-	app.Media = initMediaStore()
-
-	// Initialize the workers that push out messages.
-	go m.Run(time.Second * 5)
-	m.SpawnWorkers()
-
-	// Initialize the HTTP server.
-	var srv = echo.New()
-	srv.HideBanner = true
-
-	// Register app (*App) to be injected into all HTTP handlers.
-	srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
-		return func(c echo.Context) error {
-			c.Set("app", app)
-			return next(c)
-		}
-	})
-
-	// Parse user facing templates.
-	tpl, err := stuffbin.ParseTemplatesGlob(nil, fs, "/public/templates/*.html")
-	if err != nil {
-		logger.Fatalf("error parsing public templates: %v", err)
-	}
-	srv.Renderer = &tplRenderer{
-		templates:  tpl,
-		RootURL:    c.RootURL,
-		LogoURL:    c.LogoURL,
-		FaviconURL: c.FaviconURL}
-
-	// Register HTTP handlers and static file servers.
-	fSrv := app.FS.FileServer()
-	srv.GET("/public/*", echo.WrapHandler(fSrv))
-	srv.GET("/frontend/*", echo.WrapHandler(fSrv))
-	if ko.String("upload.provider") == "filesystem" {
-		srv.Static(ko.String("upload.filesystem.upload_uri"), ko.String("upload.filesystem.upload_path"))
-	}
-	registerHandlers(srv)
-	srv.Logger.Fatal(srv.Start(ko.String("app.address")))
+	// Initialize the main app controller that wraps all of the app's
+	// components. This is passed around HTTP handlers.
+	app := &App{
+		fs:        fs,
+		db:        db,
+		constants: initConstants(),
+		media:     initMediaStore(),
+		log:       lo,
+	}
+	_, app.queries = initQueries(queryFilePath, db, fs, true)
+	app.manager = initCampaignManager(app)
+	app.importer = initImporter(app)
+	app.messenger = initMessengers(app.manager)
+	app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.constants)
+
+	// Start the campaign workers.
+	go app.manager.Run(time.Second * 5)
+	app.manager.SpawnWorkers()
+
+	// Start and run the app server.
+	initHTTPServer(app)
 }
 }

+ 16 - 16
media.go

@@ -54,9 +54,9 @@ func handleUploadMedia(c echo.Context) error {
 	defer src.Close()
 	defer src.Close()
 
 
 	// Upload the file.
 	// Upload the file.
-	fName, err = app.Media.Put(fName, typ, src)
+	fName, err = app.media.Put(fName, typ, src)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error uploading file: %v", err)
+		app.log.Printf("error uploading file: %v", err)
 		cleanUp = true
 		cleanUp = true
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error uploading file: %s", err))
 			fmt.Sprintf("Error uploading file: %s", err))
@@ -66,8 +66,8 @@ func handleUploadMedia(c echo.Context) error {
 		// If any of the subroutines in this function fail,
 		// If any of the subroutines in this function fail,
 		// the uploaded image should be removed.
 		// the uploaded image should be removed.
 		if cleanUp {
 		if cleanUp {
-			app.Media.Delete(fName)
-			app.Media.Delete(thumbPrefix + fName)
+			app.media.Delete(fName)
+			app.media.Delete(thumbPrefix + fName)
 		}
 		}
 	}()
 	}()
 
 
@@ -75,30 +75,30 @@ func handleUploadMedia(c echo.Context) error {
 	thumbFile, err := createThumbnail(file)
 	thumbFile, err := createThumbnail(file)
 	if err != nil {
 	if err != nil {
 		cleanUp = true
 		cleanUp = true
-		app.Logger.Printf("error resizing image: %v", err)
+		app.log.Printf("error resizing image: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error resizing image: %s", err))
 			fmt.Sprintf("Error resizing image: %s", err))
 	}
 	}
 
 
 	// Upload thumbnail.
 	// Upload thumbnail.
-	thumbfName, err := app.Media.Put(thumbPrefix+fName, typ, thumbFile)
+	thumbfName, err := app.media.Put(thumbPrefix+fName, typ, thumbFile)
 	if err != nil {
 	if err != nil {
 		cleanUp = true
 		cleanUp = true
-		app.Logger.Printf("error saving thumbnail: %v", err)
+		app.log.Printf("error saving thumbnail: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error saving thumbnail: %s", err))
 			fmt.Sprintf("Error saving thumbnail: %s", err))
 	}
 	}
 
 
 	uu, err := uuid.NewV4()
 	uu, err := uuid.NewV4()
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error generating UUID: %v", err)
+		app.log.Printf("error generating UUID: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 		return echo.NewHTTPError(http.StatusInternalServerError, "Error generating UUID")
 	}
 	}
 
 
 	// Write to the DB.
 	// Write to the DB.
-	if _, err := app.Queries.InsertMedia.Exec(uu, fName, thumbfName, 0, 0); err != nil {
+	if _, err := app.queries.InsertMedia.Exec(uu, fName, thumbfName, 0, 0); err != nil {
 		cleanUp = true
 		cleanUp = true
-		app.Logger.Printf("error inserting uploaded file to db: %v", err)
+		app.log.Printf("error inserting uploaded file to db: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err)))
 	}
 	}
@@ -112,14 +112,14 @@ func handleGetMedia(c echo.Context) error {
 		out []media.Media
 		out []media.Media
 	)
 	)
 
 
-	if err := app.Queries.GetMedia.Select(&out); err != nil {
+	if err := app.queries.GetMedia.Select(&out); err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err)))
 	}
 	}
 
 
 	for i := 0; i < len(out); i++ {
 	for i := 0; i < len(out); i++ {
-		out[i].URI = app.Media.Get(out[i].Filename)
-		out[i].ThumbURI = app.Media.Get(thumbPrefix + out[i].Filename)
+		out[i].URI = app.media.Get(out[i].Filename)
+		out[i].ThumbURI = app.media.Get(thumbPrefix + out[i].Filename)
 	}
 	}
 
 
 	return c.JSON(http.StatusOK, okResp{out})
 	return c.JSON(http.StatusOK, okResp{out})
@@ -137,12 +137,12 @@ func handleDeleteMedia(c echo.Context) error {
 	}
 	}
 
 
 	var m media.Media
 	var m media.Media
-	if err := app.Queries.DeleteMedia.Get(&m, id); err != nil {
+	if err := app.queries.DeleteMedia.Get(&m, id); err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error deleting media: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error deleting media: %s", pqErrMsg(err)))
 	}
 	}
 
 
-	app.Media.Delete(m.Filename)
-	app.Media.Delete(thumbPrefix + m.Filename)
+	app.media.Delete(m.Filename)
+	app.media.Delete(thumbPrefix + m.Filename)
 	return c.JSON(http.StatusOK, okResp{true})
 	return c.JSON(http.StatusOK, okResp{true})
 }
 }

+ 4 - 27
notifications.go

@@ -2,9 +2,6 @@ package main
 
 
 import (
 import (
 	"bytes"
 	"bytes"
-	"html/template"
-
-	"github.com/knadh/stuffbin"
 )
 )
 
 
 const (
 const (
@@ -24,39 +21,19 @@ type notifData struct {
 // sendNotification sends out an e-mail notification to admins.
 // sendNotification sends out an e-mail notification to admins.
 func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error {
 func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error {
 	var b bytes.Buffer
 	var b bytes.Buffer
-	if err := app.NotifTpls.ExecuteTemplate(&b, tplName, data); err != nil {
-		app.Logger.Printf("error compiling notification template '%s': %v", tplName, err)
+	if err := app.notifTpls.ExecuteTemplate(&b, tplName, data); err != nil {
+		app.log.Printf("error compiling notification template '%s': %v", tplName, err)
 		return err
 		return err
 	}
 	}
 
 
-	err := app.Messenger.Push(app.Constants.FromEmail,
+	err := app.messenger.Push(app.constants.FromEmail,
 		toEmails,
 		toEmails,
 		subject,
 		subject,
 		b.Bytes(),
 		b.Bytes(),
 		nil)
 		nil)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error sending admin notification (%s): %v", subject, err)
+		app.log.Printf("error sending admin notification (%s): %v", subject, err)
 		return err
 		return err
 	}
 	}
 	return nil
 	return nil
 }
 }
-
-// compileNotifTpls compiles and returns e-mail notification templates that are
-// used for sending ad-hoc notifications to admins and subscribers.
-func compileNotifTpls(path string, fs stuffbin.FileSystem, app *App) (*template.Template, error) {
-	// Register utility functions that the e-mail templates can use.
-	funcs := template.FuncMap{
-		"RootURL": func() string {
-			return app.Constants.RootURL
-		},
-		"LogoURL": func() string {
-			return app.Constants.LogoURL
-		}}
-
-	tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/email-templates/*.html")
-	if err != nil {
-		return nil, err
-	}
-
-	return tpl, err
-}

+ 24 - 24
public.go

@@ -97,19 +97,19 @@ func handleSubscriptionPage(c echo.Context) error {
 	)
 	)
 	out.SubUUID = subUUID
 	out.SubUUID = subUUID
 	out.Title = "Unsubscribe from mailing list"
 	out.Title = "Unsubscribe from mailing list"
-	out.AllowBlacklist = app.Constants.Privacy.AllowBlacklist
-	out.AllowExport = app.Constants.Privacy.AllowExport
-	out.AllowWipe = app.Constants.Privacy.AllowWipe
+	out.AllowBlacklist = app.constants.Privacy.AllowBlacklist
+	out.AllowExport = app.constants.Privacy.AllowExport
+	out.AllowWipe = app.constants.Privacy.AllowWipe
 
 
 	// Unsubscribe.
 	// Unsubscribe.
 	if unsub {
 	if unsub {
 		// Is blacklisting allowed?
 		// Is blacklisting allowed?
-		if !app.Constants.Privacy.AllowBlacklist {
+		if !app.constants.Privacy.AllowBlacklist {
 			blacklist = false
 			blacklist = false
 		}
 		}
 
 
-		if _, err := app.Queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
-			app.Logger.Printf("error unsubscribing: %v", err)
+		if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
+			app.log.Printf("error unsubscribing: %v", err)
 			return c.Render(http.StatusInternalServerError, tplMessage,
 			return c.Render(http.StatusInternalServerError, tplMessage,
 				makeMsgTpl("Error", "",
 				makeMsgTpl("Error", "",
 					`Error processing request. Please retry.`))
 					`Error processing request. Please retry.`))
@@ -152,9 +152,9 @@ func handleOptinPage(c echo.Context) error {
 	}
 	}
 
 
 	// Get the list of subscription lists where the subscriber hasn't confirmed.
 	// Get the list of subscription lists where the subscriber hasn't confirmed.
-	if err := app.Queries.GetSubscriberLists.Select(&out.Lists, 0, subUUID,
+	if err := app.queries.GetSubscriberLists.Select(&out.Lists, 0, subUUID,
 		nil, pq.StringArray(out.ListUUIDs), models.SubscriptionStatusUnconfirmed, nil); err != nil {
 		nil, pq.StringArray(out.ListUUIDs), models.SubscriptionStatusUnconfirmed, nil); err != nil {
-		app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
+		app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error", "", `Error fetching lists. Please retry.`))
 			makeMsgTpl("Error", "", `Error fetching lists. Please retry.`))
 	}
 	}
@@ -168,8 +168,8 @@ func handleOptinPage(c echo.Context) error {
 
 
 	// Confirm.
 	// Confirm.
 	if confirm {
 	if confirm {
-		if _, err := app.Queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil {
-			app.Logger.Printf("error unsubscribing: %v", err)
+		if _, err := app.queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil {
+			app.log.Printf("error unsubscribing: %v", err)
 			return c.Render(http.StatusInternalServerError, tplMessage,
 			return c.Render(http.StatusInternalServerError, tplMessage,
 				makeMsgTpl("Error", "",
 				makeMsgTpl("Error", "",
 					`Error processing request. Please retry.`))
 					`Error processing request. Please retry.`))
@@ -233,9 +233,9 @@ func handleLinkRedirect(c echo.Context) error {
 	)
 	)
 
 
 	var url string
 	var url string
-	if err := app.Queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil {
+	if err := app.queries.RegisterLinkClick.Get(&url, linkUUID, campUUID, subUUID); err != nil {
 		if err != sql.ErrNoRows {
 		if err != sql.ErrNoRows {
-			app.Logger.Printf("error fetching redirect link: %s", err)
+			app.log.Printf("error fetching redirect link: %s", err)
 		}
 		}
 
 
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
@@ -255,8 +255,8 @@ func handleRegisterCampaignView(c echo.Context) error {
 		campUUID = c.Param("campUUID")
 		campUUID = c.Param("campUUID")
 		subUUID  = c.Param("subUUID")
 		subUUID  = c.Param("subUUID")
 	)
 	)
-	if _, err := app.Queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {
-		app.Logger.Printf("error registering campaign view: %s", err)
+	if _, err := app.queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {
+		app.log.Printf("error registering campaign view: %s", err)
 	}
 	}
 	c.Response().Header().Set("Cache-Control", "no-cache")
 	c.Response().Header().Set("Cache-Control", "no-cache")
 	return c.Blob(http.StatusOK, "image/png", pixelPNG)
 	return c.Blob(http.StatusOK, "image/png", pixelPNG)
@@ -272,7 +272,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 		subUUID = c.Param("subUUID")
 		subUUID = c.Param("subUUID")
 	)
 	)
 	// Is export allowed?
 	// Is export allowed?
-	if !app.Constants.Privacy.AllowExport {
+	if !app.constants.Privacy.AllowExport {
 		return c.Render(http.StatusBadRequest, tplMessage,
 		return c.Render(http.StatusBadRequest, tplMessage,
 			makeMsgTpl("Invalid request", "", "The feature is not available."))
 			makeMsgTpl("Invalid request", "", "The feature is not available."))
 	}
 	}
@@ -280,9 +280,9 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 	// Get the subscriber's data. A single query that gets the profile,
 	// Get the subscriber's data. A single query that gets the profile,
 	// list subscriptions, campaign views, and link clicks. Names of
 	// list subscriptions, campaign views, and link clicks. Names of
 	// private lists are replaced with "Private list".
 	// private lists are replaced with "Private list".
-	data, b, err := exportSubscriberData(0, subUUID, app.Constants.Privacy.Exportable, app)
+	data, b, err := exportSubscriberData(0, subUUID, app.constants.Privacy.Exportable, app)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error exporting subscriber data: %s", err)
+		app.log.Printf("error exporting subscriber data: %s", err)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error processing request", "",
 			makeMsgTpl("Error processing request", "",
 				"There was an error processing your request. Please try later."))
 				"There was an error processing your request. Please try later."))
@@ -290,8 +290,8 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 
 
 	// Send the data out to the subscriber as an atachment.
 	// Send the data out to the subscriber as an atachment.
 	var msg bytes.Buffer
 	var msg bytes.Buffer
-	if err := app.NotifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
-		app.Logger.Printf("error compiling notification template '%s': %v",
+	if err := app.notifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
+		app.log.Printf("error compiling notification template '%s': %v",
 			notifSubscriberData, err)
 			notifSubscriberData, err)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error preparing data", "",
 			makeMsgTpl("Error preparing data", "",
@@ -299,7 +299,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 	}
 	}
 
 
 	const fname = "profile.json"
 	const fname = "profile.json"
-	if err := app.Messenger.Push(app.Constants.FromEmail,
+	if err := app.messenger.Push(app.constants.FromEmail,
 		[]string{data.Email},
 		[]string{data.Email},
 		"Your profile data",
 		"Your profile data",
 		msg.Bytes(),
 		msg.Bytes(),
@@ -311,7 +311,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 			},
 			},
 		},
 		},
 	); err != nil {
 	); err != nil {
-		app.Logger.Printf("error e-mailing subscriber profile: %s", err)
+		app.log.Printf("error e-mailing subscriber profile: %s", err)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error e-mailing data", "",
 			makeMsgTpl("Error e-mailing data", "",
 				"There was an error e-mailing your data. Please try later."))
 				"There was an error e-mailing your data. Please try later."))
@@ -331,14 +331,14 @@ func handleWipeSubscriberData(c echo.Context) error {
 	)
 	)
 
 
 	// Is wiping allowed?
 	// Is wiping allowed?
-	if !app.Constants.Privacy.AllowExport {
+	if !app.constants.Privacy.AllowExport {
 		return c.Render(http.StatusBadRequest, tplMessage,
 		return c.Render(http.StatusBadRequest, tplMessage,
 			makeMsgTpl("Invalid request", "",
 			makeMsgTpl("Invalid request", "",
 				"The feature is not available."))
 				"The feature is not available."))
 	}
 	}
 
 
-	if _, err := app.Queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil {
-		app.Logger.Printf("error wiping subscriber data: %s", err)
+	if _, err := app.queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil {
+		app.log.Printf("error wiping subscriber data: %s", err)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error processing request", "",
 			makeMsgTpl("Error processing request", "",
 				"There was an error processing your request. Please try later."))
 				"There was an error processing your request. Please try later."))

+ 46 - 46
subscribers.go

@@ -74,17 +74,17 @@ func handleGetSubscriber(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid subscriber ID.")
 	}
 	}
 
 
-	err := app.Queries.GetSubscriber.Select(&out, id, nil)
+	err := app.queries.GetSubscriber.Select(&out, id, nil)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error fetching subscriber: %v", err)
+		app.log.Printf("error fetching subscriber: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 	}
 	}
 	if len(out) == 0 {
 	if len(out) == 0 {
 		return echo.NewHTTPError(http.StatusBadRequest, "Subscriber not found.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Subscriber not found.")
 	}
 	}
-	if err := out.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil {
-		app.Logger.Printf("error loading subscriber lists: %v", err)
+	if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
+		app.log.Printf("error loading subscriber lists: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			"Error loading subscriber lists.")
 			"Error loading subscriber lists.")
 	}
 	}
@@ -119,12 +119,12 @@ func handleQuerySubscribers(c echo.Context) error {
 		cond = " AND " + query
 		cond = " AND " + query
 	}
 	}
 
 
-	stmt := fmt.Sprintf(app.Queries.QuerySubscribers, cond)
+	stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond)
 
 
 	// Create a readonly transaction to prevent mutations.
 	// Create a readonly transaction to prevent mutations.
-	tx, err := app.DB.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
+	tx, err := app.db.BeginTxx(context.Background(), &sql.TxOptions{ReadOnly: true})
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error preparing subscriber query: %v", err)
+		app.log.Printf("error preparing subscriber query: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error preparing subscriber query: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error preparing subscriber query: %v", pqErrMsg(err)))
 	}
 	}
@@ -137,8 +137,8 @@ func handleQuerySubscribers(c echo.Context) error {
 	}
 	}
 
 
 	// Lazy load lists for each subscriber.
 	// Lazy load lists for each subscriber.
-	if err := out.Results.LoadLists(app.Queries.GetSubscriberListsLazy); err != nil {
-		app.Logger.Printf("error fetching subscriber lists: %v", err)
+	if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
+		app.log.Printf("error fetching subscriber lists: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching subscriber lists: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching subscriber lists: %v", pqErrMsg(err)))
 	}
 	}
@@ -211,14 +211,14 @@ func handleUpdateSubscriber(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid length for `name`.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid length for `name`.")
 	}
 	}
 
 
-	_, err := app.Queries.UpdateSubscriber.Exec(req.ID,
+	_, err := app.queries.UpdateSubscriber.Exec(req.ID,
 		strings.ToLower(strings.TrimSpace(req.Email)),
 		strings.ToLower(strings.TrimSpace(req.Email)),
 		strings.TrimSpace(req.Name),
 		strings.TrimSpace(req.Name),
 		req.Status,
 		req.Status,
 		req.Attribs,
 		req.Attribs,
 		req.Lists)
 		req.Lists)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating subscriber: %v", err)
+		app.log.Printf("error updating subscriber: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err)))
 			fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err)))
 	}
 	}
@@ -239,9 +239,9 @@ func handleGetSubscriberSendOptin(c echo.Context) error {
 	}
 	}
 
 
 	// Fetch the subscriber.
 	// Fetch the subscriber.
-	err := app.Queries.GetSubscriber.Select(&out, id, nil)
+	err := app.queries.GetSubscriber.Select(&out, id, nil)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error fetching subscriber: %v", err)
+		app.log.Printf("error fetching subscriber: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 	}
 	}
@@ -287,8 +287,8 @@ func handleBlacklistSubscribers(c echo.Context) error {
 		IDs = req.SubscriberIDs
 		IDs = req.SubscriberIDs
 	}
 	}
 
 
-	if _, err := app.Queries.BlacklistSubscribers.Exec(IDs); err != nil {
-		app.Logger.Printf("error blacklisting subscribers: %v", err)
+	if _, err := app.queries.BlacklistSubscribers.Exec(IDs); err != nil {
+		app.log.Printf("error blacklisting subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error blacklisting: %v", err))
 			fmt.Sprintf("Error blacklisting: %v", err))
 	}
 	}
@@ -335,17 +335,17 @@ func handleManageSubscriberLists(c echo.Context) error {
 	var err error
 	var err error
 	switch req.Action {
 	switch req.Action {
 	case "add":
 	case "add":
-		_, err = app.Queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
 	case "remove":
 	case "remove":
-		_, err = app.Queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
 	case "unsubscribe":
 	case "unsubscribe":
-		_, err = app.Queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
 	default:
 	default:
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
 	}
 	}
 
 
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating subscriptions: %v", err)
+		app.log.Printf("error updating subscriptions: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error processing lists: %v", err))
 			fmt.Sprintf("Error processing lists: %v", err))
 	}
 	}
@@ -383,8 +383,8 @@ func handleDeleteSubscribers(c echo.Context) error {
 		IDs = i
 		IDs = i
 	}
 	}
 
 
-	if _, err := app.Queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
-		app.Logger.Printf("error deleting subscribers: %v", err)
+	if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
+		app.log.Printf("error deleting subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error deleting subscribers: %v", err))
 			fmt.Sprintf("Error deleting subscribers: %v", err))
 	}
 	}
@@ -404,11 +404,11 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
 		return err
 		return err
 	}
 	}
 
 
-	err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
-		app.Queries.DeleteSubscribersByQuery,
-		req.ListIDs, app.DB)
+	err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
+		app.queries.DeleteSubscribersByQuery,
+		req.ListIDs, app.db)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error querying subscribers: %v", err)
+		app.log.Printf("error querying subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error: %v", err))
 			fmt.Sprintf("Error: %v", err))
 	}
 	}
@@ -428,11 +428,11 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error {
 		return err
 		return err
 	}
 	}
 
 
-	err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
-		app.Queries.BlacklistSubscribersByQuery,
-		req.ListIDs, app.DB)
+	err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
+		app.queries.BlacklistSubscribersByQuery,
+		req.ListIDs, app.db)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error blacklisting subscribers: %v", err)
+		app.log.Printf("error blacklisting subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error: %v", err))
 			fmt.Sprintf("Error: %v", err))
 	}
 	}
@@ -459,19 +459,19 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
 	var stmt string
 	var stmt string
 	switch req.Action {
 	switch req.Action {
 	case "add":
 	case "add":
-		stmt = app.Queries.AddSubscribersToListsByQuery
+		stmt = app.queries.AddSubscribersToListsByQuery
 	case "remove":
 	case "remove":
-		stmt = app.Queries.DeleteSubscriptionsByQuery
+		stmt = app.queries.DeleteSubscriptionsByQuery
 	case "unsubscribe":
 	case "unsubscribe":
-		stmt = app.Queries.UnsubscribeSubscribersFromListsByQuery
+		stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
 	default:
 	default:
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
 	}
 	}
 
 
-	err := app.Queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
-		stmt, req.ListIDs, app.DB, req.TargetListIDs)
+	err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
+		stmt, req.ListIDs, app.db, req.TargetListIDs)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error updating subscriptions: %v", err)
+		app.log.Printf("error updating subscriptions: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error: %v", err))
 			fmt.Sprintf("Error: %v", err))
 	}
 	}
@@ -496,9 +496,9 @@ func handleExportSubscriberData(c echo.Context) error {
 	// Get the subscriber's data. A single query that gets the profile,
 	// Get the subscriber's data. A single query that gets the profile,
 	// list subscriptions, campaign views, and link clicks. Names of
 	// list subscriptions, campaign views, and link clicks. Names of
 	// private lists are replaced with "Private list".
 	// private lists are replaced with "Private list".
-	_, b, err := exportSubscriberData(id, "", app.Constants.Privacy.Exportable, app)
+	_, b, err := exportSubscriberData(id, "", app.constants.Privacy.Exportable, app)
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error exporting subscriber data: %s", err)
+		app.log.Printf("error exporting subscriber data: %s", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			"Error exporting subscriber data.")
 			"Error exporting subscriber data.")
 	}
 	}
@@ -516,7 +516,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) {
 	}
 	}
 	req.UUID = uu.String()
 	req.UUID = uu.String()
 
 
-	err = app.Queries.InsertSubscriber.Get(&req.ID,
+	err = app.queries.InsertSubscriber.Get(&req.ID,
 		req.UUID,
 		req.UUID,
 		req.Email,
 		req.Email,
 		strings.TrimSpace(req.Name),
 		strings.TrimSpace(req.Name),
@@ -529,7 +529,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) {
 			return 0, echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.")
 			return 0, echo.NewHTTPError(http.StatusBadRequest, "The e-mail already exists.")
 		}
 		}
 
 
-		app.Logger.Printf("error inserting subscriber: %v", err)
+		app.log.Printf("error inserting subscriber: %v", err)
 		return 0, echo.NewHTTPError(http.StatusInternalServerError,
 		return 0, echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error inserting subscriber: %v", err))
 			fmt.Sprintf("Error inserting subscriber: %v", err))
 	}
 	}
@@ -556,8 +556,8 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
 	if subUUID != "" {
 	if subUUID != "" {
 		uu = subUUID
 		uu = subUUID
 	}
 	}
-	if err := app.Queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
-		app.Logger.Printf("error fetching subscriber export data: %v", err)
+	if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
+		app.log.Printf("error fetching subscriber export data: %v", err)
 		return data, nil, err
 		return data, nil, err
 	}
 	}
 
 
@@ -578,7 +578,7 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
 	// Marshal the data into an indented payload.
 	// Marshal the data into an indented payload.
 	b, err := json.MarshalIndent(data, "", "  ")
 	b, err := json.MarshalIndent(data, "", "  ")
 	if err != nil {
 	if err != nil {
-		app.Logger.Printf("error marshalling subscriber export data: %v", err)
+		app.log.Printf("error marshalling subscriber export data: %v", err)
 		return data, nil, err
 		return data, nil, err
 	}
 	}
 	return data, b, nil
 	return data, b, nil
@@ -591,9 +591,9 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
 
 
 	// Fetch double opt-in lists from the given list IDs.
 	// Fetch double opt-in lists from the given list IDs.
 	// Get the list of subscription lists where the subscriber hasn't confirmed.
 	// Get the list of subscription lists where the subscriber hasn't confirmed.
-	if err := app.Queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
+	if err := app.queries.GetSubscriberLists.Select(&lists, sub.ID, nil,
 		pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, nil); err != nil {
 		pq.Int64Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, nil); err != nil {
-		app.Logger.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
+		app.log.Printf("error fetching lists for opt-in: %s", pqErrMsg(err))
 		return err
 		return err
 	}
 	}
 
 
@@ -610,13 +610,13 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
 	for _, l := range out.Lists {
 	for _, l := range out.Lists {
 		qListIDs.Add("l", l.UUID)
 		qListIDs.Add("l", l.UUID)
 	}
 	}
-	out.OptinURL = fmt.Sprintf(app.Constants.OptinURL, sub.UUID, qListIDs.Encode())
+	out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
 
 
 	// Send the e-mail.
 	// Send the e-mail.
 	if err := sendNotification([]string{sub.Email},
 	if err := sendNotification([]string{sub.Email},
 		"Confirm subscription",
 		"Confirm subscription",
 		notifSubscriberOptin, out, app); err != nil {
 		notifSubscriberOptin, out, app); err != nil {
-		app.Logger.Printf("error e-mailing subscriber profile: %s", err)
+		app.log.Printf("error e-mailing subscriber profile: %s", err)
 		return err
 		return err
 	}
 	}
 
 

+ 8 - 8
templates.go

@@ -48,7 +48,7 @@ func handleGetTemplates(c echo.Context) error {
 		single = true
 		single = true
 	}
 	}
 
 
-	err := app.Queries.GetTemplates.Select(&out, id, noBody)
+	err := app.queries.GetTemplates.Select(&out, id, noBody)
 	if err != nil {
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
@@ -87,7 +87,7 @@ func handlePreviewTemplate(c echo.Context) error {
 			return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
 			return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
 		}
 		}
 
 
-		err := app.Queries.GetTemplates.Select(&tpls, id, false)
+		err := app.queries.GetTemplates.Select(&tpls, id, false)
 		if err != nil {
 		if err != nil {
 			return echo.NewHTTPError(http.StatusInternalServerError,
 			return echo.NewHTTPError(http.StatusInternalServerError,
 				fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
 				fmt.Sprintf("Error fetching templates: %s", pqErrMsg(err)))
@@ -109,12 +109,12 @@ func handlePreviewTemplate(c echo.Context) error {
 		Body:         dummyTpl,
 		Body:         dummyTpl,
 	}
 	}
 
 
-	if err := camp.CompileTemplate(app.Manager.TemplateFuncs(&camp)); err != nil {
+	if err := camp.CompileTemplate(app.manager.TemplateFuncs(&camp)); 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.
 	// Render the message body.
-	m := app.Manager.NewMessage(&camp, &dummySubscriber)
+	m := app.manager.NewMessage(&camp, &dummySubscriber)
 	if err := m.Render(); err != nil {
 	if err := m.Render(); err != nil {
 		return echo.NewHTTPError(http.StatusBadRequest,
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error rendering message: %v", err))
 			fmt.Sprintf("Error rendering message: %v", err))
@@ -140,7 +140,7 @@ func handleCreateTemplate(c echo.Context) error {
 
 
 	// Insert and read ID.
 	// Insert and read ID.
 	var newID int
 	var newID int
-	if err := app.Queries.CreateTemplate.Get(&newID,
+	if err := app.queries.CreateTemplate.Get(&newID,
 		o.Name,
 		o.Name,
 		o.Body); err != nil {
 		o.Body); err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
@@ -174,7 +174,7 @@ func handleUpdateTemplate(c echo.Context) error {
 	}
 	}
 
 
 	// TODO: PASSWORD HASHING.
 	// TODO: PASSWORD HASHING.
-	res, err := app.Queries.UpdateTemplate.Exec(o.ID, o.Name, o.Body)
+	res, err := app.queries.UpdateTemplate.Exec(o.ID, o.Name, o.Body)
 	if err != nil {
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
@@ -198,7 +198,7 @@ func handleTemplateSetDefault(c echo.Context) error {
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid ID.")
 	}
 	}
 
 
-	_, err := app.Queries.SetDefaultTemplate.Exec(id)
+	_, err := app.queries.SetDefaultTemplate.Exec(id)
 	if err != nil {
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError,
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
 			fmt.Sprintf("Error updating template: %s", pqErrMsg(err)))
@@ -221,7 +221,7 @@ func handleDeleteTemplate(c echo.Context) error {
 	}
 	}
 
 
 	var delID int
 	var delID int
-	err := app.Queries.DeleteTemplate.Get(&delID, id)
+	err := app.queries.DeleteTemplate.Get(&delID, id)
 	if err != nil {
 	if err != nil {
 		if err == sql.ErrNoRows {
 		if err == sql.ErrNoRows {
 			return c.JSON(http.StatusOK, okResp{true})
 			return c.JSON(http.StatusOK, okResp{true})

+ 0 - 64
utils.go

@@ -7,14 +7,11 @@ import (
 	"log"
 	"log"
 	"mime/multipart"
 	"mime/multipart"
 	"net/http"
 	"net/http"
-	"reflect"
 	"regexp"
 	"regexp"
 	"strconv"
 	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/disintegration/imaging"
 	"github.com/disintegration/imaging"
-	"github.com/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
 	"github.com/labstack/echo"
 	"github.com/labstack/echo"
 	"github.com/lib/pq"
 	"github.com/lib/pq"
 )
 )
@@ -26,67 +23,6 @@ var (
 	tagRegexpSpaces = regexp.MustCompile(`[\s]+`)
 	tagRegexpSpaces = regexp.MustCompile(`[\s]+`)
 )
 )
 
 
-// ScanToStruct prepares a given set of Queries and assigns the resulting
-// *sql.Stmt statements to the fields of a given struct, matching based on the name
-// in the `query` tag in the struct field names.
-func scanQueriesToStruct(obj interface{}, q goyesql.Queries, db *sqlx.DB) error {
-	ob := reflect.ValueOf(obj)
-	if ob.Kind() == reflect.Ptr {
-		ob = ob.Elem()
-	}
-
-	if ob.Kind() != reflect.Struct {
-		return fmt.Errorf("Failed to apply SQL statements to struct. Non struct type: %T", ob)
-	}
-
-	// Go through every field in the struct and look for it in the Args map.
-	for i := 0; i < ob.NumField(); i++ {
-		f := ob.Field(i)
-
-		if f.IsValid() {
-			if tag := ob.Type().Field(i).Tag.Get("query"); tag != "" && tag != "-" {
-				// Extract the value of the `query` tag.
-				var (
-					tg   = strings.Split(tag, ",")
-					name string
-				)
-				if len(tg) == 2 {
-					if tg[0] != "-" && tg[0] != "" {
-						name = tg[0]
-					}
-				} else {
-					name = tg[0]
-				}
-
-				// Query name found in the field tag is not in the map.
-				if _, ok := q[name]; !ok {
-					return fmt.Errorf("query '%s' not found in query map", name)
-				}
-
-				if !f.CanSet() {
-					return fmt.Errorf("query field '%s' is unexported", ob.Type().Field(i).Name)
-				}
-
-				switch f.Type().String() {
-				case "string":
-					// Unprepared SQL query.
-					f.Set(reflect.ValueOf(q[name].Query))
-				case "*sqlx.Stmt":
-					// Prepared query.
-					stmt, err := db.Preparex(q[name].Query)
-					if err != nil {
-						return fmt.Errorf("Error preparing query '%s': %v", name, err)
-					}
-
-					f.Set(reflect.ValueOf(stmt))
-				}
-			}
-		}
-	}
-
-	return nil
-}
-
 // validateMIME is a helper function to validate uploaded file's MIME type
 // validateMIME is a helper function to validate uploaded file's MIME type
 // against the slice of MIME types is given.
 // against the slice of MIME types is given.
 func validateMIME(typ string, mimes []string) (ok bool) {
 func validateMIME(typ string, mimes []string) (ok bool) {