Przeglądaj źródła

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 lat temu
rodzic
commit
8853809713
16 zmienionych plików z 574 dodań i 567 usunięć
  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 (
 		app = c.Get("app").(*App)
 		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{}
@@ -48,7 +48,7 @@ func handleGetDashboardStats(c echo.Context) error {
 		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,
 			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("&")))
 	}
 
-	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 {
-		app.Logger.Printf("error fetching campaigns: %v", err)
+		app.log.Printf("error fetching campaigns: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaigns: %s", pqErrMsg(err)))
 	}
@@ -112,8 +112,8 @@ func handleGetCampaigns(c echo.Context) error {
 	}
 
 	// 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,
 			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.")
 	}
 
-	err := app.Queries.GetCampaignForPreview.Get(camp, id)
+	err := app.queries.GetCampaignForPreview.Get(camp, id)
 	if err != nil {
 		if err == sql.ErrNoRows {
 			return echo.NewHTTPError(http.StatusBadRequest, "Campaign not found.")
 		}
 
-		app.Logger.Printf("error fetching campaign: %v", err)
+		app.log.Printf("error fetching campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
 
 	var sub models.Subscriber
 	// 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 {
 			// There's no subscriber. Mock one.
 			sub = dummySubscriber
 		} else {
-			app.Logger.Printf("error fetching subscriber: %v", err)
+			app.log.Printf("error fetching subscriber: %v", err)
 			return echo.NewHTTPError(http.StatusInternalServerError,
 				fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 		}
@@ -173,16 +173,16 @@ func handlePreviewCampaign(c echo.Context) error {
 		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,
 			fmt.Sprintf("Error compiling template: %v", err))
 	}
 
 	// Render the message body.
-	m := app.Manager.NewMessage(camp, &sub)
+	m := app.manager.NewMessage(camp, &sub)
 	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,
 			fmt.Sprintf("Error rendering message: %v", err))
 	}
@@ -218,20 +218,20 @@ func handleCreateCampaign(c echo.Context) error {
 		o = c
 	}
 
-	if !app.Manager.HasMessenger(o.MessengerID) {
+	if !app.manager.HasMessenger(o.MessengerID) {
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Unknown messenger %s", o.MessengerID))
 	}
 
 	uu, err := uuid.NewV4()
 	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")
 	}
 
 	// Insert and read ID.
 	var newID int
-	if err := app.Queries.CreateCampaign.Get(&newID,
+	if err := app.queries.CreateCampaign.Get(&newID,
 		uu,
 		o.Type,
 		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.")
 		}
 
-		app.Logger.Printf("error creating campaign: %v", err)
+		app.log.Printf("error creating campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error creating campaign: %v", pqErrMsg(err)))
 	}
@@ -274,12 +274,12 @@ func handleUpdateCampaign(c echo.Context) error {
 	}
 
 	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 {
 			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,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
@@ -301,7 +301,7 @@ func handleUpdateCampaign(c echo.Context) error {
 		o = c
 	}
 
-	res, err := app.Queries.UpdateCampaign.Exec(cm.ID,
+	res, err := app.queries.UpdateCampaign.Exec(cm.ID,
 		o.Name,
 		o.Subject,
 		o.FromEmail,
@@ -313,7 +313,7 @@ func handleUpdateCampaign(c echo.Context) error {
 		o.TemplateID,
 		o.ListIDs)
 	if err != nil {
-		app.Logger.Printf("error updating campaign: %v", err)
+		app.log.Printf("error updating campaign: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating campaign: %s", pqErrMsg(err)))
 	}
@@ -337,12 +337,12 @@ func handleUpdateCampaignStatus(c echo.Context) error {
 	}
 
 	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 {
 			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,
 			fmt.Sprintf("Error fetching campaign: %s", pqErrMsg(err)))
 	}
@@ -385,9 +385,9 @@ func handleUpdateCampaignStatus(c echo.Context) error {
 		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 {
-		app.Logger.Printf("error updating campaign status: %v", err)
+		app.log.Printf("error updating campaign status: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating campaign status: %s", pqErrMsg(err)))
 	}
@@ -412,12 +412,12 @@ func handleDeleteCampaign(c echo.Context) error {
 	}
 
 	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 {
 			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,
 			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.")
 	}
 
-	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,
 			fmt.Sprintf("Error deleting campaign: %v", pqErrMsg(err)))
 	}
@@ -445,12 +445,12 @@ func handleGetRunningCampaignStats(c echo.Context) error {
 		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 {
 			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,
 			fmt.Sprintf("Error fetching campaign stats: %s", pqErrMsg(err)))
 	} 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]))
 	}
 	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,
 			fmt.Sprintf("Error fetching subscribers: %s", pqErrMsg(err)))
 	} else if len(subs) == 0 {
@@ -519,12 +519,12 @@ func handleTestCampaign(c echo.Context) error {
 
 	// The 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 {
 			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,
 			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.
 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)
 	}
 
 	// Render the message body.
-	m := app.Manager.NewMessage(camp, sub)
+	m := app.manager.NewMessage(camp, sub)
 	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,
 			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
 	}
 
@@ -572,7 +572,7 @@ func sendTestMessage(sub *models.Subscriber, camp *models.Campaign, app *App) er
 // validateCampaignFields validates incoming campaign field values.
 func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
 	if c.FromEmail == "" {
-		c.FromEmail = app.Constants.FromEmail
+		c.FromEmail = app.constants.FromEmail
 	} else if !regexFromAddress.Match([]byte(c.FromEmail)) {
 		if !govalidator.IsEmail(c.FromEmail) {
 			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}
-	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)
 	}
 
@@ -621,9 +621,9 @@ func makeOptinCampaignMessage(o campaignReq, app *App) (campaignReq, error) {
 
 	// Fetch double opt-in lists from the given list IDs.
 	var lists []models.List
-	err := app.Queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), nil)
+	err := app.queries.GetListsByOptin.Select(&lists, models.ListOptinDouble, pq.Int64Array(o.ListIDs), 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,
 			"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.
 	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
 		OptinURLAttr template.HTMLAttr
 	}{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,
 			"Error compiling opt-in campaign template.")
 	}

+ 3 - 6
go.mod

@@ -7,8 +7,8 @@ require (
 	github.com/jinzhu/gorm v1.9.1
 	github.com/jmoiron/sqlx v1.2.0
 	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/kr/pretty v0.1.0 // indirect
 	github.com/labstack/echo v3.3.10+incompatible
@@ -16,14 +16,11 @@ require (
 	github.com/lib/pq v1.0.0
 	github.com/mattn/go-colorable v0.0.9 // 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/valyala/bytebufferpool v1.0.0 // 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/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/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/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0=
 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/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
 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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
 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/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 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/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 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/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/go.mod h1:yVCFaWaKPubSNibBsTAJ939q2ABHudJQxRWZWV5yh+4=
 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/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.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/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 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=
 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-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/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/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/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/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/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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 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}$")
 
 // registerHandlers registers HTTP handlers.
-func registerHandlers(e *echo.Echo) {
+func registerHTTPHandlers(e *echo.Echo) {
 	e.GET("/", handleIndexPage)
 	e.GET("/api/config.js", handleGetConfigScript)
 	e.GET("/api/dashboard/stats", handleGetDashboardStats)
@@ -128,7 +128,7 @@ func registerHandlers(e *echo.Echo) {
 func handleIndexPage(c echo.Context) error {
 	app := c.Get("app").(*App)
 
-	b, err := app.FS.Read("/frontend/index.html")
+	b, err := app.fs.Read("/frontend/index.html")
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 	}
@@ -161,8 +161,8 @@ func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc
 		)
 
 		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,
 				makeMsgTpl("Error", "",
 					`Error processing request. Please retry.`))

+ 7 - 7
import.go

@@ -25,7 +25,7 @@ func handleImportSubscribers(c echo.Context) error {
 	app := c.Get("app").(*App)
 
 	// Is an import already running?
-	if app.Importer.GetStats().Status == subimporter.StatusImporting {
+	if app.importer.GetStats().Status == subimporter.StatusImporting {
 		return echo.NewHTTPError(http.StatusBadRequest,
 			"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.
-	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 {
 		return echo.NewHTTPError(http.StatusBadRequest,
 			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]))
 	}
 
-	return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()})
+	return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
 }
 
 // handleGetImportSubscribers returns import statistics.
 func handleGetImportSubscribers(c echo.Context) error {
 	var (
 		app = c.Get("app").(*App)
-		s   = app.Importer.GetStats()
+		s   = app.importer.GetStats()
 	)
 	return c.JSON(http.StatusOK, okResp{s})
 }
@@ -110,7 +110,7 @@ func handleGetImportSubscribers(c echo.Context) error {
 // handleGetImportSubscriberStats returns import statistics.
 func handleGetImportSubscriberStats(c echo.Context) error {
 	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.
@@ -118,6 +118,6 @@ func handleGetImportSubscriberStats(c echo.Context) error {
 // is finished, it's state is cleared.
 func handleStopImportSubscribers(c echo.Context) error {
 	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/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
+	goyesqlx "github.com/knadh/goyesql/v2/sqlx"
 	"github.com/knadh/listmonk/models"
+	"github.com/knadh/stuffbin"
 	"github.com/lib/pq"
 )
 
 // install runs the first time setup of creating and
 // 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("** First time installation **")
 	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
 		fmt.Print("Continue (y/n)?  ")
 		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" {
 			fmt.Println("Installation cancelled.")
@@ -37,15 +40,15 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 	}
 
 	// Migrate the tables.
-	err := installMigrate(app.DB, app)
+	err := installMigrate(db, fs)
 	if err != nil {
-		logger.Fatalf("Error migrating DB schema: %v", err)
+		lo.Fatalf("Error migrating DB schema: %v", err)
 	}
 
 	// Load the 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.
@@ -60,7 +63,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		models.ListOptinSingle,
 		pq.StringArray{"test"},
 	); 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()),
@@ -69,7 +72,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		models.ListOptinDouble,
 		pq.StringArray{"test"},
 	); err != nil {
-		logger.Fatalf("Error creating list: %v", err)
+		lo.Fatalf("Error creating list: %v", err)
 	}
 
 	// Sample subscriber.
@@ -80,7 +83,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		`{"type": "known", "good": true, "city": "Bengaluru"}`,
 		pq.Int64Array{int64(defList)},
 	); err != nil {
-		logger.Fatalf("Error creating subscriber: %v", err)
+		lo.Fatalf("Error creating subscriber: %v", err)
 	}
 	if _, err := q.UpsertSubscriber.Exec(
 		uuid.Must(uuid.NewV4()),
@@ -89,7 +92,7 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		`{"type": "unknown", "good": true, "city": "Bengaluru"}`,
 		pq.Int64Array{int64(optinList)},
 	); err != nil {
-		logger.Fatalf("Error creating subscriber: %v", err)
+		lo.Fatalf("Error creating subscriber: %v", err)
 	}
 
 	// Default template.
@@ -103,10 +106,10 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		"Default template",
 		string(tplBody),
 	); 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 {
-		logger.Fatalf("error setting default template: %v", err)
+		lo.Fatalf("error setting default template: %v", err)
 	}
 
 	// Sample campaign.
@@ -126,17 +129,17 @@ func install(app *App, qMap goyesql.Queries, prompt bool) {
 		1,
 		pq.Int64Array{1},
 	); 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.
-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 {
 		return err
 	}
@@ -156,11 +159,7 @@ func newConfigFile() error {
 
 	// 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 {
-		return err
-	}
-
+	fs := initFS()
 	b, err := fs.Read("config.toml.sample")
 	if err != nil {
 		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
 	}
 
-	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 {
-		app.Logger.Printf("error fetching lists: %v", err)
+		app.log.Printf("error fetching lists: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error fetching lists: %s", pqErrMsg(err)))
 	}
@@ -87,20 +87,20 @@ func handleCreateList(c echo.Context) error {
 
 	uu, err := uuid.NewV4()
 	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")
 	}
 
 	// Insert and read ID.
 	var newID int
 	o.UUID = uu.String()
-	if err := app.Queries.CreateList.Get(&newID,
+	if err := app.queries.CreateList.Get(&newID,
 		o.UUID,
 		o.Name,
 		o.Type,
 		o.Optin,
 		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,
 			fmt.Sprintf("Error creating list: %s", pqErrMsg(err)))
 	}
@@ -128,10 +128,10 @@ func handleUpdateList(c echo.Context) error {
 		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)))
 	if err != nil {
-		app.Logger.Printf("error updating list: %v", err)
+		app.log.Printf("error updating list: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error updating list: %s", pqErrMsg(err)))
 	}
@@ -165,8 +165,8 @@ func handleDeleteLists(c echo.Context) error {
 		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,
 			fmt.Sprintf("Error deleting: %v", err))
 	}

+ 51 - 284
main.go

@@ -5,68 +5,41 @@ import (
 	"html/template"
 	"log"
 	"os"
-	"path/filepath"
 	"strings"
 	"time"
 
-	_ "github.com/jinzhu/gorm/dialects/postgres"
 	"github.com/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
 	"github.com/knadh/koanf"
-	"github.com/knadh/koanf/maps"
 	"github.com/knadh/koanf/parsers/toml"
 	"github.com/knadh/koanf/providers/env"
 	"github.com/knadh/koanf/providers/file"
 	"github.com/knadh/koanf/providers/posflag"
 	"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"
 	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
 // passed around, especially through HTTP handlers.
 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 (
 	// 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.
 	ko = koanf.New(".")
@@ -75,14 +48,14 @@ var (
 )
 
 func init() {
-	// Register --help handler.
 	f := flag.NewFlagSet("config", flag.ContinueOnError)
 	f.Usage = func() {
+		// Register --help handler.
 		fmt.Println(f.FlagUsages())
 		os.Exit(0)
 	}
 
-	// Setup the default configuration.
+	// Register the commandline flags.
 	f.StringSlice("config", []string{"config.toml"},
 		"Path to one or more config files (will be merged in order)")
 	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("yes", false, "Assume 'yes' to prompts, eg: during --install")
 
-	// Process flags.
 	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.
@@ -104,277 +76,72 @@ func init() {
 	// Generate new config.
 	if ok, _ := f.GetBool("new-config"); ok {
 		if err := newConfigFile(); err != nil {
-			logger.Println(err)
+			lo.Println(err)
 			os.Exit(1)
 		}
-		logger.Println("generated config.toml. Edit and run --install")
+		lo.Println("generated config.toml. Edit and run --install")
 		os.Exit(0)
 	}
 
 	// Load config files.
 	cFiles, _ := f.GetStringSlice("config")
 	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 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.
 	if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
 		return strings.Replace(strings.ToLower(
 			strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
 	}), 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 {
-		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 (
-		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()
 
-	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") {
-		install(app, qMap, !ko.Bool("yes"))
+		install(db, fs, !ko.Bool("yes"))
 		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()
 
 	// Upload the file.
-	fName, err = app.Media.Put(fName, typ, src)
+	fName, err = app.media.Put(fName, typ, src)
 	if err != nil {
-		app.Logger.Printf("error uploading file: %v", err)
+		app.log.Printf("error uploading file: %v", err)
 		cleanUp = true
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			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,
 		// the uploaded image should be removed.
 		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)
 	if err != nil {
 		cleanUp = true
-		app.Logger.Printf("error resizing image: %v", err)
+		app.log.Printf("error resizing image: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error resizing image: %s", err))
 	}
 
 	// Upload thumbnail.
-	thumbfName, err := app.Media.Put(thumbPrefix+fName, typ, thumbFile)
+	thumbfName, err := app.media.Put(thumbPrefix+fName, typ, thumbFile)
 	if err != nil {
 		cleanUp = true
-		app.Logger.Printf("error saving thumbnail: %v", err)
+		app.log.Printf("error saving thumbnail: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error saving thumbnail: %s", err))
 	}
 
 	uu, err := uuid.NewV4()
 	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")
 	}
 
 	// 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
-		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,
 			fmt.Sprintf("Error saving uploaded file to db: %s", pqErrMsg(err)))
 	}
@@ -112,14 +112,14 @@ func handleGetMedia(c echo.Context) error {
 		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,
 			fmt.Sprintf("Error fetching media list: %s", pqErrMsg(err)))
 	}
 
 	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})
@@ -137,12 +137,12 @@ func handleDeleteMedia(c echo.Context) error {
 	}
 
 	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,
 			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})
 }

+ 4 - 27
notifications.go

