├── .dockerignore
├── .gitignore
├── .gitmodules
├── CHANGELOG.md
├── Dockerfile
├── Dockerfile-Develop
├── LICENSE
├── README.md
├── TODO.md
├── UPGRADE.md
├── client
├── .gitignore
├── package-lock.json
├── package.json
├── src
│ ├── Home.js
│ ├── account
│ │ ├── API.js
│ │ ├── Account.js
│ │ ├── root.js
│ │ └── styles.scss
│ ├── blacklist
│ │ ├── List.js
│ │ └── root.js
│ ├── campaigns
│ │ ├── CUD.js
│ │ ├── Clone.js
│ │ ├── Content.js
│ │ ├── List.js
│ │ ├── Statistics.js
│ │ ├── StatisticsLinkClicks.js
│ │ ├── StatisticsOpened.js
│ │ ├── StatisticsSubsList.js
│ │ ├── Status.js
│ │ ├── TestSendModalDialog.js
│ │ ├── helpers.js
│ │ ├── root.js
│ │ ├── styles.scss
│ │ └── triggers
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ └── helpers.js
│ ├── channels
│ │ ├── CUD.js
│ │ ├── List.js
│ │ ├── root.js
│ │ └── styles.scss
│ ├── lib
│ │ ├── axios.js
│ │ ├── bootstrap-components.js
│ │ ├── decorator-helpers.js
│ │ ├── error-handling.js
│ │ ├── files.js
│ │ ├── form.js
│ │ ├── helpers.js
│ │ ├── i18n.js
│ │ ├── mjml.js
│ │ ├── modals.js
│ │ ├── namespace.js
│ │ ├── page-common.js
│ │ ├── page.js
│ │ ├── permissions.js
│ │ ├── public-path.js
│ │ ├── sandbox-common.js
│ │ ├── sandbox-common.scss
│ │ ├── sandboxed-ckeditor-root.js
│ │ ├── sandboxed-ckeditor-shared.js
│ │ ├── sandboxed-ckeditor.js
│ │ ├── sandboxed-ckeditor.scss
│ │ ├── sandboxed-codeeditor-root.js
│ │ ├── sandboxed-codeeditor-shared.js
│ │ ├── sandboxed-codeeditor.js
│ │ ├── sandboxed-codeeditor.scss
│ │ ├── sandboxed-grapesjs-root.js
│ │ ├── sandboxed-grapesjs-shared.js
│ │ ├── sandboxed-grapesjs.js
│ │ ├── sandboxed-grapesjs.scss
│ │ ├── sandboxed-mosaico-root.js
│ │ ├── sandboxed-mosaico.js
│ │ ├── sandboxed-mosaico.scss
│ │ ├── styles.scss
│ │ ├── table.js
│ │ ├── tree.js
│ │ ├── tree.scss
│ │ ├── untrusted.js
│ │ └── urls.js
│ ├── lists
│ │ ├── CUD.js
│ │ ├── List.js
│ │ ├── TriggersList.js
│ │ ├── fields
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ └── helpers.js
│ │ ├── forms
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ └── styles.scss
│ │ ├── imports
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ ├── RunStatus.js
│ │ │ ├── Status.js
│ │ │ └── helpers.js
│ │ ├── root.js
│ │ ├── segments
│ │ │ ├── CUD.js
│ │ │ ├── CUD.scss
│ │ │ ├── List.js
│ │ │ ├── RuleSettingsPane.js
│ │ │ ├── divider.ai
│ │ │ ├── divider.png
│ │ │ └── helpers.js
│ │ ├── styles.scss
│ │ └── subscriptions
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ └── helpers.js
│ ├── login
│ │ ├── Forgot.js
│ │ ├── Login.js
│ │ ├── Reset.js
│ │ └── root.js
│ ├── namespaces
│ │ ├── CUD.js
│ │ ├── List.js
│ │ └── root.js
│ ├── reports
│ │ ├── CUD.js
│ │ ├── List.js
│ │ ├── ViewAndOutput.js
│ │ ├── root.js
│ │ └── templates
│ │ │ ├── CUD.js
│ │ │ └── List.js
│ ├── root.js
│ ├── scss
│ │ ├── .gitignore
│ │ ├── _mixins.scss
│ │ ├── mailtrain.scss
│ │ └── variables.scss
│ ├── send-configurations
│ │ ├── CUD.js
│ │ ├── List.js
│ │ ├── helpers.js
│ │ ├── root.js
│ │ └── styles.scss
│ ├── settings
│ │ ├── Update.js
│ │ └── root.js
│ ├── shares
│ │ ├── Share.js
│ │ └── UserShares.js
│ ├── templates
│ │ ├── CUD.js
│ │ ├── List.js
│ │ ├── helpers.js
│ │ ├── mosaico
│ │ │ ├── CUD.js
│ │ │ ├── List.js
│ │ │ ├── helpers.js
│ │ │ └── mjml-mosaico.js
│ │ └── root.js
│ └── users
│ │ ├── CUD.js
│ │ ├── List.js
│ │ └── root.js
├── static
│ ├── fancytree
│ │ ├── jquery.fancytree-all.js
│ │ ├── jquery.fancytree-all.min.js
│ │ └── skin-bootstrap
│ │ │ ├── README.md
│ │ │ ├── icons-rtl.gif
│ │ │ ├── icons.gif
│ │ │ ├── ui.fancytree.css
│ │ │ ├── ui.fancytree.min.css
│ │ │ ├── vline-rtl.gif
│ │ │ └── vline.gif
│ ├── favicon.ico
│ ├── jquery
│ │ └── jquery-ui-1.12.1.min.js
│ ├── mailtrain-header.png
│ ├── mailtrain-notext.png
│ ├── mailtrain.png
│ ├── mosaico
│ │ ├── LICENSE
│ │ ├── NOTICE.txt
│ │ ├── README.md
│ │ ├── editor.html
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── rs
│ │ │ ├── fontawesome
│ │ │ │ ├── fontawesome-webfont.eot
│ │ │ │ ├── fontawesome-webfont.svg
│ │ │ │ ├── fontawesome-webfont.ttf
│ │ │ │ ├── fontawesome-webfont.woff
│ │ │ │ └── fontawesome-webfont.woff2
│ │ │ ├── img
│ │ │ │ ├── byvoxmail.png
│ │ │ │ ├── mosaico-badge.gif
│ │ │ │ ├── mosaico-v.gif
│ │ │ │ ├── mosaico32.png
│ │ │ │ ├── mosaicologo.png
│ │ │ │ ├── screenshot-orig.png
│ │ │ │ └── screenshot.png
│ │ │ ├── lang
│ │ │ │ ├── LICENSE
│ │ │ │ ├── README.md
│ │ │ │ ├── mosaico-de.json
│ │ │ │ ├── mosaico-en.json
│ │ │ │ ├── mosaico-es.json
│ │ │ │ ├── mosaico-fr.json
│ │ │ │ ├── mosaico-it.json
│ │ │ │ ├── mosaico-nl.json
│ │ │ │ ├── mosaico-pt_BR.json
│ │ │ │ ├── mosaico-ru.json
│ │ │ │ ├── mosaico-sr_RS.json
│ │ │ │ ├── mosaico-sv.json
│ │ │ │ └── mosaico-tr.json
│ │ │ ├── mosaico-libs-and-tinymce.min.css
│ │ │ ├── mosaico-libs-and-tinymce.min.js
│ │ │ ├── mosaico-libs-and-tinymce.min.js.map
│ │ │ ├── mosaico-material.min.css
│ │ │ ├── mosaico-material.min.css.map
│ │ │ ├── mosaico.min.css
│ │ │ ├── mosaico.min.css.map
│ │ │ ├── mosaico.min.js
│ │ │ ├── mosaico.min.js.map
│ │ │ ├── notoregular
│ │ │ │ ├── noto-sans-400-normal.eot
│ │ │ │ ├── noto-sans-400-normal.ttf
│ │ │ │ └── noto-sans-400-normal.woff
│ │ │ ├── plugins
│ │ │ │ ├── charmap
│ │ │ │ │ └── plugin.min.js
│ │ │ │ ├── emoticons
│ │ │ │ │ ├── img
│ │ │ │ │ │ ├── smiley-cool.gif
│ │ │ │ │ │ ├── smiley-cry.gif
│ │ │ │ │ │ ├── smiley-embarassed.gif
│ │ │ │ │ │ ├── smiley-foot-in-mouth.gif
│ │ │ │ │ │ ├── smiley-frown.gif
│ │ │ │ │ │ ├── smiley-innocent.gif
│ │ │ │ │ │ ├── smiley-kiss.gif
│ │ │ │ │ │ ├── smiley-laughing.gif
│ │ │ │ │ │ ├── smiley-money-mouth.gif
│ │ │ │ │ │ ├── smiley-sealed.gif
│ │ │ │ │ │ ├── smiley-smile.gif
│ │ │ │ │ │ ├── smiley-surprised.gif
│ │ │ │ │ │ ├── smiley-tongue-out.gif
│ │ │ │ │ │ ├── smiley-undecided.gif
│ │ │ │ │ │ ├── smiley-wink.gif
│ │ │ │ │ │ └── smiley-yell.gif
│ │ │ │ │ └── plugin.min.js
│ │ │ │ ├── nonbreaking
│ │ │ │ │ └── plugin.min.js
│ │ │ │ ├── searchreplace
│ │ │ │ │ └── plugin.min.js
│ │ │ │ └── visualblocks
│ │ │ │ │ ├── css
│ │ │ │ │ └── visualblocks.css
│ │ │ │ │ └── plugin.min.js
│ │ │ └── skins
│ │ │ │ └── gray-flat
│ │ │ │ ├── Variables.less
│ │ │ │ ├── content.inline.min.css
│ │ │ │ ├── content.min.css
│ │ │ │ ├── fonts
│ │ │ │ ├── readme.md
│ │ │ │ ├── tinymce-small.eot
│ │ │ │ ├── tinymce-small.json
│ │ │ │ ├── tinymce-small.svg
│ │ │ │ ├── tinymce-small.ttf
│ │ │ │ ├── tinymce-small.woff
│ │ │ │ ├── tinymce.eot
│ │ │ │ ├── tinymce.json
│ │ │ │ ├── tinymce.svg
│ │ │ │ ├── tinymce.ttf
│ │ │ │ └── tinymce.woff
│ │ │ │ ├── img
│ │ │ │ ├── anchor.gif
│ │ │ │ ├── loader.gif
│ │ │ │ ├── object.gif
│ │ │ │ └── trans.gif
│ │ │ │ ├── skin.ie7.min.css
│ │ │ │ ├── skin.json
│ │ │ │ └── skin.min.css
│ │ ├── templates
│ │ │ └── versafix-1
│ │ │ │ ├── edres
│ │ │ │ ├── _full.png
│ │ │ │ ├── bigSocialBlock.png
│ │ │ │ ├── buttonBlock.png
│ │ │ │ ├── doubleArticleBlock.png
│ │ │ │ ├── doubleImageBlock.png
│ │ │ │ ├── hrBlock.png
│ │ │ │ ├── imageBlock.png
│ │ │ │ ├── logoBlock.png
│ │ │ │ ├── shareBlock.png
│ │ │ │ ├── sideArticleBlock.png
│ │ │ │ ├── singleArticleBlock.png
│ │ │ │ ├── socialBlock.png
│ │ │ │ ├── spacerBlock.png
│ │ │ │ ├── textBlock.png
│ │ │ │ ├── titleBlock.png
│ │ │ │ ├── tripleArticleBlock.png
│ │ │ │ └── tripleImageBlock.png
│ │ │ │ ├── img
│ │ │ │ ├── icons
│ │ │ │ │ ├── README.md
│ │ │ │ │ ├── fb-black-96.png
│ │ │ │ │ ├── fb-bw-96.png
│ │ │ │ │ ├── fb-colors-96.png
│ │ │ │ │ ├── fb-coloured-96.png
│ │ │ │ │ ├── fb-rdbl-96.png
│ │ │ │ │ ├── fb-rdcol-96.png
│ │ │ │ │ ├── fb-white-96.png
│ │ │ │ │ ├── fl-black-96.png
│ │ │ │ │ ├── fl-bw-96.png
│ │ │ │ │ ├── fl-colors-96.png
│ │ │ │ │ ├── fl-coloured-96.png
│ │ │ │ │ ├── fl-rdbl-96.png
│ │ │ │ │ ├── fl-rdcol-96.png
│ │ │ │ │ ├── fl-white-96.png
│ │ │ │ │ ├── gg-black-96.png
│ │ │ │ │ ├── gg-bw-96.png
│ │ │ │ │ ├── gg-colors-96.png
│ │ │ │ │ ├── gg-coloured-96.png
│ │ │ │ │ ├── gg-rdbl-96.png
│ │ │ │ │ ├── gg-rdcol-96.png
│ │ │ │ │ ├── gg-white-96.png
│ │ │ │ │ ├── in-black-96.png
│ │ │ │ │ ├── in-bw-96.png
│ │ │ │ │ ├── in-colors-96.png
│ │ │ │ │ ├── in-coloured-96.png
│ │ │ │ │ ├── in-rdbl-96.png
│ │ │ │ │ ├── in-rdcol-96.png
│ │ │ │ │ ├── in-white-96.png
│ │ │ │ │ ├── inst-black-96.png
│ │ │ │ │ ├── inst-bw-96.png
│ │ │ │ │ ├── inst-colors-96.png
│ │ │ │ │ ├── inst-coloured-96.png
│ │ │ │ │ ├── inst-rdbl-96.png
│ │ │ │ │ ├── inst-rdcol-96.png
│ │ │ │ │ ├── inst-white-96.png
│ │ │ │ │ ├── pi-black-96.png
│ │ │ │ │ ├── pi-bw-96.png
│ │ │ │ │ ├── pi-colors-96.png
│ │ │ │ │ ├── pi-coloured-96.png
│ │ │ │ │ ├── pi-rdbl-96.png
│ │ │ │ │ ├── pi-rdcol-96.png
│ │ │ │ │ ├── pi-white-96.png
│ │ │ │ │ ├── tg-black-96.png
│ │ │ │ │ ├── tg-bw-96.png
│ │ │ │ │ ├── tg-colors-96.png
│ │ │ │ │ ├── tg-coloured-96.png
│ │ │ │ │ ├── tg-rdbl-96.png
│ │ │ │ │ ├── tg-rdcol-96.png
│ │ │ │ │ ├── tg-white-96.png
│ │ │ │ │ ├── tw-black-96.png
│ │ │ │ │ ├── tw-bw-96.png
│ │ │ │ │ ├── tw-colors-96.png
│ │ │ │ │ ├── tw-coloured-96.png
│ │ │ │ │ ├── tw-rdbl-96.png
│ │ │ │ │ ├── tw-rdcol-96.png
│ │ │ │ │ ├── tw-white-96.png
│ │ │ │ │ ├── vi-black-96.png
│ │ │ │ │ ├── vi-bw-96.png
│ │ │ │ │ ├── vi-colors-96.png
│ │ │ │ │ ├── vi-coloured-96.png
│ │ │ │ │ ├── vi-rdbl-96.png
│ │ │ │ │ ├── vi-rdcol-96.png
│ │ │ │ │ ├── vi-white-96.png
│ │ │ │ │ ├── wa-black-96.png
│ │ │ │ │ ├── wa-bw-96.png
│ │ │ │ │ ├── wa-colors-96.png
│ │ │ │ │ ├── wa-coloured-96.png
│ │ │ │ │ ├── wa-rdbl-96.png
│ │ │ │ │ ├── wa-rdcol-96.png
│ │ │ │ │ ├── wa-white-96.png
│ │ │ │ │ ├── web-black-96.png
│ │ │ │ │ ├── web-bw-96.png
│ │ │ │ │ ├── web-colors-96.png
│ │ │ │ │ ├── web-coloured-96.png
│ │ │ │ │ ├── web-rdbl-96.png
│ │ │ │ │ ├── web-rdcol-96.png
│ │ │ │ │ ├── web-white-96.png
│ │ │ │ │ ├── you-black-96.png
│ │ │ │ │ ├── you-bw-96.png
│ │ │ │ │ ├── you-colors-96.png
│ │ │ │ │ ├── you-coloured-96.png
│ │ │ │ │ ├── you-rdbl-96.png
│ │ │ │ │ ├── you-rdcol-96.png
│ │ │ │ │ └── you-white-96.png
│ │ │ │ ├── social_def
│ │ │ │ │ ├── facebook_bw_ok.png
│ │ │ │ │ ├── facebook_ok.png
│ │ │ │ │ ├── flickr_bw_ok.png
│ │ │ │ │ ├── flickr_ok.png
│ │ │ │ │ ├── google+_bw_ok.png
│ │ │ │ │ ├── google+_ok.png
│ │ │ │ │ ├── instagram_bw_ok.png
│ │ │ │ │ ├── instagram_ok.png
│ │ │ │ │ ├── linkedin_bw_ok.png
│ │ │ │ │ ├── linkedin_ok.png
│ │ │ │ │ ├── twitter_bw_ok.png
│ │ │ │ │ ├── twitter_ok.png
│ │ │ │ │ ├── vimeo_bw_ok.png
│ │ │ │ │ ├── vimeo_ok.png
│ │ │ │ │ ├── web_bw_ok.png
│ │ │ │ │ ├── web_ok.png
│ │ │ │ │ ├── youtube_bw_ok.png
│ │ │ │ │ └── youtube_ok.png
│ │ │ │ └── sponsor.gif
│ │ │ │ └── template-versafix-1.html
│ │ └── uploads
│ │ │ ├── .gitignore
│ │ │ └── README.md
│ └── subscription
│ │ ├── form-input-style.css
│ │ └── widget.js
└── webpack.config.js
├── docker-compose-develop.yml
├── docker-compose-local.yml
├── docker-compose.yml
├── docker-entrypoint.sh
├── docs
├── CNAME
├── README.md
└── access-control.md
├── locales
├── de-DE
│ └── common.json
├── en-US-last-run
│ └── common.json
├── en-US
│ └── common.json
├── es-ES
│ └── common.json
├── extract.js
├── fr-FR
│ └── common.json
├── package-lock.json
├── package.json
├── pt-BR
│ └── common.json
└── ru-RU
│ └── common.json
├── mvis
├── client
│ ├── .gitignore
│ ├── package.json
│ ├── src
│ │ ├── root-trusted.js
│ │ └── styles.scss
│ └── webpack.config.js
├── server
│ ├── .gitignore
│ ├── builder.js
│ ├── config
│ │ └── default.yaml
│ ├── extensions-common.js
│ ├── index.js
│ ├── indexer-elasticsearch.js
│ ├── knex
│ │ └── migrations
│ │ │ └── 20181226130000_add_mailtrain_metadata.js
│ ├── knexfile.js
│ ├── package.json
│ ├── routes
│ │ └── api
│ │ │ ├── embed.js
│ │ │ └── events.js
│ ├── setup-db.sh
│ └── task-handler.js
├── test-embed
│ ├── .gitignore
│ ├── index.js
│ ├── package.json
│ └── views
│ │ ├── layout.hbs
│ │ └── panel.hbs
└── tools
│ ├── delete-els-indexes.sh
│ ├── list-els-indexes.sh
│ ├── new-db.sh
│ ├── reinstall-modules.sh
│ └── reset-db.sh
├── server
├── .eslintrc
├── .gitignore
├── Gruntfile.js
├── app-builder.js
├── config
│ └── default.yaml
├── index.js
├── lib
│ ├── activity-log.js
│ ├── builtin-zone-mta.js
│ ├── campaign-content.js
│ ├── client-helpers.js
│ ├── config.js
│ ├── context-helpers.js
│ ├── dbcheck.js
│ ├── dependency-helpers.js
│ ├── dt-helpers.js
│ ├── entity-settings.js
│ ├── executor.js
│ ├── feedcheck.js
│ ├── file-cache.js
│ ├── file-helpers.js
│ ├── fork.js
│ ├── helpers.js
│ ├── importer.js
│ ├── knex.js
│ ├── log.js
│ ├── mailers.js
│ ├── message-sender.js
│ ├── namespace-helpers.js
│ ├── nodeify.js
│ ├── passport.js
│ ├── privilege-helpers.js
│ ├── report-helpers.js
│ ├── report-processor.js
│ ├── router-async.js
│ ├── senders.js
│ ├── shortid.js
│ ├── subscription-mail-helpers.js
│ ├── synchronized.js
│ ├── tools.js
│ ├── translate.js
│ └── urls.js
├── models
│ ├── blacklist.js
│ ├── campaigns.js
│ ├── channels.js
│ ├── confirmations.js
│ ├── fields.js
│ ├── files.js
│ ├── forms.js
│ ├── import-runs.js
│ ├── imports.js
│ ├── links.js
│ ├── lists.js
│ ├── mosaico-templates.js
│ ├── namespaces.js
│ ├── report-templates.js
│ ├── reports.js
│ ├── segments.js
│ ├── send-configurations.js
│ ├── settings.js
│ ├── shares.js
│ ├── subscriptions.js
│ ├── templates.js
│ ├── triggers.js
│ └── users.js
├── package-lock.json
├── package.json
├── protected
│ └── reports
│ │ ├── .gitignore
│ │ └── README.md
├── routes
│ ├── api.js
│ ├── archive.js
│ ├── campaigns.js
│ ├── files.js
│ ├── index.js
│ ├── links.js
│ ├── quick-reports.js
│ ├── reports.js
│ ├── rest
│ │ ├── account.js
│ │ ├── blacklist.js
│ │ ├── campaigns.js
│ │ ├── channels.js
│ │ ├── editors.js
│ │ ├── fields.js
│ │ ├── files.js
│ │ ├── forms.js
│ │ ├── import-runs.js
│ │ ├── imports.js
│ │ ├── lists.js
│ │ ├── mosaico-templates.js
│ │ ├── namespaces.js
│ │ ├── report-templates.js
│ │ ├── reports.js
│ │ ├── segments.js
│ │ ├── send-configurations.js
│ │ ├── settings.js
│ │ ├── shares.js
│ │ ├── subscriptions.js
│ │ ├── templates.js
│ │ ├── triggers.js
│ │ └── users.js
│ ├── sandboxed-ckeditor.js
│ ├── sandboxed-codeeditor.js
│ ├── sandboxed-grapesjs.js
│ ├── sandboxed-mosaico.js
│ ├── subscription.js
│ ├── subscriptions.js
│ └── webhooks.js
├── services
│ ├── executor.js
│ ├── feedcheck.js
│ ├── gdpr-cleanup.js
│ ├── importer.js
│ ├── postfix-bounce-server.js
│ ├── sender-master.js
│ ├── sender-worker.js
│ ├── test-server.js
│ ├── triggers.js
│ ├── tzupdate.js
│ ├── verp-server.js
│ └── workers
│ │ └── reports
│ │ ├── config
│ │ └── default.yaml
│ │ └── report-processor.js
├── setup
│ ├── docker-entrypoint-db-setup.js
│ ├── fakedata.js
│ ├── knex
│ │ ├── config.js
│ │ ├── fixes
│ │ │ └── fix-20190726150000_shorten_field_column_names.js
│ │ ├── knexfile.js
│ │ └── migrations
│ │ │ ├── 20170506102634_v1_to_v2.js
│ │ │ ├── 20181226090000_verp_header_options_in_send_configurations.js
│ │ │ ├── 20190422084800_file_cache.js
│ │ │ ├── 20190615000000_generalization_of_queued_and_file_locking.js
│ │ │ ├── 20190616000000_drop_subject_in_send_configurations.js
│ │ │ ├── 20190629000000_add_start_at_to_campaigns.js
│ │ │ ├── 20190629170000_generalization_of_queued.js
│ │ │ ├── 20190630210000_tag_language.js
│ │ │ ├── 20190705220000_test_messages.js
│ │ │ ├── 20190722110000_hash_email.js
│ │ │ ├── 20190722150000_ensure_help_column_in_custom_fields.js
│ │ │ ├── 20191007120000_add_updated_to_subscriptions.js
│ │ │ ├── 20200617172500_add_channels.js
│ │ │ ├── 20200824160149_convert_to_utf8mb4.js
│ │ │ └── 20200830140000_required_custom_fields.js
│ └── sql
│ │ ├── base.sql
│ │ ├── drop.js
│ │ ├── drop.sh
│ │ ├── dump.js
│ │ ├── init.js
│ │ ├── mailtrain-test.sql
│ │ ├── mailtrain.sql
│ │ ├── upgrade-00001.sql
│ │ ├── upgrade-00002.sql
│ │ ├── upgrade-00003.sql
│ │ ├── upgrade-00004.sql
│ │ ├── upgrade-00005.sql
│ │ ├── upgrade-00006.sql
│ │ ├── upgrade-00007.sql
│ │ ├── upgrade-00008.sql
│ │ ├── upgrade-00009.sql
│ │ ├── upgrade-00010.sql
│ │ ├── upgrade-00011.sql
│ │ ├── upgrade-00012.sql
│ │ ├── upgrade-00013.sql
│ │ ├── upgrade-00014.sql
│ │ ├── upgrade-00015.sql
│ │ ├── upgrade-00016.sql
│ │ ├── upgrade-00017.sql
│ │ ├── upgrade-00018.sql
│ │ ├── upgrade-00019.sql
│ │ ├── upgrade-00020.sql
│ │ ├── upgrade-00021.sql
│ │ ├── upgrade-00022.sql
│ │ ├── upgrade-00023.sql
│ │ ├── upgrade-00024.sql
│ │ ├── upgrade-00025.sql
│ │ ├── upgrade-00026.sql
│ │ ├── upgrade-00027.sql
│ │ ├── upgrade-00028.sql
│ │ ├── upgrade-00029.sql
│ │ ├── upgrade-00030.sql
│ │ ├── upgrade-00031.sql
│ │ ├── upgrade-00032.sql
│ │ ├── upgrade-00033.sql
│ │ ├── upgrade-00034.sql
│ │ └── upgrade-template.sql
├── test
│ └── e2e
│ │ ├── .eslintrc
│ │ ├── README.md
│ │ ├── index.js
│ │ ├── install.sh
│ │ ├── lib
│ │ ├── config.js
│ │ ├── exit-unless-test.js
│ │ ├── mail.js
│ │ ├── mocha-e2e.js
│ │ ├── page.js
│ │ ├── web.js
│ │ └── worker-counter.js
│ │ ├── page-objects
│ │ ├── home.js
│ │ ├── subscription.js
│ │ └── user.js
│ │ └── tests
│ │ ├── login.js
│ │ └── subscription.js
└── views
│ ├── archive
│ ├── layout-raw.hbs
│ ├── layout-wrapped.hbs
│ └── view.hbs
│ ├── ckeditor
│ ├── layout.hbs
│ └── root.hbs
│ ├── codeeditor
│ ├── layout.hbs
│ └── root.hbs
│ ├── error.hbs
│ ├── grapesjs
│ ├── layout.hbs
│ └── root.hbs
│ ├── layout.hbs
│ ├── mosaico
│ ├── layout.hbs
│ └── root.hbs
│ ├── partials
│ └── tracking-scripts.hbs
│ ├── root.hbs
│ ├── subscription
│ ├── capture-flash-messages.hbs
│ ├── layout.mjml.hbs
│ ├── mail-already-subscribed-html.mjml.hbs
│ ├── mail-already-subscribed-text.hbs
│ ├── mail-confirm-address-change-html.mjml.hbs
│ ├── mail-confirm-address-change-text.hbs
│ ├── mail-confirm-subscription-html.mjml.hbs
│ ├── mail-confirm-subscription-text.hbs
│ ├── mail-confirm-unsubscription-html.mjml.hbs
│ ├── mail-confirm-unsubscription-text.hbs
│ ├── mail-subscription-confirmed-html.mjml.hbs
│ ├── mail-subscription-confirmed-text.hbs
│ ├── mail-unsubscription-confirmed-html.mjml.hbs
│ ├── mail-unsubscription-confirmed-text.hbs
│ ├── partials
│ │ ├── subscription-custom-fields.hbs
│ │ ├── subscription-flash-messages.hbs
│ │ ├── subscription-footer-scripts.hbs
│ │ ├── subscription-manage-address-form.hbs
│ │ ├── subscription-manage-form.hbs
│ │ ├── subscription-subscribe-form.hbs
│ │ └── subscription-unsubscribe-form.hbs
│ ├── web-confirm-subscription-notice.mjml.hbs
│ ├── web-confirm-unsubscription-notice.mjml.hbs
│ ├── web-manage-address.mjml.hbs
│ ├── web-manage.mjml.hbs
│ ├── web-manual-unsubscribe-notice.mjml.hbs
│ ├── web-privacy-policy-notice.mjml.hbs
│ ├── web-subscribe.mjml.hbs
│ ├── web-subscribed-notice.mjml.hbs
│ ├── web-unsubscribe.mjml.hbs
│ ├── web-unsubscribed-notice.mjml.hbs
│ ├── web-updated-notice.mjml.hbs
│ └── widget-subscribe.hbs
│ └── users
│ ├── password-reset-html.hbs
│ └── password-reset-text.hbs
├── setup
├── delete-modules.sh
├── functions
├── install-centos7-https.sh
├── install-centos7-local.sh
├── install-centos8-https.sh
├── install-centos8-local.sh
├── install-debian10-https.sh
├── install-ubuntu1804-https.sh
├── install-ubuntu1804-local.sh
├── mailtrain-apache-sample.conf
├── reinstall-modules.sh
└── setup-test.sh
├── shared
├── activity-log.js
├── app.js
├── campaigns.js
├── date.js
├── imports.js
├── interoperable-errors.js
├── langs.js
├── lists.js
├── mosaico-templates.js
├── namespaces.js
├── package-lock.json
├── package.json
├── password-validator.js
├── reports.js
├── send-configurations.js
├── templates.js
├── triggers.js
├── urls.js
├── users.js
└── validators.js
└── zone-mta
├── .gitignore
├── config
└── zonemta.js
├── index.js
├── package-lock.json
├── package.json
└── plugins
├── mailtrain-main.js
└── mailtrain-receiver.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules/
2 | docs/
3 | Dockerfile
4 | *.md
5 | .git
6 | .gitignore
7 | .gitmodules
8 | docker-compose.yml
9 | docker-compose-local.yml
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /.vscode
3 | /last-failed-e2e-test.*
4 |
5 | node_modules
6 | npm-debug.log
7 | .DS_Store
8 | dump.rdb
9 |
10 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "mvis/ivis-core"]
2 | path = mvis/ivis-core
3 | url = https://github.com/smartarch/ivis-core.git
4 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 2.0.0-rc1 2018-12-25
4 |
5 | * This is a complete rewrite of Mailtrain v1 with many features added. Just check it out.
6 |
--------------------------------------------------------------------------------
/Dockerfile-Develop:
--------------------------------------------------------------------------------
1 | # Final Develop Image
2 | FROM node:10-alpine
3 |
4 | WORKDIR /app/
5 |
6 | # Install system dependencies
7 | RUN set -ex; \
8 | apk add --update --no-cache \
9 | make gcc g++ git python pwgen netcat-openbsd bash imagemagick
10 |
11 | EXPOSE 3000 3003 3004
12 |
13 | # Keep container running, so you can access it
14 | CMD tail -f /dev/null
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 | Note that some of these may be already obsolete...
2 |
3 | ### Front page
4 | - Some dashboard
5 |
6 | ### Campaigns
7 | - List of sent RSS campaigns (?)
8 |
9 | ### Pull requests
10 | - Support ldaps:// - 5325f2ea7864ce5f42a9a6df3408af7ffbd32591
11 | - Support https - abd788d8f4d18b5a977226ba1224cba7f2b7fa9b
12 | - Support warn of failed login - 4bd1e994b27420ba366d9b0429e9014e5bf01f13
13 | - Add X-Mailer header option in settings to override or disable it - 44fe8882b876bdfd9990110496d16f819dc64ac3
14 | - Add custom unsubscribe option in a campaign - 68cb8384f7dfdbcaf2932293ec5a2f1ec0a1554e
15 |
16 | ### API
17 | - Add API extensions
18 |
19 | ### GDPR
20 | - Refuse editing subscriptions which have been anonymized
21 | - Add field to subscriptions which says till when the consent has been given
22 | - Provide a link (and merge tag) that will update the consent date to now
23 | - Add campaign trigger that triggers if the consent for specific subscription field is about to expire (i.e. it is greater than now - seconds)
24 |
25 | ### RSS Campaigns
26 | - Aggregated RSS campaigns
--------------------------------------------------------------------------------
/UPGRADE.md:
--------------------------------------------------------------------------------
1 | ## Migration from Mailtrain v1 to Mailtrain v2
2 |
3 | The migration should happen almost automatically. There are however the following caveats:
4 |
5 | 1. Structure of config files (under `config`) has changed at many places. Revisit the default config (`config/default.toml`)
6 | and update your configs accordingly.
7 |
8 | 2. Images uploaded in a template editor (Mosaico, Grapesjs, etc.) need to be manually moved to a new destination (under `client`).
9 | For Mosaico, this means to move folders named by a number from `public/mosaico` to `client/static/mosaico`.
10 |
11 | 3. Directory for custom Mosaico templates has changed from `public/mosaico/templates` to `client/static/mosaico/templates`.
12 |
13 | 4. Imports are not migrated. If you have any pending imports, complete them before migration to v2.
14 |
15 | 5. Zone MTA configuration endpoint (webhooks/zone-mta/sender-config) has changed. The send-configuration CID has to be
16 | part of the URL - e.g. webhooks/zone-mta/sender-config/system.
17 |
18 | 6. If there are lists that contain birthday or date fields that were created before
19 | commit `bc73a0df0cab9943d726bd12fc1c6f2ff1279aa7` (on Jan 3, 2018), they still have TIMESTAMP data type in DB instead
20 | of DATETIME. The problem was that that commit did not introduce migration from TIMESTAMP to DATETIME.
21 | Mailtrain v2 does this migration, however in some corner cases, this may shift the date by a day back or forth.
22 |
--------------------------------------------------------------------------------
/client/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 |
--------------------------------------------------------------------------------
/client/src/Home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, {Component} from 'react';
4 | import PropTypes from 'prop-types';
5 | import {withTranslation} from './lib/i18n';
6 | import {requiresAuthenticatedUser} from './lib/page';
7 | import {withComponentMixins} from "./lib/decorator-helpers";
8 | import mailtrainConfig from 'mailtrainConfig';
9 |
10 | @withComponentMixins([
11 | withTranslation,
12 | requiresAuthenticatedUser
13 | ])
14 | export default class List extends Component {
15 | constructor(props) {
16 | super(props);
17 | }
18 |
19 | static propTypes = {
20 | }
21 |
22 | render() {
23 | const t = this.props.t;
24 |
25 | return (
26 |
27 |
{t('mailtrain2')}
28 |
{t('build') + ' 2021-05-25-0915'}
29 |
{mailtrainConfig.shoutout}
30 |
31 | );
32 | }
33 | }
--------------------------------------------------------------------------------
/client/src/account/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import Account from './Account';
5 | import API from './API';
6 |
7 |
8 | function getMenus(t) {
9 | return {
10 | 'account': {
11 | title: t('account'),
12 | link: '/account',
13 | panelComponent: Account,
14 |
15 | children: {
16 | api: {
17 | title: t('api'),
18 | link: '/account/api',
19 | panelComponent: API
20 | }
21 | }
22 | }
23 | };
24 | }
25 |
26 | export default {
27 | getMenus
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/account/styles.scss:
--------------------------------------------------------------------------------
1 | .api {
2 | :global .card h4 {
3 | margin-top: 0px;
4 | }
5 |
6 | h4 {
7 | margin-top: 45px;
8 | }
9 | }
--------------------------------------------------------------------------------
/client/src/blacklist/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from "react";
4 | import List from "./List";
5 |
6 | function getMenus(t) {
7 | return {
8 | 'blacklist': {
9 | title: t('blacklist'),
10 | link: '/blacklist',
11 | panelComponent: List,
12 | }
13 | };
14 | }
15 |
16 | export default {
17 | getMenus
18 | }
19 |
--------------------------------------------------------------------------------
/client/src/campaigns/StatisticsLinkClicks.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React, {Component} from 'react';
4 | import PropTypes from 'prop-types';
5 | import {withTranslation} from '../lib/i18n';
6 | import {requiresAuthenticatedUser, Title, withPageHelpers} from '../lib/page';
7 | import {withErrorHandling} from '../lib/error-handling';
8 | import {Table} from "../lib/table";
9 | import {withComponentMixins} from "../lib/decorator-helpers";
10 |
11 | @withComponentMixins([
12 | withTranslation,
13 | withErrorHandling,
14 | withPageHelpers,
15 | requiresAuthenticatedUser
16 | ])
17 | export default class StatisticsLinkClicks extends Component {
18 | constructor(props) {
19 | super(props);
20 |
21 | const t = props.t;
22 |
23 | this.state = {
24 | };
25 | }
26 |
27 | static propTypes = {
28 | entity: PropTypes.object,
29 | title: PropTypes.string
30 | }
31 |
32 |
33 | render() {
34 | const t = this.props.t;
35 |
36 | const linksColumns = [
37 | { data: 0, title: t('url'), render: data => {data}
},
38 | { data: 1, title: t('uniqueVisitors') },
39 | { data: 2, title: t('totalClicks') }
40 | ];
41 |
42 | return (
43 |
44 |
{t('campaignLinks')}
45 |
46 |
this.table = node} withHeader dataUrl={`rest/campaigns-link-clicks-table/${this.props.entity.id}`} columns={linksColumns} />
47 |
48 | );
49 | }
50 | }
--------------------------------------------------------------------------------
/client/src/campaigns/triggers/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {Entity, Event} from '../../../../shared/triggers';
4 |
5 | export function getTriggerTypes(t) {
6 |
7 | const entityLabels = {
8 | [Entity.SUBSCRIPTION]: t('subscription'),
9 | [Entity.CAMPAIGN]: t('campaign')
10 | };
11 |
12 | const SubscriptionEvent = Event[Entity.SUBSCRIPTION];
13 | const CampaignEvent = Event[Entity.CAMPAIGN];
14 |
15 | const eventLabels = {
16 | [Entity.SUBSCRIPTION]: {
17 | [SubscriptionEvent.CREATED]: t('created'),
18 | [SubscriptionEvent.UPDATED]: t('updated'),
19 | [SubscriptionEvent.LATEST_OPEN]: t('latestOpen'),
20 | [SubscriptionEvent.LATEST_CLICK]: t('latestClick')
21 | },
22 | [Entity.CAMPAIGN]: {
23 | [CampaignEvent.DELIVERED]: t('delivered'),
24 | [CampaignEvent.OPENED]: t('opened'),
25 | [CampaignEvent.CLICKED]: t('clicked'),
26 | [CampaignEvent.NOT_OPENED]: t('notOpened'),
27 | [CampaignEvent.NOT_CLICKED]: t('notClicked')
28 | }
29 | };
30 |
31 | return {
32 | entityLabels,
33 | eventLabels
34 | };
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/client/src/channels/styles.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/src/channels/styles.scss
--------------------------------------------------------------------------------
/client/src/lib/axios.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import csrfToken from 'csrfToken';
4 | import axios from 'axios';
5 | import interoperableErrors from '../../../shared/interoperable-errors';
6 |
7 | const axiosInst = axios.create({
8 | headers: {
9 | 'X-CSRF-TOKEN': csrfToken
10 | }
11 | });
12 |
13 | const axiosWrapper = {
14 | get: (...args) => axiosInst.get(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
15 | put: (...args) => axiosInst.put(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
16 | post: (...args) => axiosInst.post(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error }),
17 | delete: (...args) => axiosInst.delete(...args).catch(error => { throw (error.response && interoperableErrors.deserialize(error.response.data)) || error })
18 | };
19 |
20 | const HTTPMethod = {
21 | GET: axiosWrapper.get,
22 | PUT: axiosWrapper.put,
23 | POST: axiosWrapper.post,
24 | DELETE: axiosWrapper.delete
25 | };
26 |
27 | axiosWrapper.method = (method, ...args) => method(...args);
28 |
29 | export default axiosWrapper;
30 | export {
31 | HTTPMethod
32 | }
--------------------------------------------------------------------------------
/client/src/lib/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import ellipsize from "ellipsize";
4 |
5 |
6 | export function ellipsizeBreadcrumbLabel(label) {
7 | return ellipsize(label, 40)
8 | }
--------------------------------------------------------------------------------
/client/src/lib/permissions.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {getUrl} from "./urls";
4 | import axios from "./axios";
5 |
6 | export async function checkPermissions(request) {
7 | return await axios.post(getUrl('rest/permissions-check'), request);
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/lib/public-path.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {getUrl} from "./urls";
4 |
5 | __webpack_public_path__ = getUrl('client/');
6 |
--------------------------------------------------------------------------------
/client/src/lib/sandbox-common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export function getTagLanguageFromEntity(entity, entityTypeId) {
4 | if (entityTypeId === 'template') {
5 | return entity.tag_language;
6 | } else if (entityTypeId === 'campaign') {
7 | return entity.data.sourceCustom.tag_language;
8 | }
9 | }
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-ckeditor-shared.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const initialHeight = 600;
4 |
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-ckeditor.scss:
--------------------------------------------------------------------------------
1 | $editorNormalHeight: false;
2 | @import "sandbox-common";
3 |
4 | .sandbox {
5 | height: 100%;
6 | overflow: hidden;
7 | }
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-codeeditor-shared.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const CodeEditorSourceType = {
4 | MJML: 'mjml',
5 | HTML: 'html'
6 | };
7 |
8 | export const getCodeEditorSourceTypeOptions = t => [
9 | {key: CodeEditorSourceType.MJML, label: t('mjml')},
10 | {key: CodeEditorSourceType.HTML, label: t('html')}
11 | ];
12 |
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-codeeditor.scss:
--------------------------------------------------------------------------------
1 | @import "sandbox-common";
2 |
3 | .sandbox {
4 | }
5 |
6 | .aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
7 | position: absolute;
8 | height: 100%;
9 | }
10 |
11 | .aceEditorWithPreview {
12 | border-right: #e8e8e8 solid 2px;
13 | width: 50%;
14 | }
15 |
16 | .aceEditorWithoutPreview {
17 | width: 100%;
18 | }
19 |
20 | .preview {
21 | border-left: #e8e8e8 solid 2px;
22 | width: 50%;
23 | left: 50%;
24 | overflow: hidden;
25 |
26 | iframe {
27 | width: 100%;
28 | height: 100%;
29 | border: 0px none;
30 |
31 | body {
32 | margin: 0px;
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-grapesjs-shared.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | export const GrapesJSSourceType = {
4 | MJML: 'mjml',
5 | HTML: 'html'
6 | };
7 |
8 | export const getGrapesJSSourceTypeOptions = t => [
9 | {key: GrapesJSSourceType.MJML, label: t('mjml')},
10 | {key: GrapesJSSourceType.HTML, label: t('html')}
11 | ];
12 |
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-grapesjs.scss:
--------------------------------------------------------------------------------
1 | @import "sandbox-common";
2 |
3 | :global .grapesjs-body {
4 | margin: 0px;
5 | }
6 |
7 | :global .gjs-editor-cont {
8 | position: absolute;
9 | }
10 |
11 | :global .gjs-devices-c .gjs-devices {
12 | padding-right: 15px;
13 | }
14 |
15 | :global .gjs-pn-devices-c, :global .gjs-pn-views {
16 | padding: 4px;
17 | }
18 |
19 |
--------------------------------------------------------------------------------
/client/src/lib/sandboxed-mosaico.scss:
--------------------------------------------------------------------------------
1 | @import "sandbox-common";
2 |
3 | :global .mo-standalone {
4 | top: 0px;
5 | bottom: 0px;
6 | width: 100%;
7 | position: absolute;
8 | }
9 |
--------------------------------------------------------------------------------
/client/src/lists/fields/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 |
5 | export function getFieldTypes(t) {
6 |
7 | const fieldTypes = {
8 | text: {
9 | label: t('text')
10 | },
11 | website: {
12 | label: t('website')
13 | },
14 | longtext: {
15 | label: t('multilineText')
16 | },
17 | gpg: {
18 | label: t('gpgPublicKey')
19 | },
20 | number: {
21 | label: t('number')
22 | },
23 | 'checkbox-grouped': {
24 | label: t('checkboxesFromOptionFields')
25 | },
26 | 'radio-grouped': {
27 | label: t('radioButtonsFromOptionFields')
28 | },
29 | 'dropdown-grouped': {
30 | label: t('dropDownFromOptionFields')
31 | },
32 | 'radio-enum': {
33 | label: t('radioButtonsEnumerated')
34 | },
35 | 'dropdown-enum': {
36 | label: t('dropDownEnumerated')
37 | },
38 | 'date': {
39 | label: t('date')
40 | },
41 | 'birthday': {
42 | label: t('birthday')
43 | },
44 | json: {
45 | label: t('jsonValueForCustomRendering')
46 | },
47 | option: {
48 | label: t('option')
49 | }
50 | };
51 |
52 | return fieldTypes;
53 | }
54 |
55 |
--------------------------------------------------------------------------------
/client/src/lists/forms/styles.scss:
--------------------------------------------------------------------------------
1 | $editorNormalHeight: 400px;
2 | @import "../../lib/sandbox-common";
3 |
4 | .editor {
5 | margin-bottom: 15px;
6 | }
7 |
8 | .host {
9 | border: none;
10 | width: 100%;
11 | }
12 |
--------------------------------------------------------------------------------
/client/src/lists/segments/divider.ai:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/src/lists/segments/divider.ai
--------------------------------------------------------------------------------
/client/src/lists/segments/divider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/src/lists/segments/divider.png
--------------------------------------------------------------------------------
/client/src/lists/styles.scss:
--------------------------------------------------------------------------------
1 | .mapping {
2 | margin-top: 30px;
3 | }
4 |
5 | .erased {
6 | color: #808080;
7 | }
--------------------------------------------------------------------------------
/client/src/login/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from 'react';
4 | import Login from './Login';
5 | import Reset from './Forgot';
6 | import ResetLink from './Reset';
7 | import mailtrainConfig from 'mailtrainConfig';
8 |
9 |
10 | function getMenus(t) {
11 | const subPaths = {}
12 |
13 | if (mailtrainConfig.isAuthMethodLocal) {
14 | subPaths.forgot = {
15 | title: t('passwordReset-1'),
16 | extraParams: [':username?'],
17 | link: '/login/forgot',
18 | panelComponent: Reset
19 | };
20 |
21 | subPaths.reset = {
22 | title: t('passwordReset-1'),
23 | extraParams: [':username', ':resetToken'],
24 | link: '/login/reset',
25 | panelComponent: ResetLink
26 | };
27 | }
28 |
29 | return {
30 | 'login': {
31 | title: t('signIn'),
32 | link: '/login',
33 | panelComponent: Login,
34 |
35 | children: subPaths
36 | }
37 | };
38 | }
39 |
40 | export default {
41 | getMenus
42 | }
43 |
--------------------------------------------------------------------------------
/client/src/scss/.gitignore:
--------------------------------------------------------------------------------
1 | /coreui
2 |
--------------------------------------------------------------------------------
/client/src/scss/_mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin hocus {
2 | &:hover,
3 | &:focus {
4 | @content
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/client/src/scss/variables.scss:
--------------------------------------------------------------------------------
1 | $table-cell-padding: .5rem;
2 | $input-border-color: #adb5bd;
3 |
4 | $breadcrumb-bg: #f6f7f8;
5 |
6 | $navbar-dark-color: rgba(#fff, .75) !default;
7 | $navbar-dark-hover-color: #fff !default;
8 | $navbar-active-color: #fff;
9 |
10 | @import "../../node_modules/@coreui/coreui/scss/_variables.scss";
11 |
--------------------------------------------------------------------------------
/client/src/send-configurations/styles.scss:
--------------------------------------------------------------------------------
1 | textarea.dkimPrivateKey {
2 | height: 200px;
3 | }
4 |
5 | .overridableCheckbox {
6 | margin-top: -8px !important;
7 | }
--------------------------------------------------------------------------------
/client/src/settings/root.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import React from "react";
4 | import Update from "./Update";
5 |
6 | function getMenus(t) {
7 | return {
8 | 'settings': {
9 | title: t('globalSettings'),
10 | link: '/settings',
11 | resolve: {
12 | configItems: params => `rest/settings`
13 | },
14 | panelRender: props =>
15 | }
16 | };
17 | }
18 |
19 | export default {
20 | getMenus
21 | }
22 |
--------------------------------------------------------------------------------
/client/static/fancytree/skin-bootstrap/README.md:
--------------------------------------------------------------------------------
1 | The file icons-rtl.gif is assumed by skin-common.less. However skin-bootstrap does not come with icons.
2 | To avoid errors with webpack an empty file has been provided.
--------------------------------------------------------------------------------
/client/static/fancytree/skin-bootstrap/icons-rtl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/fancytree/skin-bootstrap/icons-rtl.gif
--------------------------------------------------------------------------------
/client/static/fancytree/skin-bootstrap/icons.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/fancytree/skin-bootstrap/icons.gif
--------------------------------------------------------------------------------
/client/static/fancytree/skin-bootstrap/vline-rtl.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/fancytree/skin-bootstrap/vline-rtl.gif
--------------------------------------------------------------------------------
/client/static/fancytree/skin-bootstrap/vline.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/fancytree/skin-bootstrap/vline.gif
--------------------------------------------------------------------------------
/client/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/favicon.ico
--------------------------------------------------------------------------------
/client/static/mailtrain-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mailtrain-header.png
--------------------------------------------------------------------------------
/client/static/mailtrain-notext.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mailtrain-notext.png
--------------------------------------------------------------------------------
/client/static/mailtrain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mailtrain.png
--------------------------------------------------------------------------------
/client/static/mosaico/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/favicon.ico
--------------------------------------------------------------------------------
/client/static/mosaico/rs/fontawesome/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/fontawesome/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/client/static/mosaico/rs/fontawesome/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/fontawesome/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/client/static/mosaico/rs/fontawesome/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/fontawesome/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/client/static/mosaico/rs/fontawesome/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/fontawesome/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/byvoxmail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/byvoxmail.png
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/mosaico-badge.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/mosaico-badge.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/mosaico-v.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/mosaico-v.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/mosaico32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/mosaico32.png
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/mosaicologo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/mosaicologo.png
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/screenshot-orig.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/screenshot-orig.png
--------------------------------------------------------------------------------
/client/static/mosaico/rs/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/img/screenshot.png
--------------------------------------------------------------------------------
/client/static/mosaico/rs/lang/LICENSE:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/lang/LICENSE
--------------------------------------------------------------------------------
/client/static/mosaico/rs/mosaico-libs-and-tinymce.min.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:'Noto Sans';font-style:normal;font-weight:400;src:url(./notoregular/noto-sans-400-normal.eot);src:local('Noto Sans'),local('NotoSans'),url(./notoregular/noto-sans-400-normal.eot#iefix) format('embedded-opentype'),url(./notoregular/noto-sans-400-normal.woff) format('woff'),url(./notoregular/noto-sans-400-normal.ttf) format('truetype')}
--------------------------------------------------------------------------------
/client/static/mosaico/rs/notoregular/noto-sans-400-normal.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/notoregular/noto-sans-400-normal.eot
--------------------------------------------------------------------------------
/client/static/mosaico/rs/notoregular/noto-sans-400-normal.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/notoregular/noto-sans-400-normal.ttf
--------------------------------------------------------------------------------
/client/static/mosaico/rs/notoregular/noto-sans-400-normal.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/notoregular/noto-sans-400-normal.woff
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-cool.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-cool.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-cry.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-cry.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-embarassed.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-embarassed.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-foot-in-mouth.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-foot-in-mouth.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-frown.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-frown.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-innocent.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-innocent.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-kiss.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-kiss.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-laughing.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-laughing.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-money-mouth.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-money-mouth.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-sealed.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-sealed.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-smile.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-smile.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-surprised.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-surprised.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-tongue-out.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-tongue-out.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-undecided.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-undecided.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-wink.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-wink.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/img/smiley-yell.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/client/static/mosaico/rs/plugins/emoticons/img/smiley-yell.gif
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/emoticons/plugin.min.js:
--------------------------------------------------------------------------------
1 | tinymce.PluginManager.add("emoticons",function(e,t){function n(){var e;return e='',tinymce.each(r,function(n){e+="",tinymce.each(n,function(n){var r=t+"/img/smiley-"+n+".gif";e+=' | '}),e+="
"}),e+="
"}var r=[["cool","cry","embarassed","foot-in-mouth"],["frown","innocent","kiss","laughing"],["money-mouth","sealed","smile","surprised"],["tongue-out","undecided","wink","yell"]];e.addButton("emoticons",{type:"panelbutton",panel:{role:"application",autohide:!0,html:n,onclick:function(t){var n=e.dom.getParent(t.target,"a");n&&(e.insertContent('
'),this.hide())}},tooltip:"Emoticons"})});
--------------------------------------------------------------------------------
/client/static/mosaico/rs/plugins/nonbreaking/plugin.min.js:
--------------------------------------------------------------------------------
1 | tinymce.PluginManager.add("nonbreaking",function(e){var t=e.getParam("nonbreaking_force_tab");if(e.addCommand("mceNonBreaking",function(){e.insertContent(e.plugins.visualchars&&e.plugins.visualchars.state?' ':" "),e.dom.setAttrib(e.dom.select("span.mce-nbsp"),"data-mce-bogus","1")}),e.addButton("nonbreaking",{title:"Nonbreaking space",cmd:"mceNonBreaking"}),e.addMenuItem("nonbreaking",{text:"Nonbreaking space",cmd:"mceNonBreaking",context:"insert"}),t){var n=+t>1?+t:3;e.on("keydown",function(t){if(9==t.keyCode){if(t.shiftKey)return;t.preventDefault();for(var r=0;r/public/mosaico/uploads to here (/client/static/mosaico/uploads).
3 |
--------------------------------------------------------------------------------
/docker-compose-develop.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mysql:
5 | image: mariadb:10.4
6 | environment:
7 | - MYSQL_ROOT_PASSWORD=mailtrain
8 | - MYSQL_DATABASE=mailtrain
9 | - MYSQL_USER=mailtrain
10 | - MYSQL_PASSWORD=mailtrain
11 | volumes:
12 | - mysql-data:/var/lib/mysql
13 |
14 | redis:
15 | image: redis:5
16 | volumes:
17 | - redis-data:/data
18 |
19 | mongo:
20 | image: mongo:4-xenial
21 | volumes:
22 | - mongo-data:/data/db
23 |
24 | mailtrain:
25 | build:
26 | context: .
27 | dockerfile: Dockerfile-Develop
28 | ports:
29 | - "3000:3000"
30 | - "3003:3003"
31 | - "3004:3004"
32 | volumes:
33 | - ./:/app
34 |
35 | volumes:
36 | mysql-data:
37 | redis-data:
38 | mongo-data:
--------------------------------------------------------------------------------
/docker-compose-local.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mysql:
5 | image: mariadb:10.4
6 | environment:
7 | - MYSQL_ROOT_PASSWORD=mailtrain
8 | - MYSQL_DATABASE=mailtrain
9 | - MYSQL_USER=mailtrain
10 | - MYSQL_PASSWORD=mailtrain
11 | volumes:
12 | - mysql-data:/var/lib/mysql
13 |
14 | redis:
15 | image: redis:5
16 | volumes:
17 | - redis-data:/data
18 |
19 | mongo:
20 | image: mongo:4-xenial
21 | volumes:
22 | - mongo-data:/data/db
23 |
24 | mailtrain:
25 | build: .
26 | ports:
27 | - "3000:3000"
28 | - "3003:3003"
29 | - "3004:3004"
30 | volumes:
31 | - mailtrain-files:/app/server/files
32 |
33 | volumes:
34 | mysql-data:
35 | redis-data:
36 | mongo-data:
37 | mailtrain-files:
38 |
39 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 |
3 | services:
4 | mysql:
5 | image: mariadb:10.4
6 | environment:
7 | - MYSQL_ROOT_PASSWORD=mailtrain
8 | - MYSQL_DATABASE=mailtrain
9 | - MYSQL_USER=mailtrain
10 | - MYSQL_PASSWORD=mailtrain
11 | volumes:
12 | - mysql-data:/var/lib/mysql
13 |
14 | redis:
15 | image: redis:5
16 | volumes:
17 | - redis-data:/data
18 |
19 | mongo:
20 | image: mongo:4-xenial
21 | volumes:
22 | - mongo-data:/data/db
23 |
24 | mailtrain:
25 | image: mailtrain/mailtrain:latest
26 | ports:
27 | - "3000:3000"
28 | - "3003:3003"
29 | - "3004:3004"
30 | volumes:
31 | - mailtrain-files:/app/server/files
32 |
33 | volumes:
34 | mysql-data:
35 | redis-data:
36 | mongo-data:
37 | mailtrain-files:
38 |
39 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | docs.mailtrain.org
2 |
--------------------------------------------------------------------------------
/locales/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailtrain-locales-extractor",
3 | "private": true,
4 | "version": "2.0.0",
5 | "description": "Extractor for t-functions in the code (client and server)",
6 | "main": "extract.js",
7 | "scripts": {},
8 | "license": "GPL-3.0",
9 | "homepage": "https://mailtrain.org/",
10 | "engines": {
11 | "node": ">=10.0.0"
12 | },
13 | "dependencies": {
14 | "acorn": "^6.0.4",
15 | "acorn-jsx": "^5.0.0",
16 | "camelcase": "^5.0.0",
17 | "deep-keys": "^0.5.0",
18 | "ellipsize": "^0.1.0",
19 | "fast-deep-equal": "^2.0.1",
20 | "klaw-sync": "^6.0.0",
21 | "slugify": "^1.3.3"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mvis/client/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | /node_modules
--------------------------------------------------------------------------------
/mvis/client/src/root-trusted.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import em from '../../ivis-core/client/src/lib/extension-manager';
4 | import './styles.scss';
5 |
6 | em.set('app.title', 'Mailtrain IVIS');
7 |
8 | require('../../ivis-core/client/src/root-trusted');
9 |
10 |
--------------------------------------------------------------------------------
/mvis/client/src/styles.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Mailtrain-org/mailtrain/7c6f34ef25dba981de58aec9785af48f3ea3315d/mvis/client/src/styles.scss
--------------------------------------------------------------------------------
/mvis/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const webpackConf = require('../ivis-core/client/webpack.config');
4 |
5 | webpackConf.resolve.modules = ['node_modules', '../ivis-core/client/node_modules'];
6 | webpackConf.entry = {
7 | 'index-trusted': ['@babel/polyfill', './src/root-trusted.js'],
8 | 'index-sandbox': ['@babel/polyfill', '../ivis-core/client/src/root-sandbox.js']
9 | };
10 | webpackConf.output = {
11 | filename: '[name].js',
12 | path: path.resolve(__dirname, 'dist')
13 | };
14 |
15 | module.exports = webpackConf;
--------------------------------------------------------------------------------
/mvis/server/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /config/development.yaml
3 |
--------------------------------------------------------------------------------
/mvis/server/builder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./extensions-common');
4 | require('../ivis-core/server/services/builder');
5 |
6 |
--------------------------------------------------------------------------------
/mvis/server/extensions-common.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const em = require('../ivis-core/server/lib/extension-manager');
4 | const path = require('path');
5 |
6 | em.set('config.extraDirs', [ path.join(__dirname, 'config') ]);
7 | em.set('builder.exec', path.join(__dirname, 'builder.js'));
8 | em.set('task-handler.exec', path.join(__dirname, 'task-handler.js'));
9 | em.set('indexer.elasticsearch.exec', path.join(__dirname, 'indexer-elasticsearch.js'));
10 | em.set('app.title', 'Mailtrain IVIS');
11 |
12 | em.set('models.namespaces.extraKeys', ['mt_campaign']);
13 | em.set('models.signalSets.extraKeys', ['mt_dataset_type']);
14 |
15 |
--------------------------------------------------------------------------------
/mvis/server/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./extensions-common');
4 | const em = require('../ivis-core/server/lib/extension-manager');
5 |
6 | const path = require('path');
7 |
8 | em.set('app.clientDist', path.join(__dirname, '..', 'client', 'dist'));
9 |
10 | em.on('knex.migrate', async () => {
11 | const knex = require('../ivis-core/server/lib/knex');
12 | await knex.migrateExtension('mvis', './knex/migrations').latest();
13 | });
14 |
15 | em.on('app.installAPIRoutes', app => {
16 | const embedApi = require('./routes/api/embed');
17 | app.use('/api', embedApi);
18 |
19 | const eventsApi = require('./routes/api/events');
20 | app.use('/api', eventsApi);
21 | });
22 |
23 | require('../ivis-core/server/index');
24 |
25 |
--------------------------------------------------------------------------------
/mvis/server/indexer-elasticsearch.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./extensions-common');
4 | require('../ivis-core/server/services/indexer-elasticsearch');
5 |
6 |
--------------------------------------------------------------------------------
/mvis/server/knex/migrations/20181226130000_add_mailtrain_metadata.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async () => {
2 | await knex.schema.table('signal_sets', table => {
3 | table.integer('mt_dataset_type');
4 | });
5 |
6 | await knex.schema.table('namespaces', table => {
7 | table.integer('mt_campaign').unsigned();
8 | });
9 | })();
10 |
11 | exports.down = (knex, Promise) => (async () => {
12 | })();
--------------------------------------------------------------------------------
/mvis/server/knexfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./extensions-common');
4 |
5 | const config = require('../ivis-core/server/lib/config');
6 |
7 | module.exports = {
8 | client: 'mysql',
9 | connection: config.mysql,
10 | seeds: {
11 | directory: '../ivis-core/server/knex/seeds'
12 | },
13 | migrations: {
14 | directory: '../ivis-core/server/knex/migrations'
15 | }
16 | };
17 |
--------------------------------------------------------------------------------
/mvis/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailtrain-ivis-server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {},
7 | "author": "Tomas Bures",
8 | "license": "MIT",
9 | "engines": {
10 | "node": ">=10.0.0"
11 | },
12 | "devDependencies": {},
13 | "optionalDependencies": {},
14 | "dependencies": {
15 | "axios": "^0.18.0",
16 | "js-yaml": "^3.12.0",
17 | "knex": "^0.16.3",
18 | "moment": "^2.18.1",
19 | "moment-timezone": "^0.5.27",
20 | "mysql": "^2.16.0",
21 | "npmlog": "^4.0.2"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/mvis/server/routes/api/embed.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../../ivis-core/server/lib/passport');
4 | const shares = require('../../../ivis-core/server/models/shares');
5 | const users = require('../../../ivis-core/server/models/users');
6 | const {castToInteger} = require('../../../ivis-core/server/lib/helpers');
7 | const knex = require('../../../ivis-core/server/lib/knex');
8 | const urls = require('../../../ivis-core/server/lib/urls');
9 | const contextHelpers = require('../../../ivis-core/server/lib/context-helpers');
10 |
11 | const router = require('../../../ivis-core/server/lib/router-async').create();
12 |
13 | router.getAsync('/mt-embedded-panel/:mtUserId/:panelId', passport.loggedIn, async (req, res) => {
14 | const panelId = castToInteger(req.params.panelId);
15 | const mtUserId = castToInteger(req.params.mtUserId);
16 | const userName = `mt-user-${mtUserId}`;
17 | const user = await users.getByUsername(req.context, userName);
18 |
19 | const restrictedAccessToken = await users.getRestrictedAccessToken(req.context, 'panel', {panelId, renewableBySandbox: true}, user.id);
20 |
21 | return res.json({
22 | token: restrictedAccessToken,
23 | ivisSandboxUrlBase: urls.getSandboxUrlBase()
24 | });
25 | });
26 |
27 | module.exports = router;
28 |
--------------------------------------------------------------------------------
/mvis/server/setup-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | MYSQL_PASSWORD=`pwgen 12 -1`
4 |
5 | # Setup MySQL user for Mailtrain
6 | mysql -u root -p -e "CREATE USER 'mvis'@'localhost' IDENTIFIED BY '$MYSQL_PASSWORD';"
7 | mysql -u root -p -e "GRANT ALL PRIVILEGES ON mvis.* TO 'mvis'@'localhost';"
8 | mysql -u mvis --password="$MYSQL_PASSWORD" -e "CREATE database mvis;"
9 |
10 | cat >> config/production.yaml <=10.0.0"
11 | },
12 | "devDependencies": {},
13 | "optionalDependencies": {},
14 | "dependencies": {
15 | "axios": "^0.18.0",
16 | "express": "^4.15.2",
17 | "handlebars": "^4.5.3",
18 | "hbs": "^4.0.6"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/mvis/test-embed/views/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Test EVIF embed
6 |
7 |
8 | {{{ body }}}
9 |
10 |
--------------------------------------------------------------------------------
/mvis/test-embed/views/panel.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
8 |
9 |
--------------------------------------------------------------------------------
/mvis/tools/delete-els-indexes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | SIGSETS=`curl -X GET "localhost:9200/_cat/indices?v" | grep 'signal_set_' | sed -e 's:.*\(signal_set_[^ ]*\).*:\1:g'`
4 |
5 | for SIGSET in $SIGSETS ; do
6 | curl -X DELETE "localhost:9200/$SIGSET"
7 | done
8 |
--------------------------------------------------------------------------------
/mvis/tools/list-els-indexes.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | curl -X GET "localhost:9200/_cat/indices?v"
4 |
--------------------------------------------------------------------------------
/mvis/tools/new-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | sudo mysql -e 'drop database mvis; create database mvis'
4 |
--------------------------------------------------------------------------------
/mvis/tools/reinstall-modules.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | PACKAGE_DIRS="client server test-embed ivis-core/client ivis-core/shared ivis-core/server ivis-core/embedding"
4 |
5 | for i in $PACKAGE_DIRS; do echo $i; cd $i; rm -rf node_modules; npm install; cd -; done
--------------------------------------------------------------------------------
/mvis/tools/reset-db.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | sudo mysql -e 'drop database mt_ivis; create database mt_ivis; connect mt_ivis'
4 |
--------------------------------------------------------------------------------
/server/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "nodemailer",
3 | "parserOptions": { "ecmaVersion": 2018 }
4 | }
5 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | /config/development.*
2 | /config/production.*
3 | /config/test.*
4 | /services/workers/reports/config/development.*
5 | /services/workers/reports/config/production.*
6 | /services/workers/reports/config/test.*
7 | /files
8 |
--------------------------------------------------------------------------------
/server/Gruntfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | module.exports = function (grunt) {
4 |
5 | // Project configuration.
6 | grunt.initConfig({
7 | eslint: {
8 | all: [
9 | 'lib/**/*.js',
10 | 'models/**/*.js',
11 | 'routes/**/*.js',
12 | 'services/**/*.js',
13 | 'lib/**/*.js',
14 | 'test/**/*.js',
15 | 'app-builder.js',
16 | 'index.js',
17 | 'Gruntfile.js',
18 | ]
19 | }
20 | });
21 |
22 | // Load the plugin(s)
23 | grunt.loadNpmTasks('grunt-eslint');
24 | grunt.task.loadTasks('tasks');
25 |
26 | // Tasks
27 | grunt.registerTask('default', ['eslint']);
28 | };
29 |
--------------------------------------------------------------------------------
/server/lib/config.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | const config = require('config');
4 |
5 | if (!config.roles) {
6 | config.roles = config.defaultRoles;
7 | }
8 |
9 | module.exports = config;
--------------------------------------------------------------------------------
/server/lib/context-helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const knex = require('./knex');
4 |
5 | function getRequestContext(req) {
6 | const context = {
7 | user: req.user
8 | };
9 |
10 | return context;
11 | }
12 |
13 | const adminContext = {
14 | user: {
15 | admin: true,
16 | id: 0,
17 | username: '',
18 | name: '',
19 | email: ''
20 | }
21 | };
22 |
23 | function getAdminContext() {
24 | return adminContext;
25 | }
26 |
27 | module.exports = {
28 | getRequestContext,
29 | getAdminContext
30 | };
--------------------------------------------------------------------------------
/server/lib/file-helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('./passport');
4 | const files = require('../models/files');
5 |
6 | const path = require('path');
7 | const uploadedFilesDir = path.join(files.filesDir, 'uploaded');
8 | const {castToInteger} = require('./helpers');
9 |
10 | const multer = require('multer')({
11 | dest: uploadedFilesDir
12 | });
13 |
14 | function installUploadHandler(router, url, replacementBehavior, type, subType, transformResponseFn) {
15 | router.postAsync(url, passport.loggedIn, multer.array('files[]'), async (req, res) => {
16 | return res.json(await files.createFiles(req.context, type || req.params.type, subType || req.params.subType, castToInteger(req.params.entityId), req.files, replacementBehavior, transformResponseFn));
17 | });
18 | }
19 |
20 | module.exports = {
21 | installUploadHandler,
22 | uploadedFilesDir
23 | };
--------------------------------------------------------------------------------
/server/lib/fork.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const builtinFork = require('child_process').fork;
4 |
5 | const cleanExit = () => process.exit();
6 | process.on('SIGINT', cleanExit); // catch ctrl-c
7 | process.on('SIGTERM', cleanExit); // catch kill
8 |
9 | const children = [];
10 |
11 | process.on('message', msg => {
12 | if (msg === 'exit') {
13 | cleanExit();
14 | }
15 | });
16 |
17 |
18 | process.on('exit', function() {
19 | for (const child of children) {
20 | child.send('exit');
21 | }
22 | });
23 |
24 | function fork(path, args, opts) {
25 | const child = builtinFork(path, args, opts);
26 |
27 | children.push(child);
28 | return child;
29 | }
30 |
31 | module.exports.fork = fork;
32 |
--------------------------------------------------------------------------------
/server/lib/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const crypto = require('crypto');
4 |
5 | module.exports = {
6 | enforce,
7 | cleanupFromPost,
8 | filterObject,
9 | castToInteger,
10 | normalizeEmail,
11 | hashEmail
12 | };
13 |
14 | function enforce(condition, message) {
15 | if (!condition) {
16 | throw new Error(message);
17 | }
18 | }
19 |
20 | function cleanupFromPost(value) {
21 | return (value || '').toString().trim();
22 | }
23 |
24 | function filterObject(obj, allowedKeys) {
25 | const result = {};
26 | for (const key in obj) {
27 | if (allowedKeys.has(key)) {
28 | result[key] = obj[key];
29 | }
30 | }
31 |
32 | return result;
33 | }
34 |
35 | function castToInteger(id, msg) {
36 | const val = parseInt(id);
37 |
38 | if (!Number.isInteger(val)) {
39 | throw new Error(msg || 'Invalid id');
40 | }
41 |
42 | return val;
43 | }
44 |
45 | function normalizeEmail(email) {
46 | const emailParts = email.split(/@/);
47 |
48 | if (emailParts.length !== 2) {
49 | return email;
50 | }
51 |
52 | const username = emailParts[0];
53 | const domain = emailParts[1].toLowerCase();
54 |
55 | return username + '@' + domain;
56 | }
57 |
58 | function hashEmail(email) {
59 | return crypto.createHash('sha512').update(normalizeEmail(email)).digest("base64");
60 | }
61 |
62 |
--------------------------------------------------------------------------------
/server/lib/knex.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const config = require('./config');
4 | const path = require('path');
5 |
6 | const knexConstructor = require('knex');
7 |
8 | const knex = require('knex')({
9 | client: 'mysql',
10 | connection: {
11 | ...config.mysql,
12 |
13 | charset: 'utf8mb4',
14 | multipleStatements: true,
15 |
16 | // DATE and DATETIME types contain no timezone info. The MySQL driver tries to interpret them w.r.t. to local time, which
17 | // does not work well with assigning these values in UTC and handling them as if in UTC
18 | dateStrings: [
19 | 'DATE',
20 | 'DATETIME'
21 | ]
22 | },
23 | migrations: {
24 | directory: path.join(__dirname, '..', 'setup', 'knex', 'migrations')
25 | }
26 | //, debug: true
27 | });
28 |
29 | /*
30 | This is to enable logging on mysql side:
31 | SET GLOBAL general_log = 'ON';
32 | SET GLOBAL general_log_file = '/tmp/mysql-all.log';
33 | */
34 |
35 |
36 |
37 | module.exports = knex;
38 |
--------------------------------------------------------------------------------
/server/lib/log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const config = require('./config');
4 | const log = require('npmlog');
5 |
6 | log.level = config.log.level;
7 |
8 | module.exports = log;
--------------------------------------------------------------------------------
/server/lib/namespace-helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const { enforce } = require('./helpers');
4 | const interoperableErrors = require('../../shared/interoperable-errors');
5 | const shares = require('../models/shares');
6 |
7 | async function validateEntity(tx, entity) {
8 | enforce(entity.namespace, 'Entity namespace not set');
9 | if (!await tx('namespaces').where('id', entity.namespace).first()) {
10 | throw new interoperableErrors.NamespaceNotFoundError();
11 | }
12 | }
13 |
14 | async function validateMoveTx(tx, context, entity, existing, entityTypeId, createOperation, deleteOperation) {
15 | if (existing.namespace !== entity.namespace) {
16 | await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, createOperation);
17 | await shares.enforceEntityPermissionTx(tx, context, entityTypeId, entity.id, deleteOperation);
18 | }
19 | }
20 |
21 | module.exports = {
22 | validateEntity,
23 | validateMoveTx
24 | };
--------------------------------------------------------------------------------
/server/lib/nodeify.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const nodeify = require('nodeify');
4 |
5 | module.exports.nodeifyPromise = nodeify;
6 |
7 | module.exports.nodeifyFunction = (asyncFun) => {
8 | return (...args) => {
9 | const callback = args.pop();
10 |
11 | const promise = asyncFun(...args);
12 |
13 | return module.exports.nodeifyPromise(promise, callback);
14 | };
15 | };
16 |
--------------------------------------------------------------------------------
/server/lib/router-async.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const express = require('express');
4 |
5 | function replaceLastBySafeHandler(handlers) {
6 | if (handlers.length === 0) {
7 | return [];
8 | }
9 |
10 | const lastHandler = handlers[handlers.length - 1];
11 | const ret = handlers.slice();
12 | ret[handlers.length - 1] = (req, res, next) => lastHandler(req, res, next).catch(error => next(error));
13 | return ret;
14 | }
15 |
16 | function create() {
17 | const router = new express.Router();
18 |
19 | router.allAsync = (path, ...handlers) => router.all(path, ...replaceLastBySafeHandler(handlers));
20 | router.getAsync = (path, ...handlers) => router.get(path, ...replaceLastBySafeHandler(handlers));
21 | router.postAsync = (path, ...handlers) => router.post(path, ...replaceLastBySafeHandler(handlers));
22 | router.putAsync = (path, ...handlers) => router.put(path, ...replaceLastBySafeHandler(handlers));
23 | router.deleteAsync = (path, ...handlers) => router.delete(path, ...replaceLastBySafeHandler(handlers));
24 |
25 | return router;
26 | }
27 |
28 | module.exports = {
29 | create
30 | };
31 |
32 |
--------------------------------------------------------------------------------
/server/lib/shortid.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | // Modules
4 | const nanoid = require('nanoid');
5 | const config = require('./config');
6 |
7 | // Default hardcoded values
8 | let alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
9 | let customlength = 10;
10 |
11 | // Gets from config if defined
12 | if (config.cid && config.cid.alphabet) alphabet=config.cid.alphabet;
13 | if (config.cid && config.cid.length) customlength=config.cid.length;
14 |
15 | // Create custom nanoid
16 | const customnanoid = nanoid.customAlphabet(alphabet, customlength);
17 |
18 | const re = new RegExp('['+alphabet+']{'+customlength+'}');
19 |
20 | // Implements the public methods of shortid module with nanoid and export them
21 | module.exports.generate = function() {
22 | return customnanoid();
23 | }
24 |
25 | module.exports.isValid = function(id) {
26 | return re.test(id);
27 | }
28 |
--------------------------------------------------------------------------------
/server/lib/synchronized.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // This implements a simple wrapper around an async function that prevents concurrent execution of the function from two asynchronous chains
4 | // It enforces that the running execution has to complete first before another one is started.
5 | function synchronized(asyncFn) {
6 | let ensurePromise = null;
7 |
8 | return async (...args) => {
9 | while (ensurePromise) {
10 | try {
11 | await ensurePromise;
12 | } catch (err) {
13 | }
14 | }
15 |
16 | ensurePromise = asyncFn(...args);
17 |
18 | try {
19 | return await ensurePromise;
20 | } finally {
21 | ensurePromise = null;
22 | }
23 | }
24 | }
25 |
26 | module.exports = synchronized;
--------------------------------------------------------------------------------
/server/models/confirmations.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const knex = require('../lib/knex');
4 | const shortid = require('../lib/shortid');
5 |
6 | async function addConfirmation(listId, action, ip, data) {
7 | const cid = shortid.generate();
8 | await knex('confirmations').insert({
9 | cid,
10 | list: listId,
11 | action,
12 | ip,
13 | data: JSON.stringify(data || {})
14 | });
15 |
16 | return cid;
17 | }
18 |
19 | /*
20 | Atomically retrieves confirmation from the database, removes it from the database and returns it.
21 | */
22 | async function takeConfirmation(cid) {
23 | return await knex.transaction(async tx => {
24 | const entry = await tx('confirmations').select(['cid', 'list', 'action', 'ip', 'data']).where('cid', cid).forUpdate().first();
25 |
26 | if (!entry) {
27 | return false;
28 | }
29 |
30 | await tx('confirmations').where('cid', cid).del();
31 |
32 | let data;
33 | try {
34 | data = JSON.parse(entry.data);
35 | } catch (err) {
36 | data = {};
37 | }
38 |
39 | return {
40 | list: entry.list,
41 | action: entry.action,
42 | ip: entry.ip,
43 | data
44 | };
45 | });
46 | }
47 |
48 | module.exports.addConfirmation = addConfirmation;
49 | module.exports.takeConfirmation = takeConfirmation;
50 |
--------------------------------------------------------------------------------
/server/protected/reports/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !README.md
--------------------------------------------------------------------------------
/server/protected/reports/README.md:
--------------------------------------------------------------------------------
1 | This directory serves for generated reports.
--------------------------------------------------------------------------------
/server/routes/archive.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const router = require('../lib/router-async').create();
4 | const messageSender = require('../lib/message-sender');
5 |
6 |
7 | router.get('/:campaign/:list/:subscription', (req, res, next) => {
8 | messageSender.getMessage(req.params.campaign, req.params.list, req.params.subscription)
9 | .then(result => {
10 | const {html} = result;
11 |
12 | if (html.match(/<\/body\b/i)) {
13 | res.render('partials/tracking-scripts', {
14 | layout: 'archive/layout-raw'
15 | }, (err, scripts) => {
16 | if (err) {
17 | return next(err);
18 | }
19 | const htmlWithScripts = scripts ? html.replace(/<\/body\b/i, match => scripts + match) : html;
20 |
21 | res.render('archive/view', {
22 | layout: 'archive/layout-raw',
23 | message: htmlWithScripts
24 | });
25 | });
26 |
27 | } else {
28 | res.render('archive/view', {
29 | layout: 'archive/layout-wrapped',
30 | message: html
31 | });
32 | }
33 |
34 | })
35 | .catch(err => next(err));
36 | });
37 |
38 | module.exports = router;
39 |
--------------------------------------------------------------------------------
/server/routes/files.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const router = require('../lib/router-async').create();
4 | const files = require('../models/files');
5 | const contextHelpers = require('../lib/context-helpers');
6 |
7 | router.getAsync('/:type/:subType/:entityId/:fileName', async (req, res) => {
8 | const file = await files.getFileByFilename(contextHelpers.getAdminContext(), req.params.type, req.params.subType, req.params.entityId, req.params.fileName);
9 | res.type(file.mimetype);
10 | return res.download(file.path, file.name);
11 | });
12 |
13 | module.exports = router;
14 |
--------------------------------------------------------------------------------
/server/routes/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../lib/passport');
4 | const clientHelpers = require('../lib/client-helpers');
5 | const { getTrustedUrl } = require('../lib/urls');
6 | const { AppType } = require('../../shared/app');
7 |
8 | const routerFactory = require('../lib/router-async');
9 |
10 | async function getRouter(appType) {
11 | const router = routerFactory.create();
12 |
13 | if (appType === AppType.TRUSTED) {
14 | router.getAsync('/*', passport.csrfProtection, async (req, res) => {
15 | const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
16 | if (req.user) {
17 | Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
18 | }
19 |
20 | res.render('root', {
21 | reactCsrfToken: req.csrfToken(),
22 | mailtrainConfig: JSON.stringify(mailtrainConfig),
23 | scriptFiles: [
24 | getTrustedUrl('client/root.js')
25 | ],
26 | publicPath: getTrustedUrl()
27 | });
28 | });
29 | }
30 |
31 | return router;
32 | }
33 |
34 |
35 | module.exports.getRouter = getRouter;
36 |
--------------------------------------------------------------------------------
/server/routes/reports.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../lib/passport');
4 | const reports = require('../models/reports');
5 | const reportHelpers = require('../lib/report-helpers');
6 | const shares = require('../models/shares');
7 | const contextHelpers = require('../lib/context-helpers');
8 | const {castToInteger} = require('../lib/helpers');
9 |
10 | const router = require('../lib/router-async').create();
11 |
12 | const fileSuffixes = {
13 | 'text/html': '.html',
14 | 'text/csv': '.csv'
15 | };
16 |
17 | router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
18 | const reportId = castToInteger(req.params.id);
19 | await shares.enforceEntityPermission(req.context, 'report', reportId, 'viewContent');
20 |
21 | const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), reportId, false);
22 |
23 | if (report.state == reports.ReportState.FINISHED) {
24 | const headers = {
25 | 'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + (fileSuffixes[report.mime_type] || ''),
26 | 'Content-Type': report.mime_type
27 | };
28 |
29 | res.sendFile(reportHelpers.getReportContentFile(report), {headers: headers});
30 |
31 | } else {
32 | return res.status(404).send('Report not found');
33 | }
34 | });
35 |
36 | module.exports = router;
37 |
--------------------------------------------------------------------------------
/server/routes/rest/blacklist.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 | const blacklist = require('../../models/blacklist');
5 |
6 | const router = require('../../lib/router-async').create();
7 |
8 |
9 | router.postAsync('/blacklist-table', passport.loggedIn, async (req, res) => {
10 | return res.json(await blacklist.listDTAjax(req.context, req.body));
11 | });
12 |
13 | router.postAsync('/blacklist', passport.loggedIn, passport.csrfProtection, async (req, res) => {
14 | return res.json(await blacklist.add(req.context, req.body.email));
15 | });
16 |
17 | router.deleteAsync('/blacklist/:email', passport.loggedIn, passport.csrfProtection, async (req, res) => {
18 | await blacklist.remove(req.context, req.params.email);
19 | return res.json();
20 | });
21 |
22 | router.postAsync('/blacklist-validate', passport.loggedIn, async (req, res) => {
23 | return res.json(await blacklist.serverValidate(req.context, req.body));
24 | });
25 |
26 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/rest/editors.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 |
5 | const bluebird = require('bluebird');
6 | const htmlToText = require('html-to-text');
7 |
8 | const router = require('../../lib/router-async').create();
9 |
10 | router.postAsync('/html-to-text', passport.loggedIn, passport.csrfProtection, async (req, res) => {
11 | const email = htmlToText.fromString(req.body.html, {wordwrap: 130});
12 |
13 | res.json({text: email});
14 | });
15 |
16 | module.exports = router;
17 |
--------------------------------------------------------------------------------
/server/routes/rest/files.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 | const files = require('../../models/files');
5 | const router = require('../../lib/router-async').create();
6 | const fileHelpers = require('../../lib/file-helpers');
7 | const {castToInteger} = require('../../lib/helpers');
8 |
9 | router.postAsync('/files-table/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
10 | return res.json(await files.listDTAjax(req.context, req.params.type, req.params.subType, castToInteger(req.params.entityId), req.body));
11 | });
12 |
13 | router.getAsync('/files-list/:type/:subType/:entityId', passport.loggedIn, async (req, res) => {
14 | return res.json(await files.list(req.context, req.params.type, req.params.subType, castToInteger(req.params.entityId)));
15 | });
16 |
17 |
18 | router.getAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
19 | const file = await files.getFileById(req.context, req.params.type, req.params.subType, castToInteger(req.params.fileId));
20 | res.type(file.mimetype);
21 | return res.download(file.path, file.name);
22 | });
23 |
24 | router.deleteAsync('/files/:type/:subType/:fileId', passport.loggedIn, async (req, res) => {
25 | await files.removeFile(req.context, req.params.type, req.params.subType, castToInteger(req.params.fileId));
26 | return res.json();
27 | });
28 |
29 | fileHelpers.installUploadHandler(router, '/files/:type/:subType/:entityId');
30 |
31 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/rest/import-runs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 | const importRuns = require('../../models/import-runs');
5 |
6 | const router = require('../../lib/router-async').create();
7 | const {castToInteger} = require('../../lib/helpers');
8 |
9 | router.postAsync('/import-runs-table/:listId/:importId', passport.loggedIn, async (req, res) => {
10 | return res.json(await importRuns.listDTAjax(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), req.body));
11 | });
12 |
13 | router.postAsync('/import-run-failed-table/:listId/:importId/:importRunId', passport.loggedIn, async (req, res) => {
14 | return res.json(await importRuns.listFailedDTAjax(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), castToInteger(req.params.importRunId), req.body));
15 | });
16 |
17 | router.getAsync('/import-runs/:listId/:importId/:runId', passport.loggedIn, async (req, res) => {
18 | const entity = await importRuns.getById(req.context, castToInteger(req.params.listId), castToInteger(req.params.importId), castToInteger(req.params.runId));
19 | return res.json(entity);
20 | });
21 |
22 | module.exports = router;
--------------------------------------------------------------------------------
/server/routes/rest/namespaces.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 | const namespaces = require('../../models/namespaces');
5 |
6 | const router = require('../../lib/router-async').create();
7 | const {castToInteger} = require('../../lib/helpers');
8 |
9 |
10 | router.getAsync('/namespaces/:nsId', passport.loggedIn, async (req, res) => {
11 | const ns = await namespaces.getById(req.context, castToInteger(req.params.nsId));
12 |
13 | ns.hash = namespaces.hash(ns);
14 |
15 | return res.json(ns);
16 | });
17 |
18 | router.postAsync('/namespaces', passport.loggedIn, passport.csrfProtection, async (req, res) => {
19 | return res.json(await namespaces.create(req.context, req.body));
20 | });
21 |
22 | router.putAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
23 | const ns = req.body;
24 | ns.id = castToInteger(req.params.nsId);
25 |
26 | await namespaces.updateWithConsistencyCheck(req.context, ns);
27 | return res.json();
28 | });
29 |
30 | router.deleteAsync('/namespaces/:nsId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
31 | await namespaces.remove(req.context, castToInteger(req.params.nsId));
32 | return res.json();
33 | });
34 |
35 | router.getAsync('/namespaces-tree', passport.loggedIn, async (req, res) => {
36 |
37 | const tree = await namespaces.listTree(req.context);
38 |
39 | return res.json(tree);
40 | });
41 |
42 |
43 | module.exports = router;
44 |
--------------------------------------------------------------------------------
/server/routes/rest/settings.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const passport = require('../../lib/passport');
4 | const settings = require('../../models/settings');
5 |
6 | const router = require('../../lib/router-async').create();
7 |
8 |
9 | router.getAsync('/settings', passport.loggedIn, async (req, res) => {
10 | const configItems = await settings.get(req.context);
11 | configItems.hash = settings.hash(configItems);
12 | return res.json(configItems);
13 | });
14 |
15 | router.putAsync('/settings', passport.loggedIn, passport.csrfProtection, async (req, res) => {
16 | const configItems = req.body;
17 | await settings.set(req.context, configItems);
18 | return res.json();
19 | });
20 |
21 |
22 | module.exports = router;
--------------------------------------------------------------------------------
/server/services/workers/reports/config/default.yaml:
--------------------------------------------------------------------------------
1 | # Process title visible in monitoring logs and process listing
2 | title: mailtrain
3 |
4 | log:
5 | # silly|verbose|info|http|warn|error|silent
6 | level: info
7 |
8 | # Default language to use
9 | defaultLanguage: en-US
10 |
11 | # Enabled languages
12 | enabledLanguages:
13 | - en-US
14 | - es-ES
15 | - de-DE
16 | - fr-FR
17 | - ru-RU
18 | - fk-FK
19 |
20 | mysql:
21 | host: localhost
22 | user: mailtrain
23 | password: mailtrain
24 | database: mailtrain
25 | # Some installations, eg. MAMP can use a different port (8889)
26 | # MAMP users should also turn on Allow network access to MySQL otherwise MySQL might not be accessible
27 | port: 3306
28 | charset: utf8mb4
29 | # The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
30 | # If the MySQL server runs on the same server as Mailtrain, use 'local'
31 | timezone: local
32 |
--------------------------------------------------------------------------------
/server/setup/docker-entrypoint-db-setup.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const log = require('../lib/log');
4 | const dbcheck = require('../lib/dbcheck');
5 | const knex = require('../lib/knex');
6 | const {getAdminId} = require("../../shared/users");
7 | const bluebird = require('bluebird');
8 | const bcrypt = require('bcrypt-nodejs');
9 | const bcryptHash = bluebird.promisify(bcrypt.hash.bind(bcrypt));
10 |
11 | async function init() {
12 | const args = process.argv.slice(2);
13 |
14 | if (args.length !== 2) {
15 | log.error('Usage: NODE_ENV=production node setup/docker-entrypoint-db-setup.js ')
16 | return;
17 | }
18 |
19 | const passwd = args[0];
20 | const accessToken = args[1];
21 |
22 | await dbcheck();
23 | await knex.migrate.latest();
24 |
25 |
26 | const hashedPasswd = await bcryptHash(passwd, null, null);
27 | await knex('users').where({id: getAdminId()}).update({password: hashedPasswd});
28 |
29 | if (accessToken !== '') {
30 | await knex('users').where({id: getAdminId()}).update({access_token: accessToken});
31 | }
32 |
33 | process.exit(0);
34 | }
35 |
36 | init().catch(err => {log.error('', err); process.exit(1); });
37 |
38 |
39 |
--------------------------------------------------------------------------------
/server/setup/fakedata.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let faker = require('faker');
4 | let accounts = 1000 * 1000;
5 |
6 | let row = 0;
7 | let getNext = () => {
8 |
9 | let firstName = faker.name.firstName(); // Rowan Nikolaus
10 | let lastName = faker.name.lastName(); // Rowan Nikolaus
11 | let email = faker.internet.email(firstName, lastName); // Kassandra.Haley@erich.biz
12 |
13 | let subscriber = {
14 | firstName,
15 | lastName,
16 | email,
17 | company: faker.company.companyName(),
18 | phone: faker.phone.phoneNumber()
19 | };
20 |
21 | process.stdout.write('\n' + Object.keys(subscriber).map(key => JSON.stringify(subscriber[key])).join(','));
22 | if (++row < accounts) {
23 | setImmediate(getNext);
24 | }
25 | };
26 |
27 | process.stdout.write('First name,Last name,E-Mail,Company,Phone number');
28 | getNext();
29 |
--------------------------------------------------------------------------------
/server/setup/knex/config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (!process.env.NODE_CONFIG_DIR) {
4 | process.env.NODE_CONFIG_DIR = __dirname + '/../../config';
5 | }
6 |
7 | const config = require('../../lib/config');
8 |
9 | module.exports = config;
10 |
--------------------------------------------------------------------------------
/server/setup/knex/knexfile.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let config = require('./config');
4 |
5 | config.mysql.charset="utf8mb4";
6 | config.mysql.multipleStatements=true;
7 |
8 |
9 | module.exports = {
10 | client: 'mysql',
11 | connection: config.mysql
12 | };
13 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20181226090000_verp_header_options_in_send_configurations.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | await knex.schema.table('send_configurations', table => {
3 | table.boolean('verp_disable_sender_header').defaultTo(false);
4 | });
5 | })();
6 |
7 | exports.down = (knex, Promise) => (async() => {
8 | })();
9 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190422084800_file_cache.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | await knex.schema.raw('CREATE TABLE `file_cache` (\n' +
3 | ' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
4 | ' `type` varchar(255) NOT NULL,\n' +
5 | ' `key` varchar(255) NOT NULL,\n' +
6 | ' `mimetype` varchar(255) DEFAULT NULL,\n' +
7 | ' `size` int(11) DEFAULT NULL,\n' +
8 | ' `created` timestamp NOT NULL DEFAULT current_timestamp(),\n' +
9 | ' PRIMARY KEY (`id`),\n' +
10 | ' KEY `key` (`key`(191))\n' +
11 | ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;');
12 | })();
13 |
14 | exports.down = (knex, Promise) => (async() => {
15 | })();
16 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190616000000_drop_subject_in_send_configurations.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | await knex.schema.table('send_configurations', table => {
3 | table.dropColumn('subject');
4 | table.dropColumn('subject_overridable');
5 | });
6 |
7 | await knex.schema.table('campaigns', table => {
8 | table.renameColumn('subject_override', 'subject');
9 | });
10 |
11 | await knex('campaigns').whereNull('subject').update('subject', '');
12 | })();
13 |
14 | exports.down = (knex, Promise) => (async() => {
15 | })();
16 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190629000000_add_start_at_to_campaigns.js:
--------------------------------------------------------------------------------
1 | const { CampaignType, CampaignStatus } = require('../../../../shared/campaigns');
2 |
3 | exports.up = (knex, Promise) => (async() => {
4 | await knex.schema.table('campaigns', table => {
5 | table.timestamp('start_at').nullable().defaultTo(null);
6 | });
7 |
8 | await knex('campaigns')
9 | .whereIn('type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
10 | .whereIn('status', [CampaignStatus.SCHEDULED, CampaignStatus.SENDING, CampaignStatus.PAUSING, CampaignStatus.PAUSED])
11 | .whereNotNull('scheduled')
12 | .update({
13 | start_at: knex.raw('scheduled')
14 | });
15 |
16 | await knex('campaigns')
17 | .whereIn('type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
18 | .whereIn('status', [CampaignStatus.SCHEDULED, CampaignStatus.SENDING, CampaignStatus.PAUSING, CampaignStatus.PAUSED])
19 | .whereNull('scheduled')
20 | .update({
21 | start_at: new Date()
22 | });
23 | })();
24 |
25 | exports.down = (knex, Promise) => (async() => {
26 | })();
27 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190629170000_generalization_of_queued.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | const queued = await knex('queued');
3 |
4 | for (const queuedEntry of queued) {
5 | const data = JSON.parse(queuedEntry.data);
6 |
7 | data.listId = queuedEntry.list;
8 | data.subscriptionId = queuedEntry.subscription;
9 |
10 | await knex('queued')
11 | .where('id', queuedEntry.id)
12 | .update({
13 | data: JSON.stringify(data)
14 | });
15 | }
16 |
17 | await knex.schema.table('queued', table => {
18 | table.dropColumn('list');
19 | table.dropColumn('subscription');
20 | });
21 |
22 | })();
23 |
24 | exports.down = (knex, Promise) => (async() => {
25 | })();
26 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190630210000_tag_language.js:
--------------------------------------------------------------------------------
1 | const { CampaignSource } = require('../../../../shared/campaigns');
2 | const { TagLanguages } = require('../../../../shared/templates');
3 |
4 | exports.up = (knex, Promise) => (async() => {
5 | await knex.schema.table('templates', table => {
6 | table.string('tag_language', 48);
7 | });
8 |
9 | await knex('templates').update({
10 | tag_language: 'simple'
11 | });
12 |
13 | await knex.schema.table('templates', table => {
14 | table.string('tag_language', 48).notNullable().index().alter();
15 | });
16 |
17 |
18 | await knex.schema.table('mosaico_templates', table => {
19 | table.string('tag_language', 48);
20 | });
21 |
22 | await knex('mosaico_templates').update({
23 | tag_language: TagLanguages.SIMPLE
24 | });
25 |
26 | await knex.schema.table('mosaico_templates', table => {
27 | table.string('tag_language', 48).notNullable().index().alter();
28 | });
29 |
30 | const rows = await knex('campaigns').whereIn('source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN, CampaignSource.CUSTOM_FROM_TEMPLATE]);
31 | for (const row of rows) {
32 | const data = JSON.parse(row.data);
33 |
34 | data.sourceCustom.tag_language = TagLanguages.SIMPLE;
35 |
36 | await knex('campaigns').where('id', row.id).update({data: JSON.stringify(data)});
37 | }
38 | })();
39 |
40 | exports.down = (knex, Promise) => (async() => {
41 | })();
42 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190705220000_test_messages.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | await knex.schema.raw('CREATE TABLE `test_messages` (\n' +
3 | ' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
4 | ' `campaign` int(10) unsigned NOT NULL,\n' +
5 | ' `list` int(10) unsigned NOT NULL,\n' +
6 | ' `subscription` int(10) unsigned NOT NULL,\n' +
7 | ' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
8 | ' PRIMARY KEY (`id`),\n' +
9 | ' UNIQUE KEY `cls` (`campaign`, `list`, `subscription`)\n' +
10 | ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
11 | })();
12 |
13 | exports.down = (knex, Promise) => (async() => {
14 | })();
15 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190722110000_hash_email.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | await knex.schema.raw('ALTER TABLE `campaign_messages` ADD `hash_email` char(88) CHARACTER SET ascii');
3 | await knex.schema.raw('ALTER TABLE `campaign_messages` ADD UNIQUE KEY `campaign_hash_email` (`campaign`, `hash_email`)');
4 | await knex.schema.raw('ALTER TABLE `campaign_messages` DROP KEY `created`');
5 | await knex.schema.raw('ALTER TABLE `campaign_links` DROP KEY `created_index`');
6 |
7 | const lists = await knex('lists');
8 | for (const list of lists) {
9 | await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` MODIFY `hash_email` char(88) CHARACTER SET ascii');
10 | await knex.raw('update `campaign_messages` inner join `subscription__' + list.id + '` on `campaign_messages`.`list`=' + list.id + ' and `campaign_messages`.`subscription`=`subscription__' + list.id + '`.`id` set `campaign_messages`.`hash_email`=`subscription__' + list.id + '`.`hash_email`');
11 | }
12 |
13 | await knex('campaign_messages').whereNull('hash_email').del();
14 | })();
15 |
16 | exports.down = (knex, Promise) => (async() => {
17 | })();
18 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20190722150000_ensure_help_column_in_custom_fields.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | // This is to provide upgrade path to stable to those that already have beta installed.
3 | try {
4 | await knex.schema.raw('ALTER TABLE `custom_fields` ADD COLUMN `help` text AFTER `name`');
5 | } catch (err) {
6 | if (err.code === 'ER_DUP_FIELDNAME') {
7 | // The field is already there, so we can ignore this error
8 | } else {
9 | throw err;
10 | }
11 | }
12 | })();
13 |
14 | exports.down = (knex, Promise) => (async() => {
15 | })();
16 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20191007120000_add_updated_to_subscriptions.js:
--------------------------------------------------------------------------------
1 | exports.up = (knex, Promise) => (async() => {
2 | const lists = await knex('lists');
3 | for (const list of lists) {
4 | await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` ADD COLUMN `updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `created`');
5 | await knex.schema.raw('CREATE INDEX updated ON `subscription__' + list.id + '` (`updated`)');
6 | }
7 | })();
8 |
9 | exports.down = (knex, Promise) => (async() => {
10 | })();
11 |
--------------------------------------------------------------------------------
/server/setup/knex/migrations/20200824160149_convert_to_utf8mb4.js:
--------------------------------------------------------------------------------
1 |
2 | exports.up = function(knex, Promise) {
3 | return knex.raw('SELECT table_name FROM information_schema.tables WHERE table_schema = ?', [knex.client.database()])
4 | .then(function(tablas) {
5 | let sql="";
6 | tablas=tablas[0];
7 | for(let i=0; i (async() => {
2 | await knex.schema.table('custom_fields', function(t) {
3 | t.boolean('required').notNull().defaultTo(0).after('default_value');
4 | });
5 | })();
6 |
7 | exports.down = (knex, Promise) => (async() => {
8 | })();
9 |
--------------------------------------------------------------------------------
/server/setup/sql/drop.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | mysqldump -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "-p${MYSQL_PASSWORD}" --add-drop-table --no-data "$MYSQL_DB" | grep -e '^DROP \| FOREIGN_KEY_CHECKS' | mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "-p${MYSQL_PASSWORD}" "$MYSQL_DB"
4 |
--------------------------------------------------------------------------------
/server/setup/sql/dump.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | if (process.env.NODE_ENV === 'production') {
4 | console.log('This script does not run in production'); // eslint-disable-line no-console
5 | process.exit(1);
6 | }
7 |
8 | let config = require('config');
9 | let spawn = require('child_process').spawn;
10 | let log = require('npmlog');
11 |
12 | log.level = 'verbose';
13 |
14 | function createDump(callback) {
15 | let cmd = spawn('mysqldump', ['-h', config.mysql.host || 'localhost', '-P', config.mysql.port || 3306, '-u', config.mysql.user, '-p' + config.mysql.password, '--skip-opt', '--quick', '--compact', '--complete-insert', '--create-options', '--tz-utc', '--no-set-names', '--skip-set-charset', '--skip-comments', config.mysql.database]);
16 |
17 | process.stdout.write('SET UNIQUE_CHECKS=0;\nSET FOREIGN_KEY_CHECKS=0;\n\n');
18 |
19 | cmd.stdout.pipe(process.stdout);
20 | cmd.stderr.pipe(process.stderr);
21 |
22 | cmd.on('close', code => {
23 | if (code) {
24 | return callback(new Error('mysqldump command exited with code ' + code));
25 | }
26 |
27 | process.stdout.write('\nSET UNIQUE_CHECKS=1;\nSET FOREIGN_KEY_CHECKS=1;\n');
28 |
29 | return callback(null, true);
30 | });
31 | }
32 |
33 | createDump(err => {
34 | if (err) {
35 | log.error('sqldump', err);
36 | process.exit(1);
37 | }
38 | log.info('sqldump', 'MySQL Dump Completed');
39 | process.exit(0);
40 | });
41 |
--------------------------------------------------------------------------------
/server/setup/sql/init.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let dbcheck = require('../../lib/dbcheck');
4 | let log = require('npmlog');
5 | let path = require('path');
6 | let fs = require('fs');
7 |
8 | log.level = 'verbose';
9 |
10 | if (process.env.NODE_ENV === 'production') {
11 | log.error('sqlinit', 'This script does not run in production');
12 | process.exit(1);
13 | }
14 |
15 | if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.yaml'))) {
16 | log.error('sqlinit', 'This script only runs in test if config/test.yaml (i.e. a dedicated test database) is present');
17 | process.exit(1);
18 | }
19 |
20 | dbcheck(err => {
21 | if (err) {
22 | log.error('DB', err);
23 | return process.exit(1);
24 | }
25 | return process.exit(0);
26 | });
27 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00001.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '1';
4 |
5 | # Upgrade script section
6 | CREATE TABLE `import_failed` (
7 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
8 | `import` int(11) unsigned NOT NULL,
9 | `email` varchar(255) NOT NULL DEFAULT '',
10 | `reason` varchar(255) DEFAULT NULL,
11 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
12 | PRIMARY KEY (`id`),
13 | KEY `import` (`import`),
14 | CONSTRAINT `import_failed_ibfk_1` FOREIGN KEY (`import`) REFERENCES `importer` (`id`) ON DELETE CASCADE
15 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
16 |
17 | # Temporary additions
18 | UPDATE `settings` SET `value`='smtp-pulse.com' WHERE `key`='smtp_hostname' LIMIT 1;
19 | UPDATE `settings` SET `value`='' WHERE `key`='smtp_user' LIMIT 1;
20 | UPDATE `settings` SET `value`='' WHERE `key`='smtp_pass' LIMIT 1;
21 |
22 | # Footer section
23 | LOCK TABLES `settings` WRITE;
24 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
25 | UNLOCK TABLES;
26 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00002.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '2';
4 |
5 | # Adds new column 'failed' to importer table. Includes the count of failed addresses for an import
6 | ALTER TABLE importer ADD COLUMN `failed` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `processed`;
7 | ALTER TABLE importer ADD COLUMN `new` INT(11) UNSIGNED NOT NULL DEFAULT '0' AFTER `processed`;
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00003.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '3';
4 |
5 | # Adds new column 'scheduled' to campaigns table. Indicates when the sending should actually start
6 | ALTER TABLE `campaigns` ADD COLUMN `scheduled` timestamp NULL DEFAULT NULL AFTER `status`;
7 | CREATE INDEX schedule_index ON `campaigns` (`scheduled`);
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00004.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '4';
4 |
5 | # Adds new column 'template_url' to campaigns table
6 | # Indicates that this campaign should fetch message content from this URL
7 | ALTER TABLE `campaigns` ADD COLUMN `template_url` varchar(255) CHARACTER SET ascii DEFAULT NULL AFTER `template`;
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00005.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '5';
4 |
5 | -- {{#each tables.subscription}}
6 |
7 | # Adds new column 'tz' to subscriptions table
8 | # Indicates subscriber time zone, use UTC as default
9 | ALTER TABLE `{{this}}` ADD COLUMN `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL AFTER `opt_in_country`;
10 | CREATE INDEX subscriber_tz ON `{{this}}` (`tz`);
11 |
12 | -- {{/each}}
13 |
14 | # Footer section
15 | LOCK TABLES `settings` WRITE;
16 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
17 | UNLOCK TABLES;
18 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00006.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '6';
4 |
5 | # Creates table to store timezone offsets required to calculate correct start time for sending
6 | # messages to specific subscribers
7 | CREATE TABLE `tzoffset` (
8 | `tz` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '',
9 | `offset` int(11) NOT NULL DEFAULT '0',
10 | PRIMARY KEY (`tz`)
11 | ) ENGINE=InnoDB DEFAULT CHARSET=ascii;
12 |
13 | # Footer section
14 | LOCK TABLES `settings` WRITE;
15 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
16 | UNLOCK TABLES;
17 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00007.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '7';
4 |
5 | # Rename template_url to source_url in order to use this field for different kind of urls, eg. for RSS url
6 | ALTER TABLE `campaigns` CHANGE COLUMN `template_url` `source_url` varchar(255) CHARACTER SET ascii DEFAULT NULL;
7 | # Add new column type that defines what kind of campaign is it. A normal campaign, (1), RSS (2) or drip (3)
8 | ALTER TABLE `campaigns` ADD COLUMN `type` tinyint(4) unsigned NOT NULL DEFAULT '1' AFTER `cid`;
9 | CREATE INDEX type_index ON `campaigns` (`type`);
10 |
11 | # Footer section
12 | LOCK TABLES `settings` WRITE;
13 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
14 | UNLOCK TABLES;
15 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00008.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '8';
4 |
5 | # Create new table to store RSS entries for RSS campaigns
6 | CREATE TABLE `rss` (
7 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
8 | `parent` int(11) unsigned NOT NULL,
9 | `guid` varchar(255) NOT NULL DEFAULT '',
10 | `pubdate` timestamp NULL DEFAULT NULL,
11 | `campaign` int(11) unsigned DEFAULT NULL,
12 | `found` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
13 | PRIMARY KEY (`id`),
14 | UNIQUE KEY `parent_2` (`parent`,`guid`),
15 | KEY `parent` (`parent`),
16 | CONSTRAINT `rss_ibfk_1` FOREIGN KEY (`parent`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
17 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
18 | ALTER TABLE `campaigns` ADD COLUMN `parent` int(11) unsigned DEFAULT NULL AFTER `type`;
19 | CREATE INDEX parent_index ON `campaigns` (`parent`);
20 | ALTER TABLE `campaigns` ADD COLUMN `last_check` timestamp NULL DEFAULT NULL AFTER `source_url`;
21 | ALTER TABLE `campaigns` ADD COLUMN `check_status` varchar(255) NULL DEFAULT NULL AFTER `last_check`;
22 | CREATE INDEX check_index ON `campaigns` (`last_check`);
23 | ALTER TABLE `campaigns` ADD COLUMN `html_prepared` text AFTER `html`;
24 |
25 | # Footer section
26 | LOCK TABLES `settings` WRITE;
27 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
28 | UNLOCK TABLES;
29 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00009.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '9';
4 |
5 | # Adds a column for static access tokens to be used in API authentication
6 | ALTER TABLE `users` ADD COLUMN `access_token` varchar(40) NULL DEFAULT NULL AFTER `email`;
7 | CREATE INDEX token_index ON `users` (`access_token`);
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00010.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '10';
4 |
5 | -- {{#each tables.campaign_tracker}}
6 |
7 | # Adds new column 'created' to campaign tracker table
8 | # Indicates when a subscriber first clicked a link or opened the message
9 | ALTER TABLE `{{this}}` ADD COLUMN `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `count`;
10 | CREATE INDEX created_index ON `{{this}}` (`created`);
11 |
12 | -- {{/each}}
13 |
14 | # Footer section
15 | LOCK TABLES `settings` WRITE;
16 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
17 | UNLOCK TABLES;
18 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00011.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '11';
4 |
5 | -- {{#each tables.campaign}}
6 |
7 | # Adds new index for 'status' on campaign messages table
8 | CREATE INDEX status_index ON `{{this}}` (`status`);
9 |
10 | -- {{/each}}
11 |
12 | # Footer section
13 | LOCK TABLES `settings` WRITE;
14 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
15 | UNLOCK TABLES;
16 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00012.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '12';
4 |
5 | # Message source could include inlined images which might overflow on the default 65k field length
6 | ALTER TABLE `campaigns` MODIFY `html` LONGTEXT;
7 | ALTER TABLE `campaigns` MODIFY `html_prepared` LONGTEXT;
8 | ALTER TABLE `campaigns` MODIFY `text` LONGTEXT;
9 |
10 | ALTER TABLE `templates` MODIFY `html` LONGTEXT;
11 | ALTER TABLE `templates` MODIFY `text` LONGTEXT;
12 |
13 | # Footer section
14 | LOCK TABLES `settings` WRITE;
15 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
16 | UNLOCK TABLES;
17 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00013.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '13';
4 |
5 | -- {{#each tables.campaign}}
6 |
7 | # Adds separate index for 'subscription' on campaign messages table
8 | CREATE INDEX subscription_index ON `{{this}}` (`subscription`);
9 |
10 | -- {{/each}}
11 |
12 | # Footer section
13 | LOCK TABLES `settings` WRITE;
14 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
15 | UNLOCK TABLES;
16 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00014.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '14';
4 |
5 | -- {{#each tables.subscription}}
6 |
7 | # Adds new column 'tz' to subscriptions table
8 | # Indicates subscriber time zone, use UTC as default
9 | ALTER TABLE `{{this}}` ADD COLUMN `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0' AFTER `status`;
10 | CREATE INDEX is_test ON `{{this}}` (`is_test`);
11 |
12 | -- {{/each}}
13 |
14 | # Footer section
15 | LOCK TABLES `settings` WRITE;
16 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
17 | UNLOCK TABLES;
18 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00016.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '16';
4 |
5 | ALTER TABLE `triggers` ADD COLUMN `count` int(11) unsigned NOT NULL DEFAULT '0' AFTER `dest_campaign`;
6 |
7 | # Footer section
8 | LOCK TABLES `settings` WRITE;
9 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
10 | UNLOCK TABLES;
11 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00017.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '17';
4 |
5 | # Add template field for group elements
6 | ALTER TABLE `custom_fields` ADD COLUMN `group_template` text AFTER `group`;
7 |
8 | # Footer section
9 | LOCK TABLES `settings` WRITE;
10 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
11 | UNLOCK TABLES;
12 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00018.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '18';
4 |
5 | # Add template field for group elements
6 | ALTER TABLE `campaigns` ADD COLUMN `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0' AFTER `status`;
7 | ALTER TABLE `confirmations` ADD COLUMN `opt_in_ip` varchar(100) DEFAULT NULL AFTER `email`;
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00019.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '19';
4 |
5 | CREATE TABLE `attachments` (
6 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
7 | `campaign` int(11) unsigned NOT NULL,
8 | `filename` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
9 | `content_type` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '',
10 | `content` longblob,
11 | `size` int(11) NOT NULL,
12 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
13 | PRIMARY KEY (`id`),
14 | KEY `campaign` (`campaign`),
15 | CONSTRAINT `attachments_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
17 |
18 | # Footer section
19 | LOCK TABLES `settings` WRITE;
20 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
21 | UNLOCK TABLES;
22 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00020.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '20';
4 |
5 | # Add reply_to field
6 | ALTER TABLE `campaigns` ADD COLUMN `reply_to` varchar(255) DEFAULT '' AFTER `address`;
7 |
8 | # Footer section
9 | LOCK TABLES `settings` WRITE;
10 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
11 | UNLOCK TABLES;
12 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00021.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '21';
4 |
5 | # Add fields editor_name, editor_data to templates
6 | ALTER TABLE `templates` ADD COLUMN `editor_name` varchar(50) DEFAULT '' AFTER `description`;
7 | ALTER TABLE `templates` ADD COLUMN `editor_data` longtext AFTER `editor_name`;
8 |
9 | # Add fields editor_name, editor_data to campaigns
10 | ALTER TABLE `campaigns` ADD COLUMN `editor_name` varchar(50) DEFAULT '' AFTER `source_url`;
11 | ALTER TABLE `campaigns` ADD COLUMN `editor_data` longtext AFTER `editor_name`;
12 |
13 | # Footer section
14 | LOCK TABLES `settings` WRITE;
15 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
16 | UNLOCK TABLES;
17 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00023.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '23';
4 |
5 | # Add field device_type to campaign_tracker
6 |
7 | # Create ALTER TABLE PROCEDURE
8 | DROP PROCEDURE IF EXISTS `alterbyregexp`;
9 | CREATE PROCEDURE `alterbyregexp` (`table_regexp` VARCHAR(255), `altertext` VARCHAR(255))
10 | BEGIN
11 | DECLARE done INT DEFAULT FALSE;
12 | DECLARE tbl VARCHAR(255);
13 | DECLARE curs CURSOR FOR SELECT table_name FROM information_schema.tables WHERE table_schema = (SELECT DATABASE()) and table_name like table_regexp;
14 | DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
15 | OPEN curs;
16 |
17 | read_loop: LOOP
18 | FETCH curs INTO tbl;
19 | IF done THEN
20 | LEAVE read_loop;
21 | END IF;
22 | SET @query = CONCAT('ALTER TABLE `', tbl, '`' , altertext);
23 | PREPARE stmt FROM @query;
24 | EXECUTE stmt;
25 | DEALLOCATE PREPARE stmt;
26 | END LOOP;
27 | CLOSE curs;
28 | END;
29 |
30 | # Add field device_type to campaign_tracker
31 | CALL alterbyregexp('campaign\_tracker%', 'ADD COLUMN `device_type` varchar(50) DEFAULT NULL AFTER `ip`');
32 | DROP PROCEDURE IF EXISTS `alterbyregexp`;
33 |
34 | # Footer section
35 | LOCK TABLES `settings` WRITE;
36 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
37 | UNLOCK TABLES;
38 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00024.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '24';
4 |
5 | # Add field
6 | ALTER TABLE `importer` ADD COLUMN `emailcheck` tinyint(4) unsigned DEFAULT 1 NOT NULL AFTER `delimiter`;
7 |
8 | # Footer section
9 | LOCK TABLES `settings` WRITE;
10 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
11 | UNLOCK TABLES;
12 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00025.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '25';
4 |
5 | # Create table to store global blacklist
6 | CREATE TABLE `blacklist` (
7 | `email` varchar(191) NOT NULL,
8 | PRIMARY KEY (`email`)
9 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
10 |
11 | #Alter table campaigns
12 | ALTER TABLE `campaigns` ADD COLUMN `blacklisted` int(11) unsigned NOT NULL DEFAULT '0' AFTER `delivered`;
13 |
14 | # Footer section
15 | LOCK TABLES `settings` WRITE;
16 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
17 | UNLOCK TABLES;
18 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00026.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '26';
4 |
5 | # Add field
6 | ALTER TABLE `lists` ADD COLUMN `public_subscribe` tinyint(1) unsigned DEFAULT 1 NOT NULL AFTER `created`;
7 |
8 | # Footer section
9 | LOCK TABLES `settings` WRITE;
10 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
11 | UNLOCK TABLES;
12 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00027.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '27';
4 |
5 | # Create table to report templates
6 | CREATE TABLE `report_templates` (
7 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
8 | `name` varchar(255) DEFAULT '',
9 | `mime_type` varchar(255) DEFAULT 'text/html' NOT NULL,
10 | `description` text,
11 | `user_fields` longtext,
12 | `js` longtext,
13 | `hbs` longtext,
14 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
15 | PRIMARY KEY (`id`)
16 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
17 |
18 | # Create table to store reports
19 | CREATE TABLE `reports` (
20 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
21 | `name` varchar(255) DEFAULT '',
22 | `description` text,
23 | `report_template` int(11) unsigned NOT NULL,
24 | `params` longtext,
25 | `state` int(11) unsigned NOT NULL DEFAULT 0,
26 | `last_run` DATETIME DEFAULT NULL,
27 | `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
28 | PRIMARY KEY (`id`),
29 | KEY `report_template` (`report_template`),
30 | CONSTRAINT `report_template_ibfk_1` FOREIGN KEY (`report_template`) REFERENCES `report_templates` (`id`) ON DELETE CASCADE
31 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
32 |
33 |
34 | # Footer section
35 | LOCK TABLES `settings` WRITE;
36 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
37 | UNLOCK TABLES;
38 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00029.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '29';
4 |
5 | # Rename column tracking_disabled
6 | ALTER TABLE `campaigns` ADD COLUMN `open_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL, ADD COLUMN `click_tracking_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
7 | UPDATE `campaigns` SET `open_tracking_disabled` = `tracking_disabled`, `click_tracking_disabled` = `tracking_disabled`;
8 | ALTER TABLE `campaigns` DROP COLUMN `tracking_disabled`;
9 |
10 | # Footer section
11 | LOCK TABLES `settings` WRITE;
12 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
13 | UNLOCK TABLES;
14 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00030.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '30';
4 |
5 | # Upgrade script section
6 | #### INSERT YOUR UPGRADE SCRIPT BELOW THIS LINE ######
7 |
8 | ALTER TABLE `lists` ADD COLUMN `listunsubscribe_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
9 |
10 | #### INSERT YOUR UPGRADE SCRIPT ABOVE THIS LINE ######
11 |
12 | # Footer section. Updates schema version in settings
13 | LOCK TABLES `settings` WRITE;
14 | /*!40000 ALTER TABLE `settings` DISABLE KEYS */;
15 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
16 | /*!40000 ALTER TABLE `settings` ENABLE KEYS */;
17 | UNLOCK TABLES;
18 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00031.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '31';
4 |
5 | # Add segment support for triggers
6 | ALTER TABLE `triggers` ADD `segment` INT(11) UNSIGNED NOT NULL AFTER `list`;
7 | ALTER TABLE `trigger` ADD `segment` INT(11) UNSIGNED NOT NULL AFTER `list`;
8 |
9 | # Footer section
10 | LOCK TABLES `settings` WRITE;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | UNLOCK TABLES;
13 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00032.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '32';
4 |
5 | # Set default X-Mailer header value
6 | LOCK TABLES `settings` WRITE;
7 | INSERT INTO `settings` (`key`, `value`) VALUES ('x_mailer','Mailtrain Mailer (+https://mailtrain.org)') ON DUPLICATE KEY UPDATE `value`='Mailtrain Mailer (+https://mailtrain.org)';
8 | UNLOCK TABLES;
9 |
10 | # Footer section. Updates schema version in settings
11 | LOCK TABLES `settings` WRITE;
12 | /*!40000 ALTER TABLE `settings` DISABLE KEYS */;
13 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
14 | /*!40000 ALTER TABLE `settings` ENABLE KEYS */;
15 | UNLOCK TABLES;
16 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00033.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '33';
4 |
5 | # Adds new column 'unsubscribe' to campaign table.
6 | ALTER TABLE campaigns ADD COLUMN `unsubscribe` VARCHAR(255) NOT NULL DEFAULT '' AFTER `subject`;
7 |
8 | # Footer section. Updates schema version in settings
9 | LOCK TABLES `settings` WRITE;
10 | /*!40000 ALTER TABLE `settings` DISABLE KEYS */;
11 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
12 | /*!40000 ALTER TABLE `settings` ENABLE KEYS */;
13 | UNLOCK TABLES;
14 |
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-00034.sql:
--------------------------------------------------------------------------------
1 | # Header section
2 | # Define incrementing schema version number
3 | SET @schema_version = '34';
4 |
5 | # Add template field for group elements
6 | ALTER TABLE `custom_fields` ADD COLUMN `description` text AFTER `name`;
7 |
8 | # Footer section
9 | LOCK TABLES `settings` WRITE;
10 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
11 | UNLOCK TABLES;
--------------------------------------------------------------------------------
/server/setup/sql/upgrade-template.sql:
--------------------------------------------------------------------------------
1 | # This file is a handlebars template
2 | # To modify several similar tables at once use (replace [] with {}):
3 | # [[#each tables.tablename]] ALTER TABLE `[[this]]` ... [[/each]]
4 | # NB! as this is a handlebars file, then remember to escape any template sequences
5 |
6 | # Header section
7 | # Define incrementing schema version number
8 | SET @schema_version = 'XXX';
9 |
10 | # Upgrade script section
11 | #### INSERT YOUR UPGRADE SCRIPT BELOW THIS LINE ######
12 |
13 |
14 | #### INSERT YOUR UPGRADE SCRIPT ABOVE THIS LINE ######
15 |
16 | # Footer section. Updates schema version in settings
17 | LOCK TABLES `settings` WRITE;
18 | /*!40000 ALTER TABLE `settings` DISABLE KEYS */;
19 | INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
20 | /*!40000 ALTER TABLE `settings` ENABLE KEYS */;
21 | UNLOCK TABLES;
22 |
--------------------------------------------------------------------------------
/server/test/e2e/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "strict": 0,
5 | "no-console": 0,
6 | "comma-dangle": 0,
7 | "arrow-body-style": 0,
8 | "no-await-in-loop": 0
9 | },
10 | "env": {
11 | "mocha": true
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/server/test/e2e/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | require('./lib/exit-unless-test');
4 | const mocha = require('./lib/mocha-e2e').mocha;
5 | const path = require('path');
6 |
7 | const only = 'only';
8 | const skip = 'skip';
9 |
10 | let tests = [
11 | //'login',
12 | 'subscription'
13 | ];
14 |
15 | tests = tests.map(testSpec => (testSpec.constructor === Array ? testSpec : [testSpec]));
16 | tests = tests.filter(testSpec => testSpec[1] !== skip);
17 | if (tests.some(testSpec => testSpec[1] === only)) {
18 | tests = tests.filter(testSpec => testSpec[1] === only);
19 | }
20 |
21 | for (const testSpec of tests) {
22 | const testPath = path.join(__dirname, 'tests', testSpec[0] + '.js');
23 | mocha.addFile(testPath);
24 | }
25 |
26 | mocha.run(failures => {
27 | process.exit(failures); // exit with non-zero status if there were failures
28 | });
29 |
--------------------------------------------------------------------------------
/server/test/e2e/install.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # This installation script works on Ubuntu 14.04 and 16.04
4 | # Run as root!
5 |
6 | if [[ $EUID -ne 0 ]]; then
7 | echo "This script must be run as root" 1>&2
8 | exit 1
9 | fi
10 |
11 | set -e
12 |
13 | export DEBIAN_FRONTEND=noninteractive
14 |
15 | MYSQL_PASSWORD=`pwgen 12 -1`
16 |
17 | # Setup MySQL user for Mailtrain Tests
18 | mysql -u root -e "CREATE USER 'mailtrain_test'@'localhost' IDENTIFIED BY '$MYSQL_PASSWORD';"
19 | mysql -u root -e "GRANT ALL PRIVILEGES ON mailtrain_test.* TO 'mailtrain_test'@'localhost';"
20 | mysql -u mailtrain_test --password="$MYSQL_PASSWORD" -e "CREATE database mailtrain_test;"
21 |
22 | # Setup installation configuration
23 | cat >> config/test.toml < page({
8 |
9 | async fetchMail(address) {
10 | await driver.sleep(1000);
11 | await driver.navigate().to(`${config.mailUrl}/${address}`);
12 | await this.waitUntilVisible();
13 | },
14 |
15 | async ensureUrl() {
16 | throw new Error('Unsupported method.');
17 | }
18 |
19 | }, ...extras);
20 |
--------------------------------------------------------------------------------
/server/test/e2e/lib/worker-counter.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class WorkerCounter {
4 | constructor() {
5 | this.counter = 0;
6 | }
7 |
8 | enter() {
9 | this.counter++;
10 | }
11 |
12 | exit() {
13 | this.counter--;
14 | }
15 |
16 | async waitForEmpty() {
17 | const self = this;
18 |
19 | function wait(resolve) {
20 | if (self.counter === 0) {
21 | resolve();
22 | } else {
23 | setTimeout(wait, 500, resolve);
24 | }
25 | }
26 |
27 | return new Promise(resolve => {
28 | setTimeout(wait, 500, resolve);
29 | });
30 | }
31 | }
32 |
33 | module.exports = WorkerCounter;
34 |
--------------------------------------------------------------------------------
/server/test/e2e/page-objects/home.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const config = require('../lib/config');
4 | const web = require('../lib/web');
5 |
6 | module.exports = web({
7 | baseUrl: config.baseTrustedUrl,
8 | url: '/'
9 | });
10 |
--------------------------------------------------------------------------------
/server/test/e2e/page-objects/user.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const config = require('../lib/config');
4 | const web = require('../lib/web');
5 |
6 | module.exports = {
7 | login: web({
8 | baseUrl: config.baseTrustedUrl,
9 | url: '/users/login',
10 | elementsToWaitFor: ['submitButton'],
11 | elements: {
12 | usernameInput: 'form[action="/login"] input[name="username"]',
13 | passwordInput: 'form[action="/login"] input[name="password"]',
14 | submitButton: 'form[action="/login"] [type=submit]'
15 | }
16 | }),
17 |
18 | logout: web({
19 | baseUrl: config.baseTrustedUrl,
20 | requestUrl: '/users/logout',
21 | url: '/'
22 | }),
23 |
24 | account: web({
25 | baseUrl: config.baseTrustedUrl,
26 | url: '/users/account',
27 | elementsToWaitFor: ['form'],
28 | elements: {
29 | form: 'form[action="/users/account"]',
30 | emailInput: 'form[action="/users/account"] input[name="email"]'
31 | }
32 | })
33 | };
34 |
--------------------------------------------------------------------------------
/server/views/archive/layout-raw.hbs:
--------------------------------------------------------------------------------
1 | {{{body}}}
2 |
--------------------------------------------------------------------------------
/server/views/archive/layout-wrapped.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Mailtrain
9 |
10 |
11 |
12 | {{{body}}}
13 | {{> tracking_scripts}}
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/server/views/archive/view.hbs:
--------------------------------------------------------------------------------
1 | {{{message}}}
2 |
--------------------------------------------------------------------------------
/server/views/ckeditor/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Mailtrain
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{#if mailtrainConfig}}
23 |
27 |
28 | {{#each scriptFiles}}
29 |
30 | {{/each}}
31 | {{/if}}
32 |
33 |
34 |
35 | {{{body}}}
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/server/views/ckeditor/root.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/views/codeeditor/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Mailtrain
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {{#if mailtrainConfig}}
23 |
27 |
28 | {{#each scriptFiles}}
29 |
30 | {{/each}}
31 | {{/if}}
32 |
33 |
34 |
35 | {{{body}}}
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/server/views/codeeditor/root.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/views/error.hbs:
--------------------------------------------------------------------------------
1 | {{message}}
2 |
3 | {{error.status}}
4 | {{#if error.stack}}
5 | {{error.stack}}
6 | {{/if}}
7 |
--------------------------------------------------------------------------------
/server/views/grapesjs/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Mailtrain
14 |
15 |
16 |
17 | {{#if mailtrainConfig}}
18 |
22 |
23 | {{#each scriptFiles}}
24 |
25 | {{/each}}
26 | {{/if}}
27 |
28 |
29 |
30 | {{{body}}}
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/server/views/grapesjs/root.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/views/mosaico/root.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/views/partials/tracking-scripts.hbs:
--------------------------------------------------------------------------------
1 | {{#if uaCode}}
2 |
18 | {{/if}}
19 |
--------------------------------------------------------------------------------
/server/views/root.hbs:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/server/views/subscription/capture-flash-messages.hbs:
--------------------------------------------------------------------------------
1 | {{flash_messages}}
2 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-already-subscribed-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}emailAddressAlreadyRegistered{{/translate}}
5 |
6 |
7 | {{#translate}}weHaveReceivedASubscriptionRequestYour{{/translate}}
8 |
9 |
10 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply{{/translate}}
11 |
12 |
13 | {{#translate}}ifYouWantToModifyYourSubscriptionThenYou{{/translate}}
14 | {{#translate}}manageYourPreferences{{/translate}} {{#translate}}or{{/translate}} {{#translate}}unsubscribeHere{{/translate}}.
15 |
16 |
17 | {{#translate}}returnToOurWebsite{{/translate}}
18 |
19 |
20 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
21 |
{{contactAddress}}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-already-subscribed-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}emailAddressAlreadyRegistered{{/translate}}
3 | ================================
4 |
5 | {{#translate}}weHaveReceivedASubscriptionRequestYour{{/translate}}
6 |
7 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply{{/translate}}
8 |
9 | {{#translate}}ifYouWantToModifyYourSubscriptionThenYou{{/translate}}
10 |
11 | {{#translate}}manageYourPreferences{{/translate}}: {{preferencesUrl}}
12 |
13 | - {{#translate}}or{{/translate}} -
14 |
15 | {{#translate}}unsubscribeHere{{/translate}}: {{unsubscribeUrl}}
16 |
17 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
18 |
19 | {{{contactAddress}}}
20 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-address-change-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}pleaseConfirmSubscriptionAddressChange{{/translate}}
5 |
6 |
7 | {{#translate}}yesSubscribeMeToThisList{{/translate}}
8 |
9 |
10 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-1{{/translate}}
11 |
12 |
13 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
14 |
{{contactAddress}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-address-change-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}pleaseConfirmSubscriptionAddressChange{{/translate}}
3 | ==========================================
4 |
5 | {{#translate}}yesSubscribeThisEmailAddressToTheList{{/translate}}: {{{confirmUrl}}}
6 |
7 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-1{{/translate}}
8 |
9 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
10 | {{{contactAddress}}}
11 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-subscription-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}pleaseConfirmSubscription{{/translate}}
5 |
6 |
7 | {{#translate}}yesSubscribeMeToThisList{{/translate}}
8 |
9 |
10 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-1{{/translate}}
11 |
12 |
13 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
14 |
{{contactAddress}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-subscription-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}pleaseConfirmSubscription{{/translate}}
3 | ===========================
4 |
5 | {{#translate}}yesSubscribeMeToThisList{{/translate}}: {{{confirmUrl}}}
6 |
7 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-1{{/translate}}
8 |
9 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}:
10 | {{{contactAddress}}}
11 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-unsubscription-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}pleaseConfirmUnsubscription{{/translate}}
5 |
6 |
7 | {{#translate}}yesUnsubscribeMe{{/translate}}
8 |
9 |
10 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-2{{/translate}}
11 |
12 |
13 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}:
14 |
{{contactAddress}}
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-confirm-unsubscription-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}pleaseConfirmUnsubscription{{/translate}}
3 | ===========================
4 |
5 | {{#translate}}yesUnsubscribeMe{{/translate}}: {{{confirmUrl}}}
6 |
7 | {{#translate}}ifYouReceivedThisEmailByMistakeSimply-2{{/translate}}
8 |
9 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}:
10 | {{{contactAddress}}}
11 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-subscription-confirmed-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}subscriptionConfirmed{{/translate}}
5 |
6 |
7 | {{#translate}}yourSubscriptionToOurListHasBeen{{/translate}} {{#translate}}ifYouWantToModifyYourSubscriptionThenYou{{/translate}}
8 | {{#translate}}manageYourPreferences{{/translate}} {{#translate}}or{{/translate}} {{#translate}}unsubscribeHere{{/translate}}.
9 |
10 |
11 | {{#translate}}returnToOurWebsite{{/translate}}
12 |
13 |
14 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
15 |
{{contactAddress}}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-subscription-confirmed-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}subscriptionConfirmed{{/translate}}
3 | ======================
4 |
5 | {{#translate}}yourSubscriptionToOurListHasBeen{{/translate}}
6 |
7 | {{#translate}}ifYouWantToModifyYourSubscriptionThenYou{{/translate}}:
8 |
9 | {{#translate}}manageYourPreferences{{/translate}}: {{preferencesUrl}}
10 |
11 | - {{#translate}}or{{/translate}} -
12 |
13 | {{#translate}}unsubscribeHere{{/translate}}: {{unsubscribeUrl}}
14 |
15 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
16 | {{{contactAddress}}}
17 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-unsubscription-confirmed-html.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}youAreNowUnsubscribed{{/translate}}
5 |
6 |
7 | ̣{{#translate}}weHaveRemovedYourEmailAddressFromOurList{{/translate}}
8 | {{#translate}}ifYouUnsubscribedByMistakeYouCan{{/translate}}:
9 |
10 |
11 | ̣{{#translate}}subscribe{{/translate}}
12 |
13 |
14 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
15 |
{{contactAddress}}
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/server/views/subscription/mail-unsubscription-confirmed-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | {{#translate}}youAreNowUnsubscribed{{/translate}}
3 | ========================
4 |
5 | ̣{{#translate}}weHaveRemovedYourEmailAddressFromOurList{{/translate}}
6 |
7 | {{#translate}}ifYouUnsubscribedByMistakeYouCan{{/translate}}
8 |
9 | {{#translate}}subscribe{{/translate}}: {{subscribeUrl}}
10 |
11 | {{#translate}}forQuestionsAboutThisListPleaseContact{{/translate}}
12 | {{{contactAddress}}}
13 |
--------------------------------------------------------------------------------
/server/views/subscription/partials/subscription-flash-messages.hbs:
--------------------------------------------------------------------------------
1 | {{{flashMessages}}}
2 |
--------------------------------------------------------------------------------
/server/views/subscription/partials/subscription-manage-address-form.hbs:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/server/views/subscription/partials/subscription-manage-form.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasPubkey}}
2 |
6 | {{/if}}
7 |
8 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/server/views/subscription/partials/subscription-subscribe-form.hbs:
--------------------------------------------------------------------------------
1 | {{#if hasPubkey}}
2 |
6 | {{/if}}
7 |
8 |
19 |
20 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/server/views/subscription/partials/subscription-unsubscribe-form.hbs:
--------------------------------------------------------------------------------
1 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-confirm-subscription-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}almostFinished{{/translate}}
5 |
6 |
7 | {{#translate}}weNeedToConfirmYourEmailAddressTo{{/translate}}
8 |
9 |
10 | {{#translate}}ifYouDontReceiveItPleaseCheckYourSpam{{/translate}}
11 |
12 |
13 | {{#translate}}returnToOurWebsite{{/translate}}
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/server/views/subscription/web-confirm-unsubscription-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Almost Finished
5 |
6 |
7 | We need to confirm your email address. To complete the unsubscription process, please click the link in the email we just sent you.
8 |
9 |
10 | {{#translate}}returnToOurWebsite{{/translate}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-manage-address.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Update Your Email Address
5 |
6 |
7 | {{> subscription_manage_address_form}}
8 |
9 |
10 | Update Email Address
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-manage.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Update Your Preferences
5 |
6 |
7 | {{> subscription_manage_form}}
8 |
9 |
10 | Unsubscribe
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-manual-unsubscribe-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Online Unsubscription Is Not Possible
5 |
6 |
7 | Please contact us at {{contactAddress}} to get removed from the list.
8 |
9 |
10 | {{#translate}}returnToOurWebsite{{/translate}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-privacy-policy-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Privacy Policy
5 |
6 |
7 | How we use your data ...
8 |
9 |
10 | {{#translate}}returnToOurWebsite{{/translate}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-subscribe.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}subscribeToList{{/translate}}
5 |
6 |
7 | {{> subscription_subscribe_form}}
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/server/views/subscription/web-subscribed-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}subscriptionConfirmed{{/translate}}
5 |
6 |
7 | {{#translate}}yourSubscriptionToOurListHasBeen{{/translate}}
8 | {{#translate}}thankYouForSubscribing!{{/translate}}
9 |
10 |
11 | {{#translate}}returnToOurWebsite{{/translate}}
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/server/views/subscription/web-unsubscribe.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Unsubscribe
5 |
6 |
7 | {{> subscription_unsubscribe_form}}
8 |
9 |
10 | Unsubscribe
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-unsubscribed-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{#translate}}unsubscriptionConfirmed{{/translate}}
5 |
6 |
7 | {{#translate}}youHaveBeenRemovedFrom{{/translate}}: {{title}}.
8 |
9 |
10 | {{#translate}}returnToOurWebsite{{/translate}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/subscription/web-updated-notice.mjml.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Profile Updated
5 |
6 |
7 | Your profile information has been updated.
8 |
9 |
10 | {{#translate}}returnToOurWebsite{{/translate}}
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/server/views/users/password-reset-html.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{title}}
6 |
7 |
8 |
9 | Change your password
10 |
11 | We have received a password change request for your Mailtrain account: {{username}}.
12 |
13 | Reset password
14 |
15 | If you did not ask to change your password, then you can ignore this email and your password will not be changed.
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/server/views/users/password-reset-text.hbs:
--------------------------------------------------------------------------------
1 | {{{title}}}
2 | Change your password
3 | ====================
4 |
5 | We have received a password change request for your Mailtrain account: ({{{username}}}).
6 |
7 | Reset password: {{{confirmUrl}}}
8 |
9 | If you did not ask to change your password, then you can ignore this email and your password will not be changed.
10 |
--------------------------------------------------------------------------------
/setup/delete-modules.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SCRIPT_PATH=$(dirname $(realpath -s $0))
6 | . $SCRIPT_PATH/functions
7 | cd $SCRIPT_PATH/..
8 |
9 | deleteAllModules
--------------------------------------------------------------------------------
/setup/install-centos7-https.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=centos7
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallHttps "$#" "$1" "$2" "$3" "$4"
--------------------------------------------------------------------------------
/setup/install-centos7-local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=centos7
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallLocal "$#"
--------------------------------------------------------------------------------
/setup/install-centos8-https.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=centos8
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallHttps "$#" "$1" "$2" "$3" "$4"
--------------------------------------------------------------------------------
/setup/install-centos8-local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=centos8
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallLocal "$#"
--------------------------------------------------------------------------------
/setup/install-debian10-https.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=debian10
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallHttps "$#" "$1" "$2" "$3" "$4"
12 |
--------------------------------------------------------------------------------
/setup/install-ubuntu1804-https.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=ubuntu1804
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallHttps "$#" "$1" "$2" "$3" "$4"
--------------------------------------------------------------------------------
/setup/install-ubuntu1804-local.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | hostType=ubuntu1804
6 |
7 | SCRIPT_PATH=$(dirname $(realpath -s $0))
8 | . $SCRIPT_PATH/functions
9 | cd $SCRIPT_PATH/..
10 |
11 | performInstallLocal "$#"
12 |
--------------------------------------------------------------------------------
/setup/reinstall-modules.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SCRIPT_PATH=$(dirname $(realpath -s $0))
6 | . $SCRIPT_PATH/functions
7 | cd $SCRIPT_PATH/..
8 |
9 | reinstallAllModules
--------------------------------------------------------------------------------
/setup/setup-test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | SCRIPT_PATH=$(dirname $(realpath -s $0))
6 | . $SCRIPT_PATH/functions
7 | cd $SCRIPT_PATH/..
8 |
9 | setupTest
--------------------------------------------------------------------------------
/shared/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const AppType = {
4 | TRUSTED: 0,
5 | SANDBOXED: 1,
6 | PUBLIC: 2
7 | };
8 |
9 | module.exports.AppType = AppType;
10 |
--------------------------------------------------------------------------------
/shared/lists.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const {TagLanguages} = require('./templates');
4 |
5 | const UnsubscriptionMode = {
6 | MIN: 0,
7 |
8 | ONE_STEP: 0,
9 | ONE_STEP_WITH_FORM: 1,
10 | TWO_STEP: 2,
11 | TWO_STEP_WITH_FORM: 3,
12 | MANUAL: 4,
13 |
14 | MAX: 4
15 | };
16 |
17 | const SubscriptionStatus = {
18 | MIN: 0,
19 |
20 | SUBSCRIBED: 1,
21 | UNSUBSCRIBED: 2,
22 | BOUNCED: 3,
23 | COMPLAINED: 4,
24 |
25 | MAX: 4
26 | };
27 |
28 | const SubscriptionSource = {
29 | ADMIN_FORM: -1,
30 | SUBSCRIPTION_FORM: -2,
31 | API: -3,
32 | NOT_IMPORTED_V1: -4,
33 | IMPORTED_V1: -5,
34 | ERASED: -6
35 | };
36 |
37 | const FieldWizard = {
38 | NONE: 'none',
39 | NAME: 'full_name',
40 | FIRST_LAST_NAME: 'first_last_name'
41 | }
42 |
43 | function getFieldColumn(field) {
44 | return field.column || 'grouped_' + field.id;
45 | }
46 |
47 | const toNameTagLangauge = TagLanguages.SIMPLE;
48 |
49 | module.exports = {
50 | UnsubscriptionMode,
51 | SubscriptionStatus,
52 | SubscriptionSource,
53 | FieldWizard,
54 | getFieldColumn,
55 | toNameTagLangauge
56 | };
--------------------------------------------------------------------------------
/shared/namespaces.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function getGlobalNamespaceId() {
4 | return 1;
5 | }
6 |
7 | module.exports = {
8 | getGlobalNamespaceId
9 | };
--------------------------------------------------------------------------------
/shared/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailtrain-shared",
3 | "version": "2.0.0",
4 | "description": "Self hosted email newsletter app - shared lib",
5 | "repository": {
6 | "type": "git",
7 | "url": "git://github.com/Mailtrain-org/mailtrain.git"
8 | },
9 | "license": "GPL-3.0",
10 | "homepage": "https://mailtrain.org/",
11 | "engines": {
12 | "node": ">=10.0.0"
13 | },
14 | "dependencies": {
15 | "moment": "^2.24.0",
16 | "moment-timezone": "^0.5.27",
17 | "owasp-password-strength-test": "github:bures/owasp-password-strength-test"
18 | },
19 | "devDependencies": {
20 | "@babel/cli": "^7.13.16",
21 | "@babel/core": "^7.14.0",
22 | "@babel/plugin-proposal-class-properties": "^7.13.0",
23 | "@babel/plugin-proposal-decorators": "^7.13.15",
24 | "@babel/plugin-proposal-function-bind": "^7.12.13",
25 | "@babel/preset-env": "^7.14.1",
26 | "@babel/preset-react": "^7.13.13",
27 | "babel-loader": "^8.2.2",
28 | "css-loader": "^2.1.0",
29 | "file-loader": "^3.0.1",
30 | "node-sass": "^4.14.1",
31 | "postcss-loader": "^3.0.0",
32 | "sass-loader": "^7.3.1",
33 | "style-loader": "^0.23.1",
34 | "url-loader": "^1.1.2",
35 | "webpack": "^4.46.0",
36 | "webpack-cli": "^3.3.12"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/shared/password-validator.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const util = require('util');
4 | const owaspPasswordStrengthTest = require('owasp-password-strength-test');
5 |
6 | function passwordValidator(t) {
7 | const config = {
8 | allowPassphrases: true,
9 | maxLength: 128,
10 | minLength: 10,
11 | minPhraseLength: 20,
12 | minOptionalTestsToPass: 4
13 | };
14 |
15 | if (t) {
16 | config.translate = {
17 | minLength: function (minLength) {
18 | return t('thePasswordMustBeAtLeastMinLength', { minLength });
19 | },
20 | maxLength: function (maxLength) {
21 | return t('thePasswordMustBeFewerThanMaxLength', { maxLength });
22 | },
23 | repeat: t('thePasswordMayNotContainSequencesOfThree'),
24 | lowercase: t('thePasswordMustContainAtLeastOne'),
25 | uppercase: t('thePasswordMustContainAtLeastOne-1'),
26 | number: t('thePasswordMustContainAtLeastOneNumber'),
27 | special: t('thePasswordMustContainAtLeastOneSpecial')
28 | }
29 | }
30 |
31 | const passwordValidator = owaspPasswordStrengthTest.create();
32 | passwordValidator.config(config);
33 |
34 | return passwordValidator;
35 | }
36 |
37 | module.exports = passwordValidator;
--------------------------------------------------------------------------------
/shared/reports.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const ReportState = {
4 | MIN: 0,
5 |
6 | SCHEDULED: 0,
7 | PROCESSING: 1,
8 | FINISHED: 2,
9 | FAILED: 3,
10 |
11 | MAX: 3
12 | };
13 |
14 | module.exports = {
15 | ReportState
16 | };
--------------------------------------------------------------------------------
/shared/send-configurations.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const MailerType = {
4 | GENERIC_SMTP: 'generic_smtp',
5 | ZONE_MTA: 'zone_mta',
6 | AWS_SES: 'aws_ses'
7 | };
8 |
9 | const ZoneMTAType = {
10 | REGULAR: 0,
11 | WITH_HTTP_CONF: 1,
12 | WITH_MAILTRAIN_HEADER_CONF: 2,
13 | BUILTIN: 3
14 | }
15 |
16 | function getSystemSendConfigurationId() {
17 | return 1;
18 | }
19 |
20 | function getSystemSendConfigurationCid() {
21 | return 'system';
22 | }
23 |
24 | module.exports = {
25 | MailerType,
26 | ZoneMTAType,
27 | getSystemSendConfigurationId,
28 | getSystemSendConfigurationCid
29 | };
--------------------------------------------------------------------------------
/shared/triggers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const Entity = {
4 | SUBSCRIPTION: 'subscription',
5 | CAMPAIGN: 'campaign'
6 | };
7 |
8 | const Event = {
9 | [Entity.SUBSCRIPTION]: {
10 | CREATED: 'created',
11 | UPDATED: 'updated',
12 | LATEST_OPEN: 'latest_open',
13 | LATEST_CLICK: 'latest_click'
14 | },
15 | [Entity.CAMPAIGN]: {
16 | DELIVERED: 'delivered',
17 | OPENED: 'opened',
18 | CLICKED: 'clicked',
19 | NOT_OPENED: 'not_opened',
20 | NOT_CLICKED: 'not_clicked'
21 | }
22 | };
23 |
24 | const EntityVals = {
25 | subscription: 'SUBSCRIPTION',
26 | campaign: 'CAMPAIGN'
27 | };
28 |
29 | const EventVals = {
30 | [Entity.SUBSCRIPTION]: {
31 | created: 'CREATED',
32 | updated: 'UPDATED',
33 | latest_open: 'LATEST_OPEN',
34 | latest_click: 'LATEST_CLICK'
35 | },
36 | [Entity.CAMPAIGN]: {
37 | delivered: 'DELIVERED',
38 | opened: 'OPENED',
39 | clicked: 'CLICKED',
40 | not_opened: 'NOT_OPENED',
41 | not_clicked: 'NOT_CLICKED'
42 | }
43 | };
44 |
45 | module.exports = {
46 | Entity,
47 | Event,
48 | EntityVals,
49 | EventVals
50 | };
51 |
--------------------------------------------------------------------------------
/shared/urls.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | const anonymousRestrictedAccessToken = 'anonymous';
4 |
5 | module.exports = {
6 | anonymousRestrictedAccessToken
7 | };
--------------------------------------------------------------------------------
/shared/users.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function getAdminId() {
4 | return 1;
5 | }
6 |
7 | module.exports = {
8 | getAdminId
9 | };
--------------------------------------------------------------------------------
/shared/validators.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | function mergeTagValid(mergeTag) {
4 | return /^[A-Z][A-Z0-9_]*$/.test(mergeTag);
5 | }
6 |
7 | module.exports = {
8 | mergeTagValid
9 | };
--------------------------------------------------------------------------------
/zone-mta/.gitignore:
--------------------------------------------------------------------------------
1 | /config/builtin-zonemta.json
2 |
--------------------------------------------------------------------------------
/zone-mta/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // start the app
4 | require('zone-mta');
5 |
--------------------------------------------------------------------------------
/zone-mta/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mailtrain-zone-mta",
3 | "private": true,
4 | "version": "1.0.0",
5 | "description": "Mailtrain builtin ZoneMTA instance",
6 | "main": "index.js",
7 | "scripts": {
8 | "start": "node index.js --config=config/zonemta.js"
9 | },
10 | "keywords": [],
11 | "license": "GPL-3.0",
12 | "homepage": "https://mailtrain.org/",
13 | "engines": {
14 | "node": ">=10.0.0"
15 | },
16 | "dependencies": {
17 | "zone-mta": "^2.3.0",
18 | "zonemta-delivery-counters": "^1.0.1",
19 | "zonemta-limiter": "^1.0.0",
20 | "zonemta-loop-breaker": "^1.0.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/zone-mta/plugins/mailtrain-main.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Set module title
4 | module.exports.title = 'Mailtrain integration (main)';
5 |
6 | // Initialize the module
7 | module.exports.init = (app, done) => {
8 |
9 | process.send({
10 | type: 'zone-mta-started'
11 | });
12 |
13 | process.on('message', msg => {
14 | if (msg === 'exit') {
15 | process.exit(); }
16 | });
17 |
18 | done();
19 | };
20 |
--------------------------------------------------------------------------------
/zone-mta/plugins/mailtrain-receiver.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // Set module title
4 | module.exports.title = 'Mailtrain integration (receiver)';
5 |
6 | // Initialize the module
7 | module.exports.init = (app, done) => {
8 |
9 | app.addHook('message:headers', (envelope, messageInfo, next) => {
10 | const headers = envelope.headers;
11 |
12 | if (!envelope.dkim.keys) {
13 | envelope.dkim.keys = [];
14 | }
15 |
16 | const dkimHeaderValue = require('libmime').decodeWords(headers.getFirst('x-mailtrain-dkim'));
17 |
18 | if (dkimHeaderValue) {
19 | const dkimKey = JSON.parse(dkimHeaderValue);
20 |
21 | envelope.dkim.keys.push(dkimKey);
22 |
23 | headers.remove('x-mailtrain-dkim');
24 | }
25 |
26 | return next();
27 | });
28 |
29 | app.addHook('smtp:auth', (auth, session, next) => {
30 | if (auth.username === app.config.username && auth.password === app.config.password) {
31 | next();
32 | } else {
33 | // do not provide any details about the failure
34 | const err = new Error('Authentication failed');
35 | err.responseCode = 535;
36 | return next(err);
37 | }
38 | });
39 |
40 | // all set up regarding this plugin
41 | done();
42 | };
43 |
--------------------------------------------------------------------------------