Remove superfluous consts dep in init functions by separating URL consts.

This commit is contained in:
Kailash Nadh 2025-04-05 23:03:36 +05:30
parent e2f24a140e
commit 88489223c9
15 changed files with 220 additions and 200 deletions

View file

@ -26,11 +26,11 @@ type serverConfig struct {
// GetServerConfig returns general server config.
func (a *App) GetServerConfig(c echo.Context) error {
out := serverConfig{
RootURL: a.constants.RootURL,
FromEmail: a.constants.FromEmail,
Lang: a.constants.Lang,
Permissions: a.constants.PermissionsRaw,
HasLegacyUser: a.constants.HasLegacyUser,
RootURL: a.urlCfg.RootURL,
FromEmail: a.cfg.FromEmail,
Lang: a.cfg.Lang,
Permissions: a.cfg.PermissionsRaw,
HasLegacyUser: a.cfg.HasLegacyUser,
}
// Language list.

View file

@ -53,7 +53,7 @@ func (a *App) GetCampaignArchives(c echo.Context) error {
func (a *App) GetCampaignArchivesFeed(c echo.Context) error {
var (
pg = a.paginator.NewFromURL(c.Request().URL.Query())
showFullContent = a.constants.EnablePublicArchiveRSSContent
showFullContent = a.cfg.EnablePublicArchiveRSSContent
)
// Get archives from the DB.
@ -81,8 +81,8 @@ func (a *App) GetCampaignArchivesFeed(c echo.Context) error {
// Generate the feed.
feed := &feeds.Feed{
Title: a.constants.SiteName,
Link: &feeds.Link{Href: a.constants.RootURL},
Title: a.cfg.SiteName,
Link: &feeds.Link{Href: a.urlCfg.RootURL},
Description: a.i18n.T("public.archiveTitle"),
Items: out,
}
@ -215,9 +215,9 @@ func (a *App) getCampaignArchives(offset, limit int, renderBody bool) ([]campArc
// The campaign may have a custom slug.
if camp.ArchiveSlug.Valid {
archive.URL, _ = url.JoinPath(a.constants.ArchiveURL, camp.ArchiveSlug.String)
archive.URL, _ = url.JoinPath(a.urlCfg.ArchiveURL, camp.ArchiveSlug.String)
} else {
archive.URL, _ = url.JoinPath(a.constants.ArchiveURL, camp.UUID)
archive.URL, _ = url.JoinPath(a.urlCfg.ArchiveURL, camp.UUID)
}
// Render the full template body if requested.

View file

@ -190,9 +190,9 @@ func (a *App) renderLoginPage(c echo.Context, loginErr error) error {
oidcProvider = ""
oidcLogo = ""
)
if a.constants.Security.OIDC.Enabled {
if a.cfg.Security.OIDC.Enabled {
oidcLogo = "oidc.png"
u, err := url.Parse(a.constants.Security.OIDC.Provider)
u, err := url.Parse(a.cfg.Security.OIDC.Provider)
if err == nil {
h := strings.Split(u.Hostname(), ".")
@ -330,7 +330,7 @@ func (a *App) doFirstTimeSetup(c echo.Context) error {
Type: auth.RoleTypeUser,
Name: null.NewString("Super Admin", true),
}
for p := range a.constants.Permissions {
for p := range a.cfg.Permissions {
r.Permissions = append(r.Permissions, p)
}

View file

@ -143,7 +143,7 @@ func (a *App) BounceWebhook(c echo.Context) error {
bounces = append(bounces, b)
// Amazon SES.
case service == "ses" && a.constants.BounceSESEnabled:
case service == "ses" && a.cfg.BounceSESEnabled:
switch c.Request().Header.Get("X-Amz-Sns-Message-Type") {
// SNS webhook registration confirmation. Only after these are processed will the endpoint
// start getting bounce notifications.
@ -167,7 +167,7 @@ func (a *App) BounceWebhook(c echo.Context) error {
}
// SendGrid.
case service == "sendgrid" && a.constants.BounceSendgridEnabled:
case service == "sendgrid" && a.cfg.BounceSendgridEnabled:
var (
sig = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Signature")
ts = c.Request().Header.Get("X-Twilio-Email-Event-Webhook-Timestamp")
@ -182,7 +182,7 @@ func (a *App) BounceWebhook(c echo.Context) error {
bounces = append(bounces, bs...)
// Postmark.
case service == "postmark" && a.constants.BouncePostmarkEnabled:
case service == "postmark" && a.cfg.BouncePostmarkEnabled:
bs, err := a.bounce.Postmark.ProcessBounce(rawReq, c)
if err != nil {
a.log.Printf("error processing postmark notification: %v", err)
@ -195,7 +195,7 @@ func (a *App) BounceWebhook(c echo.Context) error {
bounces = append(bounces, bs...)
// ForwardEmail.
case service == "forwardemail" && a.constants.BounceForwardemailEnabled:
case service == "forwardemail" && a.cfg.BounceForwardemailEnabled:
var (
sig = c.Request().Header.Get("X-Webhook-Signature")
)

View file

@ -553,7 +553,7 @@ func (a *App) sendTestMessage(sub models.Subscriber, camp *models.Campaign) erro
// validateCampaignFields validates incoming campaign field values.
func (a *App) validateCampaignFields(c campReq) (campReq, error) {
if c.FromEmail == "" {
c.FromEmail = a.constants.FromEmail
c.FromEmail = a.cfg.FromEmail
} else if !reFromAddress.Match([]byte(c.FromEmail)) {
if _, err := a.importer.SanitizeEmail(c.FromEmail); err != nil {
return c, errors.New(a.i18n.T("campaigns.fieldInvalidFromEmail"))

View file

@ -49,7 +49,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
// On no-auth, redirect to login page
if _, ok := u.(*echo.HTTPError); ok {
u, _ := url.Parse(a.constants.LoginURL)
u, _ := url.Parse(a.urlCfg.LoginURL)
q := url.Values{}
q.Set("next", c.Request().RequestURI)
u.RawQuery = q.Encode()
@ -197,7 +197,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
g.PUT("/api/roles/lists/:id", pm(a.UpdateListRole, "roles:manage"))
g.DELETE("/api/roles/:id", pm(a.DeleteRole, "roles:manage"))
if a.constants.BounceWebhooksEnabled {
if a.cfg.BounceWebhooksEnabled {
// Private authenticated bounce endpoint.
g.POST("/webhooks/bounce", pm(a.BounceWebhook, "webhooks:post_bounce"))
}
@ -209,7 +209,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
// Public unauthenticated endpoints.
g := e.Group("")
if a.constants.BounceWebhooksEnabled {
if a.cfg.BounceWebhooksEnabled {
// Public bounce endpoints for webservices like SES.
g.POST("/webhooks/service/:service", a.BounceWebhook)
}
@ -223,7 +223,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
g.GET(path.Join(uriAdmin, "/login"), a.LoginPage)
g.POST(path.Join(uriAdmin, "/login"), a.LoginPage)
if a.constants.Security.OIDC.Enabled {
if a.cfg.Security.OIDC.Enabled {
g.POST("/auth/oidc", a.OIDCLogin)
g.GET("/auth/oidc", a.OIDCFinish)
}
@ -231,7 +231,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
// Public APIs.
g.GET("/api/public/lists", a.GetPublicLists)
g.POST("/api/public/subscription", a.PublicSubscription)
if a.constants.EnablePublicArchive {
if a.cfg.EnablePublicArchive {
g.GET("/api/public/archive", a.GetCampaignArchives)
}
@ -249,7 +249,7 @@ func initHTTPHandlers(e *echo.Echo, a *App) {
g.GET("/campaign/:campUUID/:subUUID", noIndex(validateUUID(a.ViewCampaignMessage, "campUUID", "subUUID")))
g.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(a.RegisterCampaignView, "campUUID", "subUUID")))
if a.constants.EnablePublicArchive {
if a.cfg.EnablePublicArchive {
g.GET("/archive", a.CampaignArchivesPage)
g.GET("/archive.xml", a.GetCampaignArchivesFeed)
g.GET("/archive/:id", a.CampaignArchivePage)
@ -283,7 +283,7 @@ func (a *App) AdminPage(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
}
b = bytes.ReplaceAll(b, []byte("asset_version"), []byte(a.constants.AssetVersion))
b = bytes.ReplaceAll(b, []byte("asset_version"), []byte(a.cfg.AssetVersion))
return c.HTMLBlob(http.StatusOK, b)
}
@ -306,19 +306,19 @@ func serveCustomAppearance(name string) echo.HandlerFunc {
switch name {
case "admin.custom_css":
out = app.constants.Appearance.AdminCSS
out = app.cfg.Appearance.AdminCSS
hdr = "text/css; charset=utf-8"
case "admin.custom_js":
out = app.constants.Appearance.AdminJS
out = app.cfg.Appearance.AdminJS
hdr = "application/javascript; charset=utf-8"
case "public.custom_css":
out = app.constants.Appearance.PublicCSS
out = app.cfg.Appearance.PublicCSS
hdr = "text/css; charset=utf-8"
case "public.custom_js":
out = app.constants.Appearance.PublicJS
out = app.cfg.Appearance.PublicJS
hdr = "application/javascript; charset=utf-8"
}

View file

@ -57,19 +57,28 @@ const (
queryFilePath = "queries.sql"
)
// constants contains static, constant config values required by the app.
type constants struct {
// UrlConfig contains various URL constants used in the app.
type UrlConfig struct {
RootURL string `koanf:"root_url"`
LogoURL string `koanf:"logo_url"`
FaviconURL string `koanf:"favicon_url"`
LoginURL string `koanf:"login_url"`
UnsubURL string
LinkTrackURL string
ViewTrackURL string
OptinURL string
MessageURL string
ArchiveURL string
}
// Config contains static, constant config values required by arbitrary handlers and functions.
type Config struct {
SiteName string `koanf:"site_name"`
RootURL string `koanf:"root_url"`
LogoURL string `koanf:"logo_url"`
FaviconURL string `koanf:"favicon_url"`
LoginURL string `koanf:"login_url"`
FromEmail string `koanf:"from_email"`
NotifyEmails []string `koanf:"notify_emails"`
EnablePublicSubPage bool `koanf:"enable_public_subscription_page"`
EnablePublicArchive bool `koanf:"enable_public_archive"`
EnablePublicArchiveRSSContent bool `koanf:"enable_public_archive_rss_content"`
SendOptinConfirmation bool `koanf:"send_optin_confirmation"`
Lang string `koanf:"lang"`
DBBatchSize int `koanf:"batch_size"`
Privacy struct {
@ -105,12 +114,6 @@ type constants struct {
}
HasLegacyUser bool
UnsubURL string
LinkTrackURL string
ViewTrackURL string
OptinURL string
MessageURL string
ArchiveURL string
AssetVersion string
MediaUpload struct {
@ -386,10 +389,40 @@ func initSettings(query string, db *sqlx.DB, ko *koanf.Koanf) {
}
}
// initConstants initializes the app's global constants from the given koanf instance.
func initConstants(ko *koanf.Koanf) *constants {
func initUrlConfig(ko *koanf.Koanf) *UrlConfig {
root := strings.TrimSuffix(ko.String("app.root_url"), "/")
return &UrlConfig{
RootURL: root,
LogoURL: path.Join(root, ko.String("app.logo_url")),
FaviconURL: path.Join(root, ko.String("app.favicon_url")),
LoginURL: path.Join(uriAdmin, "/login"),
// Static URLS.
// url.com/subscription/{campaign_uuid}/{subscriber_uuid}
UnsubURL: fmt.Sprintf("%s/subscription/%%s/%%s", root),
// url.com/subscription/optin/{subscriber_uuid}
OptinURL: fmt.Sprintf("%s/subscription/optin/%%s?%%s", root),
// url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
LinkTrackURL: fmt.Sprintf("%s/link/%%s/%%s/%%s", root),
// url.com/link/{campaign_uuid}/{subscriber_uuid}
MessageURL: fmt.Sprintf("%s/campaign/%%s/%%s", root),
// url.com/archive
ArchiveURL: root + "/archive",
// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
ViewTrackURL: fmt.Sprintf("%s/campaign/%%s/%%s/px.png", root),
}
}
// initConstConfig initializes the app's global constants from the given koanf instance.
func initConstConfig(ko *koanf.Koanf) *Config {
// Read constants.
var c constants
var c Config
if err := ko.Unmarshal("app", &c); err != nil {
lo.Fatalf("error loading app config: %v", err)
}
@ -404,8 +437,6 @@ func initConstants(ko *koanf.Koanf) *constants {
lo.Fatalf("error loading app.appearance config: %v", err)
}
c.RootURL = strings.TrimRight(c.RootURL, "/")
c.LoginURL = path.Join(uriAdmin, "/login")
c.Lang = ko.String("app.lang")
c.Privacy.Exportable = koanfmaps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
c.MediaUpload.Provider = ko.String("upload.provider")
@ -413,25 +444,6 @@ func initConstants(ko *koanf.Koanf) *constants {
c.Privacy.DomainBlocklist = ko.Strings("privacy.domain_blocklist")
c.Privacy.DomainAllowlist = ko.Strings("privacy.domain_allowlist")
// 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/link/{campaign_uuid}/{subscriber_uuid}
c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
// url.com/archive
c.ArchiveURL = c.RootURL + "/archive"
// url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled")
c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
@ -484,7 +496,7 @@ func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
}
// initCampaignManager initializes the campaign manager.
func initCampaignManager(q *models.Queries, cs *constants, co *core.Core, md media.Store, i *i18n.I18n) *manager.Manager {
func initCampaignManager(q *models.Queries, u *UrlConfig, co *core.Core, md media.Store, i *i18n.I18n) *manager.Manager {
if ko.Bool("passive") {
lo.Println("running in passive mode. won't process campaigns.")
}
@ -494,15 +506,15 @@ func initCampaignManager(q *models.Queries, cs *constants, co *core.Core, md med
Concurrency: ko.Int("app.concurrency"),
MessageRate: ko.Int("app.message_rate"),
MaxSendErrors: ko.Int("app.max_send_errors"),
FromEmail: cs.FromEmail,
FromEmail: ko.MustString("app.from_email"),
IndividualTracking: ko.Bool("privacy.individual_tracking"),
UnsubURL: cs.UnsubURL,
OptinURL: cs.OptinURL,
LinkTrackURL: cs.LinkTrackURL,
ViewTrackURL: cs.ViewTrackURL,
MessageURL: cs.MessageURL,
ArchiveURL: cs.ArchiveURL,
RootURL: cs.RootURL,
UnsubURL: u.UnsubURL,
OptinURL: u.OptinURL,
LinkTrackURL: u.LinkTrackURL,
ViewTrackURL: u.ViewTrackURL,
MessageURL: u.MessageURL,
ArchiveURL: u.ArchiveURL,
RootURL: u.RootURL,
UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
SlidingWindow: ko.Bool("app.message_sliding_window"),
SlidingWindowDuration: ko.Duration("app.message_sliding_window_duration"),
@ -530,11 +542,11 @@ func initTxTemplates(m *manager.Manager, co *core.Core) {
}
// initImporter initializes the bulk subscriber importer.
func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *subimporter.Importer {
func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, i *i18n.I18n, ko *koanf.Koanf) *subimporter.Importer {
return subimporter.New(
subimporter.Options{
DomainBlocklist: app.constants.Privacy.DomainBlocklist,
DomainAllowlist: app.constants.Privacy.DomainAllowlist,
DomainBlocklist: ko.Strings("privacy.domain_blocklist"),
DomainAllowlist: ko.Strings("privacy.domain_allowlist"),
UpsertStmt: q.UpsertSubscriber.Stmt,
BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
UpdateListDateStmt: q.UpdateListsDate.Stmt,
@ -549,7 +561,7 @@ func initImporter(q *models.Queries, db *sqlx.DB, core *core.Core, app *App) *su
notifs.NotifySystem(subject, notifs.TplImport, data, nil)
return nil
},
}, db.DB, app.i18n)
}, db.DB, i)
}
// initSMTPMessenger initializes the combined and individual SMTP messengers.
@ -673,8 +685,8 @@ func initMediaStore(ko *koanf.Koanf) media.Store {
}
// initNotifs initializes the notifier with the system e-mail templates.
func initNotifs(fs stuffbin.FileSystem, i *i18n.I18n, em *email.Emailer, cs *constants, ko *koanf.Koanf) {
tpls, err := stuffbin.ParseTemplatesGlob(initTplFuncs(i, cs), fs, "/static/email-templates/*.html")
func initNotifs(fs stuffbin.FileSystem, i *i18n.I18n, em *email.Emailer, u *UrlConfig, ko *koanf.Koanf) {
tpls, err := stuffbin.ParseTemplatesGlob(initTplFuncs(i, u), fs, "/static/email-templates/*.html")
if err != nil {
lo.Fatalf("error parsing e-mail notif templates: %v", err)
}
@ -808,20 +820,20 @@ func initHTTPServer(app *App) *echo.Echo {
}
})
tpl, err := stuffbin.ParseTemplatesGlob(initTplFuncs(app.i18n, app.constants), app.fs, "/public/templates/*.html")
tpl, err := stuffbin.ParseTemplatesGlob(initTplFuncs(app.i18n, app.urlCfg), app.fs, "/public/templates/*.html")
if err != nil {
lo.Fatalf("error parsing public templates: %v", err)
}
srv.Renderer = &tplRenderer{
templates: tpl,
SiteName: app.constants.SiteName,
RootURL: app.constants.RootURL,
LogoURL: app.constants.LogoURL,
FaviconURL: app.constants.FaviconURL,
AssetVersion: app.constants.AssetVersion,
EnablePublicSubPage: app.constants.EnablePublicSubPage,
EnablePublicArchive: app.constants.EnablePublicArchive,
IndividualTracking: app.constants.Privacy.IndividualTracking,
SiteName: app.cfg.SiteName,
RootURL: app.urlCfg.RootURL,
LogoURL: app.urlCfg.LogoURL,
FaviconURL: app.urlCfg.FaviconURL,
AssetVersion: app.cfg.AssetVersion,
EnablePublicSubPage: app.cfg.EnablePublicSubPage,
EnablePublicArchive: app.cfg.EnablePublicArchive,
IndividualTracking: app.cfg.Privacy.IndividualTracking,
}
// Initialize the static file server.
@ -928,13 +940,13 @@ func joinFSPaths(root string, paths []string) []string {
// initTplFuncs returns a generic template func map with custom template
// functions and sprig template functions.
func initTplFuncs(i *i18n.I18n, cs *constants) template.FuncMap {
func initTplFuncs(i *i18n.I18n, u *UrlConfig) template.FuncMap {
funcs := template.FuncMap{
"RootURL": func() string {
return cs.RootURL
return u.RootURL
},
"LogoURL": func() string {
return cs.LogoURL
return u.LogoURL
},
"Date": func(layout string) string {
if layout == "" {

View file

@ -270,7 +270,7 @@ func checkSchema(db *sqlx.DB) (bool, error) {
}
func installUser(username, password string, q *models.Queries) {
consts := initConstants(ko)
consts := initConstConfig(ko)
// Super admin role.
perms := []string{}

View file

@ -37,25 +37,27 @@ const (
// App contains the "global" shared components, controllers and fields.
type App struct {
core *core.Core
fs stuffbin.FileSystem
db *sqlx.DB
queries *models.Queries
constants *constants
manager *manager.Manager
importer *subimporter.Importer
messengers []manager.Messenger
emailMessenger manager.Messenger
auth *auth.Auth
media media.Store
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
captcha *captcha.Captcha
events *events.Events
about about
log *log.Logger
bufLog *buflog.BufLog
core *core.Core
fs stuffbin.FileSystem
db *sqlx.DB
queries *models.Queries
cfg *Config
urlCfg *UrlConfig
manager *manager.Manager
importer *subimporter.Importer
messengers []manager.Messenger
emailMessenger manager.Messenger
auth *auth.Auth
media media.Store
i18n *i18n.I18n
bounce *bounce.Manager
paginator *paginator.Paginator
captcha *captcha.Captcha
events *events.Events
optinNotifyHook func(models.Subscriber, []int) (int, error)
about about
log *log.Logger
bufLog *buflog.BufLog
// Channel for passing reload signals.
chReload chan os.Signal
@ -173,7 +175,8 @@ func main() {
app := &App{
fs: fs,
db: db,
constants: initConstants(ko),
cfg: initConstConfig(ko),
urlCfg: initUrlConfig(ko),
media: initMediaStore(ko),
messengers: []manager.Messenger{},
log: lo,
@ -192,10 +195,10 @@ func main() {
}
// Load i18n language map.
app.i18n = initI18n(app.constants.Lang, fs)
app.i18n = initI18n(ko.MustString("app.lang"), fs)
cOpt := &core.Opt{
Constants: core.Constants{
SendOptinConfirmation: app.constants.SendOptinConfirmation,
SendOptinConfirmation: ko.Bool("app.send_optin_confirmation"),
CacheSlowQueries: ko.Bool("app.cache_slow_queries"),
},
Queries: queries,
@ -204,19 +207,19 @@ func main() {
Log: lo,
}
// Load bounce config.
// Load bounce config into the core.
if err := ko.Unmarshal("bounce.actions", &cOpt.Constants.BounceActions); err != nil {
lo.Fatalf("error unmarshalling bounce config: %v", err)
}
// Initialize the CRUD core.
app.core = core.New(cOpt, &core.Hooks{
SendOptinConfirmation: app.optinConfirmNotify(),
})
optinNotify := makeOptinNotifyHook(ko.Bool("app.send_optin_confirmation"), app.urlCfg, queries, app.i18n)
app.optinNotifyHook = optinNotify
app.core = core.New(cOpt, &core.Hooks{SendOptinConfirmation: optinNotify})
app.queries = queries
app.manager = initCampaignManager(app.queries, app.constants, app.core, app.media, app.i18n)
app.importer = initImporter(app.queries, db, app.core, app)
app.manager = initCampaignManager(app.queries, app.urlCfg, app.core, app.media, app.i18n)
app.importer = initImporter(app.queries, db, app.core, app.i18n, ko)
hasUsers, auth := initAuth(app.core, db.DB, ko)
app.auth = auth
@ -241,7 +244,7 @@ func main() {
}
// Initialize admin email notification templates.
initNotifs(app.fs, app.i18n, app.emailMessenger.(*email.Emailer), app.constants, ko)
initNotifs(app.fs, app.i18n, app.emailMessenger.(*email.Emailer), app.urlCfg, ko)
initTxTemplates(app.manager, app.core)
// Initialize any additional postback messengers.

View file

@ -46,8 +46,8 @@ func (a *App) UploadMedia(c echo.Context) error {
)
// Validate file extension.
if !inArray("*", a.constants.MediaUpload.Extensions) {
if ok := inArray(ext, a.constants.MediaUpload.Extensions); !ok {
if !inArray("*", a.cfg.MediaUpload.Extensions) {
if ok := inArray(ext, a.cfg.MediaUpload.Extensions); !ok {
return echo.NewHTTPError(http.StatusBadRequest,
a.i18n.Ts("media.unsupportedFileType", "type", ext))
}
@ -131,7 +131,7 @@ func (a *App) UploadMedia(c echo.Context) error {
}
// Insert the media into the DB.
m, err := a.core.InsertMedia(fName, thumbfName, contentType, meta, a.constants.MediaUpload.Provider, a.media)
m, err := a.core.InsertMedia(fName, thumbfName, contentType, meta, a.cfg.MediaUpload.Provider, a.media)
if err != nil {
cleanUp = true
return err
@ -158,7 +158,7 @@ func (a *App) GetMedia(c echo.Context) error {
pg = a.paginator.NewFromURL(c.Request().URL.Query())
query = c.FormValue("query")
)
res, total, err := a.core.QueryMedia(a.constants.MediaUpload.Provider, a.media, query, pg.Offset, pg.Limit)
res, total, err := a.core.QueryMedia(a.cfg.MediaUpload.Provider, a.media, query, pg.Offset, pg.Limit)
if err != nil {
return err
}

View file

@ -206,10 +206,10 @@ func (a *App) SubscriptionPage(c echo.Context) error {
Subscriber: s,
SubUUID: subUUID,
publicTpl: publicTpl{Title: a.i18n.T("public.unsubscribeTitle")},
AllowBlocklist: a.constants.Privacy.AllowBlocklist,
AllowExport: a.constants.Privacy.AllowExport,
AllowWipe: a.constants.Privacy.AllowWipe,
AllowPreferences: a.constants.Privacy.AllowPreferences,
AllowBlocklist: a.cfg.Privacy.AllowBlocklist,
AllowExport: a.cfg.Privacy.AllowExport,
AllowWipe: a.cfg.Privacy.AllowWipe,
AllowPreferences: a.cfg.Privacy.AllowPreferences,
}
// If the subscriber is blocklisted, throw an error.
@ -218,7 +218,7 @@ func (a *App) SubscriptionPage(c echo.Context) error {
}
// Only show preference management if it's enabled in settings.
if a.constants.Privacy.AllowPreferences {
if a.cfg.Privacy.AllowPreferences {
out.ShowManage = showManage
// Get the subscriber's lists from the DB to render in the template.
@ -261,7 +261,7 @@ func (a *App) SubscriptionPrefs(c echo.Context) error {
var (
campUUID = c.Param("campUUID")
subUUID = c.Param("subUUID")
blocklist = a.constants.Privacy.AllowBlocklist && req.Blocklist
blocklist = a.cfg.Privacy.AllowBlocklist && req.Blocklist
)
if !req.Manage || blocklist {
if err := a.core.UnsubscribeByCampaign(subUUID, campUUID, blocklist); err != nil {
@ -274,7 +274,7 @@ func (a *App) SubscriptionPrefs(c echo.Context) error {
}
// Is preference management enabled?
if !a.constants.Privacy.AllowPreferences {
if !a.cfg.Privacy.AllowPreferences {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.T("public.invalidFeature")))
}
@ -375,7 +375,7 @@ func (a *App) OptinPage(c echo.Context) error {
// Confirm.
if confirm {
meta := models.JSON{}
if a.constants.Privacy.RecordOptinIP {
if a.cfg.Privacy.RecordOptinIP {
if h := c.Request().Header.Get("X-Forwarded-For"); h != "" {
meta["optin_ip"] = h
} else if h := c.Request().RemoteAddr; h != "" {
@ -405,7 +405,7 @@ func (a *App) OptinPage(c echo.Context) error {
// SubscriptionFormPage handles subscription requests coming from public
// HTML subscription forms.
func (a *App) SubscriptionFormPage(c echo.Context) error {
if !a.constants.EnablePublicSubPage {
if !a.cfg.EnablePublicSubPage {
return c.Render(http.StatusNotFound, tplMessage,
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.invalidFeature")))
}
@ -428,8 +428,8 @@ func (a *App) SubscriptionFormPage(c echo.Context) error {
out.Lists = lists
// Captcha is enabled. Set the key for the template to render.
if a.constants.Security.EnableCaptcha {
out.CaptchaKey = a.constants.Security.CaptchaKey
if a.cfg.Security.EnableCaptcha {
out.CaptchaKey = a.cfg.Security.CaptchaKey
}
return c.Render(http.StatusOK, "subscription-form", out)
@ -444,7 +444,7 @@ func (a *App) SubscriptionForm(c echo.Context) error {
}
// Process CAPTCHA.
if a.constants.Security.EnableCaptcha {
if a.cfg.Security.EnableCaptcha {
err, ok := a.captcha.Verify(c.FormValue("h-captcha-response"))
if err != nil {
a.log.Printf("Captcha request failed: %v", err)
@ -479,7 +479,7 @@ func (a *App) SubscriptionForm(c echo.Context) error {
// PublicSubscription handles subscription requests coming from public
// API calls.
func (a *App) PublicSubscription(c echo.Context) error {
if !a.constants.EnablePublicSubPage {
if !a.cfg.EnablePublicSubPage {
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.T("public.invalidFeature"))
}
@ -499,7 +499,7 @@ func (a *App) PublicSubscription(c echo.Context) error {
func (a *App) LinkRedirect(c echo.Context) error {
// If individual tracking is disabled, do not record the subscriber ID.
subUUID := c.Param("subUUID")
if !a.constants.Privacy.IndividualTracking {
if !a.cfg.Privacy.IndividualTracking {
subUUID = ""
}
@ -524,7 +524,7 @@ func (a *App) LinkRedirect(c echo.Context) error {
func (a *App) RegisterCampaignView(c echo.Context) error {
// If individual tracking is disabled, do not record the subscriber ID.
subUUID := c.Param("subUUID")
if !a.constants.Privacy.IndividualTracking {
if !a.cfg.Privacy.IndividualTracking {
subUUID = ""
}
@ -546,7 +546,7 @@ func (a *App) RegisterCampaignView(c echo.Context) error {
// is dependent on the configuration.
func (a *App) SelfExportSubscriberData(c echo.Context) error {
// Is export allowed?
if !a.constants.Privacy.AllowExport {
if !a.cfg.Privacy.AllowExport {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.invalidFeature")))
}
@ -555,7 +555,7 @@ func (a *App) SelfExportSubscriberData(c echo.Context) error {
// list subscriptions, campaign views, and link clicks. Names of
// private lists are replaced with "Private list".
subUUID := c.Param("subUUID")
data, b, err := a.exportSubscriberData(0, subUUID, a.constants.Privacy.Exportable)
data, b, err := a.exportSubscriberData(0, subUUID, a.cfg.Privacy.Exportable)
if err != nil {
a.log.Printf("error exporting subscriber data: %s", err)
return c.Render(http.StatusInternalServerError, tplMessage,
@ -576,7 +576,7 @@ func (a *App) SelfExportSubscriberData(c echo.Context) error {
// E-mail the data as a JSON attachment to the subscriber.
const fname = "data.json"
if err := a.emailMessenger.Push(models.Message{
From: a.constants.FromEmail,
From: a.cfg.FromEmail,
To: []string{data.Email},
Subject: subject,
Body: body,
@ -602,7 +602,7 @@ func (a *App) SelfExportSubscriberData(c echo.Context) error {
// clicks remain as orphan data unconnected to any subscriber.
func (a *App) WipeSubscriberData(c echo.Context) error {
// Is wiping allowed?
if !a.constants.Privacy.AllowWipe {
if !a.cfg.Privacy.AllowWipe {
return c.Render(http.StatusBadRequest, tplMessage,
makeMsgTpl(a.i18n.T("public.errorTitle"), "", a.i18n.Ts("public.invalidFeature")))
}

View file

@ -165,7 +165,7 @@ func (a *App) validateUserRole(r auth.Role) error {
}
for _, p := range r.Permissions {
if _, ok := a.constants.Permissions[p]; !ok {
if _, ok := a.cfg.Permissions[p]; !ok {
return echo.NewHTTPError(http.StatusBadRequest, a.i18n.Ts("globals.messages.invalidFields", "name", fmt.Sprintf("permission: %s", p)))
}
}

View file

@ -330,7 +330,7 @@ func (a *App) TestSMTPSettings(c echo.Context) error {
}
m := models.Message{}
m.From = a.constants.FromEmail
m.From = a.cfg.FromEmail
m.To = []string{to}
m.Subject = a.i18n.T("settings.smtp.testConnection")
m.Body = b.Bytes()

View file

@ -11,10 +11,12 @@ import (
"strings"
"github.com/knadh/listmonk/internal/auth"
"github.com/knadh/listmonk/internal/i18n"
"github.com/knadh/listmonk/internal/notifs"
"github.com/knadh/listmonk/internal/subimporter"
"github.com/knadh/listmonk/models"
"github.com/labstack/echo/v4"
"github.com/lib/pq"
)
const (
@ -133,7 +135,7 @@ func (a *App) ExportSubscribers(c echo.Context) error {
// Get the batched export iterator.
query := sanitizeSQLExp(c.FormValue("query"))
exp, err := a.core.ExportSubscribers(query, subIDs, listIDs, subStatus, a.constants.DBBatchSize)
exp, err := a.core.ExportSubscribers(query, subIDs, listIDs, subStatus, a.cfg.DBBatchSize)
if err != nil {
return err
}
@ -262,7 +264,7 @@ func (a *App) SubscriberSendOptin(c echo.Context) error {
}
// Trigger the opt-in confirmation e-mail hook.
if _, err := a.optinConfirmNotify()(out, nil); err != nil {
if _, err := a.optinNotifyHook(out, nil); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, a.i18n.T("subscribers.errorSendingOptin"))
}
@ -510,7 +512,7 @@ func (a *App) ExportSubscriberData(c echo.Context) error {
// Get the subscriber's data. A single query that gets the profile,
// list subscriptions, campaign views, and link clicks. Names of
// private lists are replaced with "Private list".
_, b, err := a.exportSubscriberData(id, "", a.constants.Privacy.Exportable)
_, b, err := a.exportSubscriberData(id, "", a.cfg.Privacy.Exportable)
if err != nil {
a.log.Printf("error exporting subscriber data: %s", err)
return echo.NewHTTPError(http.StatusInternalServerError,
@ -557,54 +559,6 @@ func (a *App) exportSubscriberData(id int, subUUID string, exportables map[strin
return data, b, nil
}
// optinConfirmNotify returns an enclosed callback that sends optin confirmation e-mails.
// This is plugged into the 'core' package to send optin confirmations when a new subscriber is
// created via `core.CreateSubscriber()`.
func (app *App) optinConfirmNotify() func(sub models.Subscriber, listIDs []int) (int, error) {
return func(sub models.Subscriber, listIDs []int) (int, error) {
lists, err := app.core.GetSubscriberLists(sub.ID, "", listIDs, nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble)
if err != nil {
return 0, err
}
// None.
if len(lists) == 0 {
return 0, nil
}
var (
out = subOptin{Subscriber: sub, Lists: lists}
qListIDs = url.Values{}
)
// Construct the opt-in URL with list IDs.
for _, l := range out.Lists {
qListIDs.Add("l", l.UUID)
}
out.OptinURL = fmt.Sprintf(app.constants.OptinURL, sub.UUID, qListIDs.Encode())
out.UnsubURL = fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
// Unsub headers.
hdr := textproto.MIMEHeader{}
hdr.Set(models.EmailHeaderSubscriberUUID, sub.UUID)
// Attach List-Unsubscribe headers?
if app.constants.Privacy.UnsubHeader {
unsubURL := fmt.Sprintf(app.constants.UnsubURL, dummyUUID, sub.UUID)
hdr.Set("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
hdr.Set("List-Unsubscribe", `<`+unsubURL+`>`)
}
// Send the e-mail.
if err := notifs.Notify([]string{sub.Email}, app.i18n.T("subscribers.optinSubject"), notifs.TplSubscriberOptin, out, hdr); err != nil {
app.log.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err)
return 0, err
}
return len(lists), nil
}
}
// hasSubPerm checks whether the current user has permission to access the given list
// of subscriber IDs.
func (a *App) hasSubPerm(u auth.User, subIDs []int) error {
@ -694,3 +648,54 @@ func getQueryInts(param string, qp url.Values) ([]int, error) {
return out, nil
}
// makeOptinNotifyHook returns an enclosed callback that sends optin confirmation e-mails.
// This is plugged into the 'core' package to send optin confirmations when a new subscriber is
// created via `core.CreateSubscriber()`.
func makeOptinNotifyHook(unsubHeader bool, u *UrlConfig, q *models.Queries, i *i18n.I18n) func(sub models.Subscriber, listIDs []int) (int, error) {
return func(sub models.Subscriber, listIDs []int) (int, error) {
// Fetch double opt-in lists from the given list IDs.
// Get the list of subscription lists where the subscriber hasn't confirmed.
var lists = []models.List{}
if err := q.GetSubscriberLists.Select(&lists, sub.ID, "", pq.Array(listIDs), nil, models.SubscriptionStatusUnconfirmed, models.ListOptinDouble); err != nil {
lo.Printf("error fetching lists for opt-in: %s", err)
return 0, err
}
// None.
if len(lists) == 0 {
return 0, nil
}
var (
out = subOptin{Subscriber: sub, Lists: lists}
qListIDs = url.Values{}
)
// Construct the opt-in URL with list IDs.
for _, l := range out.Lists {
qListIDs.Add("l", l.UUID)
}
out.OptinURL = fmt.Sprintf(u.OptinURL, sub.UUID, qListIDs.Encode())
out.UnsubURL = fmt.Sprintf(u.UnsubURL, dummyUUID, sub.UUID)
// Unsub headers.
hdr := textproto.MIMEHeader{}
hdr.Set(models.EmailHeaderSubscriberUUID, sub.UUID)
// Attach List-Unsubscribe headers?
if unsubHeader {
unsubURL := fmt.Sprintf(u.UnsubURL, dummyUUID, sub.UUID)
hdr.Set("List-Unsubscribe-Post", "List-Unsubscribe=One-Click")
hdr.Set("List-Unsubscribe", `<`+unsubURL+`>`)
}
// Send the e-mail.
if err := notifs.Notify([]string{sub.Email}, i.T("subscribers.optinSubject"), notifs.TplSubscriberOptin, out, hdr); err != nil {
lo.Printf("error sending opt-in e-mail for subscriber %d (%s): %s", sub.ID, sub.UUID, err)
return 0, err
}
return len(lists), nil
}
}

View file

@ -191,7 +191,7 @@ func (a *App) validateTxMessage(m models.TxMessage) (models.TxMessage, error) {
}
if m.FromEmail == "" {
m.FromEmail = a.constants.FromEmail
m.FromEmail = a.cfg.FromEmail
}
if m.Messenger == "" {