Merge branch 'development' of github.com:Mailtrain-org/mailtrain into development

This commit is contained in:
Tomas Bures 2019-12-07 08:08:54 +01:00
commit c44ed4f7fa
33 changed files with 1492 additions and 242 deletions

View file

@ -1 +1,9 @@
node_modules node_modules/
docs/
Dockerfile
*.md
.git
.gitignore
.gitmodules
docker-compose.yml
docker-compose-local.yml

View file

@ -1,8 +1,12 @@
FROM node:10.14-alpine # Mutistaged Node.js Build
FROM node:10-alpine as builder
RUN apk add --update pwgen netcat-openbsd python make gcc git g++ bash imagemagick # Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
make gcc g++ git python
# First install dependencies # Copy package.json dependencies
COPY server/package.json /app/server/package.json COPY server/package.json /app/server/package.json
COPY server/package-lock.json /app/server/package-lock.json COPY server/package-lock.json /app/server/package-lock.json
COPY client/package.json /app/client/package.json COPY client/package.json /app/client/package.json
@ -12,15 +16,32 @@ COPY shared/package-lock.json /app/shared/package-lock.json
COPY zone-mta/package.json /app/zone-mta/package.json COPY zone-mta/package.json /app/zone-mta/package.json
COPY zone-mta/package-lock.json /app/zone-mta/package-lock.json COPY zone-mta/package-lock.json /app/zone-mta/package-lock.json
WORKDIR /app/ # Install dependencies in each directory
RUN cd /app/client && npm install
RUN for idx in client shared server zone-mta; do (cd $idx && npm install); done RUN cd /app/shared && npm install --production
RUN cd /app/server && npm install --production
RUN cd /app/zone-mta && npm install --production
# Later, copy the app files. That improves development speed as buiding the Docker image will not have # Later, copy the app files. That improves development speed as buiding the Docker image will not have
# to download and install all the NPM dependencies every time there's a change in the source code # to download and install all the NPM dependencies every time there's a change in the source code
COPY . /app COPY . /app
RUN cd client && npm run build RUN set -ex; \
cd /app/client && \
npm run build && \
rm -rf node_modules
# Final Image
FROM node:10-alpine
WORKDIR /app/
# Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
pwgen netcat-openbsd bash imagemagick
COPY --from=builder /app/ /app/
EXPOSE 3000 3003 3004 EXPOSE 3000 3003 3004
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"] ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]

View file

