Merge branch 'master' of github.com:Mailtrain-org/mailtrain into access
Conflicts: views/users/api.hbs
This commit is contained in:
commit
ad1e4c58f5
21 changed files with 5088 additions and 284 deletions
1
.dockerignore
Normal file
1
.dockerignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -27,3 +27,7 @@ public/grapejs/uploads/*
|
||||||
public/grapejs/templates/*
|
public/grapejs/templates/*
|
||||||
!public/grapejs/templates/demo
|
!public/grapejs/templates/demo
|
||||||
!public/grapejs/templates/aves
|
!public/grapejs/templates/aves
|
||||||
|
|
||||||
|
config/production.toml
|
||||||
|
workers/reports/config/production.toml
|
||||||
|
docker-compose.override.yml
|
14
Dockerfile
14
Dockerfile
|
@ -1,9 +1,13 @@
|
||||||
FROM centos
|
FROM node:8.1
|
||||||
RUN curl --silent --location https://rpm.nodesource.com/setup_7.x | bash -
|
|
||||||
RUN yum install -y git make gcc nodejs ImageMagick && yum clean all
|
# First install dependencies
|
||||||
COPY . /app
|
COPY ./package.json ./app/
|
||||||
WORKDIR /app/
|
WORKDIR /app/
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV production
|
||||||
RUN npm install --no-progress --production && npm install --no-progress passport-ldapjs
|
RUN npm install --no-progress --production && npm install --no-progress passport-ldapjs
|
||||||
|
# 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
|
||||||
|
COPY . /app
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
CMD ["/usr/bin/node", "index.js"]
|
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]
|
||||||
|
CMD ["node", "index.js"]
|
|
@ -5,7 +5,7 @@ module.exports = function (grunt) {
|
||||||
// Project configuration.
|
// Project configuration.
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
eslint: {
|
eslint: {
|
||||||
all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'Gruntfile.js', 'app.js', 'index.js', 'routes/editorapi.js']
|
all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'services/**/*.js', 'Gruntfile.js', 'app.js', 'index.js', 'routes/editorapi.js']
|
||||||
},
|
},
|
||||||
|
|
||||||
nodeunit: {
|
nodeunit: {
|
||||||
|
|
83
README.md
83
README.md
|
@ -120,78 +120,25 @@ If you are using the bundled ZoneMTA then you should make sure you are using a p
|
||||||
With proper SPF, DKIM and PTR records (DMARC wouldn't hurt either) I got perfect 10/10 score out from [MailTester](https://www.mail-tester.com/) when sending a campaign message to a MailTester test address. I did not have VERP turned on, so the sender address matched return path address.
|
With proper SPF, DKIM and PTR records (DMARC wouldn't hurt either) I got perfect 10/10 score out from [MailTester](https://www.mail-tester.com/) when sending a campaign message to a MailTester test address. I did not have VERP turned on, so the sender address matched return path address.
|
||||||
|
|
||||||
### Simple Install (Docker)
|
### Simple Install (Docker)
|
||||||
##### Requirements:
|
#### Requirements:
|
||||||
* Docker
|
|
||||||
* docker-compose
|
|
||||||
|
|
||||||
1. Download Mailtrain files using git: `git clone git://github.com/Mailtrain-org/mailtrain.git` (or download [zipped repo](https://github.com/Mailtrain-org/mailtrain/archive/master.zip)) and open Mailtrain folder `cd mailtrain`
|
* [Docker](https://www.docker.com/)
|
||||||
2. Run `sudo docker build -t mailtrain-node:latest .`
|
* [Docker Compose](https://docs.docker.com/compose/)
|
||||||
3. Copy default.toml to production.toml. Run `sudo mkdir -p /etc/mailtrain && sudo cp config/default.toml /etc/mailtrain/production.toml`
|
|
||||||
4. Create `/etc/docker-compose.yml`. Example (dont forget change MYSQL_ROOT_PASS and MYSQL_USER_PASSWORD to your passwords):
|
|
||||||
```
|
|
||||||
version: '2'
|
|
||||||
services:
|
|
||||||
mailtrain-mysql:
|
|
||||||
image: mysql:latest
|
|
||||||
ports:
|
|
||||||
- "3306:3306"
|
|
||||||
container_name: "mailtrain-mysql"
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
MYSQL_ROOT_PASSWORD: "MYSQL_ROOT_PASS"
|
|
||||||
MYSQL_DATABASE: "mailtrain"
|
|
||||||
MYSQL_USER: "mailtrain"
|
|
||||||
MYSQL_PASSWORD: "MYSQL_USER_PASSWORD"
|
|
||||||
volumes:
|
|
||||||
- mailtrain-mysq-data:/var/lib/mysql
|
|
||||||
|
|
||||||
mailtrain-redis:
|
#### Install:
|
||||||
image: redis:3.0
|
|
||||||
container_name: "mailtrain-redis"
|
|
||||||
volumes:
|
|
||||||
- mailtrain-redis-data:/data
|
|
||||||
|
|
||||||
mailtrain-node:
|
* Download Mailtrain files using git: `git clone git://github.com/Mailtrain-org/mailtrain.git` (or download [zipped repo](https://github.com/Mailtrain-org/mailtrain/archive/master.zip)) and open Mailtrain folder `cd mailtrain`
|
||||||
image: mailtrain-node:latest
|
* **Note**: depending on how you have configured your system and Docker you may need to prepend the commands below with `sudo`.
|
||||||
container_name: "mailtrain-node"
|
* Copy the file `docker-compose.override.yml.tmpl` to `docker-compose.override.yml.tmpl` and modify it if you need to.
|
||||||
links:
|
* Bring up the stack with: `docker-compose up -d`, by default it will use the included `docker-compose.yml` file and override some configurations taken from the `docker-compose.override.yml` file.
|
||||||
- "mailtrain-mysql:mailtrain-mysql"
|
* If you want to use only / copy the `docker-compose.yml` file (for example, if you were deploying with Rancher), you may need to first run `docker-compose build` to make sure your system has a Docker image `mailtrain:latest`.
|
||||||
- "mailtrain-redis:mailtrain-redis"
|
* Open [http://localhost:3000/](http://localhost:3000/) (change the host name `localhost` to the name of the host where you are deploying the system).
|
||||||
ports:
|
* Authenticate as user `admin` with password `test`
|
||||||
- "3000:3000"
|
* Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration.
|
||||||
volumes:
|
* Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password.
|
||||||
- "/etc/mailtrain/production.toml:/app/config/production.toml"
|
|
||||||
- "mailtrain-node-data:/app/public/grapejs/uploads"
|
|
||||||
- "mailtrain-node-data:/app/public/mosaico/uploads"
|
|
||||||
volumes:
|
|
||||||
mailtrain-mysq-data: {}
|
|
||||||
mailtrain-redis-data: {}
|
|
||||||
mailtrain-node-data: {}
|
|
||||||
|
|
||||||
```
|
|
||||||
5. Update MySQL and Redis credintial in `/etc/mailtrain/production.toml` like this:
|
|
||||||
```
|
|
||||||
[mysql]
|
|
||||||
host="mailtrain-mysql"
|
|
||||||
user="mailtrain"
|
|
||||||
password="MYSQL_USER_PASSWORD"
|
|
||||||
database="mailtrain"
|
|
||||||
port=3306
|
|
||||||
charset="utf8mb4"
|
|
||||||
timezone="UTC"
|
|
||||||
|
|
||||||
[redis]
|
|
||||||
enabled=true
|
|
||||||
host="mailtrain-redis"
|
|
||||||
port=6379
|
|
||||||
db=5
|
|
||||||
```
|
|
||||||
6. Run docker container with command `sudo docker-compose -f /etc/docker-compose.yml up -d`
|
|
||||||
7. Open [http://localhost:3000/](http://localhost:3000/)
|
|
||||||
8. Authenticate as `admin`:`test`
|
|
||||||
9. Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration
|
|
||||||
10. Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password
|
|
||||||
|
|
||||||
|
**Note**: If you need to add or modify custom configurations, copy the file `config/docker-production.toml.tmpl` to `config/production.toml` and modify as you need. By default, the Docker image will do just that, automatically, so you can bring up the stack and it will work with default configurations.
|
||||||
|
|
||||||
|
|
||||||
### Manual Install (any OS that supports Node.js)
|
### Manual Install (any OS that supports Node.js)
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,54 @@ export default class API extends Component {
|
||||||
<pre>curl -XPOST {serviceUrl}api/delete/B16uVTdW?access_token={accessToken} \
|
<pre>curl -XPOST {serviceUrl}api/delete/B16uVTdW?access_token={accessToken} \
|
||||||
--data 'EMAIL=test@example.com'</pre>
|
--data 'EMAIL=test@example.com'</pre>
|
||||||
|
|
||||||
|
<h3>POST /api/field/:listId – {t('Add new custom field')}</h3>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{t('This API call creates a new custom field for a list.')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>GET</strong> {t('arguments')}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>access_token</strong> – {t('your personal access token')}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>POST</strong> {t('arguments')}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>NAME</strong> – {t('field name')} (<em>{t('required')}</em>)</li>
|
||||||
|
<li><strong>TYPE</strong> – {t('one of the following types:')}
|
||||||
|
<ul>
|
||||||
|
<li><strong>text</strong> Text</li>
|
||||||
|
<li><strong>website</strong> Website</li>
|
||||||
|
<li><strong>longtext</strong> Multi-line text</li>
|
||||||
|
<li><strong>gpg</strong> GPG Public Key</li>
|
||||||
|
<li><strong>number</strong> Number</li>
|
||||||
|
<li><strong>radio</strong> Radio Buttons</li>
|
||||||
|
<li><strong>checkbox</strong> Checkboxes</li>
|
||||||
|
<li><strong>dropdown</strong> Drop Down</li>
|
||||||
|
<li><strong>date-us</strong> Date (MM/DD/YYY)</li>
|
||||||
|
<li><strong>date-eur</strong> Date (DD/MM/YYYY)</li>
|
||||||
|
<li><strong>birthday-us</strong> Birthday (MM/DD)</li>
|
||||||
|
<li><strong>birthday-eur</strong> Birthday (DD/MM)</li>
|
||||||
|
<li><strong>json</strong> JSON value for custom rendering</li>
|
||||||
|
<li><strong>option</strong> Option</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li><strong>GROUP</strong> – {t('If the type is 'option' then you also need to specify the parent element ID')}</li>
|
||||||
|
<li><strong>GROUP_TEMPLATE</strong> – {t('Template for the group element. If not set, then values of the elements are joined with commas')}</li>
|
||||||
|
<li><strong>VISIBLE</strong> – yes/no, {t('if not visible then the subscriber can not view or modify this value at the profile page')}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>{t('Example')}</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre>curl -XPOST {{serviceUrl}}api/field/B16uVTdW?access_token={{accessToken}} \
|
||||||
|
--data 'NAME=Birthday&TYPE=birthday-us&VISIBLE=yes'</pre>
|
||||||
|
|
||||||
<h3>GET /api/blacklist/get – {t('Get list of blacklisted emails')}</h3>
|
<h3>GET /api/blacklist/get – {t('Get list of blacklisted emails')}</h3>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
|
9
config/docker-production.toml.tmpl
Normal file
9
config/docker-production.toml.tmpl
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[mysql]
|
||||||
|
host="mysql"
|
||||||
|
|
||||||
|
[redis]
|
||||||
|
enabled=true
|
||||||
|
host="redis"
|
||||||
|
|
||||||
|
[reports]
|
||||||
|
enabled=true
|
14
docker-compose.override.yml.tmpl
Normal file
14
docker-compose.override.yml.tmpl
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
restart: always
|
||||||
|
redis:
|
||||||
|
restart: always
|
||||||
|
mailtrain:
|
||||||
|
build: ./
|
||||||
|
# volumes:
|
||||||
|
# - ./:/app
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
restart: always
|
||||||
|
|
31
docker-compose.yml
Normal file
31
docker-compose.yml
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
version: '2'
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:5.7
|
||||||
|
environment:
|
||||||
|
- MYSQL_ROOT_PASSWORD=mailtrain
|
||||||
|
- MYSQL_DATABASE=mailtrain
|
||||||
|
- MYSQL_USER=mailtrain
|
||||||
|
- MYSQL_PASSWORD=mailtrain
|
||||||
|
volumes:
|
||||||
|
- mailtrain-mysq-data:/var/lib/mysql
|
||||||
|
redis:
|
||||||
|
image: redis:3.0
|
||||||
|
volumes:
|
||||||
|
- mailtrain-redis-data:/data
|
||||||
|
mailtrain:
|
||||||
|
image: mailtrain:latest
|
||||||
|
depends_on:
|
||||||
|
- mysql
|
||||||
|
- redis
|
||||||
|
volumes:
|
||||||
|
- mailtrain-node-config:/app/config
|
||||||
|
- mailtrain-node-data:/app/public/grapejs/uploads
|
||||||
|
- mailtrain-node-data:/app/public/mosaico/uploads
|
||||||
|
- mailtrain-node-reports:/app/protected/reports
|
||||||
|
volumes:
|
||||||
|
mailtrain-mysq-data: {}
|
||||||
|
mailtrain-redis-data: {}
|
||||||
|
mailtrain-node-data: {}
|
||||||
|
mailtrain-node-config: {}
|
||||||
|
mailtrain-node-reports: {}
|
18
docker-entrypoint.sh
Normal file
18
docker-entrypoint.sh
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -f "/app/config/production.toml" ] ; then
|
||||||
|
echo "No production.toml, copying from docker-production.toml.tmpl"
|
||||||
|
cp /app/config/docker-production.toml.tmpl /app/config/production.toml
|
||||||
|
fi
|
||||||
|
if [ ! -f "/app/workers/reports/config/production.toml" ] ; then
|
||||||
|
echo "No production.toml for reports"
|
||||||
|
if [ -f "/app/config/production.toml" ] ; then
|
||||||
|
echo "copying config/production.toml to reports config directory"
|
||||||
|
cp /app/config/production.toml /app/workers/reports/config/production.toml
|
||||||
|
else
|
||||||
|
echo "copying config/docker-production.toml.tmpl to reports config directory as production.toml"
|
||||||
|
cp /app/config/docker-production.toml.tmpl /app/workers/reports/config/production.toml
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
exec "$@"
|
BIN
languages/es_ES.mo
Normal file
BIN
languages/es_ES.mo
Normal file
Binary file not shown.
4668
languages/es_ES.po
Normal file
4668
languages/es_ES.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -98,7 +98,10 @@ module.exports.getQuery = (id, callback) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
let limit = 300;
|
let limit = 300;
|
||||||
let treshold = 3600 * 6; // time..NOW..time+6h, 6 hour window after trigger target to detect it
|
|
||||||
|
// time..NOW..time + 24h, 24 hour window after trigger target to detect it
|
||||||
|
//We need a 24 hour window for triggers as the format for dates added via the API are stored as 00:00:00
|
||||||
|
let treshold = 3600 * 24;
|
||||||
|
|
||||||
let intervalQuery = (column, seconds, treshold) => column + ' <= NOW() - INTERVAL ' + seconds + ' SECOND AND ' + column + ' >= NOW() - INTERVAL ' + (treshold + seconds) + ' SECOND';
|
let intervalQuery = (column, seconds, treshold) => column + ' <= NOW() - INTERVAL ' + seconds + ' SECOND AND ' + column + ' >= NOW() - INTERVAL ' + (treshold + seconds) + ' SECOND';
|
||||||
|
|
||||||
|
|
|
@ -375,6 +375,7 @@ router.post('/upload', passport.csrfProtection, (req, res) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const dirName = getDirName();
|
const dirName = getDirName();
|
||||||
|
const serviceUrlParts = url.parse(serviceUrl);
|
||||||
|
|
||||||
if (dirName === false) {
|
if (dirName === false) {
|
||||||
return res.status(500).send(_('Invalid resource type or ID'));
|
return res.status(500).send(_('Invalid resource type or ID'));
|
||||||
|
@ -391,7 +392,8 @@ router.post('/upload', passport.csrfProtection, (req, res) => {
|
||||||
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
|
uploadDir: path.join(__dirname, '..', 'public', req.query.editor, 'uploads', dirName),
|
||||||
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
|
uploadUrl: '/' + req.query.editor + '/uploads/' + dirName, // must be root relative
|
||||||
acceptFileTypes: /\.(gif|jpe?g|png)$/i,
|
acceptFileTypes: /\.(gif|jpe?g|png)$/i,
|
||||||
hostname: url.parse(serviceUrl).host // include port
|
hostname: serviceUrlParts.host, // include port
|
||||||
|
ssl: serviceUrlParts.protocol === 'https:'
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockres = httpMocks.createResponse({
|
const mockres = httpMocks.createResponse({
|
||||||
|
|
|
@ -1,131 +1,131 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
/* Privileged executor. If Mailtrain is started as root, this process keeps the root privilege to be able to spawn workers
|
/* Privileged executor. If Mailtrain is started as root, this process keeps the root privilege to be able to spawn workers
|
||||||
that can chroot.
|
that can chroot.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const fileHelpers = require('../lib/file-helpers');
|
const fileHelpers = require('../lib/file-helpers');
|
||||||
const fork = require('child_process').fork;
|
const fork = require('child_process').fork;
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const log = require('npmlog');
|
const log = require('npmlog');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const privilegeHelpers = require('../lib/privilege-helpers');
|
const privilegeHelpers = require('../lib/privilege-helpers');
|
||||||
|
|
||||||
let processes = {};
|
let processes = {};
|
||||||
|
|
||||||
function spawnProcess(tid, executable, args, outFile, errFile, cwd, uid, gid) {
|
function spawnProcess(tid, executable, args, outFile, errFile, cwd, uid, gid) {
|
||||||
|
|
||||||
function reportFail(msg) {
|
function reportFail(msg) {
|
||||||
process.send({
|
process.send({
|
||||||
type: 'process-failed',
|
type: 'process-failed',
|
||||||
msg,
|
msg,
|
||||||
tid
|
tid
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.open(outFile, 'w', (err, outFd) => {
|
fs.open(outFile, 'w', (err, outFd) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Executor', err);
|
log.error('Executor', err);
|
||||||
reportFail('Cannot create standard output file.');
|
reportFail('Cannot create standard output file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.open(errFile, 'w', (err, errFd) => {
|
fs.open(errFile, 'w', (err, errFd) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Executor', err);
|
log.error('Executor', err);
|
||||||
reportFail('Cannot create standard error file.');
|
reportFail('Cannot create standard error file.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
privilegeHelpers.ensureMailtrainOwner(outFile, (err) => {
|
privilegeHelpers.ensureMailtrainOwner(outFile, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.warn('Executor', 'Cannot change owner of output file of process tid:%s.', tid)
|
log.warn('Executor', 'Cannot change owner of output file of process tid:%s.', tid);
|
||||||
}
|
}
|
||||||
|
|
||||||
privilegeHelpers.ensureMailtrainOwner(errFile, (err) => {
|
privilegeHelpers.ensureMailtrainOwner(errFile, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.warn('Executor', 'Cannot change owner of error output file of process tid:%s.', tid)
|
log.warn('Executor', 'Cannot change owner of error output file of process tid:%s.', tid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
stdio: ['ignore', outFd, errFd, 'ipc'],
|
stdio: ['ignore', outFd, errFd, 'ipc'],
|
||||||
cwd,
|
cwd,
|
||||||
env: {NODE_ENV: process.env.NODE_ENV},
|
env: {NODE_ENV: process.env.NODE_ENV},
|
||||||
uid,
|
uid,
|
||||||
gid
|
gid
|
||||||
};
|
};
|
||||||
|
|
||||||
let child;
|
let child;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
child = fork(executable, args, options);
|
child = fork(executable, args, options);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Executor', 'Cannot start process with tid:%s.', tid);
|
log.error('Executor', 'Cannot start process with tid:%s.', tid);
|
||||||
reportFail('Cannot start process.');
|
reportFail('Cannot start process.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pid = child.pid;
|
const pid = child.pid;
|
||||||
processes[tid] = child;
|
processes[tid] = child;
|
||||||
|
|
||||||
log.info('Executor', 'Process started with tid:%s pid:%s.', tid, pid);
|
log.info('Executor', 'Process started with tid:%s pid:%s.', tid, pid);
|
||||||
process.send({
|
process.send({
|
||||||
type: 'process-started',
|
type: 'process-started',
|
||||||
tid
|
tid
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code, signal) => {
|
child.on('close', (code, signal) => {
|
||||||
|
|
||||||
delete processes[tid];
|
delete processes[tid];
|
||||||
log.info('Executor', 'Process tid:%s pid:%s exited with code %s signal %s.', tid, pid, code, signal);
|
log.info('Executor', 'Process tid:%s pid:%s exited with code %s signal %s.', tid, pid, code, signal);
|
||||||
|
|
||||||
fs.close(outFd, (err) => {
|
fs.close(outFd, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Executor', err);
|
log.error('Executor', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.close(errFd, (err) => {
|
fs.close(errFd, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Executor', err);
|
log.error('Executor', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.send({
|
process.send({
|
||||||
type: 'process-finished',
|
type: 'process-finished',
|
||||||
tid,
|
tid,
|
||||||
code,
|
code,
|
||||||
signal
|
signal
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('message', msg => {
|
process.on('message', msg => {
|
||||||
if (msg) {
|
if (msg) {
|
||||||
const type = msg.type;
|
const type = msg.type;
|
||||||
|
|
||||||
if (type === 'start-report-processor-worker') {
|
if (type === 'start-report-processor-worker') {
|
||||||
|
|
||||||
const ids = privilegeHelpers.getConfigROUidGid();
|
const ids = privilegeHelpers.getConfigROUidGid();
|
||||||
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], fileHelpers.getReportContentFile(msg.data), fileHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
spawnProcess(msg.tid, path.join(__dirname, '..', 'workers', 'reports', 'report-processor.js'), [msg.data.id], fileHelpers.getReportContentFile(msg.data), fileHelpers.getReportOutputFile(msg.data), path.join(__dirname, '..', 'workers', 'reports'), ids.uid, ids.gid);
|
||||||
|
|
||||||
} else if (type === 'stop-process') {
|
} else if (type === 'stop-process') {
|
||||||
const child = processes[msg.tid];
|
const child = processes[msg.tid];
|
||||||
|
|
||||||
if (child) {
|
if (child) {
|
||||||
log.info('Executor', 'Killing process tid:%s pid:%s', msg.tid, child.pid);
|
log.info('Executor', 'Killing process tid:%s pid:%s', msg.tid, child.pid);
|
||||||
child.kill();
|
child.kill();
|
||||||
} else {
|
} else {
|
||||||
log.info('Executor', 'No running process found with tid:%s pid:%s', msg.tid, child.pid);
|
log.info('Executor', 'No running process found with tid:%s pid:%s', msg.tid, child.pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.send({
|
process.send({
|
||||||
type: 'executor-started'
|
type: 'executor-started'
|
||||||
});
|
});
|
||||||
|
|
|
@ -135,7 +135,7 @@ function checkEntries(parent, entries, callback) {
|
||||||
if (/\[RSS_ENTRY[\w]*\]/i.test(html)) {
|
if (/\[RSS_ENTRY[\w]*\]/i.test(html)) {
|
||||||
html = html.replace(/\[RSS_ENTRY\]/, entry.content); //for backward compatibility
|
html = html.replace(/\[RSS_ENTRY\]/, entry.content); //for backward compatibility
|
||||||
Object.keys(entry).forEach(key => {
|
Object.keys(entry).forEach(key => {
|
||||||
html = html.replace('\[RSS_ENTRY_'+key.toUpperCase()+'\]', entry[key])
|
html = html.replace('[RSS_ENTRY_' + key.toUpperCase() + ']', entry[key]);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
html = entry.content + html;
|
html = entry.content + html;
|
||||||
|
|
|
@ -141,72 +141,72 @@ function processImport(data, callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertToSubscription() {
|
function insertToSubscription() {
|
||||||
subscriptions.insert(listId, {
|
subscriptions.insert(listId, {
|
||||||
imported: data.id,
|
imported: data.id,
|
||||||
status: data.type,
|
status: data.type,
|
||||||
partial: true
|
partial: true
|
||||||
}, entry, (err, response) => {
|
}, entry, (err, response) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
// ignore
|
// ignore
|
||||||
log.error('Import', err.stack);
|
log.error('Import', err.stack);
|
||||||
} else if (response.entryId) {
|
} else if (response.entryId) {
|
||||||
//log.verbose('Import', 'Inserted %s as %s', entry.email, entryId);
|
//log.verbose('Import', 'Inserted %s as %s', entry.email, entryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
db.getConnection((err, connection) => {
|
|
||||||
if (err) {
|
|
||||||
log.error('Import', err.stack);
|
|
||||||
return setImmediate(processRows);
|
|
||||||
}
|
|
||||||
|
|
||||||
let query;
|
|
||||||
if (response.inserted) {
|
|
||||||
// this record did not exist before, count as new
|
|
||||||
query = 'UPDATE importer SET `processed`=`processed`+1, `new`=`new`+1 WHERE `id`=? LIMIT 1';
|
|
||||||
} else {
|
|
||||||
// it's an existing record
|
|
||||||
query = 'UPDATE importer SET `processed`=`processed`+1 WHERE `id`=? LIMIT 1';
|
|
||||||
}
|
|
||||||
|
|
||||||
connection.query(query, [data.id], () => {
|
|
||||||
connection.release();
|
|
||||||
return setImmediate(processRows);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.emailcheck === 1) {
|
|
||||||
tools.validateEmail(entry.email, true, err => {
|
|
||||||
if (err) {
|
|
||||||
let reason = (err.message || '').toString().trim().replace(/^[a-z]Error:\s*/i, '');
|
|
||||||
log.verbose('Import', 'Failed processing row %s: %s', entry.email, reason);
|
|
||||||
db.getConnection((err, connection) => {
|
db.getConnection((err, connection) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('Import', err.stack);
|
log.error('Import', err.stack);
|
||||||
return setImmediate(processRows);
|
return setImmediate(processRows);
|
||||||
}
|
}
|
||||||
|
|
||||||
let query = 'INSERT INTO import_failed (`import`, `email`, `reason`) VALUES(?,?,?)';
|
let query;
|
||||||
connection.query(query, [data.id, entry.email, reason], err => {
|
if (response.inserted) {
|
||||||
if (err) {
|
// this record did not exist before, count as new
|
||||||
connection.release();
|
query = 'UPDATE importer SET `processed`=`processed`+1, `new`=`new`+1 WHERE `id`=? LIMIT 1';
|
||||||
return setImmediate(processRows);
|
} else {
|
||||||
}
|
// it's an existing record
|
||||||
let query = 'UPDATE importer SET `failed`=`failed`+1 WHERE `id`=? LIMIT 1';
|
query = 'UPDATE importer SET `processed`=`processed`+1 WHERE `id`=? LIMIT 1';
|
||||||
connection.query(query, [data.id], () => {
|
}
|
||||||
connection.release();
|
|
||||||
return setImmediate(processRows);
|
connection.query(query, [data.id], () => {
|
||||||
});
|
connection.release();
|
||||||
|
return setImmediate(processRows);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return;
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.emailcheck === 1) {
|
||||||
|
tools.validateEmail(entry.email, true, err => {
|
||||||
|
if (err) {
|
||||||
|
let reason = (err.message || '').toString().trim().replace(/^[a-z]Error:\s*/i, '');
|
||||||
|
log.verbose('Import', 'Failed processing row %s: %s', entry.email, reason);
|
||||||
|
db.getConnection((err, connection) => {
|
||||||
|
if (err) {
|
||||||
|
log.error('Import', err.stack);
|
||||||
|
return setImmediate(processRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
let query = 'INSERT INTO import_failed (`import`, `email`, `reason`) VALUES(?,?,?)';
|
||||||
|
connection.query(query, [data.id, entry.email, reason], err => {
|
||||||
|
if (err) {
|
||||||
|
connection.release();
|
||||||
|
return setImmediate(processRows);
|
||||||
|
}
|
||||||
|
let query = 'UPDATE importer SET `failed`=`failed`+1 WHERE `id`=? LIMIT 1';
|
||||||
|
connection.query(query, [data.id], () => {
|
||||||
|
connection.release();
|
||||||
|
return setImmediate(processRows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
insertToSubscription();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
insertToSubscription();
|
insertToSubscription();
|
||||||
});
|
}
|
||||||
} else {
|
|
||||||
insertToSubscription();
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
parser.on('readable', () => {
|
parser.on('readable', () => {
|
||||||
|
|
|
@ -70,17 +70,40 @@ let server = net.createServer(socket => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', err => {
|
|
||||||
log.error('POSTFIXBOUNCE', err && err.stack);
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = callback => {
|
module.exports = callback => {
|
||||||
if (config.postfixbounce.enabled) {
|
if (!config.postfixbounce.enabled) {
|
||||||
server.listen(config.postfixbounce.port, config.postfixbounce.host, () => {
|
return setImmediate(callback);
|
||||||
log.info('POSTFIXBOUNCE', 'Server listening on port %s', config.postfixbounce.port);
|
|
||||||
setImmediate(callback);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setImmediate(callback);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
server.on('error', err => {
|
||||||
|
const port = config.postfixbounce.port;
|
||||||
|
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
|
||||||
|
|
||||||
|
switch (err.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
log.error('POSTFIXBOUNCE', '%s requires elevated privileges.', bind);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
log.error('POSTFIXBOUNCE', '%s is already in use', bind);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.error('POSTFIXBOUNCE', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.listen(config.postfixbounce.port, config.postfixbounce.host, () => {
|
||||||
|
if (started) {
|
||||||
|
return server.close();
|
||||||
|
}
|
||||||
|
started = true;
|
||||||
|
log.info('POSTFIXBOUNCE', 'Server listening on port %s', config.postfixbounce.port);
|
||||||
|
setImmediate(callback);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -164,6 +164,10 @@ let mailBoxServer = http.createServer((req, res) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mailBoxServer.on('error', err => {
|
||||||
|
log.error('Test SMTP Mailbox Server', err);
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = callback => {
|
module.exports = callback => {
|
||||||
if (config.testserver.enabled) {
|
if (config.testserver.enabled) {
|
||||||
server.listen(config.testserver.port, config.testserver.host, () => {
|
server.listen(config.testserver.port, config.testserver.host, () => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ let server = new SMTPServer({
|
||||||
let user = address.address.split('@').shift();
|
let user = address.address.split('@').shift();
|
||||||
let host = address.address.split('@').pop();
|
let host = address.address.split('@').pop();
|
||||||
|
|
||||||
if (host !== configItems.verpHostname || !/^[a-z0-9_\-]+\.[a-z0-9_\-]+\.[a-z0-9_\-]+$/i.test(user)) {
|
if (host !== configItems.verpHostname || !/^[a-z0-9_-]+\.[a-z0-9_-]+\.[a-z0-9_-]+$/i.test(user)) {
|
||||||
err = new Error('Unknown user ' + address.address);
|
err = new Error('Unknown user ' + address.address);
|
||||||
err.responseCode = 510;
|
err.responseCode = 510;
|
||||||
return callback(err);
|
return callback(err);
|
||||||
|
@ -99,15 +99,36 @@ let server = new SMTPServer({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('error', err => {
|
|
||||||
log.error('VERP', err);
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = callback => {
|
module.exports = callback => {
|
||||||
if (!config.verp.enabled) {
|
if (!config.verp.enabled) {
|
||||||
return setImmediate(callback);
|
return setImmediate(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let started = false;
|
||||||
|
|
||||||
|
server.on('error', err => {
|
||||||
|
const port = config.verp.port;
|
||||||
|
const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port;
|
||||||
|
|
||||||
|
switch (err.code) {
|
||||||
|
case 'EACCES':
|
||||||
|
log.error('VERP', '%s requires elevated privileges', bind);
|
||||||
|
break;
|
||||||
|
case 'EADDRINUSE':
|
||||||
|
log.error('VERP', '%s is already in use', bind);
|
||||||
|
break;
|
||||||
|
case 'ECONNRESET': // Usually happens when a client does not disconnect cleanly
|
||||||
|
case 'EPIPE': // Remote connection was closed before the server attempted to send data
|
||||||
|
default:
|
||||||
|
log.error('VERP', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!started) {
|
||||||
|
started = true;
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let hosts;
|
let hosts;
|
||||||
if (typeof config.verp.host === 'string' && config.verp.host) {
|
if (typeof config.verp.host === 'string' && config.verp.host) {
|
||||||
hosts = config.verp.host.trim().split(',').map(host => host.trim()).filter(host => host.trim());
|
hosts = config.verp.host.trim().split(',').map(host => host.trim()).filter(host => host.trim());
|
||||||
|
@ -121,10 +142,14 @@ module.exports = callback => {
|
||||||
let pos = 0;
|
let pos = 0;
|
||||||
let startNextHost = () => {
|
let startNextHost = () => {
|
||||||
if (pos >= hosts.length) {
|
if (pos >= hosts.length) {
|
||||||
|
started = true;
|
||||||
return setImmediate(callback);
|
return setImmediate(callback);
|
||||||
}
|
}
|
||||||
let host = hosts[pos++];
|
let host = hosts[pos++];
|
||||||
server.listen(config.verp.port, host, () => {
|
server.listen(config.verp.port, host, () => {
|
||||||
|
if (started) {
|
||||||
|
return server.close();
|
||||||
|
}
|
||||||
log.info('VERP', 'Server listening on %s:%s', host || '*', config.verp.port);
|
log.info('VERP', 'Server listening on %s:%s', host || '*', config.verp.port);
|
||||||
setImmediate(startNextHost);
|
setImmediate(startNextHost);
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,10 +8,13 @@
|
||||||
<form class="form-horizontal" method="post" action="/blacklist/ajax/add">
|
<form class="form-horizontal" method="post" action="/blacklist/ajax/add">
|
||||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||||
<input type="hidden" name="next" value="/blacklist">
|
<input type="hidden" name="next" value="/blacklist">
|
||||||
<div class="col-sm-4">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control input-md" name="email" id="add-email-input" value="" placeholder="{{#translate}}Add email to blacklist{{/translate}}">
|
<div class="col-sm-4 col-xs-8">
|
||||||
|
<input type="text" class="form-control input-md" name="email" id="add-email-input" value="" placeholder="{{#translate}}Add email to blacklist{{/translate}}">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>{{#translate}}Add{{/translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-success"><span class="glyphicon glyphicon-plus" aria-hidden="true"></span>{{#translate}}Add{{/translate}}</button>
|
<p class="help-block">A blacklisted email address will not receive messages from any campaign. The blacklist does not apply to transactional messages.</p>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form class="form-horizontal" id="delete-email-form" method="post" action="/blacklist/ajax/delete">
|
<form class="form-horizontal" id="delete-email-form" method="post" action="/blacklist/ajax/delete">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue