init.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "html/template"
  7. "os"
  8. "path"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "syscall"
  13. "time"
  14. "github.com/Masterminds/sprig/v3"
  15. "github.com/jmoiron/sqlx"
  16. "github.com/jmoiron/sqlx/types"
  17. "github.com/knadh/goyesql/v2"
  18. goyesqlx "github.com/knadh/goyesql/v2/sqlx"
  19. "github.com/knadh/koanf/maps"
  20. "github.com/knadh/koanf/parsers/toml"
  21. "github.com/knadh/koanf/providers/confmap"
  22. "github.com/knadh/koanf/providers/file"
  23. "github.com/knadh/koanf/providers/posflag"
  24. "github.com/knadh/koanf/v2"
  25. "github.com/knadh/listmonk/internal/bounce"
  26. "github.com/knadh/listmonk/internal/bounce/mailbox"
  27. "github.com/knadh/listmonk/internal/captcha"
  28. "github.com/knadh/listmonk/internal/i18n"
  29. "github.com/knadh/listmonk/internal/manager"
  30. "github.com/knadh/listmonk/internal/media"
  31. "github.com/knadh/listmonk/internal/media/providers/filesystem"
  32. "github.com/knadh/listmonk/internal/media/providers/s3"
  33. "github.com/knadh/listmonk/internal/messenger/email"
  34. "github.com/knadh/listmonk/internal/messenger/postback"
  35. "github.com/knadh/listmonk/internal/subimporter"
  36. "github.com/knadh/listmonk/models"
  37. "github.com/knadh/stuffbin"
  38. "github.com/labstack/echo/v4"
  39. "github.com/lib/pq"
  40. flag "github.com/spf13/pflag"
  41. )
  42. const (
  43. queryFilePath = "queries.sql"
  44. // Root URI of the admin frontend.
  45. adminRoot = "/admin"
  46. )
  47. // constants contains static, constant config values required by the app.
  48. type constants struct {
  49. SiteName string `koanf:"site_name"`
  50. RootURL string `koanf:"root_url"`
  51. LogoURL string `koanf:"logo_url"`
  52. FaviconURL string `koanf:"favicon_url"`
  53. FromEmail string `koanf:"from_email"`
  54. NotifyEmails []string `koanf:"notify_emails"`
  55. EnablePublicSubPage bool `koanf:"enable_public_subscription_page"`
  56. EnablePublicArchive bool `koanf:"enable_public_archive"`
  57. EnablePublicArchiveRSSContent bool `koanf:"enable_public_archive_rss_content"`
  58. SendOptinConfirmation bool `koanf:"send_optin_confirmation"`
  59. Lang string `koanf:"lang"`
  60. DBBatchSize int `koanf:"batch_size"`
  61. Privacy struct {
  62. IndividualTracking bool `koanf:"individual_tracking"`
  63. AllowPreferences bool `koanf:"allow_preferences"`
  64. AllowBlocklist bool `koanf:"allow_blocklist"`
  65. AllowExport bool `koanf:"allow_export"`
  66. AllowWipe bool `koanf:"allow_wipe"`
  67. RecordOptinIP bool `koanf:"record_optin_ip"`
  68. Exportable map[string]bool `koanf:"-"`
  69. DomainBlocklist []string `koanf:"-"`
  70. } `koanf:"privacy"`
  71. Security struct {
  72. EnableCaptcha bool `koanf:"enable_captcha"`
  73. CaptchaKey string `koanf:"captcha_key"`
  74. CaptchaSecret string `koanf:"captcha_secret"`
  75. } `koanf:"security"`
  76. AdminUsername []byte `koanf:"admin_username"`
  77. AdminPassword []byte `koanf:"admin_password"`
  78. Appearance struct {
  79. AdminCSS []byte `koanf:"admin.custom_css"`
  80. AdminJS []byte `koanf:"admin.custom_js"`
  81. PublicCSS []byte `koanf:"public.custom_css"`
  82. PublicJS []byte `koanf:"public.custom_js"`
  83. }
  84. UnsubURL string
  85. LinkTrackURL string
  86. ViewTrackURL string
  87. OptinURL string
  88. MessageURL string
  89. ArchiveURL string
  90. MediaUpload struct {
  91. Provider string
  92. Extensions []string
  93. }
  94. BounceWebhooksEnabled bool
  95. BounceSESEnabled bool
  96. BounceSendgridEnabled bool
  97. BouncePostmarkEnabled bool
  98. }
  99. type notifTpls struct {
  100. tpls *template.Template
  101. contentType string
  102. }
  103. func initFlags() {
  104. f := flag.NewFlagSet("config", flag.ContinueOnError)
  105. f.Usage = func() {
  106. // Register --help handler.
  107. fmt.Println(f.FlagUsages())
  108. os.Exit(0)
  109. }
  110. // Register the commandline flags.
  111. f.StringSlice("config", []string{"config.toml"},
  112. "path to one or more config files (will be merged in order)")
  113. f.Bool("install", false, "setup database (first time)")
  114. f.Bool("idempotent", false, "make --install run only if the database isn't already setup")
  115. f.Bool("upgrade", false, "upgrade database to the current version")
  116. f.Bool("version", false, "show current version of the build")
  117. f.Bool("new-config", false, "generate sample config file")
  118. f.String("static-dir", "", "(optional) path to directory with static files")
  119. f.String("i18n-dir", "", "(optional) path to directory with i18n language files")
  120. f.Bool("yes", false, "assume 'yes' to prompts during --install/upgrade")
  121. f.Bool("passive", false, "run in passive mode where campaigns are not processed")
  122. if err := f.Parse(os.Args[1:]); err != nil {
  123. lo.Fatalf("error loading flags: %v", err)
  124. }
  125. if err := ko.Load(posflag.Provider(f, ".", ko), nil); err != nil {
  126. lo.Fatalf("error loading config: %v", err)
  127. }
  128. }
  129. // initConfigFiles loads the given config files into the koanf instance.
  130. func initConfigFiles(files []string, ko *koanf.Koanf) {
  131. for _, f := range files {
  132. lo.Printf("reading config: %s", f)
  133. if err := ko.Load(file.Provider(f), toml.Parser()); err != nil {
  134. if os.IsNotExist(err) {
  135. lo.Fatal("config file not found. If there isn't one yet, run --new-config to generate one.")
  136. }
  137. lo.Fatalf("error loading config from file: %v.", err)
  138. }
  139. }
  140. }
  141. // initFileSystem initializes the stuffbin FileSystem to provide
  142. // access to bundled static assets to the app.
  143. func initFS(appDir, frontendDir, staticDir, i18nDir string) stuffbin.FileSystem {
  144. var (
  145. // stuffbin real_path:virtual_alias paths to map local assets on disk
  146. // when there an embedded filestystem is not found.
  147. // These paths are joined with appDir.
  148. appFiles = []string{
  149. "./config.toml.sample:config.toml.sample",
  150. "./queries.sql:queries.sql",
  151. "./schema.sql:schema.sql",
  152. }
  153. frontendFiles = []string{
  154. // Admin frontend's static assets accessible at /admin/* during runtime.
  155. // These paths are sourced from frontendDir.
  156. "./:/admin",
  157. }
  158. staticFiles = []string{
  159. // These paths are joined with staticDir.
  160. "./email-templates:static/email-templates",
  161. "./public:/public",
  162. }
  163. i18nFiles = []string{
  164. // These paths are joined with i18nDir.
  165. "./:/i18n",
  166. }
  167. )
  168. // Get the executable's path.
  169. path, err := os.Executable()
  170. if err != nil {
  171. lo.Fatalf("error getting executable path: %v", err)
  172. }
  173. // Load embedded files in the executable.
  174. hasEmbed := true
  175. fs, err := stuffbin.UnStuff(path)
  176. if err != nil {
  177. hasEmbed = false
  178. // Running in local mode. Load local assets into
  179. // the in-memory stuffbin.FileSystem.
  180. lo.Printf("unable to initialize embedded filesystem (%v). Using local filesystem", err)
  181. fs, err = stuffbin.NewLocalFS("/")
  182. if err != nil {
  183. lo.Fatalf("failed to initialize local file for assets: %v", err)
  184. }
  185. }
  186. // If the embed failed, load app and frontend files from the compile-time paths.
  187. files := []string{}
  188. if !hasEmbed {
  189. files = append(files, joinFSPaths(appDir, appFiles)...)
  190. files = append(files, joinFSPaths(frontendDir, frontendFiles)...)
  191. }
  192. // Irrespective of the embeds, if there are user specified static or i18n paths,
  193. // load files from there and override default files (embedded or picked up from CWD).
  194. if !hasEmbed || i18nDir != "" {
  195. if i18nDir == "" {
  196. // Default dir in cwd.
  197. i18nDir = "i18n"
  198. }
  199. lo.Printf("loading i18n files from: %v", i18nDir)
  200. files = append(files, joinFSPaths(i18nDir, i18nFiles)...)
  201. }
  202. if !hasEmbed || staticDir != "" {
  203. if staticDir == "" {
  204. // Default dir in cwd.
  205. staticDir = "static"
  206. }
  207. lo.Printf("loading static files from: %v", staticDir)
  208. files = append(files, joinFSPaths(staticDir, staticFiles)...)
  209. }
  210. // No additional files to load.
  211. if len(files) == 0 {
  212. return fs
  213. }
  214. // Load files from disk and overlay into the FS.
  215. fStatic, err := stuffbin.NewLocalFS("/", files...)
  216. if err != nil {
  217. lo.Fatalf("failed reading static files from disk: '%s': %v", staticDir, err)
  218. }
  219. if err := fs.Merge(fStatic); err != nil {
  220. lo.Fatalf("error merging static files: '%s': %v", staticDir, err)
  221. }
  222. return fs
  223. }
  224. // initDB initializes the main DB connection pool and parse and loads the app's
  225. // SQL queries into a prepared query map.
  226. func initDB() *sqlx.DB {
  227. var c struct {
  228. Host string `koanf:"host"`
  229. Port int `koanf:"port"`
  230. User string `koanf:"user"`
  231. Password string `koanf:"password"`
  232. DBName string `koanf:"database"`
  233. SSLMode string `koanf:"ssl_mode"`
  234. Params string `koanf:"params"`
  235. MaxOpen int `koanf:"max_open"`
  236. MaxIdle int `koanf:"max_idle"`
  237. MaxLifetime time.Duration `koanf:"max_lifetime"`
  238. }
  239. if err := ko.Unmarshal("db", &c); err != nil {
  240. lo.Fatalf("error loading db config: %v", err)
  241. }
  242. lo.Printf("connecting to db: %s:%d/%s", c.Host, c.Port, c.DBName)
  243. db, err := sqlx.Connect("postgres",
  244. fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s %s", c.Host, c.Port, c.User, c.Password, c.DBName, c.SSLMode, c.Params))
  245. if err != nil {
  246. lo.Fatalf("error connecting to DB: %v", err)
  247. }
  248. db.SetMaxOpenConns(c.MaxOpen)
  249. db.SetMaxIdleConns(c.MaxIdle)
  250. db.SetConnMaxLifetime(c.MaxLifetime)
  251. return db
  252. }
  253. // readQueries reads named SQL queries from the SQL queries file into a query map.
  254. func readQueries(sqlFile string, db *sqlx.DB, fs stuffbin.FileSystem) goyesql.Queries {
  255. // Load SQL queries.
  256. qB, err := fs.Read(sqlFile)
  257. if err != nil {
  258. lo.Fatalf("error reading SQL file %s: %v", sqlFile, err)
  259. }
  260. qMap, err := goyesql.ParseBytes(qB)
  261. if err != nil {
  262. lo.Fatalf("error parsing SQL queries: %v", err)
  263. }
  264. return qMap
  265. }
  266. // prepareQueries queries prepares a query map and returns a *Queries
  267. func prepareQueries(qMap goyesql.Queries, db *sqlx.DB, ko *koanf.Koanf) *models.Queries {
  268. var (
  269. countQuery = "get-campaign-analytics-counts"
  270. linkSel = "*"
  271. )
  272. if ko.Bool("privacy.individual_tracking") {
  273. countQuery = "get-campaign-analytics-unique-counts"
  274. linkSel = "DISTINCT subscriber_id"
  275. }
  276. // These don't exist in the SQL file but are in the queries struct to be prepared.
  277. qMap["get-campaign-view-counts"] = &goyesql.Query{
  278. Query: fmt.Sprintf(qMap[countQuery].Query, "campaign_views"),
  279. Tags: map[string]string{"name": "get-campaign-view-counts"},
  280. }
  281. qMap["get-campaign-click-counts"] = &goyesql.Query{
  282. Query: fmt.Sprintf(qMap[countQuery].Query, "link_clicks"),
  283. Tags: map[string]string{"name": "get-campaign-click-counts"},
  284. }
  285. qMap["get-campaign-link-counts"].Query = fmt.Sprintf(qMap["get-campaign-link-counts"].Query, linkSel)
  286. // Scan and prepare all queries.
  287. var q models.Queries
  288. if err := goyesqlx.ScanToStruct(&q, qMap, db.Unsafe()); err != nil {
  289. lo.Fatalf("error preparing SQL queries: %v", err)
  290. }
  291. return &q
  292. }
  293. // initSettings loads settings from the DB into the given Koanf map.
  294. func initSettings(query string, db *sqlx.DB, ko *koanf.Koanf) {
  295. var s types.JSONText
  296. if err := db.Get(&s, query); err != nil {
  297. msg := err.Error()
  298. if err, ok := err.(*pq.Error); ok {
  299. if err.Detail != "" {
  300. msg = fmt.Sprintf("%s. %s", err, err.Detail)
  301. }
  302. }
  303. lo.Fatalf("error reading settings from DB: %s", msg)
  304. }
  305. // Setting keys are dot separated, eg: app.favicon_url. Unflatten them into
  306. // nested maps {app: {favicon_url}}.
  307. var out map[string]interface{}
  308. if err := json.Unmarshal(s, &out); err != nil {
  309. lo.Fatalf("error unmarshalling settings from DB: %v", err)
  310. }
  311. if err := ko.Load(confmap.Provider(out, "."), nil); err != nil {
  312. lo.Fatalf("error parsing settings from DB: %v", err)
  313. }
  314. }
  315. func initConstants() *constants {
  316. // Read constants.
  317. var c constants
  318. if err := ko.Unmarshal("app", &c); err != nil {
  319. lo.Fatalf("error loading app config: %v", err)
  320. }
  321. if err := ko.Unmarshal("privacy", &c.Privacy); err != nil {
  322. lo.Fatalf("error loading app.privacy config: %v", err)
  323. }
  324. if err := ko.Unmarshal("security", &c.Security); err != nil {
  325. lo.Fatalf("error loading app.security config: %v", err)
  326. }
  327. if err := ko.UnmarshalWithConf("appearance", &c.Appearance, koanf.UnmarshalConf{FlatPaths: true}); err != nil {
  328. lo.Fatalf("error loading app.appearance config: %v", err)
  329. }
  330. c.RootURL = strings.TrimRight(c.RootURL, "/")
  331. c.Lang = ko.String("app.lang")
  332. c.Privacy.Exportable = maps.StringSliceToLookupMap(ko.Strings("privacy.exportable"))
  333. c.MediaUpload.Provider = ko.String("upload.provider")
  334. c.MediaUpload.Extensions = ko.Strings("upload.extensions")
  335. c.Privacy.DomainBlocklist = ko.Strings("privacy.domain_blocklist")
  336. // Static URLS.
  337. // url.com/subscription/{campaign_uuid}/{subscriber_uuid}
  338. c.UnsubURL = fmt.Sprintf("%s/subscription/%%s/%%s", c.RootURL)
  339. // url.com/subscription/optin/{subscriber_uuid}
  340. c.OptinURL = fmt.Sprintf("%s/subscription/optin/%%s?%%s", c.RootURL)
  341. // url.com/link/{campaign_uuid}/{subscriber_uuid}/{link_uuid}
  342. c.LinkTrackURL = fmt.Sprintf("%s/link/%%s/%%s/%%s", c.RootURL)
  343. // url.com/link/{campaign_uuid}/{subscriber_uuid}
  344. c.MessageURL = fmt.Sprintf("%s/campaign/%%s/%%s", c.RootURL)
  345. // url.com/archive
  346. c.ArchiveURL = c.RootURL + "/archive"
  347. // url.com/campaign/{campaign_uuid}/{subscriber_uuid}/px.png
  348. c.ViewTrackURL = fmt.Sprintf("%s/campaign/%%s/%%s/px.png", c.RootURL)
  349. c.BounceWebhooksEnabled = ko.Bool("bounce.webhooks_enabled")
  350. c.BounceSESEnabled = ko.Bool("bounce.ses_enabled")
  351. c.BounceSendgridEnabled = ko.Bool("bounce.sendgrid_enabled")
  352. c.BouncePostmarkEnabled = ko.Bool("bounce.postmark.enabled")
  353. return &c
  354. }
  355. // initI18n initializes a new i18n instance with the selected language map
  356. // loaded from the filesystem. English is a loaded first as the default map
  357. // and then the selected language is loaded on top of it so that if there are
  358. // missing translations in it, the default English translations show up.
  359. func initI18n(lang string, fs stuffbin.FileSystem) *i18n.I18n {
  360. i, ok, err := getI18nLang(lang, fs)
  361. if err != nil {
  362. if ok {
  363. lo.Println(err)
  364. } else {
  365. lo.Fatal(err)
  366. }
  367. }
  368. return i
  369. }
  370. // initCampaignManager initializes the campaign manager.
  371. func initCampaignManager(q *models.Queries, cs *constants, app *App) *manager.Manager {
  372. campNotifCB := func(subject string, data interface{}) error {
  373. return app.sendNotification(cs.NotifyEmails, subject, notifTplCampaign, data)
  374. }
  375. if ko.Int("app.concurrency") < 1 {
  376. lo.Fatal("app.concurrency should be at least 1")
  377. }
  378. if ko.Int("app.message_rate") < 1 {
  379. lo.Fatal("app.message_rate should be at least 1")
  380. }
  381. if ko.Bool("passive") {
  382. lo.Println("running in passive mode. won't process campaigns.")
  383. }
  384. return manager.New(manager.Config{
  385. BatchSize: ko.Int("app.batch_size"),
  386. Concurrency: ko.Int("app.concurrency"),
  387. MessageRate: ko.Int("app.message_rate"),
  388. MaxSendErrors: ko.Int("app.max_send_errors"),
  389. FromEmail: cs.FromEmail,
  390. IndividualTracking: ko.Bool("privacy.individual_tracking"),
  391. UnsubURL: cs.UnsubURL,
  392. OptinURL: cs.OptinURL,
  393. LinkTrackURL: cs.LinkTrackURL,
  394. ViewTrackURL: cs.ViewTrackURL,
  395. MessageURL: cs.MessageURL,
  396. ArchiveURL: cs.ArchiveURL,
  397. UnsubHeader: ko.Bool("privacy.unsubscribe_header"),
  398. SlidingWindow: ko.Bool("app.message_sliding_window"),
  399. SlidingWindowDuration: ko.Duration("app.message_sliding_window_duration"),
  400. SlidingWindowRate: ko.Int("app.message_sliding_window_rate"),
  401. ScanInterval: time.Second * 5,
  402. ScanCampaigns: !ko.Bool("passive"),
  403. }, newManagerStore(q, app.core, app.media), campNotifCB, app.i18n, lo)
  404. }
  405. func initTxTemplates(m *manager.Manager, app *App) {
  406. tpls, err := app.core.GetTemplates(models.TemplateTypeTx, false)
  407. if err != nil {
  408. lo.Fatalf("error loading transactional templates: %v", err)
  409. }
  410. for _, t := range tpls {
  411. tpl := t
  412. if err := tpl.Compile(app.manager.GenericTemplateFuncs()); err != nil {
  413. lo.Printf("error compiling transactional template %d: %v", tpl.ID, err)
  414. continue
  415. }
  416. m.CacheTpl(tpl.ID, &tpl)
  417. }
  418. }
  419. // initImporter initializes the bulk subscriber importer.
  420. func initImporter(q *models.Queries, db *sqlx.DB, app *App) *subimporter.Importer {
  421. return subimporter.New(
  422. subimporter.Options{
  423. DomainBlocklist: app.constants.Privacy.DomainBlocklist,
  424. UpsertStmt: q.UpsertSubscriber.Stmt,
  425. BlocklistStmt: q.UpsertBlocklistSubscriber.Stmt,
  426. UpdateListDateStmt: q.UpdateListsDate.Stmt,
  427. NotifCB: func(subject string, data interface{}) error {
  428. app.sendNotification(app.constants.NotifyEmails, subject, notifTplImport, data)
  429. return nil
  430. },
  431. }, db.DB, app.i18n)
  432. }
  433. // initSMTPMessenger initializes the SMTP messenger.
  434. func initSMTPMessenger(m *manager.Manager) manager.Messenger {
  435. var (
  436. mapKeys = ko.MapKeys("smtp")
  437. servers = make([]email.Server, 0, len(mapKeys))
  438. )
  439. items := ko.Slices("smtp")
  440. if len(items) == 0 {
  441. lo.Fatalf("no SMTP servers found in config")
  442. }
  443. // Load the config for multiple SMTP servers.
  444. for _, item := range items {
  445. if !item.Bool("enabled") {
  446. continue
  447. }
  448. // Read the SMTP config.
  449. var s email.Server
  450. if err := item.UnmarshalWithConf("", &s, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  451. lo.Fatalf("error reading SMTP config: %v", err)
  452. }
  453. servers = append(servers, s)
  454. lo.Printf("loaded email (SMTP) messenger: %s@%s",
  455. item.String("username"), item.String("host"))
  456. }
  457. if len(servers) == 0 {
  458. lo.Fatalf("no SMTP servers enabled in settings")
  459. }
  460. // Initialize the e-mail messenger with multiple SMTP servers.
  461. msgr, err := email.New(servers...)
  462. if err != nil {
  463. lo.Fatalf("error loading e-mail messenger: %v", err)
  464. }
  465. return msgr
  466. }
  467. // initPostbackMessengers initializes and returns all the enabled
  468. // HTTP postback messenger backends.
  469. func initPostbackMessengers(m *manager.Manager) []manager.Messenger {
  470. items := ko.Slices("messengers")
  471. if len(items) == 0 {
  472. return nil
  473. }
  474. var out []manager.Messenger
  475. for _, item := range items {
  476. if !item.Bool("enabled") {
  477. continue
  478. }
  479. // Read the Postback server config.
  480. var (
  481. name = item.String("name")
  482. o postback.Options
  483. )
  484. if err := item.UnmarshalWithConf("", &o, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  485. lo.Fatalf("error reading Postback config: %v", err)
  486. }
  487. // Initialize the Messenger.
  488. p, err := postback.New(o)
  489. if err != nil {
  490. lo.Fatalf("error initializing Postback messenger %s: %v", name, err)
  491. }
  492. out = append(out, p)
  493. lo.Printf("loaded Postback messenger: %s", name)
  494. }
  495. return out
  496. }
  497. // initMediaStore initializes Upload manager with a custom backend.
  498. func initMediaStore() media.Store {
  499. switch provider := ko.String("upload.provider"); provider {
  500. case "s3":
  501. var o s3.Opt
  502. ko.Unmarshal("upload.s3", &o)
  503. up, err := s3.NewS3Store(o)
  504. if err != nil {
  505. lo.Fatalf("error initializing s3 upload provider %s", err)
  506. }
  507. lo.Println("media upload provider: s3")
  508. return up
  509. case "filesystem":
  510. var o filesystem.Opts
  511. ko.Unmarshal("upload.filesystem", &o)
  512. o.RootURL = ko.String("app.root_url")
  513. o.UploadPath = filepath.Clean(o.UploadPath)
  514. o.UploadURI = filepath.Clean(o.UploadURI)
  515. up, err := filesystem.New(o)
  516. if err != nil {
  517. lo.Fatalf("error initializing filesystem upload provider %s", err)
  518. }
  519. lo.Println("media upload provider: filesystem")
  520. return up
  521. default:
  522. lo.Fatalf("unknown provider. select filesystem or s3")
  523. }
  524. return nil
  525. }
  526. // initNotifTemplates compiles and returns e-mail notification templates that are
  527. // used for sending ad-hoc notifications to admins and subscribers.
  528. func initNotifTemplates(path string, fs stuffbin.FileSystem, i *i18n.I18n, cs *constants) *notifTpls {
  529. // Register utility functions that the e-mail templates can use.
  530. funcs := template.FuncMap{
  531. "RootURL": func() string {
  532. return cs.RootURL
  533. },
  534. "LogoURL": func() string {
  535. return cs.LogoURL
  536. },
  537. "Date": func(layout string) string {
  538. if layout == "" {
  539. layout = time.ANSIC
  540. }
  541. return time.Now().Format(layout)
  542. },
  543. "L": func() *i18n.I18n {
  544. return i
  545. },
  546. "Safe": func(safeHTML string) template.HTML {
  547. return template.HTML(safeHTML)
  548. },
  549. }
  550. for k, v := range sprig.GenericFuncMap() {
  551. funcs[k] = v
  552. }
  553. tpls, err := stuffbin.ParseTemplatesGlob(funcs, fs, "/static/email-templates/*.html")
  554. if err != nil {
  555. lo.Fatalf("error parsing e-mail notif templates: %v", err)
  556. }
  557. html, err := fs.Read("/static/email-templates/base.html")
  558. if err != nil {
  559. lo.Fatalf("error reading static/email-templates/base.html: %v", err)
  560. }
  561. out := &notifTpls{
  562. tpls: tpls,
  563. contentType: models.CampaignContentTypeHTML,
  564. }
  565. // Determine whether the notification templates are HTML or plaintext.
  566. // Copy the first few (arbitrary) bytes of the template and check if has the <!doctype html> tag.
  567. ln := 256
  568. if len(html) < ln {
  569. ln = len(html)
  570. }
  571. h := make([]byte, ln)
  572. copy(h, html[0:ln])
  573. if !bytes.Contains(bytes.ToLower(h), []byte("<!doctype html")) {
  574. out.contentType = models.CampaignContentTypePlain
  575. lo.Println("system e-mail templates are plaintext")
  576. }
  577. return out
  578. }
  579. // initBounceManager initializes the bounce manager that scans mailboxes and listens to webhooks
  580. // for incoming bounce events.
  581. func initBounceManager(app *App) *bounce.Manager {
  582. opt := bounce.Opt{
  583. WebhooksEnabled: ko.Bool("bounce.webhooks_enabled"),
  584. SESEnabled: ko.Bool("bounce.ses_enabled"),
  585. SendgridEnabled: ko.Bool("bounce.sendgrid_enabled"),
  586. SendgridKey: ko.String("bounce.sendgrid_key"),
  587. Postmark: struct {
  588. Enabled bool
  589. Username string
  590. Password string
  591. }{
  592. ko.Bool("bounce.postmark.enabled"),
  593. ko.String("bounce.postmark.username"),
  594. ko.String("bounce.postmark.password"),
  595. },
  596. RecordBounceCB: app.core.RecordBounce,
  597. }
  598. // For now, only one mailbox is supported.
  599. for _, b := range ko.Slices("bounce.mailboxes") {
  600. if !b.Bool("enabled") {
  601. continue
  602. }
  603. var boxOpt mailbox.Opt
  604. if err := b.UnmarshalWithConf("", &boxOpt, koanf.UnmarshalConf{Tag: "json"}); err != nil {
  605. lo.Fatalf("error reading bounce mailbox config: %v", err)
  606. }
  607. opt.MailboxType = b.String("type")
  608. opt.MailboxEnabled = true
  609. opt.Mailbox = boxOpt
  610. break
  611. }
  612. b, err := bounce.New(opt, &bounce.Queries{
  613. RecordQuery: app.queries.RecordBounce,
  614. }, app.log)
  615. if err != nil {
  616. lo.Fatalf("error initializing bounce manager: %v", err)
  617. }
  618. return b
  619. }
  620. func initAbout(q *models.Queries, db *sqlx.DB) about {
  621. var (
  622. mem runtime.MemStats
  623. )
  624. // Memory / alloc stats.
  625. runtime.ReadMemStats(&mem)
  626. info := types.JSONText(`{}`)
  627. if err := db.QueryRow(q.GetDBInfo).Scan(&info); err != nil {
  628. lo.Printf("WARNING: error getting database version: %v", err)
  629. }
  630. hostname, err := os.Hostname()
  631. if err != nil {
  632. lo.Printf("WARNING: error getting hostname: %v", err)
  633. }
  634. return about{
  635. Version: versionString,
  636. Build: buildString,
  637. GoArch: runtime.GOARCH,
  638. GoVersion: runtime.Version(),
  639. Database: info,
  640. System: aboutSystem{
  641. NumCPU: runtime.NumCPU(),
  642. },
  643. Host: aboutHost{
  644. OS: runtime.GOOS,
  645. Machine: runtime.GOARCH,
  646. Hostname: hostname,
  647. },
  648. }
  649. }
  650. // initHTTPServer sets up and runs the app's main HTTP server and blocks forever.
  651. func initHTTPServer(app *App) *echo.Echo {
  652. // Initialize the HTTP server.
  653. var srv = echo.New()
  654. srv.HideBanner = true
  655. // Register app (*App) to be injected into all HTTP handlers.
  656. srv.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  657. return func(c echo.Context) error {
  658. c.Set("app", app)
  659. return next(c)
  660. }
  661. })
  662. // Parse and load user facing templates.
  663. tpl, err := stuffbin.ParseTemplatesGlob(template.FuncMap{
  664. "L": func() *i18n.I18n {
  665. return app.i18n
  666. }}, app.fs, "/public/templates/*.html")
  667. if err != nil {
  668. lo.Fatalf("error parsing public templates: %v", err)
  669. }
  670. srv.Renderer = &tplRenderer{
  671. templates: tpl,
  672. SiteName: app.constants.SiteName,
  673. RootURL: app.constants.RootURL,
  674. LogoURL: app.constants.LogoURL,
  675. FaviconURL: app.constants.FaviconURL,
  676. EnablePublicSubPage: app.constants.EnablePublicSubPage,
  677. EnablePublicArchive: app.constants.EnablePublicArchive,
  678. }
  679. // Initialize the static file server.
  680. fSrv := app.fs.FileServer()
  681. // Public (subscriber) facing static files.
  682. srv.GET("/public/static/*", echo.WrapHandler(fSrv))
  683. // Admin (frontend) facing static files.
  684. srv.GET("/admin/static/*", echo.WrapHandler(fSrv))
  685. // Public (subscriber) facing media upload files.
  686. if ko.String("upload.provider") == "filesystem" && ko.String("upload.filesystem.upload_uri") != "" {
  687. srv.Static(ko.String("upload.filesystem.upload_uri"), ko.String("upload.filesystem.upload_path"))
  688. }
  689. // Register all HTTP handlers.
  690. initHTTPHandlers(srv, app)
  691. // Start the server.
  692. go func() {
  693. if err := srv.Start(ko.String("app.address")); err != nil {
  694. if strings.Contains(err.Error(), "Server closed") {
  695. lo.Println("HTTP server shut down")
  696. } else {
  697. lo.Fatalf("error starting HTTP server: %v", err)
  698. }
  699. }
  700. }()
  701. return srv
  702. }
  703. func initCaptcha() *captcha.Captcha {
  704. return captcha.New(captcha.Opt{
  705. CaptchaSecret: ko.String("security.captcha_secret"),
  706. })
  707. }
  708. func awaitReload(sigChan chan os.Signal, closerWait chan bool, closer func()) chan bool {
  709. // The blocking signal handler that main() waits on.
  710. out := make(chan bool)
  711. // Respawn a new process and exit the running one.
  712. respawn := func() {
  713. if err := syscall.Exec(os.Args[0], os.Args, os.Environ()); err != nil {
  714. lo.Fatalf("error spawning process: %v", err)
  715. }
  716. os.Exit(0)
  717. }
  718. // Listen for reload signal.
  719. go func() {
  720. for range sigChan {
  721. lo.Println("reloading on signal ...")
  722. go closer()
  723. select {
  724. case <-closerWait:
  725. // Wait for the closer to finish.
  726. respawn()
  727. case <-time.After(time.Second * 3):
  728. // Or timeout and force close.
  729. respawn()
  730. }
  731. }
  732. }()
  733. return out
  734. }
  735. func joinFSPaths(root string, paths []string) []string {
  736. out := make([]string, 0, len(paths))
  737. for _, p := range paths {
  738. // real_path:stuffbin_alias
  739. f := strings.Split(p, ":")
  740. out = append(out, path.Join(root, f[0])+":"+f[1])
  741. }
  742. return out
  743. }