@ -193,17 +193,35 @@ These are the steps to start Mailtrain via docker-compose:
docker-compose up docker-compose up
``` ```
You can specify Mailtrain's URL bases via the `MAILTRAIN_SETTINGS` environment variable as follows. The `--withProxy` parameter is to be used when Mailtrain is put behind a reverse proxy.
```
MAILTRAIN_SETTINGS="--trustedUrlBase https://mailtrain.example.com --sandboxUrlBase https://sbox.mailtrain.example.com --publicUrlBase https://lists.example.com --withProxy" docker-compose up
```
3. Open the trusted endpoint http://localhost:3000 3. Open the trusted endpoint http://localhost:3000
4. Authenticate as `admin`:`test` 4. Authenticate as `admin`:`test`
The instructions above use an automatically built Docker image on DockerHub (https://hub.docker.com/r/mailtrain/mailtrain). If you want to build the Docker image yourself (e.g. when doing development), use the `docker-compose-local.yml` located in the project's root directory. The instructions above use an automatically built Docker image on DockerHub (https://hub.docker.com/r/mailtrain/mailtrain). If you want to build the Docker image yourself (e.g. when doing development), use the `docker-compose-local.yml` located in the project's root directory.
### Docker Environment Variables
| Parameter | Description |
| --------- | ----------- |
| URL_BASE_TRUSTED | sets the trusted url of the instance (default: http://localhost:3000) |
| URL_BASE_SANDBOX | sets the sandbox url of the instance (default: http://localhost:3003) |
| URL_BASE_PUBLIC | sets the public url of the instance (default: http://localhost:3004) |
| WITH_PROXY | use if Mailtrain is behind an http reverse proxy |
| MONGO_HOST | sets mongo host (default: mongo) |
| REDIS_HOST | sets redis host (default: redis) |
| MYSQL_HOST | sets mysql host (default: mysql) |
| MYSQL_DATABASE | sets mysql database (default: mailtrain) |
| MYSQL_USER | sets mysql user (default: mailtrain) |
| MYSQL_PASSWORD | sets mysql password (default: mailtrain) |
| WITH_LDAP | use if you want to enable LDAP authentication |
| LDAP_HOST | LDAP Host for authentication (default: ldap) |
| LDAP_PORT | LDAP port (default: 389) |
| LDAP_SECURE | use if you want to use LDAP with ldaps protocol |
| LDAP_BIND_USER | User for LDAP connexion |
| LDAP_BIND_PASS | Password for LDAP connexion |
| LDAP_FILTER | LDAP filter |
| LDAP_BASEDN | LDAP base DN |
| LDAP_UIDTAG | LDAP UID tag (e.g. uid/cn/username) |
## License ## License

234
client/package-lock.json generated
View file

@ -2013,6 +2013,12 @@
} }
} }
}, },
"ansi-colors": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz",
"integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==",
"dev": true
},
"ansi-regex": { "ansi-regex": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
@ -2121,6 +2127,21 @@
"integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=",
"dev": true "dev": true
}, },
"array-union": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
"integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
"dev": true,
"requires": {
"array-uniq": "^1.0.1"
}
},
"array-uniq": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
"integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=",
"dev": true
},
"array-unique": { "array-unique": {
"version": "0.2.1", "version": "0.2.1",
"resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
@ -3285,6 +3306,143 @@
"resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz",
"integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40="
}, },
"copy-webpack-plugin": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.0.5.tgz",
"integrity": "sha512-7N68eIoQTyudAuxkfPT7HzGoQ+TsmArN/I3HFwG+lVE3FNzqvZKIiaxtYh4o3BIznioxUvx9j26+Rtsc9htQUQ==",
"dev": true,
"requires": {
"cacache": "^12.0.3",
"find-cache-dir": "^2.1.0",
"glob-parent": "^3.1.0",
"globby": "^7.1.1",
"is-glob": "^4.0.1",
"loader-utils": "^1.2.3",
"minimatch": "^3.0.4",
"normalize-path": "^3.0.0",
"p-limit": "^2.2.1",
"schema-utils": "^1.0.0",
"serialize-javascript": "^2.1.0",
"webpack-log": "^2.0.0"
},
"dependencies": {
"cacache": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz",
"integrity": "sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw==",
"dev": true,
"requires": {
"bluebird": "^3.5.5",
"chownr": "^1.1.1",
"figgy-pudding": "^3.5.1",
"glob": "^7.1.4",
"graceful-fs": "^4.1.15",
"infer-owner": "^1.0.3",
"lru-cache": "^5.1.1",
"mississippi": "^3.0.0",
"mkdirp": "^0.5.1",
"move-concurrently": "^1.0.1",
"promise-inflight": "^1.0.1",
"rimraf": "^2.6.3",
"ssri": "^6.0.1",
"unique-filename": "^1.1.1",
"y18n": "^4.0.0"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"glob-parent": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz",
"integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=",
"dev": true,
"requires": {
"is-glob": "^3.1.0",
"path-dirname": "^1.0.0"
},
"dependencies": {
"is-glob": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
"integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
"dev": true,
"requires": {
"is-extglob": "^2.1.0"
}
}
}
},
"is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
"dev": true
},
"is-glob": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
}
},
"lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"requires": {
"yallist": "^3.0.2"
}
},
"normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true
},
"p-limit": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz",
"integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==",
"dev": true,
"requires": {
"p-try": "^2.0.0"
}
},
"serialize-javascript": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.0.tgz",
"integrity": "sha512-a/mxFfU00QT88umAJQsNWOnUKckhNCqOl028N48e7wFmo2/EHpTo9Wso+iJJCMrQnmFvcjto5RJdAHEvVhcyUQ==",
"dev": true
},
"y18n": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
"dev": true
},
"yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true
}
}
},
"core-js": { "core-js": {
"version": "2.6.5", "version": "2.6.5",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
@ -3645,6 +3803,32 @@
"randombytes": "^2.0.0" "randombytes": "^2.0.0"
} }
}, },
"dir-glob": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz",
"integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==",
"dev": true,
"requires": {
"path-type": "^3.0.0"
},
"dependencies": {
"path-type": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz",
"integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==",
"dev": true,
"requires": {
"pify": "^3.0.0"
}
},
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
}
}
},
"dnd-core": { "dnd-core": {
"version": "7.7.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-7.7.0.tgz", "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-7.7.0.tgz",
@ -5262,6 +5446,34 @@
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true "dev": true
}, },
"globby": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz",
"integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=",
"dev": true,
"requires": {
"array-union": "^1.0.1",
"dir-glob": "^2.0.0",
"glob": "^7.1.2",
"ignore": "^3.3.5",
"pify": "^3.0.0",
"slash": "^1.0.0"
},
"dependencies": {
"pify": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
"integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=",
"dev": true
},
"slash": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
"integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=",
"dev": true
}
}
},
"globule": { "globule": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz", "resolved": "https://registry.npmjs.org/globule/-/globule-1.2.1.tgz",
@ -5614,6 +5826,12 @@
"integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=",
"dev": true "dev": true
}, },
"ignore": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
"integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==",
"dev": true
},
"ignore-by-default": { "ignore-by-default": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@ -5673,6 +5891,12 @@
"integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
"dev": true "dev": true
}, },
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
"integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==",
"dev": true
},
"inflight": { "inflight": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -12169,6 +12393,16 @@
} }
} }
}, },
"webpack-log": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz",
"integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==",
"dev": true,
"requires": {
"ansi-colors": "^3.0.0",
"uuid": "^3.3.2"
}
},
"webpack-sources": { "webpack-sources": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.3.0.tgz",

View file

@ -72,6 +72,7 @@
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"clean-css-cli": "^4.2.1", "clean-css-cli": "^4.2.1",
"copy-webpack-plugin": "^5.0.5",
"css-loader": "^2.1.0", "css-loader": "^2.1.0",
"file-loader": "^3.0.1", "file-loader": "^3.0.1",
"node-sass": "^4.12.0", "node-sass": "^4.12.0",

View file

@ -292,8 +292,8 @@ export default class API extends Component {
<strong>{t('example')}</strong> <strong>{t('example')}</strong>
</p> </p>
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \<br/> <pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre> --data 'EMAIL=test@example.com'</pre>
<h4>POST /api/blacklist/delete {t('deleteEmailFromBlacklist')}</h4> <h4>POST /api/blacklist/delete {t('deleteEmailFromBlacklist')}</h4>
@ -320,7 +320,7 @@ export default class API extends Component {
</p> </p>
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \<br/> <pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \<br/>
--data 'EMAIL=test@example.com&amp;'</pre> --data 'EMAIL=test@example.com'</pre>
<h4>GET /api/lists/:email {t('getTheListsAUserHasSubscribedTo')}</h4> <h4>GET /api/lists/:email {t('getTheListsAUserHasSubscribedTo')}</h4>

View file

@ -401,7 +401,7 @@ export default class CUD extends Component {
} }
if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) { if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) {
state.setIn(['data_sourceCustom_tag_language', 'error'], t('Tag language must be selected')); state.setIn(['data_sourceCustom_tag_language', 'error'], t('tagLanguageMustBeSelected'));
} }
if (customTemplateTypeKey) { if (customTemplateTypeKey) {
@ -732,7 +732,7 @@ export default class CUD extends Component {
templateEdit = <div> templateEdit = <div>
<Dropdown id="data_sourceCustom_type" label={t('type')} options={this.customTemplateTypeOptions}/> <Dropdown id="data_sourceCustom_type" label={t('type')} options={this.customTemplateTypeOptions}/>
<Dropdown id="data_sourceCustom_tag_language" label={t('Tag language')} options={this.customTemplateTagLanguageOptions} disabled={isEdit}/> <Dropdown id="data_sourceCustom_tag_language" label={t('tagLanguage')} options={this.customTemplateTagLanguageOptions} disabled={isEdit}/>
{customTemplateTypeForm} {customTemplateTypeForm}
</div>; </div>;

View file

@ -131,7 +131,7 @@ export default class CustomContent extends Component {
const t = this.props.t; const t = this.props.t;
if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) { if (!state.getIn(['data_sourceCustom_tag_language', 'value'])) {
state.setIn(['data_sourceCustom_tag_language', 'error'], t('Tag language must be selected')); state.setIn(['data_sourceCustom_tag_language', 'error'], t('tagLanguageMustBeSelected'));
} else { } else {
state.setIn(['data_sourceCustom_tag_language', 'error'], null); state.setIn(['data_sourceCustom_tag_language', 'error'], null);
} }
@ -277,7 +277,7 @@ export default class CustomContent extends Component {
{customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName} {customTemplateTypeKey && this.templateTypes[customTemplateTypeKey].typeName}
</StaticField> </StaticField>
<Dropdown id="data_sourceCustom_tag_language" label={t('Tag language')} options={this.customTemplateTagLanguageOptions} disabled={true}/> <Dropdown id="data_sourceCustom_tag_language" label={t('tagLanguage')} options={this.customTemplateTagLanguageOptions} disabled={true}/>
{customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)} {customTemplateTypeKey && getTypeForm(this, customTemplateTypeKey, true)}

View file

@ -12,12 +12,13 @@ import {createComponentMixin} from "./decorator-helpers";
import lang_en_US_common from "../../../locales/en-US/common"; import lang_en_US_common from "../../../locales/en-US/common";
import lang_es_ES_common from "../../../locales/es-ES/common"; import lang_es_ES_common from "../../../locales/es-ES/common";
import lang_pt_BR_common from "../../../locales/pt-BR/common"; import lang_pt_BR_common from "../../../locales/pt-BR/common";
import lang_de_DE_common from "../../../locales/de-DE/common";
const resourcesCommon = { const resourcesCommon = {
'en-US': lang_en_US_common, 'en-US': lang_en_US_common,
'es-ES': lang_es_ES_common, 'es-ES': lang_es_ES_common,
'pt-BR': lang_pt_BR_common, 'pt-BR': lang_pt_BR_common,
'de-DE': lang_de_DE_common,
'fk-FK': convertToFake(lang_en_US_common) 'fk-FK': convertToFake(lang_en_US_common)
}; };

View file

@ -277,7 +277,7 @@ export default class CUD extends Component {
const label = matches[2].trim(); const label = matches[2].trim();
options.push({ key, label }); options.push({ key, label });
} else { } else {
errors.push(t('errrorOnLineLine', { line: lineIdx + 1})); errors.push(t('errorOnLineLine', { line: lineIdx + 1}));
} }
} }
} }
@ -511,7 +511,7 @@ export default class CUD extends Component {
<InputField id="key" label={t('mergeTag-1')}/> <InputField id="key" label={t('mergeTag-1')}/>
<TextArea id="help" label={t('Help text')}/> <TextArea id="help" label={t('helpText')}/>
{fieldSettings} {fieldSettings}

View file

@ -291,7 +291,7 @@ export function getMailerTypes(t) {
<Fieldset label={t('mailerSettings')}> <Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/> <Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/> <InputField id="sesKey" label={t('accessKey')} placeholder={t('awsAccessKeyId')}/>
<InputField id="sesSecret" label={t('port')} placeholder={t('awsSecretAccessKey')}/> <InputField id="sesSecret" label={t('accessSecret')} placeholder={t('awsSecretAccessKey')} type="password"/>
<Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/> <Dropdown id="sesRegion" label={t('region')} options={sesRegionOptions}/>
</Fieldset> </Fieldset>
<Fieldset label={t('advancedMailerSettings')}> <Fieldset label={t('advancedMailerSettings')}>

View file

@ -171,7 +171,7 @@ export default class CUD extends Component {
} }
if (!state.getIn(['tag_language', 'value'])) { if (!state.getIn(['tag_language', 'value'])) {
state.setIn(['tag_language', 'error'], t('Tag language must be selected')); state.setIn(['tag_language', 'error'], t('tagLanguageMustBeSelected'));
} }
if (typeKey) { if (typeKey) {
@ -320,7 +320,7 @@ export default class CUD extends Component {
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName }, { data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Tag language'), render: data => this.tagLanguages[data].name }, { data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() }, { data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }, { data: 6, title: t('namespace') },
]; ];
@ -376,7 +376,7 @@ export default class CUD extends Component {
{typeForm} {typeForm}
<Dropdown id="tag_language" label={t('Tag language')} options={tagLanguageOptions} disabled={isEdit}/> <Dropdown id="tag_language" label={t('tagLanguage')} options={tagLanguageOptions} disabled={isEdit}/>
</> </>
} }

View file

@ -45,7 +45,7 @@ export default class List extends Component {
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName }, { data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Tag language'), render: data => this.tagLanguages[data].name }, { data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() }, { data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }, { data: 6, title: t('namespace') },
{ {

View file

@ -132,7 +132,7 @@ export default class CUD extends Component {
} }
if (!state.getIn(['tag_language', 'value'])) { if (!state.getIn(['tag_language', 'value'])) {
state.setIn(['tag_language', 'error'], t('Tag language must be selected')); state.setIn(['tag_language', 'error'], t('tagLanguageMustBeSelected'));
} else { } else {
state.setIn(['tag_language', 'error'], null); state.setIn(['tag_language', 'error'], null);
} }
@ -218,7 +218,7 @@ export default class CUD extends Component {
<Dropdown id="type" label={t('type')} options={this.typeOptions}/> <Dropdown id="type" label={t('type')} options={this.typeOptions}/>
} }
<Dropdown id="tag_language" label={t('Tag language')} options={tagLanguageOptions}/> <Dropdown id="tag_language" label={t('tagLanguage')} options={tagLanguageOptions}/>
<NamespaceSelect/> <NamespaceSelect/>

View file

@ -45,7 +45,7 @@ export default class List extends Component {
{ data: 1, title: t('name') }, { data: 1, title: t('name') },
{ data: 2, title: t('description') }, { data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName }, { data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
{ data: 4, title: t('Tag language'), render: data => this.tagLanguages[data].name }, { data: 4, title: t('tagLanguage'), render: data => this.tagLanguages[data].name },
{ data: 5, title: t('created'), render: data => moment(data).fromNow() }, { data: 5, title: t('created'), render: data => moment(data).fromNow() },
{ data: 6, title: t('namespace') }, { data: 6, title: t('namespace') },
{ {

View file

@ -1,4 +1,5 @@
const webpack = require('webpack'); const webpack = require('webpack');
const CopyPlugin = require('copy-webpack-plugin');
const path = require('path'); const path = require('path');
module.exports = { module.exports = {
@ -97,7 +98,13 @@ module.exports = {
mailtrainConfig: 'mailtrainConfig' mailtrainConfig: 'mailtrainConfig'
}, },
plugins: [ plugins: [
// new webpack.optimize.UglifyJsPlugin(), new CopyPlugin([
{ from: './node_modules/jquery/dist/jquery.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/popper.js/dist/popper.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/bootstrap/dist/js/bootstrap.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/@coreui/coreui/dist/js/coreui.min.js', to: path.resolve(__dirname, 'dist') },
{ from: './node_modules/@fortawesome/fontawesome-free/webfonts/', to: path.resolve(__dirname, 'dist', 'webfonts'), toType: 'dir'}
]),
], ],
watchOptions: { watchOptions: {
ignored: 'node_modules/', ignored: 'node_modules/',

View file

@ -23,7 +23,6 @@ services:
mailtrain: mailtrain:
build: . build: .
command: ${MAILTRAIN_SETTINGS}
ports: ports:
- "3000:3000" - "3000:3000"
- "3003:3003" - "3003:3003"

View file

@ -23,7 +23,6 @@ services:
mailtrain: mailtrain:
image: mailtrain/mailtrain:latest image: mailtrain/mailtrain:latest
command: ${MAILTRAIN_SETTINGS}
ports: ports:
- "3000:3000" - "3000:3000"
- "3003:3003" - "3003:3003"

View file

@ -1,201 +1,120 @@
#!/bin/bash #!/bin/bash
# Entrypoint for Docker Container
set -e set -e
function printHelp { URL_BASE_TRUSTED=${URL_BASE_TRUSTED:-'http://localhost:3000'}
cat <<EOF URL_BASE_SANDBOX=${URL_BASE_SANDBOX:-'http://localhost:3003'}
URL_BASE_PUBLIC=${URL_BASE_PUBLIC:-'http://localhost:3004'}
Optional parameters: WWW_PROXY=${WWW_PROXY:-'false'}
--trustedUrlBase XXX - sets the trusted url of the instance (default: http://localhost:3000) WITH_LDAP=${WITH_LDAP:-'false'}
--sandboxUrlBase XXX - sets the sandbox url of the instance (default: http://localhost:3003) LDAP_HOST=${LDAP_HOST:-'ldap'}
--publicUrlBase XXX - sets the public url of the instance (default: http://localhost:3004) LDAP_PORT=${LDAP_PORT:-'389'}
--withProxy - use if Mailtrain is behind an http reverse proxy LDAP_SECURE=${LDAP_SECURE:-'false'}
--mongoHost XXX - sets mongo host (default: mongo) LDAP_BIND_USER=${LDAP_BIND_USER:-}
--redisHost XXX - sets redis host (default: redis) LDAP_BIND_PASS=${LDAP_BIND_PASS:-}
--mySqlHost XXX - sets mysql host (default: mysql) LDAP_FILTER=${LDAP_FILTER:-}
--withLdap - use if you want to enable LDAP authentication LDAP_BASEDN=${LDAP_BASEDN:-}
--ldapHost XXX - LDAP Host for authentication (default: ldap) LDAP_UIDTAG=${LDAP_UIDTAG:-}
--ldapPort XXX - LDAP port (default: 389) MONGO_HOST=${MONG_HOST:-'mongo'}
--ldapSecure - use if you want to use LDAP with ldaps protocol REDIS_HOST=${REDIS_HOST:-'redis'}
--ldapBindUser XXX - User for LDAP connexion MYSQL_HOST=${MYSQL_HOST:-'mysql'}
--ldapBindPass XXX - Password for LDAP connexion MYSQL_DATABASE=${MYSQL_DATABASE:-'mailtrain'}
--ldapFilter XXX - LDAP filter MYSQL_USER=${MYSQL_USER:-'mailtrain'}
--ldapBaseDN XXX - LDAP base DN MYSQL_PASSWORD=${MYSQL_PASSWORD:-'mailtrain'}
--ldapUidTag XXX - LDAP UID tag (e.g. uid/cn/username)
EOF
# Warning for users that already rely on the MAILTRAIN_SETTING variable
# Can probably be removed in the future.
MAILTRAIN_SETTING=${MAILTRAIN_SETTINGS:-}
if [ ! -z "$MAILTRAIN_SETTING" ]; then
echo 'Error: MAILTRAIN_SETTINGS is no longer supported. See README'
exit 1 exit 1
}
urlBaseTrusted=http://localhost:3000
urlBaseSandbox=http://localhost:3003
urlBasePublic=http://localhost:3004
wwwProxy=false
withLdap=false
ldapHost=ldap
ldapPort=389
ldapSecure=false
ldapBindUser=""
ldapBindPass=""
ldapFilter=""
ldapBaseDN=""
ldapUidTag=""
mongoHost=mongo
redisHost=redis
mySqlHost=mysql
while [ $# -gt 0 ]; do
case "$1" in
--help)
printHelp
;;
--trustedUrlBase)
urlBaseTrusted="$2"
shift 2
;;
--sandboxUrlBase)
urlBaseSandbox="$2"
shift 2
;;
--publicUrlBase)
urlBasePublic="$2"
shift 2
;;
--withProxy)
wwwProxy=true
shift 1
;;
--mongoHost)
mongoHost="$2"
shift 2
;;
--redisHost)
redisHost="$2"
shift 2
;;
--mySqlHost)
mySqlHost="$2"
shift 2
;;
--withLdap)
withLdap=true
shift 1
;;
--ldapHost)
ldapHost="$2"
shift 2
;;
--ldapPort)
ldapPort="$2"
shift 2
;;
--ldapSecure)
ldapSecure=true
shift 1
;;
--ldapBindUser)
ldapBindUser="$2"
shift 2
;;
--ldapBindPass)
ldapBindPass="$2"
shift 2
;;
--ldapFilter)
ldapFilter="$2"
shift 2
;;
--ldapBaseDN)
ldapBaseDN="$2"
shift 2
;;
--ldapUidTag)
ldapUidTag="$2"
shift 2
;;
*)
echo "Error: unrecognized option $1."
printHelp
esac
done
if [ "$ldapBindUser" == "" ]; then
ldapBindUserLine=""
else
ldapBindUserLine="bindUser: $ldapBindUser"
fi
if [ "$ldapBindPass" == "" ]; then
ldapBindPassLine=""
else
ldapBindPassLine="bindPassword: $ldapBindPass"
fi
if [ "$ldapFilter" == "" ]; then
ldapFilterLine=""
else
ldapFilterLine="filter: $ldapFilter"
fi
if [ "$ldapBaseDN" == "" ]; then
ldapBaseDNLine=""
else
ldapBaseDNLine="baseDN: $ldapBaseDN"
fi
if [ "$ldapUidTag" == "" ]; then
ldapUidTagLine=""
else
ldapUidTagLine="uidTag: $ldapUidTag"
fi fi
cat > server/config/production.yaml <<EOT if [ -f application/config/config.php ]; then
www: echo 'Info: application/production.yaml already provisioned'
host: 0.0.0.0 else
proxy: $wwwProxy echo 'Info: Generating application/production.yaml'
secret: "`pwgen -1`"
trustedUrlBase: $urlBaseTrusted
sandboxUrlBase: $urlBaseSandbox
publicUrlBase: $urlBasePublic
mysql: # Basic configuration
host: $mySqlHost cat > server/config/production.yaml <<EOT
www:
host: 0.0.0.0
proxy: $WWW_PROXY
secret: "`pwgen -1`"
trustedUrlBase: $URL_BASE_TRUSTED
sandboxUrlBase: $URL_BASE_SANDBOX
publicUrlBase: $URL_BASE_PUBLIC
redis: mysql:
enabled: true host: $MYSQL_HOST
host: $redisHost database: $MYSQL_DATABASE
user: $MYSQL_USER
password: $MYSQL_PASSWORD
log: redis:
level: info enabled: true
host: $REDIS_HOST
builtinZoneMTA: log:
log: level: info
level: warn
mongo: mongodb://${mongoHost}:27017/zone-mta
redis: redis://${redisHost}:6379/2
queue: builtinZoneMTA:
processes: 5 log:
level: warn
mongo: mongodb://${MONGO_HOST}:27017/zone-mta
redis: redis://${REDIS_HOST}:6379/2
ldap: queue:
enabled: $withLdap processes: 5
host: $ldapHost
port: $ldapPort
secure: $ldapSecure
$ldapBindUserLine
$ldapBindPassLine
$ldapFilterLine
$ldapBaseDNLine
$ldapUidTagLine
EOT EOT
cat > server/services/workers/reports/config/production.yaml <<EOT # Manage LDAP if enabled
mysql: if [ "$WITH_LDAP" = "true" ]; then
host: $mySqlHost echo 'Info: LDAP enabled'
log: cat >> server/config/production.yaml <<EOT
level: warn ldap:
enabled: true
host: $LDAP_HOST
port: $LDAP_PORT
secure: $LDAP_SECURE
bindUser: $LDAP_BIND_USER
bindPasswort: $LDAP_BIND_PASS
filter: $LDAP_FILTER
baseDN: $LDAP_BASEDN
uidTag: $LDAP_UIDTAG
EOT EOT
else
echo 'Info: LDAP not enabled'
cat >> server/config/production.yaml <<EOT
ldap:
enabled: false
EOT
fi
fi
if [ -f server/services/workers/reports/config/production.yaml ]; then
echo 'Info: server/production.yaml already provisioned'
else
echo 'Info: Generating server/production.yaml'
cat > server/services/workers/reports/config/production.yaml <<EOT
mysql:
host: $MYSQL_HOST
log:
level: warn
EOT
fi
# Wait for the other services to start # Wait for the other services to start
while ! nc -z $mySqlHost 3306; do sleep 1; done echo 'Info: Waiting for MySQL Server'
while ! nc -z $redisHost 6379; do sleep 1; done while ! nc -z $MYSQL_HOST 3306; do sleep 1; done
while ! nc -z $mongoHost 27017; do sleep 1; done
echo 'Info: Waiting for Redis Server'
while ! nc -z $REDIS_HOST 6379; do sleep 1; done
echo 'Info: Waiting for MongoDB Server'
while ! nc -z $MONGO_HOST 27017; do sleep 1; done
cd server cd server
NODE_ENV=production node index.js NODE_ENV=production node index.js