@@ -2,9 +2,6 @@ package main
 
 import (
 	"bytes"
-	"html/template"
-
-	"github.com/knadh/stuffbin"
 )
 
 const (
@@ -24,39 +21,19 @@ type notifData struct {
 // sendNotification sends out an e-mail notification to admins.
 func sendNotification(toEmails []string, subject, tplName string, data interface{}, app *App) error {
 	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
 	}
 
-	err := app.Messenger.Push(app.Constants.FromEmail,
+	err := app.messenger.Push(app.constants.FromEmail,
 		toEmails,
 		subject,
 		b.Bytes(),
 		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 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.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.
 	if unsub {
 		// Is blacklisting allowed?
-		if !app.Constants.Privacy.AllowBlacklist {
+		if !app.constants.Privacy.AllowBlacklist {
 			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,
 				makeMsgTpl("Error", "",
 					`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.
-	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 {
-		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,
 			makeMsgTpl("Error", "", `Error fetching lists. Please retry.`))
 	}
@@ -168,8 +168,8 @@ func handleOptinPage(c echo.Context) error {
 
 	// 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,
 				makeMsgTpl("Error", "",
 					`Error processing request. Please retry.`))
@@ -233,9 +233,9 @@ func handleLinkRedirect(c echo.Context) error {
 	)
 
 	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 {
-			app.Logger.Printf("error fetching redirect link: %s", err)
+			app.log.Printf("error fetching redirect link: %s", err)
 		}
 
 		return c.Render(http.StatusInternalServerError, tplMessage,
@@ -255,8 +255,8 @@ func handleRegisterCampaignView(c echo.Context) error {
 		campUUID = c.Param("campUUID")
 		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")
 	return c.Blob(http.StatusOK, "image/png", pixelPNG)
@@ -272,7 +272,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 		subUUID = c.Param("subUUID")
 	)
 	// Is export allowed?
-	if !app.Constants.Privacy.AllowExport {
+	if !app.constants.Privacy.AllowExport {
 		return c.Render(http.StatusBadRequest, tplMessage,
 			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,
 	// list subscriptions, campaign views, and link clicks. Names of
 	// 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 {
-		app.Logger.Printf("error exporting subscriber data: %s", err)
+		app.log.Printf("error exporting subscriber data: %s", err)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error processing request", "",
 				"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.
 	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)
 		return c.Render(http.StatusInternalServerError, tplMessage,
 			makeMsgTpl("Error preparing data", "",
@@ -299,7 +299,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 	}
 
 	const fname = "profile.json"
-	if err := app.Messenger.Push(app.Constants.FromEmail,
+	if err := app.messenger.Push(app.constants.FromEmail,
 		[]string{data.Email},
 		"Your profile data",
 		msg.Bytes(),
@@ -311,7 +311,7 @@ func handleSelfExportSubscriberData(c echo.Context) error {
 			},
 		},
 	); 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,
 			makeMsgTpl("Error e-mailing data", "",
 				"There was an error e-mailing your data. Please try later."))
@@ -331,14 +331,14 @@ func handleWipeSubscriberData(c echo.Context) error {
 	)
 
 	// Is wiping allowed?
-	if !app.Constants.Privacy.AllowExport {
+	if !app.constants.Privacy.AllowExport {
 		return c.Render(http.StatusBadRequest, tplMessage,
 			makeMsgTpl("Invalid request", "",
 				"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,
 			makeMsgTpl("Error processing request", "",
 				"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.")
 	}
 
-	err := app.Queries.GetSubscriber.Select(&out, id, nil)
+	err := app.queries.GetSubscriber.Select(&out, id, 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,
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 	}
 	if len(out) == 0 {
 		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,
 			"Error loading subscriber lists.")
 	}
@@ -119,12 +119,12 @@ func handleQuerySubscribers(c echo.Context) error {
 		cond = " AND " + query
 	}
 
-	stmt := fmt.Sprintf(app.Queries.QuerySubscribers, cond)
+	stmt := fmt.Sprintf(app.queries.QuerySubscribers, cond)
 
 	// 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 {
-		app.Logger.Printf("error preparing subscriber query: %v", err)
+		app.log.Printf("error preparing subscriber query: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			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.
-	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,
 			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`.")
 	}
 
-	_, err := app.Queries.UpdateSubscriber.Exec(req.ID,
+	_, err := app.queries.UpdateSubscriber.Exec(req.ID,
 		strings.ToLower(strings.TrimSpace(req.Email)),
 		strings.TrimSpace(req.Name),
 		req.Status,
 		req.Attribs,
 		req.Lists)
 	if err != nil {
-		app.Logger.Printf("error updating subscriber: %v", err)
+		app.log.Printf("error updating subscriber: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error updating subscriber: %v", pqErrMsg(err)))
 	}
@@ -239,9 +239,9 @@ func handleGetSubscriberSendOptin(c echo.Context) error {
 	}
 
 	// Fetch the subscriber.
-	err := app.Queries.GetSubscriber.Select(&out, id, nil)
+	err := app.queries.GetSubscriber.Select(&out, id, 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,
 			fmt.Sprintf("Error fetching subscriber: %s", pqErrMsg(err)))
 	}
@@ -287,8 +287,8 @@ func handleBlacklistSubscribers(c echo.Context) error {
 		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,
 			fmt.Sprintf("Error blacklisting: %v", err))
 	}
@@ -335,17 +335,17 @@ func handleManageSubscriberLists(c echo.Context) error {
 	var err error
 	switch req.Action {
 	case "add":
-		_, err = app.Queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.AddSubscribersToLists.Exec(IDs, req.TargetListIDs)
 	case "remove":
-		_, err = app.Queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.DeleteSubscriptions.Exec(IDs, req.TargetListIDs)
 	case "unsubscribe":
-		_, err = app.Queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
+		_, err = app.queries.UnsubscribeSubscribersFromLists.Exec(IDs, req.TargetListIDs)
 	default:
 		return echo.NewHTTPError(http.StatusBadRequest, "Invalid action.")
 	}
 
 	if err != nil {
-		app.Logger.Printf("error updating subscriptions: %v", err)
+		app.log.Printf("error updating subscriptions: %v", err)
 		return echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error processing lists: %v", err))
 	}
@@ -383,8 +383,8 @@ func handleDeleteSubscribers(c echo.Context) error {
 		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,
 			fmt.Sprintf("Error deleting subscribers: %v", err))
 	}
@@ -404,11 +404,11 @@ func handleDeleteSubscribersByQuery(c echo.Context) error {
 		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 {
-		app.Logger.Printf("error querying subscribers: %v", err)
+		app.log.Printf("error querying subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error: %v", err))
 	}
@@ -428,11 +428,11 @@ func handleBlacklistSubscribersByQuery(c echo.Context) error {
 		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 {
-		app.Logger.Printf("error blacklisting subscribers: %v", err)
+		app.log.Printf("error blacklisting subscribers: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 			fmt.Sprintf("Error: %v", err))
 	}
@@ -459,19 +459,19 @@ func handleManageSubscriberListsByQuery(c echo.Context) error {
 	var stmt string
 	switch req.Action {
 	case "add":
-		stmt = app.Queries.AddSubscribersToListsByQuery
+		stmt = app.queries.AddSubscribersToListsByQuery
 	case "remove":
-		stmt = app.Queries.DeleteSubscriptionsByQuery
+		stmt = app.queries.DeleteSubscriptionsByQuery
 	case "unsubscribe":
-		stmt = app.Queries.UnsubscribeSubscribersFromListsByQuery
+		stmt = app.queries.UnsubscribeSubscribersFromListsByQuery
 	default:
 		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 {
-		app.Logger.Printf("error updating subscriptions: %v", err)
+		app.log.Printf("error updating subscriptions: %v", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 			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,
 	// list subscriptions, campaign views, and link clicks. Names of
 	// 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 {
-		app.Logger.Printf("error exporting subscriber data: %s", err)
+		app.log.Printf("error exporting subscriber data: %s", err)
 		return echo.NewHTTPError(http.StatusBadRequest,
 			"Error exporting subscriber data.")
 	}
@@ -516,7 +516,7 @@ func insertSubscriber(req subimporter.SubReq, app *App) (int, error) {
 	}
 	req.UUID = uu.String()
 
-	err = app.Queries.InsertSubscriber.Get(&req.ID,
+	err = app.queries.InsertSubscriber.Get(&req.ID,
 		req.UUID,
 		req.Email,
 		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.")
 		}
 
-		app.Logger.Printf("error inserting subscriber: %v", err)
+		app.log.Printf("error inserting subscriber: %v", err)
 		return 0, echo.NewHTTPError(http.StatusInternalServerError,
 			fmt.Sprintf("Error inserting subscriber: %v", err))
 	}
@@ -556,8 +556,8 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
 	if 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
 	}
 
@@ -578,7 +578,7 @@ func exportSubscriberData(id int64, subUUID string, exportables map[string]bool,
 	// Marshal the data into an indented payload.
 	b, err := json.MarshalIndent(data, "", "  ")
 	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, 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.
 	// 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 {
-		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
 	}
 
@@ -610,13 +610,13 @@ func sendOptinConfirmation(sub models.Subscriber, listIDs []int64, app *App) err
 	for _, l := range out.Lists {
 		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.
 	if err := sendNotification([]string{sub.Email},
 		"Confirm subscription",
 		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
 	}
 

+ 8 - 8
templates.go

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

+ 0 - 64
utils.go

@@ -7,14 +7,11 @@ import (
 	"log"
 	"mime/multipart"
 	"net/http"
-	"reflect"
 	"regexp"
 	"strconv"
 	"strings"
 
 	"github.com/disintegration/imaging"
-	"github.com/jmoiron/sqlx"
-	"github.com/knadh/goyesql"
 	"github.com/labstack/echo"
 	"github.com/lib/pq"
 )
@@ -26,67 +23,6 @@ var (
 	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
 // against the slice of MIME types is given.
 func validateMIME(typ string, mimes []string) (ok bool) {