2018-10-25 13:51:47 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
2020-07-08 11:00:14 +00:00
|
|
|
"context"
|
2018-10-25 13:51:47 +00:00
|
|
|
"fmt"
|
2020-10-10 18:24:03 +00:00
|
|
|
"io"
|
2018-10-25 13:51:47 +00:00
|
|
|
"log"
|
|
|
|
"os"
|
2020-07-08 11:00:14 +00:00
|
|
|
"os/signal"
|
2018-11-02 18:27:07 +00:00
|
|
|
"strings"
|
2020-07-08 11:00:14 +00:00
|
|
|
"sync"
|
|
|
|
"syscall"
|
2018-10-25 13:51:47 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/jmoiron/sqlx"
|
2019-06-26 11:23:23 +00:00
|
|
|
"github.com/knadh/koanf"
|
2019-10-01 10:49:40 +00:00
|
|
|
"github.com/knadh/koanf/providers/env"
|
2021-05-24 17:11:48 +00:00
|
|
|
"github.com/knadh/listmonk/internal/bounce"
|
2020-10-10 18:24:03 +00:00
|
|
|
"github.com/knadh/listmonk/internal/buflog"
|
2022-04-03 15:24:40 +00:00
|
|
|
"github.com/knadh/listmonk/internal/core"
|
2020-12-19 10:55:52 +00:00
|
|
|
"github.com/knadh/listmonk/internal/i18n"
|
2020-03-07 18:47:54 +00:00
|
|
|
"github.com/knadh/listmonk/internal/manager"
|
|
|
|
"github.com/knadh/listmonk/internal/media"
|
|
|
|
"github.com/knadh/listmonk/internal/messenger"
|
|
|
|
"github.com/knadh/listmonk/internal/subimporter"
|
2022-04-03 15:24:40 +00:00
|
|
|
"github.com/knadh/listmonk/models"
|
2019-01-03 11:18:47 +00:00
|
|
|
"github.com/knadh/stuffbin"
|
2018-10-25 13:51:47 +00:00
|
|
|
)
|
|
|
|
|
2020-09-20 11:01:24 +00:00
|
|
|
const (
|
|
|
|
emailMsgr = "email"
|
|
|
|
)
|
|
|
|
|
2018-10-25 13:51:47 +00:00
|
|
|
// App contains the "global" components that are
|
|
|
|
// passed around, especially through HTTP handlers.
|
|
|
|
type App struct {
|
2022-04-03 15:24:40 +00:00
|
|
|
core *core.Core
|
2020-09-20 11:01:24 +00:00
|
|
|
fs stuffbin.FileSystem
|
|
|
|
db *sqlx.DB
|
2022-04-03 15:24:40 +00:00
|
|
|
queries *models.Queries
|
2020-09-20 11:01:24 +00:00
|
|
|
constants *constants
|
|
|
|
manager *manager.Manager
|
|
|
|
importer *subimporter.Importer
|
|
|
|
messengers map[string]messenger.Messenger
|
|
|
|
media media.Store
|
2021-01-23 14:04:30 +00:00
|
|
|
i18n *i18n.I18n
|
2021-05-24 17:11:48 +00:00
|
|
|
bounce *bounce.Manager
|
2021-10-28 14:39:06 +00:00
|
|
|
notifTpls *notifTpls
|
2020-09-20 11:01:24 +00:00
|
|
|
log *log.Logger
|
2020-10-10 18:24:03 +00:00
|
|
|
bufLog *buflog.BufLog
|
2020-07-08 11:00:14 +00:00
|
|
|
|
|
|
|
// Channel for passing reload signals.
|
|
|
|
sigChan chan os.Signal
|
|
|
|
|
|
|
|
// Global variable that stores the state indicating that a restart is required
|
|
|
|
// after a settings update.
|
|
|
|
needsRestart bool
|
2020-08-08 11:29:47 +00:00
|
|
|
|
|
|
|
// Global state that stores data on an available remote update.
|
|
|
|
update *AppUpdate
|
2020-07-08 11:00:14 +00:00
|
|
|
sync.Mutex
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2019-06-26 11:23:23 +00:00
|
|
|
var (
|
2020-10-10 18:24:03 +00:00
|
|
|
// Buffered log writer for storing N lines of log entries for the UI.
|
|
|
|
bufLog = buflog.New(5000)
|
|
|
|
lo = log.New(io.MultiWriter(os.Stdout, bufLog), "",
|
|
|
|
log.Ldate|log.Ltime|log.Lshortfile)
|
2019-07-08 12:51:44 +00:00
|
|
|
|
2020-10-10 18:24:03 +00:00
|
|
|
ko = koanf.New(".")
|
2020-07-08 11:00:14 +00:00
|
|
|
fs stuffbin.FileSystem
|
|
|
|
db *sqlx.DB
|
2022-04-03 15:24:40 +00:00
|
|
|
queries *models.Queries
|
2020-07-08 11:00:14 +00:00
|
|
|
|
2021-07-11 05:03:00 +00:00
|
|
|
// Compile-time variables.
|
2020-08-08 11:29:47 +00:00
|
|
|
buildString string
|
|
|
|
versionString string
|
2021-07-11 05:03:00 +00:00
|
|
|
|
|
|
|
// If these are set in build ldflags and static assets (*.sql, config.toml.sample. ./frontend)
|
|
|
|
// are not embedded (in make dist), these paths are looked up. The default values before, when not
|
|
|
|
// overridden by build flags, are relative to the CWD at runtime.
|
|
|
|
appDir string = "."
|
|
|
|
frontendDir string = "frontend"
|
2019-06-26 11:23:23 +00:00
|
|
|
)
|
2018-10-25 13:51:47 +00:00
|
|
|
|
2019-07-08 12:51:44 +00:00
|
|
|
func init() {
|
2020-07-08 11:00:14 +00:00
|
|
|
initFlags()
|
2019-06-26 11:23:23 +00:00
|
|
|
|
2019-07-08 12:51:44 +00:00
|
|
|
// Display version.
|
2020-07-08 11:00:14 +00:00
|
|
|
if ko.Bool("version") {
|
2019-07-08 12:51:44 +00:00
|
|
|
fmt.Println(buildString)
|
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2020-10-18 17:29:22 +00:00
|
|
|
lo.Println(buildString)
|
|
|
|
|
2019-07-08 12:51:44 +00:00
|
|
|
// Generate new config.
|
2020-07-08 11:00:14 +00:00
|
|
|
if ko.Bool("new-config") {
|
2021-06-29 14:40:59 +00:00
|
|
|
path := ko.Strings("config")[0]
|
|
|
|
if err := newConfigFile(path); err != nil {
|
2020-03-07 18:33:22 +00:00
|
|
|
lo.Println(err)
|
2019-07-08 12:51:44 +00:00
|
|
|
os.Exit(1)
|
|
|
|
}
|
2021-06-29 14:40:59 +00:00
|
|
|
lo.Printf("generated %s. Edit and run --install", path)
|
2019-07-08 12:51:44 +00:00
|
|
|
os.Exit(0)
|
|
|
|
}
|
|
|
|
|
2020-07-08 11:00:14 +00:00
|
|
|
// Load config files to pick up the database settings first.
|
|
|
|
initConfigFiles(ko.Strings("config"), ko)
|
|
|
|
|
2020-08-07 12:38:24 +00:00
|
|
|
// Load environment variables and merge into the loaded config.
|
|
|
|
if err := ko.Load(env.Provider("LISTMONK_", ".", func(s string) string {
|
|
|
|
return strings.Replace(strings.ToLower(
|
|
|
|
strings.TrimPrefix(s, "LISTMONK_")), "__", ".", -1)
|
|
|
|
}), nil); err != nil {
|
|
|
|
lo.Fatalf("error loading config from env: %v", err)
|
|
|
|
}
|
|
|
|
|
2020-07-08 11:00:14 +00:00
|
|
|
// Connect to the database, load the filesystem to read SQL queries.
|
|
|
|
db = initDB()
|
2021-07-11 05:03:00 +00:00
|
|
|
fs = initFS(appDir, frontendDir, ko.String("static-dir"), ko.String("i18n-dir"))
|
2020-07-08 11:00:14 +00:00
|
|
|
|
|
|
|
// Installer mode? This runs before the SQL queries are loaded and prepared
|
|
|
|
// as the installer needs to work on an empty DB.
|
|
|
|
if ko.Bool("install") {
|
2020-08-03 13:32:23 +00:00
|
|
|
// Save the version of the last listed migration.
|
2021-08-22 14:39:39 +00:00
|
|
|
install(migList[len(migList)-1].version, db, fs, !ko.Bool("yes"), ko.Bool("idempotent"))
|
2020-08-03 13:32:23 +00:00
|
|
|
os.Exit(0)
|
|
|
|
}
|
2020-11-10 16:51:25 +00:00
|
|
|
|
|
|
|
// Check if the DB schema is installed.
|
|
|
|
if ok, err := checkSchema(db); err != nil {
|
|
|
|
log.Fatalf("error checking schema in DB: %v", err)
|
|
|
|
} else if !ok {
|
|
|
|
lo.Fatal("the database does not appear to be setup. Run --install.")
|
|
|
|
}
|
|
|
|
|
2020-08-03 13:32:23 +00:00
|
|
|
if ko.Bool("upgrade") {
|
|
|
|
upgrade(db, fs, !ko.Bool("yes"))
|
|
|
|
os.Exit(0)
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
2020-03-07 18:33:22 +00:00
|
|
|
|
2020-08-03 13:32:23 +00:00
|
|
|
// Before the queries are prepared, see if there are pending upgrades.
|
|
|
|
checkUpgrade(db)
|
|
|
|
|
2022-02-01 18:10:03 +00:00
|
|
|
// Read the SQL queries from the queries file.
|
|
|
|
qMap := readQueries(queryFilePath, db, fs)
|
2020-07-08 11:00:14 +00:00
|
|
|
|
|
|
|
// Load settings from DB.
|
2022-02-01 18:10:03 +00:00
|
|
|
if q, ok := qMap["get-settings"]; ok {
|
|
|
|
initSettings(q.Query, db, ko)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Prepare queries.
|
|
|
|
queries = prepareQueries(qMap, db, ko)
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
2020-03-07 18:33:22 +00:00
|
|
|
func main() {
|
|
|
|
// Initialize the main app controller that wraps all of the app's
|
|
|
|
// components. This is passed around HTTP handlers.
|
|
|
|
app := &App{
|
2020-09-20 11:01:24 +00:00
|
|
|
fs: fs,
|
|
|
|
db: db,
|
|
|
|
constants: initConstants(),
|
|
|
|
media: initMediaStore(),
|
|
|
|
messengers: make(map[string]messenger.Messenger),
|
|
|
|
log: lo,
|
2020-10-10 18:24:03 +00:00
|
|
|
bufLog: bufLog,
|
2020-03-07 18:33:22 +00:00
|
|
|
}
|
2020-12-19 10:55:52 +00:00
|
|
|
|
|
|
|
// Load i18n language map.
|
|
|
|
app.i18n = initI18n(app.constants.Lang, fs)
|
|
|
|
|
2022-04-03 15:24:40 +00:00
|
|
|
app.core = core.New(&core.Opt{
|
|
|
|
Constants: core.Constants{
|
|
|
|
SendOptinConfirmation: app.constants.SendOptinConfirmation,
|
2022-05-01 09:23:01 +00:00
|
|
|
MaxBounceCount: ko.MustInt("bounce.count"),
|
|
|
|
BounceAction: ko.MustString("bounce.action"),
|
2022-04-03 15:24:40 +00:00
|
|
|
},
|
|
|
|
Queries: queries,
|
|
|
|
DB: db,
|
|
|
|
I18n: app.i18n,
|
|
|
|
Log: lo,
|
|
|
|
}, &core.Hooks{
|
|
|
|
SendOptinConfirmation: sendOptinConfirmationHook(app),
|
|
|
|
})
|
|
|
|
|
2022-02-01 18:10:03 +00:00
|
|
|
app.queries = queries
|
2020-03-08 10:08:47 +00:00
|
|
|
app.manager = initCampaignManager(app.queries, app.constants, app)
|
|
|
|
app.importer = initImporter(app.queries, db, app)
|
2020-12-19 10:55:52 +00:00
|
|
|
app.notifTpls = initNotifTemplates("/email-templates/*.html", fs, app.i18n, app.constants)
|
2022-07-02 10:00:17 +00:00
|
|
|
initTxTemplates(app.manager, app)
|
2020-03-07 18:33:22 +00:00
|
|
|
|
2021-05-24 17:11:48 +00:00
|
|
|
if ko.Bool("bounce.enabled") {
|
|
|
|
app.bounce = initBounceManager(app)
|
|
|
|
go app.bounce.Run()
|
|
|
|
}
|
|
|
|
|
2020-09-20 11:01:24 +00:00
|
|
|
// Initialize the default SMTP (`email`) messenger.
|
|
|
|
app.messengers[emailMsgr] = initSMTPMessenger(app.manager)
|
|
|
|
|
|
|
|
// Initialize any additional postback messengers.
|
|
|
|
for _, m := range initPostbackMessengers(app.manager) {
|
|
|
|
app.messengers[m.Name()] = m
|
|
|
|
}
|
|
|
|
|
|
|
|
// Attach all messengers to the campaign manager.
|
|
|
|
for _, m := range app.messengers {
|
|
|
|
app.manager.AddMessenger(m)
|
|
|
|
}
|
|
|
|
|
2020-03-08 06:57:41 +00:00
|
|
|
// Start the campaign workers. The campaign batches (fetch from DB, push out
|
|
|
|
// messages) get processed at the specified interval.
|
2021-10-29 09:10:22 +00:00
|
|
|
go app.manager.Run()
|
2020-03-07 18:33:22 +00:00
|
|
|
|
2020-07-08 11:00:14 +00:00
|
|
|
// Start the app server.
|
|
|
|
srv := initHTTPServer(app)
|
|
|
|
|
2020-08-08 11:29:47 +00:00
|
|
|
// Star the update checker.
|
2021-05-16 11:24:55 +00:00
|
|
|
if ko.Bool("app.check_updates") {
|
|
|
|
go checkUpdates(versionString, time.Hour*24, app)
|
|
|
|
}
|
2020-08-08 11:29:47 +00:00
|
|
|
|
2020-07-08 11:00:14 +00:00
|
|
|
// Wait for the reload signal with a callback to gracefully shut down resources.
|
|
|
|
// The `wait` channel is passed to awaitReload to wait for the callback to finish
|
|
|
|
// within N seconds, or do a force reload.
|
|
|
|
app.sigChan = make(chan os.Signal)
|
|
|
|
signal.Notify(app.sigChan, syscall.SIGHUP)
|
|
|
|
|
|
|
|
closerWait := make(chan bool)
|
|
|
|
<-awaitReload(app.sigChan, closerWait, func() {
|
|
|
|
// Stop the HTTP server.
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
|
|
|
defer cancel()
|
|
|
|
srv.Shutdown(ctx)
|
|
|
|
|
|
|
|
// Close the campaign manager.
|
|
|
|
app.manager.Close()
|
|
|
|
|
|
|
|
// Close the DB pool.
|
|
|
|
app.db.DB.Close()
|
|
|
|
|
|
|
|
// Close the messenger pool.
|
2020-09-20 11:01:24 +00:00
|
|
|
for _, m := range app.messengers {
|
|
|
|
m.Close()
|
|
|
|
}
|
2020-07-08 11:00:14 +00:00
|
|
|
|
|
|
|
// Signal the close.
|
|
|
|
closerWait <- true
|
|
|
|
})
|
2018-10-25 13:51:47 +00:00
|
|
|
}
|