1032
locales/de-DE/common.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -267,7 +267,7 @@
"viewStatistics": "View statistics", "viewStatistics": "View statistics",
"campaignIsBeingSentOut": "Campaign is being sent out.", "campaignIsBeingSentOut": "Campaign is being sent out.",
"stop": "Stop", "stop": "Stop",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you you want to send this campaign to new subscribers.", "allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you want to send this campaign to new subscribers.",
"continue": "Continue", "continue": "Continue",
"reset": "Reset", "reset": "Reset",
"yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.", "yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.",
@ -398,7 +398,7 @@
"defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date", "defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date",
"defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date", "defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date",
"defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options", "defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options",
"errrorOnLineLine": "Errror on line {{ line }}", "errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated", "fieldUpdated": "Field updated",
"fieldCreated": "Field created", "fieldCreated": "Field created",
"notVisible": "Not visible", "notVisible": "Not visible",
@ -864,6 +864,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"", "beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.", "signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key", "accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID", "awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key", "awsSecretAccessKey": "AWS secret access key",
"region": "Region", "region": "Region",

View file

@ -267,7 +267,7 @@
"viewStatistics": "View statistics", "viewStatistics": "View statistics",
"campaignIsBeingSentOut": "Campaign is being sent out.", "campaignIsBeingSentOut": "Campaign is being sent out.",
"stop": "Stop", "stop": "Stop",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you you want to send this campaign to new subscribers.", "allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you want to send this campaign to new subscribers.",
"continue": "Continue", "continue": "Continue",
"reset": "Reset", "reset": "Reset",
"yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.", "yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.",
@ -398,7 +398,7 @@
"defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date", "defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date",
"defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date", "defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date",
"defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options", "defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options",
"errrorOnLineLine": "Errror on line {{ line }}", "errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated", "fieldUpdated": "Field updated",
"fieldCreated": "Field created", "fieldCreated": "Field created",
"notVisible": "Not visible", "notVisible": "Not visible",
@ -864,6 +864,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"", "beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.", "signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key", "accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID", "awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key", "awsSecretAccessKey": "AWS secret access key",
"region": "Region", "region": "Region",
@ -1024,5 +1025,8 @@
"thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter", "thePasswordMustContainAtLeastOne": "The password must contain at least one lowercase letter",
"thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter", "thePasswordMustContainAtLeastOne-1": "The password must contain at least one uppercase letter",
"thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number", "thePasswordMustContainAtLeastOneNumber": "The password must contain at least one number",
"thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character" "thePasswordMustContainAtLeastOneSpecial": "The password must contain at least one special character",
"tagLanguage": "Tag language",
"tagLanguageMustBeSelected": "Tag language must be selected",
"helpText": "Help text"
} }

View file

@ -277,7 +277,7 @@
"viewStatistics": "View statistics", "viewStatistics": "View statistics",
"campaignIsBeingSentOut": "Campaign is being sent out.", "campaignIsBeingSentOut": "Campaign is being sent out.",
"stop": "Stop", "stop": "Stop",
"allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you you want to send this campaign to new subscribers.", "allMessagesSent!HitContinueIfYouYouWant": "All messages sent! Hit \"Continue\" if you want to send this campaign to new subscribers.",
"continue": "Continue", "continue": "Continue",
"reset": "Reset", "reset": "Reset",
"yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.", "yourCampaignIsCurrentlyDisabledClick": "Your campaign is currently disabled. Click Enable button to start enable it.",
@ -425,7 +425,7 @@
"defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date", "defaultValueIsNotAProperlyFormattedDate": "Default value is not a properly formatted date",
"defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date", "defaultValueIsNotAProperlyFormatted": "Default value is not a properly formatted birthday date",
"defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options", "defaultValueIsNotOneOfTheAllowedOptions": "Default value is not one of the allowed options",
"errrorOnLineLine": "Errror on line {{ line }}", "errorOnLineLine": "Error on line {{ line }}",
"fieldUpdated": "Field updated", "fieldUpdated": "Field updated",
"fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated", "fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated",
"fieldCreated": "Field created", "fieldCreated": "Field created",
@ -915,6 +915,7 @@
"beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"", "beginsWithBeginRsaPrivateKey": "Begins with \"-----BEGIN RSA PRIVATE KEY-----\"",
"signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.", "signingIsDisabledWithoutAValidPrivateKey": "Signing is disabled without a valid private key.",
"accessKey": "Access key", "accessKey": "Access key",
"accessSecret": "Access Secret",
"awsAccessKeyId": "AWS access key ID", "awsAccessKeyId": "AWS access key ID",
"awsSecretAccessKey": "AWS secret access key", "awsSecretAccessKey": "AWS secret access key",
"region": "Region", "region": "Region",

