init.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "html/template"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "syscall"
  10. "time"
  11. "github.com/jmoiron/sqlx"
  12. "github.com/jmoiron/sqlx/types"
  13. "github.com/knadh/goyesql/v2"
  14. goyesqlx "github.com/knadh/goyesql/v2/sqlx"
  15. "github.com/knadh/koanf"
  16. "github.com/knadh/koanf/maps"
  17. "github.com/knadh/koanf/parsers/toml"
  18. "github.com/knadh/koanf/providers/confmap"
  19. "github.com/knadh/koanf/providers/file"
  20. "github.com/knadh/koanf/providers/posflag"
  21. "github.com/knadh/listmonk/internal/manager"
  22. "github.com/knadh/listmonk/internal/media"
  23. "github.com/knadh/listmonk/internal/media/providers/filesystem"
  24. "github.com/knadh/listmonk/internal/media/providers/s3"
  25. "github.com/knadh/listmonk/internal/messenger"
  26. "github.com/knadh/listmonk/internal/subimporter"
  27. "github.com/knadh/stuffbin"
  28. "github.com/labstack/echo"
  29. flag "github.com/spf13/pflag"
  30. )
  31. const (
  32. queryFilePath = "queries.sql"
  33. )
  34. // constants contains static, constant config values required by the app.
  35. type constants struct {
  36. RootURL string `koanf:"root"`
  37. LogoURL string `koanf:"logo_url"`
  38. FaviconURL string `koanf:"favicon_url"`
  39. FromEmail string `koanf:"from_email"`
  40. NotifyEmails []string `koanf:"notify_emails"`
  41. Privacy struct {
  42. AllowBlocklist bool `koanf:"allow_blocklist"`
  43. AllowExport bool `koanf:"allow_export"`
  44. AllowWipe bool `koanf:"allow_wipe"`
  45. Exportable map[string]bool `koanf:"-"`
  46. } `koanf:"privacy"`
  47. UnsubURL string
  48. LinkTrackURL string
  49. ViewTrackURL string
  50. OptinURL string
  51. MessageURL string
  52. MediaProvider string
  53. }
  54. func initFlags() {
  55. f := flag.NewFlagSet("config", flag.ContinueOnError)
  56. f.Usage = func() {
  57. // Register --help handler.
  58. fmt.Println(f.FlagUsages())
  59. os.Exit(0)
  60. }
  61. // Register the commandline flags.
  62. f.StringSlice("config", []string{"config.toml"},
  63. "path to one or more config files (will be merged in order)")
  64. f.Bool("install", false, "run first time installation")
  65. f.Bool("version", false, "current version of the build")
  66. f.Bool("new-config", false, "generate sample config file")
  67. f.String("static-dir", "", "(optional) path to directory with static files")
  68. f.Bool("yes", false, "assume 'yes' to prompts, eg: during --install")
  69. if err := f.Parse(os.Args[1:]); err != nil {
  70. lo.Fatalf("error loading flags: %v", err)
  71. }
  72. if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
  73. lo.Fatalf("error loading config: %v", err)
  74. }
  75. }
  76. // initConfigFiles loads the given config files into the koanf instance.
  77. func initConfigFiles(files []string, ko *koanf.Koanf) {
  78. for _, f := range files {
  79. lo.Printf("reading config: %s", f)
  80. if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
  81. if os.IsNotExist(err) {
  82. lo.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.")
  83. }
  84. lo.Fatalf("error loadng config from file: %v.", err)
  85. }
  86. }
  87. }
  88. // initFileSystem initializes the stuffbin FileSystem to provide
  89. // access to bunded static assets to the app.
  90. func initFS(staticDir string) stuffbin.FileSystem {
  91. // Get the executable's path.
  92. path, err := os.Executable()
  93. if err != nil {
  94. lo.Fatalf("error getting executable path: %v", err)
  95. }
  96. // Load the static files stuffed in the binary.
  97. fs, err := stuffbin.UnStuff(path)
  98. if err != nil {
  99. // Running in local mode. Load local assets into
  100. // the in-memory stuffbin.FileSystem.
  101. lo.Printf("unable to initialize embedded filesystem: %v", err)
  102. lo.Printf("using local filesystem for static assets")
  103. files := []string{
  104. "config.toml.sample",
  105. "queries.sql",
  106. "schema.sql",
  107. "static/email-templates",
  108. // Alias /static/public to /public for the HTTP fileserver.
  109. "static/public:/public",
  110. // The frontend app's static assets are aliased to /frontend
  111. // so that they are accessible at /frontend/js/* etc.
  112. // Alias all files inside dist/ and dist/frontend to frontend/*.
  113. "frontend/dist/:/frontend",
  114. "frontend/dist/frontend:/frontend",
  115. }
  116. fs, err = stuffbin.NewLocalFS("/", files...)
  117. if err != nil {
  118. lo.Fatalf("failed to initialize local file for assets: %v", err)
  119. }
  120. }
  121. // Optional static directory to override files.
  122. if staticDir != "" {
  123. lo.Printf("loading static files from: %v", staticDir)
  124. fStatic, err := stuffbin.NewLocalFS("/", []string{
  125. filepath.Join(staticDir, "/email-templates") + ":/static/email-templates",
  126. // Alias /static/public to /public for the HTTP fileserver.
  127. filepath.Join(staticDir, "/public") + ":/public",
  128. }...)
  129. if err != nil {
  130. lo.Fatalf("failed reading static directory: %s: %v", staticDir, err)
  131. }
  132. if err := fs.Merge(fStatic); err != nil {
  133. lo.Fatalf("error merging static directory: %s: %v", staticDir, err)
  134. }
  135. }
  136. return fs
  137. }
  138. // initDB initializes the main DB connection pool and parse and loads the app's
  139. // SQL queries into a prepared query map.
  140. func initDB() *sqlx.DB {
  141. var dbCfg dbConf
  142. if err := ko.Unmarshal("db", &dbCfg); err != nil {
  143. lo.Fatalf("error loading db config: %v", err)
  144. }
  145. lo.Printf("connecting to db: %s:%d/%s", dbCfg.Host, dbCfg.Port, dbCfg.DBName)
  146. db, err := connectDB(dbCfg)
  147. if err != nil {
  148. lo.Fatalf("error connecting to DB: %v", err)
  149. }
  150. return db
  151. }
  152. // initQueries loads named SQL queries from the queries file and optionally
  153. // prepares them.
  154. func initQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem, prepareQueries bool) (goyesql.Queries, *Queries) {
  155. // Load SQL queries.
  156. qB, err := fs.Read(sqlFile)
  157. if err != nil {
  158. lo.Fatalf("error reading SQL file %s: %v", sqlFile, err)
  159. }
  160. qMap, err := goyesql.ParseBytes(qB)
  161. if err != nil {
  162. lo.Fatalf("error parsing SQL queries: %v", err)
  163. }
  164. if !prepareQueries {
  165. return qMap, nil
  166. }
  167. // Prepare queries.
  168. var q Queries
  169. if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
  170. lo.Fatalf("error preparing SQL queries: %v", err)
  171. }
  172. return qMap, &q
  173. }
  174. // initSettings loads settings from the DB.
  175. func initSettings(q *Queries) {
  176. var s types.JSONText
  177. if err := q.GetSettings.Get(&s); err != nil {
  178. lo.Fatalf("error reading settings from DB: %s", pqErrMsg(err))
  179. }
  180. // Setting keys are dot separated, eg: app.favicon_url. Unflatten them into
  181. // nested maps {app: {favicon_url}}.
  182. var out map[string]interface{}
  183. if err := json.Unmarshal(s, &out); err != nil {
  184. lo.Fatalf("error unmarshalling settings from DB: %v", err)
  185. }
  186. if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
  187. lo.Fatalf("error parsing settings from DB: %v", err)
  188. }
  189. }
  190. func initConstants() *constants {
  191. // Read constants.
  192. var c constants
  193. if err := ko.Unmarshal("app", &c); err != nil {
  194. lo.Fatalf("error loading app config: %v", err)
  195. }
  196. if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
  197. lo.Fatalf("error loading app config: %v", err)
  198. }
  199. c.RootURL = strings.TrimRight(c.RootURL, "/")
  200. c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
  201. c.MediaProvider = ko.String("upload.provider")
  202. // Static URLS.
  203. // url.com/subscription/{campaign_uuid}/{subscriber_uuid}
  204. c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", c.RootURL)
  205. // url.com/subscription/optin/{subscriber_uuid}
  206. c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", c.RootURL)
  207. // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
  208. c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", c.RootURL)
  209. // url.com/link/{campaign_uuid}/{subscriber_uuid}
  210. c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
  211. // url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
  212. c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
  213. return &c
  214. }
  215. // initCampaignManager initializes the campaign manager.
  216. func initCampaignManager(q *Queries, cs *constants, app *App) *manager.Manager {
  217. campNotifCB := func(subject string, data interface{}) error {
  218. return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data)
  219. }
  220. if ko.Int("app.concurrency") < 1 {
  221. lo.Fatal("app.concurrency should be at least 1")
  222. }
  223. if ko.Int("app.message_rate") < 1 {
  224. lo.Fatal("app.message_rate should be at least 1")
  225. }
  226. return manager.New(manager.Config{
  227. BatchSize: ko.Int("app.batch_size"),
  228. Concurrency: ko.Int("app.concurrency"),
  229. MessageRate: ko.Int("app.message_rate"),
  230. MaxSendErrors: ko.Int("app.max_send_errors"),
  231. FromEmail: cs.FromEmail,
  232. UnsubURL: cs.UnsubURL,
  233. OptinURL: cs.OptinURL,
  234. LinkTrackURL: cs.LinkTrackURL,
  235. ViewTrackURL: cs.ViewTrackURL,
  236. MessageURL: cs.MessageURL,
  237. }, newManagerDB(q), campNotifCB, lo)
  238. }
  239. // initImporter initializes the bulk subscriber importer.
  240. func initImporter(q *Queries, db *sqlx.DB, app *App) *subimporter.Importer {
  241. return subimporter.New(
  242. subimporter.Options{
  243. UpsertStmt: q.UpsertSubscriber.Stmt,
  244. BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
  245. UpdateListDateStmt: q.UpdateListsDate.Stmt,
  246. NotifCB: func(subject string, data interface{}) error {
  247. app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data)
  248. return nil
  249. },
  250. }, db.DB)
  251. }
  252. // initMessengers initializes various messenger backends.
  253. func initMessengers(m *manager.Manager) messenger.Messenger {
  254. var (
  255. mapKeys = ko.MapKeys("smtp")
  256. servers = make([]messenger.Server, 0, len(mapKeys))
  257. )
  258. items := ko.Slices("smtp")
  259. if len(items) == 0 {
  260. lo.Fatalf("no SMTP servers found in config")
  261. }
  262. // Load the default SMTP messengers.
  263. for _, item := range items {
  264. if !item.Bool("enabled") {
  265. continue
  266. }
  267. // Read the SMTP config.
  268. var s messenger.Server
  269. if err := item.UnmarshalWithConf("", &s, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  270. lo.Fatalf("error loading SMTP: %v", err)
  271. }
  272. servers = append(servers, s)
  273. lo.Printf("loaded SMTP: %s@%s", item.String("username"), item.String("host"))
  274. }
  275. if len(servers) == 0 {
  276. lo.Fatalf("no SMTP servers enabled in settings")
  277. }
  278. // Initialize the default e-mail messenger.
  279. msgr, err := messenger.NewEmailer(servers...)
  280. if err != nil {
  281. lo.Fatalf("error loading e-mail messenger: %v", err)
  282. }
  283. if err := m.AddMessenger(msgr); err != nil {
  284. lo.Printf("error registering messenger %s", err)
  285. }
  286. return msgr
  287. }
  288. // initMediaStore initializes Upload manager with a custom backend.
  289. func initMediaStore() media.Store {
  290. switch provider := ko.String("upload.provider"); provider {
  291. case "s3":
  292. var o s3.Opts
  293. ko.Unmarshal("upload.s3", &o)
  294. up, err := s3.NewS3Store(o)
  295. if err != nil {
  296. lo.Fatalf("error initializing s3 upload provider %s", err)
  297. }
  298. lo.Println("media upload provider: s3")
  299. return up
  300. case "filesystem":
  301. var o filesystem.Opts
  302. ko.Unmarshal("upload.filesystem", &o)
  303. o.RootURL = ko.String("app.root")
  304. o.UploadPath = filepath.Clean(o.UploadPath)
  305. o.UploadURI = filepath.Clean(o.UploadURI)
  306. up, err := filesystem.NewDiskStore(o)
  307. if err != nil {
  308. lo.Fatalf("error initializing filesystem upload provider %s", err)
  309. }
  310. lo.Println("media upload provider: filesystem")
  311. return up
  312. default:
  313. lo.Fatalf("unknown provider. select filesystem or s3")
  314. }
  315. return nil
  316. }
  317. // initNotifTemplates compiles and returns e-mail notification templates that are
  318. // used for sending ad-hoc notifications to admins and subscribers.
  319. func initNotifTemplates(path string, fs stuffbin.FileSystem, cs *constants) *template.Template {
  320. // Register utility functions that the e-mail templates can use.
  321. funcs := template.FuncMap{
  322. "RootURL": func() string {
  323. return cs.RootURL
  324. },
  325. "LogoURL": func() string {
  326. return cs.LogoURL
  327. }}
  328. tpl, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/static/email-templates/*.html")
  329. if err != nil {
  330. lo.Fatalf("error parsing e-mail notif templates: %v", err)
  331. }
  332. return tpl
  333. }
  334. // initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
  335. func initHTTPServer(app *App) *echo.Echo {
  336. // Initialize the HTTP server.
  337. var srv = echo.New()
  338. srv.HideBanner = true
  339. // Register app (*App) to be injected into all HTTP handlers.
  340. srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  341. return func(c echo.Context) error {
  342. c.Set("app", app)
  343. return next(c)
  344. }
  345. })
  346. // Parse and load user facing templates.
  347. tpl, err := stuffbin.ParseTemplatesGlob(nil, app.fs, "/public/templates/*.html")
  348. if err != nil {
  349. lo.Fatalf("error parsing public templates: %v", err)
  350. }
  351. srv.Renderer = &tplRenderer{
  352. templates: tpl,
  353. RootURL: app.constants.RootURL,
  354. LogoURL: app.constants.LogoURL,
  355. FaviconURL: app.constants.FaviconURL}
  356. // Initialize the static file server.
  357. fSrv := app.fs.FileServer()
  358. srv.GET("/public/*", echo.WrapHandler(fSrv))
  359. srv.GET("/frontend/*", echo.WrapHandler(fSrv))
  360. if ko.String("upload.provider") == "filesystem" {
  361. srv.Static(ko.String("upload.filesystem.upload_uri"),
  362. ko.String("upload.filesystem.upload_path"))
  363. }
  364. // Register all HTTP handlers.
  365. registerHTTPHandlers(srv)
  366. // Start the server.
  367. go func() {
  368. if err := srv.Start(ko.String("app.address")); err != nil {
  369. if strings.Contains(err.Error(), "Server closed") {
  370. lo.Println("HTTP server shut down")
  371. } else {
  372. lo.Fatalf("error starting HTTP server: %v", err)
  373. }
  374. }
  375. }()
  376. return srv
  377. }
  378. func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
  379. // The blocking signal handler that main() waits on.
  380. out := make(chan bool)
  381. // Respawn a new process and exit the running one.
  382. respawn := func() {
  383. if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil {
  384. lo.Fatalf("error spawning process: %v", err)
  385. }
  386. os.Exit(0)
  387. }
  388. // Listen for reload signal.
  389. go func() {
  390. for range sigChan {
  391. lo.Println("reloading on signal ...")
  392. go closer()
  393. select {
  394. case <-closerWait:
  395. // Wait for the closer to finish.
  396. respawn()
  397. case <-time.After(time.Second * 3):
  398. // Or timeout and force close.
  399. respawn()
  400. }
  401. }
  402. }()
  403. return out
  404. }