Added MJML/HTML codeeditor with a preview for template design.

This commit is contained in:
Tomas Bures 2018-11-13 21:35:33 +01:00
parent c7d7b1fe0c
commit 7e52000219
24 changed files with 887 additions and 279 deletions

View file

@ -25,6 +25,7 @@ const subscription = require('./routes/subscription');
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
const sandboxedGrapesJS = require('./routes/sandboxed-grapesjs');
const sandboxedCodeEditor = require('./routes/sandboxed-codeeditor');
const files = require('./routes/files');
const links = require('./routes/links');
const archive = require('./routes/archive');
@ -226,6 +227,7 @@ function createApp(appType) {
useWith404Fallback('/mosaico', sandboxedMosaico.getRouter(appType));
useWith404Fallback('/ckeditor', sandboxedCKEditor.getRouter(appType));
useWith404Fallback('/grapesjs', sandboxedGrapesJS.getRouter(appType));
useWith404Fallback('/codeeditor', sandboxedCodeEditor.getRouter(appType));
if (appType === AppType.TRUSTED || appType === AppType.SANDBOXED) {
if (config.reports && config.reports.enabled === true) {

332
client/package-lock.json generated
View file

@ -2180,11 +2180,6 @@
"safe-buffer": "5.1.1"
}
},
"ckeditor": {
"version": "4.11.1",
"resolved": "https://registry.npmjs.org/ckeditor/-/ckeditor-4.11.1.tgz",
"integrity": "sha512-UhHe02cc/wWJquDQZysEgh0ohLMEMU56zDx+s8prDdjylY/aBDY2xdIiIpbgCBTXdjhrEPIAPyiDS9g3RxYXig=="
},
"ckeditor5": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/ckeditor5/-/ckeditor5-11.1.1.tgz",
@ -3413,9 +3408,9 @@
"dev": true
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
},
"deep-is": {
"version": "0.1.3",
@ -4624,6 +4619,16 @@
"mjml": "3.3.5"
},
"dependencies": {
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -4632,6 +4637,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -4748,6 +4758,27 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-divider": {
@ -4914,17 +4945,48 @@
}
}
},
"grapesjs-plugin-ckeditor": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/grapesjs-plugin-ckeditor/-/grapesjs-plugin-ckeditor-0.0.9.tgz",
"integrity": "sha512-QXyAcSwgi09pzigGVS/NsHag5Skuw4zTkVGmEiBN/Qi8KU12/cQBG/OjAcjAB3/ZpToyPoglI33Ydjgj2nJuxQ=="
},
"grapesjs-preset-newsletter": {
"version": "0.2.20",
"resolved": "https://registry.npmjs.org/grapesjs-preset-newsletter/-/grapesjs-preset-newsletter-0.2.20.tgz",
"integrity": "sha512-rffUeuznf9Saig+kIUddmGfhWwbLjxdaqAYf6Hoge4b0sfT8knOS4mQXJBdRsSROfzuRhFe6ybRHm4yC32lHxA==",
"requires": {
"juice": "4.3.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"har-validator": {
@ -5819,14 +5881,14 @@
}
},
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/juice/-/juice-5.0.1.tgz",
"integrity": "sha512-3XJgQxfXo4uHGbCCI6hKwlVtovj0IM+2BVAUCUfWlIiOn1Mljsm4+pYLatOyzY6SF0ks7eT2MSUmOBvue/39sQ==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"cross-spawn": "6.0.5",
"deep-extend": "0.6.0",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
@ -5838,11 +5900,13 @@
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
"requires": {
"lru-cache": "4.1.1",
"nice-try": "1.0.5",
"path-key": "2.0.1",
"semver": "5.5.0",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
@ -6313,6 +6377,21 @@
"react": "15.6.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6321,6 +6400,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -6346,6 +6430,22 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-validator": {
@ -6369,6 +6469,21 @@
"react": "15.6.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6377,6 +6492,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -6402,6 +6522,22 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-validator": {
@ -6428,6 +6564,21 @@
"react": "15.6.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6436,6 +6587,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -6461,6 +6617,22 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-table": {
@ -6493,6 +6665,21 @@
"react": "15.6.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6501,6 +6688,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -6526,6 +6718,22 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-validator": {
@ -6551,6 +6759,21 @@
"react": "15.6.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -6559,6 +6782,11 @@
"ms": "2.0.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"hoist-non-react-statics": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz",
@ -6584,6 +6812,22 @@
"react": "15.6.2",
"react-dom": "15.6.2",
"warning": "3.0.0"
},
"dependencies": {
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
}
}
},
"mjml-image": {
@ -6631,6 +6875,40 @@
"warning": "4.0.2"
},
"dependencies": {
"commander": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz",
"integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg=="
},
"cross-spawn": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
"requires": {
"lru-cache": "4.1.1",
"shebang-command": "1.2.0",
"which": "1.3.0"
}
},
"deep-extend": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
"integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
},
"juice": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
"integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
"requires": {
"cheerio": "0.22.0",
"commander": "2.19.0",
"cross-spawn": "5.1.0",
"deep-extend": "0.5.1",
"mensch": "0.3.3",
"slick": "1.12.2",
"web-resource-inliner": "4.2.1"
}
},
"warning": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.2.tgz",
@ -6683,6 +6961,11 @@
"integrity": "sha1-7XFfP+neArV6XmJS2QqWZ14fCFo=",
"dev": true
},
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
},
"no-case": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz",
@ -7111,6 +7394,11 @@
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
"dev": true
},
"path-key": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",