View file

@ -17,7 +17,7 @@ const deepKeys = require('deep-keys');
const localeMain = 'en-US/common.json'; const localeMain = 'en-US/common.json';
const localeMainPrevious = 'en-US-last-run/common.json'; const localeMainPrevious = 'en-US-last-run/common.json';
const localeTranslations = ['es-ES/common.json', 'pt-BR/common.json']; const localeTranslations = ['es-ES/common.json', 'pt-BR/common.json', 'de-DE/common.json'];
const searchDirs = [ const searchDirs = [
'../client/src', '../client/src',
'../server', '../server',

View file

@ -426,7 +426,7 @@
"defaultValueIsNotAProperlyFormattedDate": "O valor padrão não é uma data formatada corretamente", "defaultValueIsNotAProperlyFormattedDate": "O valor padrão não é uma data formatada corretamente",
"defaultValueIsNotAProperlyFormatted": "O valor padrão não é uma data de aniversário formatada corretamente", "defaultValueIsNotAProperlyFormatted": "O valor padrão não é uma data de aniversário formatada corretamente",
"defaultValueIsNotOneOfTheAllowedOptions": "O valor padrão não é uma das opções permitidas", "defaultValueIsNotOneOfTheAllowedOptions": "O valor padrão não é uma das opções permitidas",
"errrorOnLineLine": "Errror on line {{line}}", "errorOnLineLine": "Error on line {{line}}",
"fieldUpdated": "Field updated", "fieldUpdated": "Field updated",
"fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated", "fieldUpdated - TODO: update line above and then delete this line to mark that the translation has been fixed": "Field updated",
"fieldCreated": "Field created", "fieldCreated": "Field created",
@ -916,6 +916,7 @@
"beginsWithBeginRsaPrivateKey": "Começa com \"----- INICIO DA CHAVE PRIVADA RSA ----- \"", "beginsWithBeginRsaPrivateKey": "Começa com \"----- INICIO DA CHAVE PRIVADA RSA ----- \"",
"signingIsDisabledWithoutAValidPrivateKey": "A assinatura está desativada sem uma chave privada válida.", "signingIsDisabledWithoutAValidPrivateKey": "A assinatura está desativada sem uma chave privada válida.",
"accessKey": "Chave de acesso", "accessKey": "Chave de acesso",
"accessSecret": "Acesso secreto",
"awsAccessKeyId": "ID da chave de acesso da AWS", "awsAccessKeyId": "ID da chave de acesso da AWS",
"awsSecretAccessKey": "Chave de acesso secreto da AWS", "awsSecretAccessKey": "Chave de acesso secreto da AWS",
"region": "Região", "region": "Região",

View file

@ -237,11 +237,11 @@ async function createApp(appType) {
useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static'))); useWith404Fallback('/static', express.static(path.join(__dirname, '..', 'client', 'static')));
useWith404Fallback('/client', express.static(path.join(__dirname, '..', 'client', 'dist'))); useWith404Fallback('/client', express.static(path.join(__dirname, '..', 'client', 'dist')));
useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts'))); useWith404Fallback('/static-npm/fontawesome', express.static(path.join(__dirname, '..', 'client', 'dist', 'webfonts')));
useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'jquery', 'dist', 'jquery.min.js'))); useWith404Fallback('/static-npm/jquery.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'jquery.min.js')));
useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'popper.js', 'dist', 'umd', 'popper.min.js'))); useWith404Fallback('/static-npm/popper.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'popper.min.js')));
useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', 'bootstrap', 'dist', 'js', 'bootstrap.min.js'))); useWith404Fallback('/static-npm/bootstrap.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'bootstrap.min.js')));
useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'node_modules', '@coreui', 'coreui', 'dist', 'js', 'coreui.min.js'))); useWith404Fallback('/static-npm/coreui.min.js', express.static(path.join(__dirname, '..', 'client', 'dist', 'coreui.min.js')));
// Make sure flash messages are available // Make sure flash messages are available

View file

@ -49,6 +49,7 @@ enabledLanguages:
- en-US - en-US
- es-ES - es-ES
- pt-BR - pt-BR
- de-DE
- fk-FK - fk-FK
# Inject custom scripts in subscription/layout.mjml.hbs # Inject custom scripts in subscription/layout.mjml.hbs

View file

@ -863,7 +863,7 @@ async function getListsWithEmail(context, email) {
// FIXME - this methods is rather suboptimal if there are many lists. It quite needs permission caching in shares.js // FIXME - this methods is rather suboptimal if there are many lists. It quite needs permission caching in shares.js
return await knex.transaction(async tx => { return await knex.transaction(async tx => {
const lsts = await tx('lists').select(['id', 'name']); const lsts = await tx('lists').select(['id', 'cid', 'name']);
const result = []; const result = [];
for (const list of lsts) { for (const list of lsts) {

View file

@ -360,7 +360,6 @@
"version": "1.0.10", "version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": { "requires": {
"sprintf-js": "~1.0.2" "sprintf-js": "~1.0.2"
} }
@ -3735,7 +3734,6 @@
"version": "3.13.1", "version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": { "requires": {
"argparse": "^1.0.7", "argparse": "^1.0.7",
"esprima": "^4.0.0" "esprima": "^4.0.0"
@ -3744,8 +3742,7 @@
"esprima": { "esprima": {
"version": "4.0.1", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
"dev": true
} }
} }
}, },
@ -6658,8 +6655,7 @@
"sprintf-js": { "sprintf-js": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
"integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
"dev": true
}, },
"sqlstring": { "sqlstring": {
"version": "2.3.1", "version": "2.3.1",

View file

@ -78,6 +78,7 @@
"humanize": "0.0.9", "humanize": "0.0.9",
"i18next": "^13.1.0", "i18next": "^13.1.0",
"isemail": "^3.2.0", "isemail": "^3.2.0",
"js-yaml": "^3.13.1",
"jsdom": "^13.1.0", "jsdom": "^13.1.0",
"juice": "^5.2.0", "juice": "^5.2.0",
"klaw-sync": "^6.0.0", "klaw-sync": "^6.0.0",

View file

@ -361,7 +361,7 @@ async function processCampaign(campaignId) {
} }
const subs = await knex('campaign_messages') const subs = await knex('campaign_messages')
.where({status: CampaignMessageStatus.SCHEDULED}) .where({status: CampaignMessageStatus.SCHEDULED, campaign: campaignId})
.whereNotIn('hash_email', messagesInProcessing.map(x => x.hash_email)) .whereNotIn('hash_email', messagesInProcessing.map(x => x.hash_email))
.limit(retrieveBatchSize); .limit(retrieveBatchSize);

View file

@ -12,6 +12,7 @@ defaultLanguage: en-US
enabledLanguages: enabledLanguages:
- en-US - en-US
- es-ES - es-ES
- de-DE
- fk-FK - fk-FK
mysql: mysql:

View file

@ -56,6 +56,11 @@ const langCodes = {
getLabel: t => 'Português', getLabel: t => 'Português',
longCode: 'pt-BR' longCode: 'pt-BR'
}, },
'de-DE': {
getShortLabel: t => 'DE',
getLabel: t => 'Deutsch',
longCode: 'de-DE'
},
'fk-FK': { 'fk-FK': {
getShortLabel: t => 'FK', getShortLabel: t => 'FK',
getLabel: t => 'Fake', getLabel: t => 'Fake',