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.
This commit is contained in:
Kailash Nadh 2020-03-08 00:03:22 +05:30
parent 83b49df39d
commit 8853809713
16 changed files with 572 additions and 565 deletions

View File

@ -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, RootURL: app.constants.RootURL,
FromEmail: app.Constants.FromEmail, FromEmail: app.constants.FromEmail,
Messengers: app.Manager.GetMessengerNames(), 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)))
} }

View File

@ -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 { if err := out.Results.LoadStats(app.queries.GetCampaignStats); err != nil {
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: %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 { if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
app.Logger.Printf("error compiling template: %v", err) 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 { if _, err := app.queries.DeleteCampaign.Exec(cm.ID); err != nil {
app.Logger.Printf("error deleting campaign: %v", err) 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 { if err := app.queries.GetSubscribersByEmails.Select(&subs, req.SubscriberEmails); err != nil {
app.Logger.Printf("error fetching subscribers: %v", err) 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 { if err := camp.CompileTemplate(app.manager.TemplateFuncs(camp)); err != nil {
app.Logger.Printf("error compiling template: %v", err) 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.")
} }

9
go.mod
View File

@ -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/goyesql/v2 v2.1.1
github.com/knadh/koanf v0.4.4 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
go.sum
View File

@ -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=

View File

@ -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 { if err := app.queries.SubscriberExists.Get(&exists, 0, subUUID); err != nil {
app.Logger.Printf("error checking subscriber existence: %v", err) 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.`))

View File

@ -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() app.importer.Stop()
return c.JSON(http.StatusOK, okResp{app.Importer.GetStats()}) return c.JSON(http.StatusOK, okResp{app.importer.GetStats()})
} }

310
init.go Normal file
View File

@ -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")))
}

View File

@ -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 { if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
logger.Fatalf("error loading SQL queries: %v", err) 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") lo.Printf("Setup complete")
logger.Printf(`Run the program and access the dashboard at %s`, ko.String("app.address")) 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 { func installMigrate(db *sqlx.DB, fs stuffbin.FileSystem) error {
q, err := app.FS.Read("/schema.sql") 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]) fs := initFS()
if err != nil {
return err
}
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)

View File

@ -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 { if _, err := app.queries.DeleteLists.Exec(ids); err != nil {
app.Logger.Printf("error deleting lists: %v", err) 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))
} }

331
main.go
View File

@ -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 fs stuffbin.FileSystem
DB *sqlx.DB db *sqlx.DB
Queries *Queries queries *Queries
Importer *subimporter.Importer constants *constants
Manager *manager.Manager manager *manager.Manager
FS stuffbin.FileSystem importer *subimporter.Importer
Logger *log.Logger messenger messenger.Messenger
NotifTpls *template.Template media media.Store
Messenger messenger.Messenger notifTpls *template.Template
Media media.Store 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.
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)) {
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() { func main() {
// Connect to the DB. // Initialize the DB and the filesystem that are required by the installer
var dbCfg dbConf // and the app.
if err := ko.Unmarshal("db", &dbCfg); err != nil { var (
log.Fatalf("error loading db config: %v", err) fs = initFS()
} db = initDB()
db, err := connectDB(dbCfg) )
if err != nil {
logger.Fatalf("error connecting to DB: %v", err)
}
defer db.Close() defer db.Close()
var c constants // Installer mode? This runs before the SQL queries are loaded and prepared
if err := ko.Unmarshal("app", &c); err != nil { // as the installer needs to work on an empty DB.
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.
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. // Initialize the main app controller that wraps all of the app's
q := &Queries{} // components. This is passed around HTTP handlers.
if err := scanQueriesToStruct(q, qMap, db.Unsafe()); err != nil { app := &App{
logger.Fatalf("no SQL queries loaded: %v", err) fs: fs,
db: db,
constants: initConstants(),
media: initMediaStore(),
log: lo,
} }
app.Queries = q _, 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)
// Initialize the bulk subscriber importer. // Start the campaign workers.
importNotifCB := func(subject string, data interface{}) error { go app.manager.Run(time.Second * 5)
go sendNotification(app.Constants.NotifyEmails, subject, notifTplImport, data, app) app.manager.SpawnWorkers()
return nil
}
app.Importer = subimporter.New(q.UpsertSubscriber.Stmt,
q.UpsertBlacklistSubscriber.Stmt,
q.UpdateListsDate.Stmt,
db.DB,
importNotifCB)
// Prepare notification e-mail templates. // Start and run the app server.
notifTpls, err := compileNotifTpls("/email-templates/*.html", fs, app) initHTTPServer(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")))
} }

View File

@ -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(fName)
app.Media.Delete(thumbPrefix + 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].URI = app.media.Get(out[i].Filename)
out[i].ThumbURI = app.Media.Get(thumbPrefix + 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(m.Filename)
app.Media.Delete(thumbPrefix + m.Filename) app.media.Delete(thumbPrefix + m.Filename)
return c.JSON(http.StatusOK, okResp{true}) return c.JSON(http.StatusOK, okResp{true})
} }

View File

@ -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 { if err := app.notifTpls.ExecuteTemplate(&b, tplName, data); err != nil {
app.Logger.Printf("error compiling notification template '%s': %v", tplName, err) 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
}

View File

@ -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.AllowBlacklist = app.constants.Privacy.AllowBlacklist
out.AllowExport = app.Constants.Privacy.AllowExport out.AllowExport = app.constants.Privacy.AllowExport
out.AllowWipe = app.Constants.Privacy.AllowWipe 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 { if _, err := app.queries.Unsubscribe.Exec(campUUID, subUUID, blacklist); err != nil {
app.Logger.Printf("error unsubscribing: %v", err) 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 { if _, err := app.queries.ConfirmSubscriptionOptin.Exec(subUUID, pq.StringArray(out.ListUUIDs)); err != nil {
app.Logger.Printf("error unsubscribing: %v", err) 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 { if _, err := app.queries.RegisterCampaignView.Exec(campUUID, subUUID); err != nil {
app.Logger.Printf("error registering campaign view: %s", err) 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 { if err := app.notifTpls.ExecuteTemplate(&msg, notifSubscriberData, data); err != nil {
app.Logger.Printf("error compiling notification template '%s': %v", 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 { if _, err := app.queries.DeleteSubscribers.Exec(nil, pq.StringArray{subUUID}); err != nil {
app.Logger.Printf("error wiping subscriber data: %s", err) 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."))

View File

@ -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 { if err := out.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
app.Logger.Printf("error loading subscriber lists: %v", err) 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 { if err := out.Results.LoadLists(app.queries.GetSubscriberListsLazy); err != nil {
app.Logger.Printf("error fetching subscriber lists: %v", err) 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 { if _, err := app.queries.BlacklistSubscribers.Exec(IDs); err != nil {
app.Logger.Printf("error blacklisting subscribers: %v", err) 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 { if _, err := app.queries.DeleteSubscribers.Exec(IDs, nil); err != nil {
app.Logger.Printf("error deleting subscribers: %v", err) 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), err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
app.Queries.DeleteSubscribersByQuery, app.queries.DeleteSubscribersByQuery,
req.ListIDs, app.DB) 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), err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
app.Queries.BlacklistSubscribersByQuery, app.queries.BlacklistSubscribersByQuery,
req.ListIDs, app.DB) 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), err := app.queries.execSubscriberQueryTpl(sanitizeSQLExp(req.Query),
stmt, req.ListIDs, app.DB, req.TargetListIDs) 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 { if err := app.queries.ExportSubscriberData.Get(&data, id, uu); err != nil {
app.Logger.Printf("error fetching subscriber export data: %v", err) 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
} }

View File

@ -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})

View File

@ -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) {