├── .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(''+n.getAttribute('),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 |
2 | 3 | 4 | 5 | 6 | 10 | 11 | 15 | 16 |

17 | {{#translate}}youWillReceiveAConfirmationRequestToYour{{/translate}} 18 |

19 | 20 | 21 | 22 |
23 | -------------------------------------------------------------------------------- /server/views/subscription/partials/subscription-manage-form.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasPubkey}} 2 | 6 | {{/if}} 7 | 8 |
9 | 10 | 11 | 12 | 13 | {{> subscription_custom_fields}} 14 | 15 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /server/views/subscription/partials/subscription-subscribe-form.hbs: -------------------------------------------------------------------------------- 1 | {{#if hasPubkey}} 2 | 6 | {{/if}} 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | {{> subscription_custom_fields}} 16 | 17 | 18 |
19 | 20 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /server/views/subscription/partials/subscription-unsubscribe-form.hbs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 |
10 | 11 | 12 |
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 | --------------------------------------------------------------------------------