View file

@ -43,6 +43,7 @@
"i18next": "^8.4.3",
"i18next-xhr-backend": "^1.4.2",
"immutable": "^3.8.1",
"juice": "^5.0.1",
"mjml4-in-browser": "^1.0.1",
"moment": "^2.18.1",
"moment-timezone": "^0.5.13",

View file

@ -0,0 +1,71 @@
$navbarHeight: 34px;
$editorNormalHeight: 800px !default;
.editor {
.host {
@if $editorNormalHeight {
height: $editorNormalHeight;
}
}
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
.navbar {
background: #DE4320;
width: 100%;
height: $navbarHeight;
}
.logo {
float: left;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
padding: 5px 0 5px 10px;
font-size: 18px;
font-family: sans-serif;
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
.btn {
display: block;
float: right;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
}
.btn:hover {
background-color: #b1381e;
color: white;
}

View file

@ -1,72 +1,7 @@
$navbarHeight: 34px;
.editor {
.host {
}
}
$editorNormalHeight: false;
@import "sandbox-common";
.sandbox {
height: 100%;
overflow: hidden;
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
.navbar {
background: #DE4320;
width: 100%;
height: $navbarHeight;
}
.logo {
float: left;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
padding: 5px 0 5px 10px;
font-size: 18px;
font-family: sans-serif;
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
.btn {
display: block;
float: right;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
}
.btn:hover {
background-color: #b1381e;
color: white;
}
}

View file

@ -0,0 +1,166 @@
'use strict';
import './public-path';
import React, {Component} from 'react';
import ReactDOM
from 'react-dom';
import {
I18nextProvider,
translate,
} from 'react-i18next';
import i18n
from './i18n';
import {
parentRPC,
UntrustedContentRoot
} from './untrusted';
import PropTypes
from "prop-types";
import styles
from "./sandboxed-codeeditor.scss";
import {
getPublicUrl,
getSandboxUrl,
getTrustedUrl
} from "./urls";
import {
base,
unbase
} from "../../../shared/templates";
import brace from 'brace';
import ACEEditorRaw from 'react-ace';
import 'brace/theme/github';
import 'brace/ext/searchbox';
import 'brace/mode/html';
import {CodeEditorSourceType} from "./sandboxed-codeeditor-shared";
import mjml2html from "mjml4-in-browser";
import juice from "juice";
@translate(null, { withRef: true })
class CodeEditorSandbox extends Component {
constructor(props) {
super(props);
let defaultSource;
if (props.sourceType === CodeEditorSourceType.MJML) {
defaultSource =
'<mjml>\n' +
' <mj-body>\n' +
' <mj-section>\n' +
' <mj-column>\n' +
' <!-- First column content -->\n' +
' </mj-column>\n' +
' <mj-column>\n' +
' <!-- Second column content -->\n' +
' </mj-column>\n' +
' </mj-section>\n' +
' </mj-body>\n' +
'</mjml>';
} else if (props.sourceType === CodeEditorSourceType.HTML) {
defaultSource =
'<!DOCTYPE html>\n' +
'<html>\n' +
'<head>\n' +
' <meta charset="UTF-8">\n' +
' <title>Title of the document</title>\n' +
'</head>\n' +
'<body>\n' +
' Content of the document......\n' +
'</body>\n' +
'</html>';
}
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
const source = this.props.initialSource ? base(this.props.initialSource, trustedUrlBase, sandboxUrlBase, publicUrlBase) : defaultSource;
this.state = {
source,
preview: props.initialPreview
};
}
static propTypes = {
entityTypeId: PropTypes.string,
entityId: PropTypes.number,
initialSource: PropTypes.string,
sourceType: PropTypes.string,
initialPreview: PropTypes.bool
}
async exportState(method, params) {
const trustedUrlBase = getTrustedUrl();
const sandboxUrlBase = getSandboxUrl();
const publicUrlBase = getPublicUrl();
return {
source: unbase(this.state.source, trustedUrlBase, sandboxUrlBase, publicUrlBase, true)
};
}
async setPreview(method, preview) {
this.setState({
preview
});
}
componentDidMount() {
parentRPC.setMethodHandler('exportState', ::this.exportState);
parentRPC.setMethodHandler('setPreview', ::this.setPreview);
}
render() {
let previewContents;
if (this.props.sourceType === CodeEditorSourceType.MJML) {
const res = mjml2html(this.state.source);
previewContents = res.html;
} else if (this.props.sourceType === CodeEditorSourceType.HTML) {
previewContents = juice(this.state.source);
}
return (
<div className={styles.sandbox}>
<div className={this.state.preview ? styles.aceEditorWithPreview : styles.aceEditorWithoutPreview}>
<ACEEditorRaw
mode="html"
theme="github"
width="100%"
height="100%"
onChange={data => this.setState({source: data})}
fontSize={12}
showPrintMargin={false}
value={this.state.source}
tabSize={2}
setOptions={{useWorker: false}} // This disables syntax check because it does not always work well (e.g. in case of JS code in report templates)
/>
</div>
{
this.state.preview &&
<div className={styles.preview}>
<iframe src={"data:text/html;charset=utf-8," + escape(previewContents)}></iframe>
</div>
}
</div>
);
}
}
export default function() {
parentRPC.init();
ReactDOM.render(
<I18nextProvider i18n={ i18n }>
<UntrustedContentRoot render={props => <CodeEditorSandbox {...props} />} />
</I18nextProvider>,
document.getElementById('root')
);
};

View file

@ -0,0 +1,11 @@
'use strict';
export const CodeEditorSourceType = {
MJML: 'mjml',
HTML: 'html'
};
export const getCodeEditorSourceTypeOptions = t => [
{key: CodeEditorSourceType.MJML, label: t('MJML')},
{key: CodeEditorSourceType.HTML, label: t('HTML')}
];

View file

@ -0,0 +1,86 @@
'use strict';
import React, {Component} from 'react';
import {translate} from 'react-i18next';
import PropTypes
from "prop-types";
import styles
from "./sandboxed-codeeditor.scss";
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
@translate(null, { withRef: true })
export class CodeEditorHost extends Component {
constructor(props) {
super(props);
this.state = {
fullscreen: false,
preview: true
}
}
static propTypes = {
entityTypeId: PropTypes.string,
entity: PropTypes.object,
initialSource: PropTypes.string,
sourceType: PropTypes.string,
title: PropTypes.string,
onFullscreenAsync: PropTypes.func
}
async toggleFullscreenAsync() {
const fullscreen = !this.state.fullscreen;
this.setState({
fullscreen
});
await this.props.onFullscreenAsync(fullscreen);
}
async togglePreviewAsync() {
const preview = !this.state.preview;
this.setState({
preview
});
await this.contentNode.ask('setPreview', preview);
}
async exportState() {
return await this.contentNode.ask('exportState');
}
render() {
const t = this.props.t;
const editorData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id,
initialSource: this.props.initialSource,
sourceType: this.props.sourceType,
initialPreview: this.state.preview
};
const tokenData = {
entityTypeId: this.props.entityTypeId,
entityId: this.props.entity.id
};
return (
<div className={this.state.fullscreen ? styles.editorFullscreen : styles.editor}>
<div className={styles.navbar}>
{this.state.fullscreen && <img className={styles.logo} src={getTrustedUrl('static/mailtrain-notext.png')}/>}
<div className={styles.title}>{this.props.title}</div>
<a className={styles.btn} onClick={::this.toggleFullscreenAsync}><Icon icon="fullscreen"/></a>
<a className={styles.btn} onClick={::this.togglePreviewAsync}><Icon icon={this.state.preview ? 'eye-close': 'eye-open'}/></a>
</div>
<UntrustedContentHost ref={node => this.contentNode = node} className={styles.host} singleToken={true} contentProps={editorData} contentSrc="codeeditor/editor" tokenMethod="codeeditor" tokenParams={tokenData}/>
</div>
);
}
}
CodeEditorHost.prototype.exportState = async function() {
return await this.getWrappedInstance().exportState();
};

View file

@ -0,0 +1,35 @@
@import "sandbox-common";
.sandbox {
}
.aceEditorWithPreview, .aceEditorWithoutPreview, .preview {
position: absolute;
height: 100%;
}
.aceEditorWithPreview {
border-right: #e8e8e8 solid 2px;
width: 50%;
}
.aceEditorWithoutPreview {
width: 100%;
}
.preview {
border-left: #e8e8e8 solid 2px;
width: 50%;
left: 50%;
overflow: hidden;
iframe {
width: 100%;
height: 100%;
border: 0px none;
body {
margin: 0px;
}
}
}

View file

@ -4,3 +4,8 @@ export const GrapesJSSourceType = {
MJML: 'mjml',
HTML: 'html'
};
export const getGrapesJSSourceTypeOptions = t => [
{key: GrapesJSSourceType.MJML, label: t('MJML')},
{key: GrapesJSSourceType.HTML, label: t('HTML')}
];

View file

@ -10,7 +10,6 @@ import styles
import {UntrustedContentHost} from './untrusted';
import {Icon} from "./bootstrap-components";
import {getTrustedUrl} from "./urls";
import {GrapesJSSourceType} from "./sandboxed-grapesjs-shared";
@translate(null, { withRef: true })
export class GrapesJSHost extends Component {

View file

@ -1,72 +1,4 @@
$navbarHeight: 34px;
.editor {
.host {
height: 800px;
}
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
.navbar {
background: #DE4320;
width: 100%;
height: $navbarHeight;
}
.logo {
float: left;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
padding: 5px 0 5px 10px;
font-size: 18px;
font-family: sans-serif;
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
.btn {
display: block;
float: right;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
}
.btn:hover {
background-color: #b1381e;
color: white;
}
@import "sandbox-common";
:global .grapesjs-body {
margin: 0px;

View file

@ -1,29 +1,4 @@
$navbarHeight: 34px;
.editor {
.host {
height: 800px;
}
}
.editorFullscreen {
position: fixed;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
z-index: 1000;
background: white;
margin-top: $navbarHeight;
.navbar {
margin-top: -$navbarHeight;
}
.host {
height: 100%;
}
}
@import "sandbox-common";
:global .mo-standalone {
top: 0px;
@ -31,45 +6,3 @@ $navbarHeight: 34px;
width: 100%;
position: absolute;
}
.navbar {
background: #DE4320;
width: 100%;
height: $navbarHeight;
}
.logo {
float: left;
height: $navbarHeight;
padding: 5px 0 5px 10px;
filter: brightness(0) invert(1);
}
.title {
padding: 5px 0 5px 10px;
font-size: 18px;
font-family: sans-serif;
font-family: "Ubuntu",Tahoma,"Helvetica Neue",Helvetica,Arial,sans-serif;
font-weight: bold;
float: left;
color: white;
height: $navbarHeight;
}
.btn {
display: block;
float: right;
padding: 0px 15px;
line-height: $navbarHeight;
text-align: center;
color: white;
font-size: 14px;
font-weight: bold;
font-family: sans-serif;
cursor: pointer;
}
.btn:hover {
background-color: #b1381e;
color: white;
}

View file

@ -4,6 +4,7 @@ import React from "react";
import {
ACEEditor,
AlignedRow,
CheckBox,
CKEditor,
Dropdown,
StaticField,
@ -15,6 +16,17 @@ import 'brace/mode/html';
import { MosaicoHost } from "../lib/sandboxed-mosaico";
import { CKEditorHost } from "../lib/sandboxed-ckeditor";
import { GrapesJSHost } from "../lib/sandboxed-grapesjs";
import { CodeEditorHost } from "../lib/sandboxed-codeeditor";
import {
getGrapesJSSourceTypeOptions,
GrapesJSSourceType
} from "../lib/sandboxed-grapesjs-shared";
import {
getCodeEditorSourceTypeOptions,
CodeEditorSourceType
} from "../lib/sandboxed-codeeditor-shared";
import {getTemplateTypes as getMosaicoTemplateTypes} from './mosaico/helpers';
import {getSandboxUrl} from "../lib/urls";
@ -26,7 +38,6 @@ import {
import {Trans} from "react-i18next";
import styles from "../lib/styles.scss";
import {GrapesJSSourceType} from "../lib/sandboxed-grapesjs-shared";
export const ResourceType = {
TEMPLATE: 'template',
@ -177,21 +188,17 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
};
const grapesJSSourceTypes = [
{key: GrapesJSSourceType.MJML, label: t('MJML')},
{key: GrapesJSSourceType.HTML, label: t('HTML')}
];
const grapesJSSourceTypes = getGrapesJSSourceTypeOptions(t);
const grapesJSSourceTypeLabels = {};
for ({key, label} of grapesJSSourceTypes) {
grapesJSSourceTypeLabels[key] = label;
}
templateTypes.grapesjs = {
typeName: t('GrapeJS'),
typeName: t('GrapesJS'),
getTypeForm: (owner, isEdit) => {
if (isEdit) {
return <StaticField id={prefix + 'grapesJSSourceType'} className={styles.formDisabled} label={t('Type')}>{grapesJSSourceTypeLabels[(owner.getFormValue(prefix + 'grapesJSSourceType'))]}</StaticField>;
return <StaticField id={prefix + 'grapesJSSourceType'} className={styles.formDisabled} label={t('Type')}>{grapesJSSourceTypeLabels[owner.getFormValue(prefix + 'grapesJSSourceType')]}</StaticField>;
} else {
return <Dropdown id={prefix + 'grapesJSSourceType'} label={t('Type')} options={grapesJSSourceTypes}/>;
}
@ -283,30 +290,62 @@ export function getTemplateTypes(t, prefix = '', entityTypeId = ResourceType.TEM
validate: state => {}
};
const codeEditorSourceTypes = getCodeEditorSourceTypeOptions(t);
const codeEditorSourceTypeLabels = {};
for ({key, label} of codeEditorSourceTypes) {
codeEditorSourceTypeLabels[key] = label;
}
templateTypes.codeeditor = {
typeName: t('Code Editor'),
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => <ACEEditor id={prefix + 'html'} height="600px" mode="html" label={t('Template content (HTML)')}/>,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
getTypeForm: (owner, isEdit) => {
const sourceType = owner.getFormValue(prefix + 'codeEditorSourceType');
if (isEdit) {
return <StaticField id={prefix + 'codeEditorSourceType'} className={styles.formDisabled} label={t('Type')}>{codeEditorSourceTypeLabels[sourceType]}</StaticField>;
} else {
return <Dropdown id={prefix + 'codeEditorSourceType'} label={t('Type')} options={codeEditorSourceTypes}/>;
}
},
afterTypeChange: mutState => {},
validate: state => {}
};
getHTMLEditor: owner =>
<AlignedRow label={t('Template content (HTML)')}>
<CodeEditorHost
ref={node => owner.editorNode = node}
entity={owner.props.entity}
entityTypeId={entityTypeId}
initialSource={owner.getFormValue(prefix + 'codeEditorData').source}
sourceType={owner.getFormValue(prefix + 'codeEditorSourceType')}
title={t('Code Editor Template Designer')}
onFullscreenAsync={::owner.setElementInFullscreen}
/>
</AlignedRow>,
exportHTMLEditorData: async owner => {
const {html, source} = await owner.editorNode.exportState();
owner.updateFormValue(prefix + 'html', html);
owner.updateFormValue(prefix + 'codeEditorData', {
source
});
},
initData: () => ({
[prefix + 'codeEditorSourceType']: CodeEditorSourceType.HTML,
[prefix + 'codeEditorData']: {}
}),
afterLoad: data => {
data[prefix + 'codeEditorSourceType'] = data[prefix + 'data'].sourceType;
data[prefix + 'codeEditorData'] = {
source: data[prefix + 'data'].source
};
},
beforeSave: data => {
data[prefix + 'data'] = {
sourceType: data[prefix + 'codeEditorSourceType'],
source: data[prefix + 'codeEditorData'].source
};
templateTypes.mjml = { // FIXME
getTypeForm: (owner, isEdit) => null,
getHTMLEditor: owner => null,
exportHTMLEditorData: async owner => {},
initData: () => ({}),
afterLoad: data => {},
beforeSave: data => {
clearBeforeSave(data);
},
afterTypeChange: mutState => {},
afterTypeChange: mutState => {
initFieldsIfMissing(mutState, 'codeeditor');
},
validate: state => {}
};

View file

@ -6,7 +6,7 @@ import 'brace/mode/html'
import 'brace/mode/xml'
export function getTemplateTypesOrder() {
return ['mjml', 'html'];
return [/* 'mjml' , */ 'html'];
}
export function getTemplateTypes(t) {

View file

@ -17,6 +17,7 @@ module.exports = {
"mosaico-root": ['babel-polyfill', './src/lib/sandboxed-mosaico-root.js'],
"ckeditor-root": ['babel-polyfill', './src/lib/sandboxed-ckeditor-root.js'],
"grapesjs-root": ['babel-polyfill', './src/lib/sandboxed-grapesjs-root.js'],
"codeeditor-root": ['babel-polyfill', './src/lib/sandboxed-codeeditor-root.js'],
},
output: {
library: 'MailtrainReactBody',

View file

@ -16,12 +16,12 @@ title: mailtrain
# Enabled HTML editors
editors:
- ckeditor4
- ckeditor5
- codeeditor
- grapesjs
- mosaico
- mosaicoWithFsTemplate
- grapesjs
- ckeditor5
- ckeditor4
- codeeditor
# Default language to use
language: en

View file

@ -150,7 +150,7 @@ async function prepareHtml(html) {
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
ProcessExternalResources: false // do not execute JS within script blocks
}
});
});pre
const head = win.document.querySelector('head');
let hasCharsetTag = false;

View file

@ -20,7 +20,7 @@ users.registerRestrictedAccessTokenMethod('ckeditor', async ({entityTypeId, enti
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'ckeditor') {
if (tmpl.type === 'ckeditor4') {
return {
permissions: {
'template': {

View file

@ -0,0 +1,59 @@
'use strict';
const routerFactory = require('../lib/router-async');
const passport = require('../lib/passport');
const clientHelpers = require('../lib/client-helpers');
const users = require('../models/users');
const files = require('../models/files');
const fileHelpers = require('../lib/file-helpers');
const templates = require('../models/templates');
const contextHelpers = require('../lib/context-helpers');
const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
const { AppType } = require('../shared/app');
users.registerRestrictedAccessTokenMethod('codeeditor', async ({entityTypeId, entityId}) => {
if (entityTypeId === 'template') {
const tmpl = await templates.getById(contextHelpers.getAdminContext(), entityId, false);
if (tmpl.type === 'codeeditor') {
return {
permissions: {
'template': {
[entityId]: new Set(['manageFiles', 'view'])
}
}
};
}
}
});
function getRouter(appType) {
const router = routerFactory.create();
if (appType === AppType.SANDBOXED) {
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, appType);
res.render('ckeditor/root', {
layout: 'ckeditor/layout',
reactCsrfToken: req.csrfToken(),
mailtrainConfig: JSON.stringify(mailtrainConfig),
scriptFiles: [
getSandboxUrl('mailtrain/common.js'),
getSandboxUrl('mailtrain/codeeditor-root.js')
],
publicPath: getSandboxUrl()
});
});
}
return router;
}
module.exports.getRouter = getRouter;

View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{#translate}}Self hosted email newsletter app{{/translate}}">
<link rel="shortcut icon" href="{{publicPath}}favicon.ico" type="image/x-icon" />
<link rel="icon" href="{{publicPath}}static/favicon.ico">
<title>Mailtrain</title>
<link rel="stylesheet" href="{{publicPath}}static/bootstrap/themes/united.min.css">
<script src="{{publicPath}}static/jquery-2.2.1.min.js"></script>
<script src="{{publicPath}}static/bootstrap/js/bootstrap.min.js"></script>
{{#if mailtrainConfig}}
<script>
{{#if reactCsrfToken}}window.csfrToken = '{{reactCsrfToken}}';{{/if}}
window.mailtrainConfig = {{{mailtrainConfig}}};
</script>
{{#each scriptFiles}}
<script src="{{this}}"></script>
{{/each}}
{{/if}}
</head>
<body>
{{{body}}}
</body>
</html>

View file

@ -0,0 +1,6 @@
<div id="root"></div>
<script>
document.addEventListener('DOMContentLoaded', function() {
MailtrainReactBody.default();
});
</script>

View file

@ -1,18 +0,0 @@
# Process title visible in monitoring logs and process listing
title="mailtrain"
# Default language to use
language="en"
[log]
# silly|verbose|info|http|warn|error|silent
level="verbose"
[mysql]
host="localhost"
user="mailtrain"
password="mailtrain"
database="mailtrain"
port=3306
charset="utf8mb4"
timezone="local"

View file

@ -0,0 +1,22 @@
# Process title visible in monitoring logs and process listing
title: mailtrain
# Default language to use
language: en
log:
# silly|verbose|info|http|warn|error|silent
level: verbose
mysql:
host: localhost
user: mailtrain
password: mailtrain
database: mailtrain
# Some installations, eg. MAMP can use a different port (8889)
# MAMP users should also turn on Allow network access to MySQL otherwise MySQL might not be accessible
port: 3306
charset: utf8mb4
# The timezone configured on the MySQL server. This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM
timezone: local