Added CSV export of subscribers
Fixed some bugs in subscriptions Updated some packages to avoid warnings about vulnerabilities Completed RSS feed campaigns
This commit is contained in:
parent
8683f8c91e
commit
bf69e633c4
47 changed files with 5255 additions and 9651 deletions
20
Gruntfile.js
20
Gruntfile.js
|
@ -6,31 +6,13 @@ module.exports = function (grunt) {
|
||||||
grunt.initConfig({
|
grunt.initConfig({
|
||||||
eslint: {
|
eslint: {
|
||||||
all: ['lib/**/*.js', 'test/**/*.js', 'config/**/*.js', 'services/**/*.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: {
|
|
||||||
all: ['test/nodeunit/**/*-test.js']
|
|
||||||
},
|
|
||||||
|
|
||||||
jsxgettext: {
|
|
||||||
test: {
|
|
||||||
files: [{
|
|
||||||
src: ['views/**/*.hbs', 'lib/**/*.js', 'routes/**/*.js', 'services/**/*.js', 'app.js', 'index.js', '!ignored'],
|
|
||||||
output: 'mailtrain.pot',
|
|
||||||
'output-dir': './languages/'
|
|
||||||
}],
|
|
||||||
options: {
|
|
||||||
keyword: ['translate', '_']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load the plugin(s)
|
// Load the plugin(s)
|
||||||
grunt.loadNpmTasks('grunt-eslint');
|
grunt.loadNpmTasks('grunt-eslint');
|
||||||
grunt.loadNpmTasks('grunt-contrib-nodeunit');
|
|
||||||
grunt.task.loadTasks('tasks');
|
grunt.task.loadTasks('tasks');
|
||||||
|
|
||||||
// Tasks
|
// Tasks
|
||||||
grunt.registerTask('default', ['eslint', 'nodeunit', 'jsxgettext']);
|
grunt.registerTask('default', ['eslint']);
|
||||||
};
|
};
|
||||||
|
|
|
@ -21,6 +21,7 @@ const api = require('./routes/api');
|
||||||
|
|
||||||
// These are routes for the new React-based client
|
// These are routes for the new React-based client
|
||||||
const reports = require('./routes/reports');
|
const reports = require('./routes/reports');
|
||||||
|
const subscriptions = require('./routes/subscriptions');
|
||||||
const subscription = require('./routes/subscription');
|
const subscription = require('./routes/subscription');
|
||||||
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
const sandboxedMosaico = require('./routes/sandboxed-mosaico');
|
||||||
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
const sandboxedCKEditor = require('./routes/sandboxed-ckeditor');
|
||||||
|
@ -234,6 +235,7 @@ function createApp(appType) {
|
||||||
useWith404Fallback('/reports', reports);
|
useWith404Fallback('/reports', reports);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useWith404Fallback('/subscriptions', subscriptions);
|
||||||
useWith404Fallback('/webhooks', webhooks);
|
useWith404Fallback('/webhooks', webhooks);
|
||||||
|
|
||||||
// API endpoints
|
// API endpoints
|
||||||
|
|
4984
client/package-lock.json
generated
4984
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -61,7 +61,7 @@
|
||||||
"react-router-dom": "^4.1.1",
|
"react-router-dom": "^4.1.1",
|
||||||
"react-sortable-tree": "^1.2.0",
|
"react-sortable-tree": "^1.2.0",
|
||||||
"slugify": "^1.1.0",
|
"slugify": "^1.1.0",
|
||||||
"url-parse": "^1.1.9"
|
"url-parse": "^1.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
|
"@ckeditor/ckeditor5-dev-utils": "^11.0.1",
|
||||||
|
@ -76,12 +76,12 @@
|
||||||
"css-loader": "^0.28.4",
|
"css-loader": "^0.28.4",
|
||||||
"file-loader": "^2.0.0",
|
"file-loader": "^2.0.0",
|
||||||
"i18next-conv": "^3.0.3",
|
"i18next-conv": "^3.0.3",
|
||||||
"node-sass": "^4.5.3",
|
"node-sass": "^4.10.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^3.0.0",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
"sass-loader": "^6.0.6",
|
"sass-loader": "^6.0.6",
|
||||||
"style-loader": "^0.18.2",
|
"style-loader": "^0.18.2",
|
||||||
"url-loader": "^0.5.9",
|
"url-loader": "^1.1.2",
|
||||||
"webpack": "^2.6.1"
|
"webpack": "^2.6.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,13 +24,21 @@ import {
|
||||||
} from '../lib/error-handling';
|
} from '../lib/error-handling';
|
||||||
import {getCampaignLabels} from './helpers';
|
import {getCampaignLabels} from './helpers';
|
||||||
import {Table} from "../lib/table";
|
import {Table} from "../lib/table";
|
||||||
import {Button} from "../lib/bootstrap-components";
|
import {
|
||||||
|
Button,
|
||||||
|
Icon
|
||||||
|
} from "../lib/bootstrap-components";
|
||||||
import axios from "../lib/axios";
|
import axios from "../lib/axios";
|
||||||
import {getUrl, getPublicUrl} from "../lib/urls";
|
import {getUrl, getPublicUrl} from "../lib/urls";
|
||||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||||
import {CampaignStatus} from "../../../shared/campaigns";
|
import {
|
||||||
|
CampaignSource,
|
||||||
|
CampaignStatus,
|
||||||
|
CampaignType
|
||||||
|
} from "../../../shared/campaigns";
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import campaignsStyles from "./styles.scss";
|
import campaignsStyles from "./styles.scss";
|
||||||
|
import {tableDeleteDialogAddDeleteButton} from "../lib/modals";
|
||||||
|
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
|
@ -207,10 +215,21 @@ class SendControls extends Component {
|
||||||
await this.refreshEntity();
|
await this.refreshEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async enableAsync() {
|
||||||
|
await this.postAndMaskStateError(`rest/campaign-enable/${this.props.entity.id}`);
|
||||||
|
await this.refreshEntity();
|
||||||
|
}
|
||||||
|
|
||||||
|
async disableAsync() {
|
||||||
|
await this.postAndMaskStateError(`rest/campaign-disable/${this.props.entity.id}`);
|
||||||
|
await this.refreshEntity();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const t = this.props.t;
|
const t = this.props.t;
|
||||||
const entity = this.props.entity;
|
const entity = this.props.entity;
|
||||||
|
|
||||||
|
console.log(entity);
|
||||||
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
|
if (entity.status === CampaignStatus.IDLE || entity.status === CampaignStatus.PAUSED || (entity.status === CampaignStatus.SCHEDULED && entity.scheduled)) {
|
||||||
|
|
||||||
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers')})`;
|
const subscrInfo = entity.subscriptionsTotal === undefined ? '' : ` (${entity.subscriptionsToSend} ${t('subscribers')})`;
|
||||||
|
@ -266,6 +285,30 @@ class SendControls extends Component {
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
} else if (entity.status === CampaignStatus.INACTIVE) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AlignedRow label={t('Send status')}>
|
||||||
|
{t('Your campaign is currently disabled. Click Enable button to start enable it.')}
|
||||||
|
</AlignedRow>
|
||||||
|
<ButtonRow>
|
||||||
|
<Button className="btn-primary" icon="play" label={t('Enable')} onClickAsync={::this.enableAsync}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
} else if (entity.status === CampaignStatus.ACTIVE) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<AlignedRow label={t('Send status')}>
|
||||||
|
{t('Your campaign is enabled and sending messages.')}
|
||||||
|
</AlignedRow>
|
||||||
|
<ButtonRow>
|
||||||
|
<Button className="btn-primary" icon="stop" label={t('Disable')} onClickAsync={::this.disableAsync}/>
|
||||||
|
</ButtonRow>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -362,6 +405,30 @@ export default class Status extends Component {
|
||||||
{ data: 3, title: t('List namespace') }
|
{ data: 3, title: t('List namespace') }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const campaignsChildrenColumns = [
|
||||||
|
{ data: 1, title: t('Name') },
|
||||||
|
{ data: 2, title: t('ID'), render: data => <code>{data}</code> },
|
||||||
|
{ data: 5, title: t('Status'), render: (data, display, rowData) => this.campaignStatusLabels[data] },
|
||||||
|
{ data: 8, title: t('Created'), render: data => moment(data).fromNow() },
|
||||||
|
{
|
||||||
|
actions: data => {
|
||||||
|
const actions = [];
|
||||||
|
const perms = data[10];
|
||||||
|
const campaignType = data[4];
|
||||||
|
const campaignSource = data[7];
|
||||||
|
|
||||||
|
if (perms.includes('viewStats')) {
|
||||||
|
actions.push({
|
||||||
|
label: <Icon icon="send" title={t('Status')}/>,
|
||||||
|
link: `/campaigns/${data[0]}/status`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Title>{t('Campaign Status')}</Title>
|
<Title>{t('Campaign Status')}</Title>
|
||||||
|
@ -383,6 +450,15 @@ export default class Status extends Component {
|
||||||
<hr/>
|
<hr/>
|
||||||
|
|
||||||
<SendControls entity={entity} refreshEntity={::this.refreshEntity}/>
|
<SendControls entity={entity} refreshEntity={::this.refreshEntity}/>
|
||||||
|
|
||||||
|
{entity.type === CampaignType.RSS &&
|
||||||
|
<div>
|
||||||
|
<hr/>
|
||||||
|
<h3>RSS Entries</h3>
|
||||||
|
<p>{t('If a new entry is found from campaign feed a new subcampaign is created of that entry and it will be listed here')}</p>
|
||||||
|
<Table withHeader dataUrl={`rest/campaigns-children/${this.props.entity.id}`} columns={campaignsChildrenColumns} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,55 +31,4 @@ export function getCampaignLabels(t) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FIXME - this is not used at the moment, but it's kept here because it will be probably needed at some later point of time.
|
|
||||||
export function getDefaultMergeTags(t) {
|
|
||||||
return [{
|
|
||||||
key: 'LINK_UNSUBSCRIBE',
|
|
||||||
value: t('URL that points to the unsubscribe page')
|
|
||||||
}, {
|
|
||||||
key: 'LINK_PREFERENCES',
|
|
||||||
value: t('URL that points to the preferences page of the subscriber')
|
|
||||||
}, {
|
|
||||||
key: 'LINK_BROWSER',
|
|
||||||
value: t('URL to preview the message in a browser')
|
|
||||||
}, {
|
|
||||||
key: 'EMAIL',
|
|
||||||
value: t('Email address')
|
|
||||||
}, {
|
|
||||||
key: 'SUBSCRIPTION_ID',
|
|
||||||
value: t('Unique ID that identifies the recipient')
|
|
||||||
}, {
|
|
||||||
key: 'LIST_ID',
|
|
||||||
value: t('Unique ID that identifies the list used for this campaign')
|
|
||||||
}, {
|
|
||||||
key: 'CAMPAIGN_ID',
|
|
||||||
value: t('Unique ID that identifies current campaign')
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRSSMergeTags(t) {
|
|
||||||
return [{
|
|
||||||
key: 'RSS_ENTRY',
|
|
||||||
value: t('content from an RSS entry')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_TITLE',
|
|
||||||
value: t('RSS entry title')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_DATE',
|
|
||||||
value: t('RSS entry date')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_LINK',
|
|
||||||
value: t('RSS entry link')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_CONTENT',
|
|
||||||
value: t('content from an RSS entry')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_SUMMARY',
|
|
||||||
value: t('RSS entry summary')
|
|
||||||
}, {
|
|
||||||
key: 'RSS_ENTRY_IMAGE_URL',
|
|
||||||
value: t('RSS entry image URL')
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ import {Icon, ModalDialog} from "./bootstrap-components";
|
||||||
import axios from './axios';
|
import axios from './axios';
|
||||||
import styles from "./styles.scss";
|
import styles from "./styles.scss";
|
||||||
import {withPageHelpers} from "./page";
|
import {withPageHelpers} from "./page";
|
||||||
import {getUrl} from "./urls";
|
import {getUrl, getPublicUrl} from "./urls";
|
||||||
|
|
||||||
@translate()
|
@translate()
|
||||||
@withErrorHandling
|
@withErrorHandling
|
||||||
|
@ -119,9 +119,9 @@ export default class Files extends Component {
|
||||||
|
|
||||||
let downloadUrl;
|
let downloadUrl;
|
||||||
if (this.props.usePublicDownloadUrls) {
|
if (this.props.usePublicDownloadUrls) {
|
||||||
downloadUrl =`/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`;
|
downloadUrl = getPublicUrl(`files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${this.props.entity.id}/${data[2]}`);
|
||||||
} else {
|
} else {
|
||||||
downloadUrl =`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`;
|
downloadUrl = getUrl(`rest/files/${this.props.entityTypeId}/${this.props.entitySubTypeId}/${data[0]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
actions.push({
|
actions.push({
|
||||||
|
|
|
@ -37,7 +37,8 @@ import {
|
||||||
} from '../../../../shared/imports';
|
} from '../../../../shared/imports';
|
||||||
import axios from "../../lib/axios";
|
import axios from "../../lib/axios";
|
||||||
import {getUrl} from "../../lib/urls";
|
import {getUrl} from "../../lib/urls";
|
||||||
import styles from "../styles.scss";
|
import listStyles from "../styles.scss";
|
||||||
|
import styles from "../../lib/styles.scss";
|
||||||
|
|
||||||
|
|
||||||
function truncate(str, len, ending = '...') {
|
function truncate(str, len, ending = '...') {
|
||||||
|
@ -372,7 +373,7 @@ export default class CUD extends Component {
|
||||||
mappingSettings = (
|
mappingSettings = (
|
||||||
<div>
|
<div>
|
||||||
{settingsRows}
|
{settingsRows}
|
||||||
<Fieldset label={t('Mapping')} className={styles.mapping}>
|
<Fieldset label={t('Mapping')} className={listStyles.mapping}>
|
||||||
{mappingRows}
|
{mappingRows}
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -156,7 +156,8 @@ export default class List extends Component {
|
||||||
<div>
|
<div>
|
||||||
{tableDeleteDialogRender(this, `rest/subscriptions/${this.props.list.id}`, t('Deleting subscription ...'), t('Subscription deleted'))}
|
{tableDeleteDialogRender(this, `rest/subscriptions/${this.props.list.id}`, t('Deleting subscription ...'), t('Subscription deleted'))}
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)} className="btn-default"><Button label={t('Subscription Form')} className="btn-default"/></a>
|
<a href={getPublicUrl(`subscription/${this.props.list.cid}`)}><Button label={t('Subscription Form')} className="btn-default"/></a>
|
||||||
|
<a href={getUrl(`subscriptions/export/${this.props.list.id}/`+ (this.props.segmentId || 0))}><Button label={t('Export as CSV')} className="btn-primary"/></a>
|
||||||
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
|
<NavButton linkTo={`/lists/${this.props.list.id}/subscriptions/create`} className="btn-primary" icon="plus" label={t('Add Subscriber')}/>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default class Output extends Component {
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async loadOutput() {
|
async loadOutput() {
|
||||||
const id = parseInt(this.props.match.params.id);
|
const id = parseInt(this.props.match.params.reportId);
|
||||||
const outputRespPromise = axios.get(getUrl(`rest/report-output/${id}`));
|
const outputRespPromise = axios.get(getUrl(`rest/report-output/${id}`));
|
||||||
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
||||||
const [outputResp, reportResp] = await Promise.all([outputRespPromise, reportRespPromise]);
|
const [outputResp, reportResp] = await Promise.all([outputRespPromise, reportRespPromise]);
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default class View extends Component {
|
||||||
|
|
||||||
@withAsyncErrorHandler
|
@withAsyncErrorHandler
|
||||||
async loadContent() {
|
async loadContent() {
|
||||||
const id = parseInt(this.props.match.params.id);
|
const id = parseInt(this.props.match.params.reportId);
|
||||||
const contentRespPromise = axios.get(getUrl(`rest/report-content/${id}`));
|
const contentRespPromise = axios.get(getUrl(`rest/report-content/${id}`));
|
||||||
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
const reportRespPromise = axios.get(getUrl(`rest/reports/${id}`));
|
||||||
const [contentResp, reportResp] = await Promise.all([contentRespPromise, reportRespPromise]);
|
const [contentResp, reportResp] = await Promise.all([contentRespPromise, reportRespPromise]);
|
||||||
|
|
|
@ -509,6 +509,69 @@ export function getEditForm(owner, typeKey, prefix = '') {
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<Trans><p>For RSS campaigns, the following further tags can be used.</p></Trans>
|
||||||
|
<table className="table table-bordered table-condensed table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<Trans>Merge tag</Trans>
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<Trans>Description</Trans>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_TITLE]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>RSS entry title</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_DATE]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>RSS entry date</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_LINK]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>RSS entry link</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_CONTENT]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>Content of an RSS entry</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_SUMMARY]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>RSS entry summary</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
[RSS_ENTRY_IMAGE_URL]
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<Trans>RSS entry image URL</Trans>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>}
|
</div>}
|
||||||
</AlignedRow>
|
</AlignedRow>
|
||||||
|
|
||||||
|
|
|
@ -174,18 +174,18 @@ reports:
|
||||||
# then it's safer to switch off the reporting functionality below.
|
# then it's safer to switch off the reporting functionality below.
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
testserver:
|
testServer:
|
||||||
# Starts a vanity server that redirects all mail to /dev/null
|
# Starts a vanity server that redirects all mail to /dev/null
|
||||||
# Mostly needed for local development
|
# Mostly needed for local development
|
||||||
enabled: false
|
enabled: false
|
||||||
port: 5587
|
port: 5587
|
||||||
mailboxserverport: 3001
|
mailboxServerPort: 3001
|
||||||
host: 0.0.0.0
|
host: 0.0.0.0
|
||||||
username: testuser
|
username: testuser
|
||||||
password: testpass
|
password: testpass
|
||||||
logger: false
|
logger: false
|
||||||
|
|
||||||
seleniumwebdriver:
|
seleniumWebDriver:
|
||||||
browser: phantomjs
|
browser: phantomjs
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -273,7 +273,7 @@ If using VERP with iRedMail, see [this post](http://www.iredmail.org/forum/post4
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
There is a built in /dev/null server in Mailtrain that you can use to load test your installation. Check the `[testserver]` section in the configuration file for details. By default the test server is disabled. The server uses only cleartext connections, so select "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
|
There is a built in /dev/null server in Mailtrain that you can use to load test your installation. Check the `[testServer]` section in the configuration file for details. By default the test server is disabled. The server uses only cleartext connections, so select "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
|
||||||
|
|
||||||
Additionally you can generate CSV import files with fake subscriber data:
|
Additionally you can generate CSV import files with fake subscriber data:
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ const lists = require('../models/lists');
|
||||||
const fields = require('../models/fields');
|
const fields = require('../models/fields');
|
||||||
const sendConfigurations = require('../models/send-configurations');
|
const sendConfigurations = require('../models/send-configurations');
|
||||||
const links = require('../models/links');
|
const links = require('../models/links');
|
||||||
const {CampaignSource} = require('../shared/campaigns');
|
const {CampaignSource, CampaignType} = require('../shared/campaigns');
|
||||||
const {SubscriptionStatus} = require('../shared/lists');
|
const {SubscriptionStatus} = require('../shared/lists');
|
||||||
const tools = require('../lib/tools');
|
const tools = require('../lib/tools');
|
||||||
const request = require('request-promise');
|
const request = require('request-promise');
|
||||||
|
@ -68,7 +68,7 @@ class CampaignSender {
|
||||||
return prefix + 'cid:' + cid;
|
return prefix + 'cid:' + cid;
|
||||||
});
|
});
|
||||||
|
|
||||||
html = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, false, true);
|
html = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true);
|
||||||
|
|
||||||
text = (text || '').trim()
|
text = (text || '').trim()
|
||||||
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text)
|
? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text)
|
||||||
|
@ -91,7 +91,7 @@ class CampaignSender {
|
||||||
replyTo: getOverridable('reply_to'),
|
replyTo: getOverridable('reply_to'),
|
||||||
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
||||||
to: {
|
to: {
|
||||||
name: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false, false),
|
name: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false),
|
||||||
address: subscriptionGrouped.email
|
address: subscriptionGrouped.email
|
||||||
},
|
},
|
||||||
sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
||||||
|
@ -125,7 +125,7 @@ class CampaignSender {
|
||||||
list: {
|
list: {
|
||||||
unsubscribe: null
|
unsubscribe: null
|
||||||
},
|
},
|
||||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false, false),
|
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
|
||||||
|
@ -240,7 +240,7 @@ class CampaignSender {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, false, true) : html;
|
html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true) : html;
|
||||||
|
|
||||||
text = (text || '').trim()
|
text = (text || '').trim()
|
||||||
? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text)
|
? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text)
|
||||||
|
@ -253,12 +253,28 @@ class CampaignSender {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getExtraTags(campaign) {
|
||||||
|
const tags = {};
|
||||||
|
|
||||||
|
if (campaign.type === CampaignType.RSS_ENTRY) {
|
||||||
|
const rssEntry = campaign.data.rssEntry;
|
||||||
|
tags['RSS_ENTRY_TITLE'] = rssEntry.title;
|
||||||
|
tags['RSS_ENTRY_DATE'] = rssEntry.date;
|
||||||
|
tags['RSS_ENTRY_LINK'] = rssEntry.link;
|
||||||
|
tags['RSS_ENTRY_CONTENT'] = rssEntry.content;
|
||||||
|
tags['RSS_ENTRY_SUMMARY'] = rssEntry.summary;
|
||||||
|
tags['RSS_ENTRY_IMAGE_URL'] = rssEntry.image_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
async getMessage(listCid, subscriptionCid) {
|
async getMessage(listCid, subscriptionCid) {
|
||||||
const list = this.listsByCid.get(listCid);
|
const list = this.listsByCid.get(listCid);
|
||||||
const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
|
const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
|
||||||
const flds = this.listsFieldsGrouped.get(list.id);
|
const flds = this.listsFieldsGrouped.get(list.id);
|
||||||
const campaign = this.campaign;
|
const campaign = this.campaign;
|
||||||
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped);
|
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
|
||||||
|
|
||||||
return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false);
|
return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false);
|
||||||
}
|
}
|
||||||
|
@ -272,7 +288,8 @@ class CampaignSender {
|
||||||
const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
|
const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), list.id, email);
|
||||||
const flds = this.listsFieldsGrouped.get(listId);
|
const flds = this.listsFieldsGrouped.get(listId);
|
||||||
const campaign = this.campaign;
|
const campaign = this.campaign;
|
||||||
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped);
|
|
||||||
|
const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
|
||||||
|
|
||||||
const encryptionKeys = [];
|
const encryptionKeys = [];
|
||||||
for (const fld of flds) {
|
for (const fld of flds) {
|
||||||
|
@ -314,7 +331,7 @@ class CampaignSender {
|
||||||
replyTo: getOverridable('reply_to'),
|
replyTo: getOverridable('reply_to'),
|
||||||
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
|
||||||
to: {
|
to: {
|
||||||
name: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false, false),
|
name: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false),
|
||||||
address: subscriptionGrouped.email
|
address: subscriptionGrouped.email
|
||||||
},
|
},
|
||||||
sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
|
||||||
|
@ -348,7 +365,7 @@ class CampaignSender {
|
||||||
list: {
|
list: {
|
||||||
unsubscribe: listUnsubscribe
|
unsubscribe: listUnsubscribe
|
||||||
},
|
},
|
||||||
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false, false),
|
subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
|
||||||
html,
|
html,
|
||||||
text,
|
text,
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ const fs = require('fs');
|
||||||
const pathlib = require('path');
|
const pathlib = require('path');
|
||||||
const Handlebars = require('handlebars');
|
const Handlebars = require('handlebars');
|
||||||
|
|
||||||
const highestLegacySchemaVersion = 29;
|
const highestLegacySchemaVersion = 33;
|
||||||
|
|
||||||
const mysqlConfig = {
|
const mysqlConfig = {
|
||||||
multipleStatements: true
|
multipleStatements: true
|
||||||
|
@ -69,13 +69,13 @@ function getSchemaVersion(callback) {
|
||||||
}
|
}
|
||||||
|
|
||||||
connection.query('SHOW TABLES LIKE "knex_migrations"', (err, rows) => {
|
connection.query('SHOW TABLES LIKE "knex_migrations"', (err, rows) => {
|
||||||
if (rows) {
|
|
||||||
connection.release();
|
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
connection.release();
|
||||||
|
|
||||||
callback(null, highestLegacySchemaVersion);
|
callback(null, highestLegacySchemaVersion);
|
||||||
} else {
|
} else {
|
||||||
connection.query('SELECT `value` FROM `settings` WHERE `key`=?', ['db_schema_version'], (err, rows) => {
|
connection.query('SELECT `value` FROM `settings` WHERE `key`=?', ['db_schema_version'], (err, rows) => {
|
||||||
|
|
|
@ -29,6 +29,10 @@ const entityTypes = {
|
||||||
entitiesTable: 'campaigns',
|
entitiesTable: 'campaigns',
|
||||||
sharesTable: 'shares_campaign',
|
sharesTable: 'shares_campaign',
|
||||||
permissionsTable: 'permissions_campaign',
|
permissionsTable: 'permissions_campaign',
|
||||||
|
dependentPermissions: {
|
||||||
|
extraColumns: ['parent'],
|
||||||
|
getParent: entity => entity.parent
|
||||||
|
},
|
||||||
files: {
|
files: {
|
||||||
file: {
|
file: {
|
||||||
table: 'files_campaign_file',
|
table: 'files_campaign_file',
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
const fork = require('child_process').fork;
|
const fork = require('child_process').fork;
|
||||||
const log = require('./log');
|
const log = require('./log');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const senders = require('./senders');
|
||||||
|
|
||||||
let feedcheckProcess;
|
let feedcheckProcess;
|
||||||
|
|
||||||
|
@ -23,6 +24,8 @@ function spawn(callback) {
|
||||||
if (msg.type === 'feedcheck-started') {
|
if (msg.type === 'feedcheck-started') {
|
||||||
log.info('Feed', 'Feedcheck process started');
|
log.info('Feed', 'Feedcheck process started');
|
||||||
return callback();
|
return callback();
|
||||||
|
} else if (msg.type === 'entries-added') {
|
||||||
|
senders.scheduleCheck();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -88,7 +88,7 @@ async function tryStartWorkers() {
|
||||||
startWorker(report);
|
startWorker(report);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
log.info('ReportProcessor', 'No more report to start a worker for');
|
log.info('ReportProcessor', 'No more reports to start a worker for');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,9 +108,7 @@ function validateEmailGetMessage(result, address) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatMessage(campaign, list, subscription, mergeTags, message, filter, isHTML) {
|
function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) {
|
||||||
filter = typeof filter === 'function' ? filter : (str => str);
|
|
||||||
|
|
||||||
const links = getMessageLinks(campaign, list, subscription);
|
const links = getMessageLinks(campaign, list, subscription);
|
||||||
|
|
||||||
const getValue = key => {
|
const getValue = key => {
|
||||||
|
@ -135,7 +133,7 @@ function formatMessage(campaign, list, subscription, mergeTags, message, filter,
|
||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
value = (value || fallback || '').trim();
|
value = (value || fallback || '').trim();
|
||||||
return filter(value);
|
return value;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +148,7 @@ async function prepareHtml(html) {
|
||||||
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
|
FetchExternalResources: false, // disables resource loading over HTTP / filesystem
|
||||||
ProcessExternalResources: false // do not execute JS within script blocks
|
ProcessExternalResources: false // do not execute JS within script blocks
|
||||||
}
|
}
|
||||||
});pre
|
});
|
||||||
|
|
||||||
const head = win.document.querySelector('head');
|
const head = win.document.querySelector('head');
|
||||||
let hasCharsetTag = false;
|
let hasCharsetTag = false;
|
||||||
|
|
|
@ -22,6 +22,7 @@ const allowedKeysCommon = ['name', 'description', 'segment', 'namespace',
|
||||||
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
'send_configuration', 'from_name_override', 'from_email_override', 'reply_to_override', 'subject_override', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
|
||||||
|
|
||||||
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
|
||||||
|
const allowedKeysCreateRssEntry = new Set(['type', 'source', 'parent', ...allowedKeysCommon]);
|
||||||
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
|
const allowedKeysUpdate = new Set([...allowedKeysCommon]);
|
||||||
|
|
||||||
const Content = {
|
const Content = {
|
||||||
|
@ -32,7 +33,6 @@ const Content = {
|
||||||
SETTINGS_WITH_STATS: 4
|
SETTINGS_WITH_STATS: 4
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function hash(entity, content) {
|
function hash(entity, content) {
|
||||||
let filteredEntity;
|
let filteredEntity;
|
||||||
|
|
||||||
|
@ -63,11 +63,25 @@ async function listDTAjax(context, params) {
|
||||||
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||||
params,
|
params,
|
||||||
builder => builder.from('campaigns')
|
builder => builder.from('campaigns')
|
||||||
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace'),
|
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||||
|
.whereNull('campaigns.parent'),
|
||||||
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listChildrenDTAjax(context, campaignId, params) {
|
||||||
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
|
context,
|
||||||
|
[{ entityTypeId: 'campaign', requiredOperations: ['view'] }],
|
||||||
|
params,
|
||||||
|
builder => builder.from('campaigns')
|
||||||
|
.innerJoin('namespaces', 'namespaces.id', 'campaigns.namespace')
|
||||||
|
.where('campaigns.parent', campaignId),
|
||||||
|
['campaigns.id', 'campaigns.name', 'campaigns.cid', 'campaigns.description', 'campaigns.type', 'campaigns.status', 'campaigns.scheduled', 'campaigns.source', 'campaigns.created', 'namespaces.name']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function listWithContentDTAjax(context, params) {
|
async function listWithContentDTAjax(context, params) {
|
||||||
return await dtHelpers.ajaxListWithPermissions(
|
return await dtHelpers.ajaxListWithPermissions(
|
||||||
context,
|
context,
|
||||||
|
@ -368,12 +382,21 @@ async function _createTx(tx, context, entity, content) {
|
||||||
|
|
||||||
await _validateAndPreprocess(tx, context, entity, true, content);
|
await _validateAndPreprocess(tx, context, entity, true, content);
|
||||||
|
|
||||||
const filteredEntity = filterObject(entity, allowedKeysCreate);
|
const filteredEntity = filterObject(entity, entity.type === CampaignType.RSS_ENTRY ? allowedKeysCreateRssEntry : allowedKeysCreate);
|
||||||
filteredEntity.cid = shortid.generate();
|
filteredEntity.cid = shortid.generate();
|
||||||
|
|
||||||
const data = filteredEntity.data;
|
const data = filteredEntity.data;
|
||||||
|
|
||||||
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
filteredEntity.data = JSON.stringify(filteredEntity.data);
|
||||||
|
|
||||||
|
if (filteredEntity.type === CampaignType.RSS || filteredEntity.type === CampaignType.TRIGGERED) {
|
||||||
|
filteredEntity.status = CampaignStatus.ACTIVE;
|
||||||
|
} else if (filteredEntity.type === CampaignType.RSS_ENTRY) {
|
||||||
|
filteredEntity.status = CampaignStatus.SCHEDULED;
|
||||||
|
} else {
|
||||||
|
filteredEntity.status = CampaignStatus.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
const ids = await tx('campaigns').insert(filteredEntity);
|
const ids = await tx('campaigns').insert(filteredEntity);
|
||||||
const id = ids[0];
|
const id = ids[0];
|
||||||
|
|
||||||
|
@ -386,7 +409,11 @@ async function _createTx(tx, context, entity, content) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filteredEntity.parent) {
|
||||||
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id, parentId: filteredEntity.parent });
|
||||||
|
} else {
|
||||||
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'campaign', entityId: id });
|
||||||
|
}
|
||||||
|
|
||||||
if (copyFilesFrom) {
|
if (copyFilesFrom) {
|
||||||
await files.copyAllTx(tx, context, copyFilesFrom.entityType, 'file', copyFilesFrom.entityId, 'campaign', 'file', id);
|
await files.copyAllTx(tx, context, copyFilesFrom.entityType, 'file', copyFilesFrom.entityId, 'campaign', 'file', id);
|
||||||
|
@ -459,15 +486,24 @@ async function updateWithConsistencyCheck(context, entity, content) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function remove(context, id) {
|
async function _removeTx(tx, context, id, existing = null) {
|
||||||
await knex.transaction(async tx => {
|
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
|
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete');
|
||||||
|
|
||||||
const existing = tx('campaigns').where('id', id);
|
if (!existing) {
|
||||||
|
existing = await tx('campaigns').where('id', id).select(['id', 'status', 'type']).first();
|
||||||
|
}
|
||||||
|
|
||||||
if (existing.status === CampaignStatus.SENDING) {
|
if (existing.status === CampaignStatus.SENDING) {
|
||||||
return new interoperableErrors.InvalidStateError;
|
return new interoperableErrors.InvalidStateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enforce(existing.type === CampaignType.REGULAR || existing.type === CampaignType.RSS || existing.type === CampaignType.TRIGGERED, 'This campaign cannot be removed by user.');
|
||||||
|
|
||||||
|
const childCampaigns = await tx('campaigns').where('parent', id).select(['id', 'status', 'type']);
|
||||||
|
for (const childCampaign of childCampaigns) {
|
||||||
|
await _removeTx(tx, contect, childCampaign.id, childCampaign);
|
||||||
|
}
|
||||||
|
|
||||||
await files.removeAllTx(tx, context, 'campaign', 'file', id);
|
await files.removeAllTx(tx, context, 'campaign', 'file', id);
|
||||||
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
|
await files.removeAllTx(tx, context, 'campaign', 'attachment', id);
|
||||||
|
|
||||||
|
@ -482,6 +518,12 @@ async function remove(context, id) {
|
||||||
.del();
|
.del();
|
||||||
|
|
||||||
await tx('campaigns').where('id', id).del();
|
await tx('campaigns').where('id', id).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function remove(context, id) {
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await _removeTx(tx, context, id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -705,8 +747,6 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
|
||||||
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(scheduled);
|
|
||||||
|
|
||||||
await tx('campaigns').where('id', campaignId).update({
|
await tx('campaigns').where('id', campaignId).update({
|
||||||
status: newState,
|
status: newState,
|
||||||
scheduled
|
scheduled
|
||||||
|
@ -747,9 +787,20 @@ async function reset(context, campaignId) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function enable(context, campaignId) {
|
||||||
|
await _changeStatus(context, campaignId, [CampaignStatus.INACTIVE], CampaignStatus.ACTIVE, 'Cannot enable campaign unless it is in INACTIVE state');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disable(context, campaignId) {
|
||||||
|
await _changeStatus(context, campaignId, [CampaignStatus.ACTIVE], CampaignStatus.INACTIVE, 'Cannot disable campaign unless it is in ACTIVE state');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports.Content = Content;
|
module.exports.Content = Content;
|
||||||
module.exports.hash = hash;
|
module.exports.hash = hash;
|
||||||
module.exports.listDTAjax = listDTAjax;
|
module.exports.listDTAjax = listDTAjax;
|
||||||
|
module.exports.listChildrenDTAjax = listChildrenDTAjax;
|
||||||
module.exports.listWithContentDTAjax = listWithContentDTAjax;
|
module.exports.listWithContentDTAjax = listWithContentDTAjax;
|
||||||
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;
|
module.exports.listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax;
|
||||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||||
|
@ -774,6 +825,8 @@ module.exports.getSubscribersQueryGeneratorTx = getSubscribersQueryGeneratorTx;
|
||||||
module.exports.start = start;
|
module.exports.start = start;
|
||||||
module.exports.stop = stop;
|
module.exports.stop = stop;
|
||||||
module.exports.reset = reset;
|
module.exports.reset = reset;
|
||||||
|
module.exports.enable = enable;
|
||||||
|
module.exports.disable = disable;
|
||||||
|
|
||||||
module.exports.rawGetByTx = rawGetByTx;
|
module.exports.rawGetByTx = rawGetByTx;
|
||||||
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
|
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
|
|
@ -694,10 +694,11 @@ async function forHbs(context, listId, subscription) { // assumes grouped subscr
|
||||||
return forHbsWithFieldsGrouped(flds, subscription);
|
return forHbsWithFieldsGrouped(flds, subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMergeTags(fieldsGrouped, subscription) { // assumes grouped subscription
|
function getMergeTags(fieldsGrouped, subscription, extraTags = {}) { // assumes grouped subscription
|
||||||
const mergeTags = {
|
const mergeTags = {
|
||||||
'EMAIL': subscription.email,
|
'EMAIL': subscription.email,
|
||||||
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl())
|
...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
|
||||||
|
...extraTags
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const fld of fieldsGrouped) {
|
for (const fld of fieldsGrouped) {
|
||||||
|
|
|
@ -195,24 +195,6 @@ async function remove(context, id) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getMergeTags(context, id) {
|
|
||||||
return await knex.transaction(async tx => {
|
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', id, ['view']);
|
|
||||||
const groupedFields = await fields.listGroupedTx(tx, id);
|
|
||||||
|
|
||||||
const mergeTags = [];
|
|
||||||
for (const field of groupedFields) {
|
|
||||||
mergeTags.push({
|
|
||||||
key: field.key,
|
|
||||||
value: field.name
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeTags;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports.UnsubscriptionMode = UnsubscriptionMode;
|
module.exports.UnsubscriptionMode = UnsubscriptionMode;
|
||||||
module.exports.hash = hash;
|
module.exports.hash = hash;
|
||||||
|
@ -226,4 +208,3 @@ module.exports.getByCid = getByCid;
|
||||||
module.exports.create = create;
|
module.exports.create = create;
|
||||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||||
module.exports.remove = remove;
|
module.exports.remove = remove;
|
||||||
module.exports.getMergeTags = getMergeTags;
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ function hash(entity) {
|
||||||
return hasher.hash(filterObject(entity, allowedKeys));
|
return hasher.hash(filterObject(entity, allowedKeys));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getByIdWithTemplate(context, id) {
|
async function getByIdWithTemplate(context, id, withPermissions = true) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view');
|
await shares.enforceEntityPermissionTx(tx, context, 'report', id, 'view');
|
||||||
|
|
||||||
|
@ -33,7 +33,9 @@ async function getByIdWithTemplate(context, id) {
|
||||||
entity.user_fields = JSON.parse(entity.user_fields);
|
entity.user_fields = JSON.parse(entity.user_fields);
|
||||||
entity.params = JSON.parse(entity.params);
|
entity.params = JSON.parse(entity.params);
|
||||||
|
|
||||||
|
if (withPermissions) {
|
||||||
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
|
entity.permissions = await shares.getPermissionsTx(tx, context, 'report', id);
|
||||||
|
}
|
||||||
|
|
||||||
return entity;
|
return entity;
|
||||||
});
|
});
|
||||||
|
|
|
@ -100,7 +100,14 @@ async function assign(context, entityTypeId, entityId, userId, role) {
|
||||||
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
await enforceEntityPermissionTx(tx, context, entityTypeId, entityId, 'share');
|
||||||
|
|
||||||
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
enforce(await tx('users').where('id', userId).select('id').first(), 'Invalid user id');
|
||||||
enforce(await tx(entityType.entitiesTable).where('id', entityId).select('id').first(), 'Invalid entity id');
|
|
||||||
|
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
|
||||||
|
const entity = await tx(entityType.entitiesTable).where('id', entityId).select(['id', ...extraColumns]).first();
|
||||||
|
enforce(entity, 'Invalid entity id');
|
||||||
|
|
||||||
|
if (entityType.dependentPermissions) {
|
||||||
|
enforce(!entityType.dependentPermissions.getParent(entity), 'Cannot share/unshare a dependent entity');
|
||||||
|
}
|
||||||
|
|
||||||
const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();
|
const entry = await tx(entityType.sharesTable).where({user: userId, entity: entityId}).select('role').first();
|
||||||
|
|
||||||
|
@ -310,13 +317,51 @@ async function rebuildPermissionsTx(tx, restriction) {
|
||||||
}
|
}
|
||||||
await expungeQuery;
|
await expungeQuery;
|
||||||
|
|
||||||
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace']);
|
const extraColumns = entityType.dependentPermissions ? entityType.dependentPermissions.extraColumns : [];
|
||||||
|
const entitiesQuery = tx(entityType.entitiesTable).select(['id', 'namespace', ...extraColumns]);
|
||||||
|
|
||||||
|
|
||||||
|
const notToBeInserted = new Set();
|
||||||
if (restriction.entityId) {
|
if (restriction.entityId) {
|
||||||
|
if (restriction.parentId) {
|
||||||
|
notToBeInserted.add(restriction.parentId);
|
||||||
|
entitiesQuery.whereIn('id', [restriction.entityId, restriction.parentId]);
|
||||||
|
} else {
|
||||||
entitiesQuery.where('id', restriction.entityId);
|
entitiesQuery.where('id', restriction.entityId);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const entities = await entitiesQuery;
|
const entities = await entitiesQuery;
|
||||||
|
|
||||||
|
// TODO - process restriction.parentId
|
||||||
|
|
||||||
|
const parentEntities = new Map();
|
||||||
|
let nonChildEntities;
|
||||||
|
if (entityType.dependentPermissions) {
|
||||||
|
nonChildEntities = [];
|
||||||
|
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
|
const parent = entityType.dependentPermissions.getParent(entity);
|
||||||
|
|
||||||
|
if (parent) {
|
||||||
|
let childEntities;
|
||||||
|
if (parentEntities.has(parent)) {
|
||||||
|
childEntities = parentEntities.get(parent);
|
||||||
|
} else {
|
||||||
|
childEntities = [];
|
||||||
|
parentEntities.set(parent, childEntities);
|
||||||
|
}
|
||||||
|
|
||||||
|
childEntities.push(entity.id);
|
||||||
|
} else {
|
||||||
|
nonChildEntities.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
nonChildEntities = entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const entity of nonChildEntities) {
|
||||||
const permsPerUser = new Map();
|
const permsPerUser = new Map();
|
||||||
|
|
||||||
if (entity.namespace) { // The root namespace has not parent namespace, thus the test
|
if (entity.namespace) { // The root namespace has not parent namespace, thus the test
|
||||||
|
@ -350,6 +395,7 @@ async function rebuildPermissionsTx(tx, restriction) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!notToBeInserted.has(entity.id)) {
|
||||||
for (const userPermsPair of permsPerUser.entries()) {
|
for (const userPermsPair of permsPerUser.entries()) {
|
||||||
const data = [];
|
const data = [];
|
||||||
|
|
||||||
|
@ -362,6 +408,27 @@ async function rebuildPermissionsTx(tx, restriction) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (parentEntities.has(entity.id)) {
|
||||||
|
const childEntities = parentEntities.get(entity.id);
|
||||||
|
|
||||||
|
for (const childId of childEntities) {
|
||||||
|
for (const userPermsPair of permsPerUser.entries()) {
|
||||||
|
const data = [];
|
||||||
|
|
||||||
|
for (const operation of userPermsPair[1]) {
|
||||||
|
if (operation !== 'share') {
|
||||||
|
data.push({user: userPermsPair[0], entity: childId, operation});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length > 0) {
|
||||||
|
await tx(entityType.permissionsTable).insert(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -81,7 +81,7 @@ function getSubscriptionTableName(listId) {
|
||||||
return `subscription__${listId}`;
|
return `subscription__${listId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getGroupedFieldsMap(tx, listId) {
|
async function getGroupedFieldsMapTx(tx, listId) {
|
||||||
const groupedFields = await fields.listGroupedTx(tx, listId);
|
const groupedFields = await fields.listGroupedTx(tx, listId);
|
||||||
const result = {};
|
const result = {};
|
||||||
for (const fld of groupedFields) {
|
for (const fld of groupedFields) {
|
||||||
|
@ -189,7 +189,7 @@ function hashByAllowedKeys(allowedKeys, entity) {
|
||||||
|
|
||||||
async function hashByList(listId, entity) {
|
async function hashByList(listId, entity) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
||||||
return hashByAllowedKeys(allowedKeys, entity);
|
return hashByAllowedKeys(allowedKeys, entity);
|
||||||
});
|
});
|
||||||
|
@ -204,7 +204,7 @@ async function _getByTx(tx, context, listId, key, value, grouped) {
|
||||||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
|
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
groupSubscription(groupedFieldsMap, entity);
|
groupSubscription(groupedFieldsMap, entity);
|
||||||
|
@ -248,7 +248,7 @@ async function listDTAjax(context, listId, segmentId, params) {
|
||||||
// to group the fields. Then we copy relevant values form grouped subscription to ajaxList's data which then get
|
// to group the fields. Then we copy relevant values form grouped subscription to ajaxList's data which then get
|
||||||
// returned to the client. During the copy, we also render the values.
|
// returned to the client. During the copy, we also render the values.
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
|
const listFlds = await fields.listByOrderListTx(tx, listId, ['column', 'id']);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
@ -387,7 +387,7 @@ async function list(context, listId, grouped = true, offset, limit) {
|
||||||
const entities = await entitiesQry;
|
const entities = await entitiesQry;
|
||||||
|
|
||||||
if (grouped) {
|
if (grouped) {
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
|
|
||||||
for (const entity of entities) {
|
for (const entity of entities) {
|
||||||
groupSubscription(groupedFieldsMap, entity);
|
groupSubscription(groupedFieldsMap, entity);
|
||||||
|
@ -401,6 +401,48 @@ async function list(context, listId, grouped = true, offset, limit) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note that this does not do all the work in the transaction. Thus it is prone to fail if the list is deleted in during the run of the function
|
||||||
|
async function* listIterator(context, listId, segmentId, grouped = true) {
|
||||||
|
let groupedFieldsMap;
|
||||||
|
let addSegmentQuery;
|
||||||
|
|
||||||
|
await knex.transaction(async tx => {
|
||||||
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
|
||||||
|
|
||||||
|
if (grouped) {
|
||||||
|
groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
|
}
|
||||||
|
|
||||||
|
addSegmentQuery = segmentId ? await segments.getQueryGeneratorTx(tx, listId, segmentId) : () => {};
|
||||||
|
});
|
||||||
|
|
||||||
|
let lastId = 0;
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const entities = await knex(getSubscriptionTableName(listId))
|
||||||
|
.orderBy('id', 'asc')
|
||||||
|
.where('id', '>', lastId)
|
||||||
|
.where(function() {
|
||||||
|
addSegmentQuery(this);
|
||||||
|
})
|
||||||
|
.limit(500);
|
||||||
|
|
||||||
|
if (entities.length > 0) {
|
||||||
|
for (const entity of entities) {
|
||||||
|
if (grouped) {
|
||||||
|
groupSubscription(groupedFieldsMap, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
yield entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastId = entities[entities.length - 1].id;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function serverValidate(context, listId, data) {
|
async function serverValidate(context, listId, data) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
@ -563,7 +605,7 @@ async function createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMa
|
||||||
|
|
||||||
async function create(context, listId, entity, source, meta) {
|
async function create(context, listId, entity, source, meta) {
|
||||||
return await knex.transaction(async tx => {
|
return await knex.transaction(async tx => {
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
return await createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMap, entity, source, meta);
|
return await createTxWithGroupedFieldsMap(tx, context, listId, groupedFieldsMap, entity, source, meta);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -577,7 +619,7 @@ async function updateWithConsistencyCheck(context, listId, entity, source) {
|
||||||
throw new interoperableErrors.NotFoundError();
|
throw new interoperableErrors.NotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
const allowedKeys = getAllowedKeys(groupedFieldsMap);
|
||||||
|
|
||||||
groupSubscription(groupedFieldsMap, existing);
|
groupSubscription(groupedFieldsMap, existing);
|
||||||
|
@ -718,7 +760,7 @@ async function updateManaged(context, listId, cid, entity) {
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
|
||||||
|
|
||||||
const groupedFieldsMap = await getGroupedFieldsMap(tx, listId);
|
const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
|
||||||
|
|
||||||
const update = {};
|
const update = {};
|
||||||
for (const key in groupedFieldsMap) {
|
for (const key in groupedFieldsMap) {
|
||||||
|
@ -764,11 +806,12 @@ module.exports.getByCidTx = getByCidTx;
|
||||||
module.exports.getByCid = getByCid;
|
module.exports.getByCid = getByCid;
|
||||||
module.exports.getByEmail = getByEmail;
|
module.exports.getByEmail = getByEmail;
|
||||||
module.exports.list = list;
|
module.exports.list = list;
|
||||||
|
module.exports.listIterator = listIterator;
|
||||||
module.exports.listDTAjax = listDTAjax;
|
module.exports.listDTAjax = listDTAjax;
|
||||||
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
|
||||||
module.exports.serverValidate = serverValidate;
|
module.exports.serverValidate = serverValidate;
|
||||||
module.exports.create = create;
|
module.exports.create = create;
|
||||||
module.exports.getGroupedFieldsMap = getGroupedFieldsMap;
|
module.exports.getGroupedFieldsMapTx = getGroupedFieldsMapTx;
|
||||||
module.exports.createTxWithGroupedFieldsMap = createTxWithGroupedFieldsMap;
|
module.exports.createTxWithGroupedFieldsMap = createTxWithGroupedFieldsMap;
|
||||||
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
|
||||||
module.exports.remove = remove;
|
module.exports.remove = remove;
|
||||||
|
|
6593
package-lock.json
generated
6593
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "mailtrain",
|
"name": "mailtrain",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.24.0",
|
"version": "2.0.0",
|
||||||
"description": "Self hosted email newsletter app",
|
"description": "Self hosted email newsletter app",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -21,11 +21,10 @@
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/Mailtrain-org/mailtrain.git"
|
"url": "git://github.com/Mailtrain-org/mailtrain.git"
|
||||||
},
|
},
|
||||||
"author": "Andris Reinman",
|
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"homepage": "https://mailtrain.org/",
|
"homepage": "https://mailtrain.org/",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=5.0.0"
|
"node": ">=10.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-eslint": "^8.1.2",
|
"babel-eslint": "^8.1.2",
|
||||||
|
@ -33,9 +32,7 @@
|
||||||
"eslint-config-nodemailer": "^1.2.0",
|
"eslint-config-nodemailer": "^1.2.0",
|
||||||
"grunt": "^1.0.3",
|
"grunt": "^1.0.3",
|
||||||
"grunt-cli": "^1.2.0",
|
"grunt-cli": "^1.2.0",
|
||||||
"grunt-contrib-nodeunit": "^2.0.0",
|
|
||||||
"grunt-eslint": "^20.1.0",
|
"grunt-eslint": "^20.1.0",
|
||||||
"jsxgettext-andris": "^0.9.0-patch.1",
|
|
||||||
"lodash": "^4.17.10",
|
"lodash": "^4.17.10",
|
||||||
"mocha": "^5.2.0",
|
"mocha": "^5.2.0",
|
||||||
"phantomjs-prebuilt": "^2.1.15",
|
"phantomjs-prebuilt": "^2.1.15",
|
||||||
|
@ -43,7 +40,7 @@
|
||||||
"url-pattern": "^1.0.3"
|
"url-pattern": "^1.0.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"posix": "^4.1.1"
|
"posix": "^4.1.2"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"aws-sdk": "^2.307.0",
|
"aws-sdk": "^2.307.0",
|
||||||
|
@ -58,7 +55,8 @@
|
||||||
"cors": "^2.8.4",
|
"cors": "^2.8.4",
|
||||||
"crypto": "^1.0.1",
|
"crypto": "^1.0.1",
|
||||||
"csurf": "^1.9.0",
|
"csurf": "^1.9.0",
|
||||||
"csv-parse": "^1.2.3",
|
"csv-parse": "^1.3.3",
|
||||||
|
"csv-stringify": "^4.3.1",
|
||||||
"device": "^0.3.8",
|
"device": "^0.3.8",
|
||||||
"dompurify": "^1.0.2",
|
"dompurify": "^1.0.2",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
|
@ -87,7 +85,7 @@
|
||||||
"mjml": "^4.2.0",
|
"mjml": "^4.2.0",
|
||||||
"moment": "^2.18.1",
|
"moment": "^2.18.1",
|
||||||
"moment-timezone": "^0.5.13",
|
"moment-timezone": "^0.5.13",
|
||||||
"morgan": "^1.8.2",
|
"morgan": "^1.9.1",
|
||||||
"multer": "^1.3.0",
|
"multer": "^1.3.0",
|
||||||
"mysql2": "^1.3.5",
|
"mysql2": "^1.3.5",
|
||||||
"node-gettext": "^2.0.0-rc.1",
|
"node-gettext": "^2.0.0-rc.1",
|
||||||
|
@ -110,6 +108,7 @@
|
||||||
"slugify": "^1.2.8",
|
"slugify": "^1.2.8",
|
||||||
"smtp-server": "^3.1.0",
|
"smtp-server": "^3.1.0",
|
||||||
"toml": "^2.3.3",
|
"toml": "^2.3.3",
|
||||||
"try-require": "^1.2.1"
|
"try-require": "^1.2.1",
|
||||||
|
"xmldom": "^0.1.27"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ const router = require('../lib/router-async').create();
|
||||||
const links = require('../models/links');
|
const links = require('../models/links');
|
||||||
const interoperableErrors = require('../shared/interoperable-errors');
|
const interoperableErrors = require('../shared/interoperable-errors');
|
||||||
|
|
||||||
const trackImg = new Buffer('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
const trackImg = Buffer.from('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7', 'base64');
|
||||||
|
|
||||||
router.getAsync('/:campaign/:list/:subscription', async (req, res) => {
|
router.getAsync('/:campaign/:list/:subscription', async (req, res) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
|
|
|
@ -8,6 +8,11 @@ const contextHelpers = require('../lib/context-helpers');
|
||||||
|
|
||||||
const router = require('../lib/router-async').create();
|
const router = require('../lib/router-async').create();
|
||||||
|
|
||||||
|
const fileSuffixes = {
|
||||||
|
'text/html': '.html',
|
||||||
|
'text/csv': '.csv'
|
||||||
|
};
|
||||||
|
|
||||||
router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
||||||
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
await shares.enforceEntityPermission(req.context, 'report', req.params.id, 'viewContent');
|
||||||
|
|
||||||
|
@ -15,7 +20,7 @@ router.getAsync('/:id/download', passport.loggedIn, async (req, res) => {
|
||||||
|
|
||||||
if (report.state == reports.ReportState.FINISHED) {
|
if (report.state == reports.ReportState.FINISHED) {
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + '.csv',
|
'Content-Disposition': 'attachment;filename=' + reportHelpers.nameToFileName(report.name) + (fileSuffixes[report.mime_type] || ''),
|
||||||
'Content-Type': report.mime_type
|
'Content-Type': report.mime_type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,10 @@ router.postAsync('/campaigns-others-by-list-table/:campaignId/:listIds', passpor
|
||||||
return res.json(await campaigns.listOthersWhoseListsAreIncludedDTAjax(req.context, castToInteger(req.params.campaignId), req.params.listIds.split(';').map(x => castToInteger(x)), req.body));
|
return res.json(await campaigns.listOthersWhoseListsAreIncludedDTAjax(req.context, castToInteger(req.params.campaignId), req.params.listIds.split(';').map(x => castToInteger(x)), req.body));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.postAsync('/campaigns-children/:campaignId', passport.loggedIn, async (req, res) => {
|
||||||
|
return res.json(await campaigns.listChildrenDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
|
||||||
|
});
|
||||||
|
|
||||||
router.postAsync('/campaigns-test-users-table/:campaignId', passport.loggedIn, async (req, res) => {
|
router.postAsync('/campaigns-test-users-table/:campaignId', passport.loggedIn, async (req, res) => {
|
||||||
return res.json(await campaigns.listTestUsersDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
|
return res.json(await campaigns.listTestUsersDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
|
||||||
});
|
});
|
||||||
|
@ -82,4 +86,13 @@ router.postAsync('/campaign-reset/:campaignId', passport.loggedIn, passport.csrf
|
||||||
return res.json(await campaigns.reset(req.context, castToInteger(req.params.campaignId)));
|
return res.json(await campaigns.reset(req.context, castToInteger(req.params.campaignId)));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.postAsync('/campaign-enable/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
return res.json(await campaigns.enable(req.context, castToInteger(req.params.campaignId), null));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.postAsync('/campaign-disable/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||||
|
return res.json(await campaigns.disable(req.context, castToInteger(req.params.campaignId), null));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
|
@ -10,7 +10,7 @@ const contextHelpers = require('../../lib/context-helpers');
|
||||||
|
|
||||||
const router = require('../../lib/router-async').create();
|
const router = require('../../lib/router-async').create();
|
||||||
const {castToInteger} = require('../../lib/helpers');
|
const {castToInteger} = require('../../lib/helpers');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
|
router.getAsync('/reports/:reportId', passport.loggedIn, async (req, res) => {
|
||||||
const report = await reports.getByIdWithTemplate(req.context, castToInteger(req.params.reportId));
|
const report = await reports.getByIdWithTemplate(req.context, castToInteger(req.params.reportId));
|
||||||
|
@ -44,7 +44,7 @@ router.postAsync('/report-start/:id', passport.loggedIn, passport.csrfProtection
|
||||||
|
|
||||||
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
|
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
|
||||||
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
|
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
|
||||||
|
|
||||||
await reportProcessor.start(id);
|
await reportProcessor.start(id);
|
||||||
|
@ -56,7 +56,7 @@ router.postAsync('/report-stop/:id', async (req, res) => {
|
||||||
|
|
||||||
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
|
await shares.enforceEntityPermission(req.context, 'report', id, 'execute');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
|
||||||
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
|
await shares.enforceEntityPermission(req.context, 'reportTemplate', report.report_template, 'execute');
|
||||||
|
|
||||||
await reportProcessor.stop(id);
|
await reportProcessor.stop(id);
|
||||||
|
@ -68,8 +68,14 @@ router.getAsync('/report-content/:id', async (req, res) => {
|
||||||
|
|
||||||
await shares.enforceEntityPermission(req.context, 'report', id, 'viewContent');
|
await shares.enforceEntityPermission(req.context, 'report', id, 'viewContent');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
|
||||||
res.sendFile(reportHelpers.getReportContentFile(report));
|
const file = reportHelpers.getReportContentFile(report);
|
||||||
|
|
||||||
|
if (await fs.pathExists(file)) {
|
||||||
|
res.sendFile(file);
|
||||||
|
} else {
|
||||||
|
res.send('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
router.getAsync('/report-output/:id', async (req, res) => {
|
router.getAsync('/report-output/:id', async (req, res) => {
|
||||||
|
@ -77,8 +83,14 @@ router.getAsync('/report-output/:id', async (req, res) => {
|
||||||
|
|
||||||
await shares.enforceEntityPermission(req.context, 'report', id, 'viewOutput');
|
await shares.enforceEntityPermission(req.context, 'report', id, 'viewOutput');
|
||||||
|
|
||||||
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id);
|
const report = await reports.getByIdWithTemplate(contextHelpers.getAdminContext(), id, false);
|
||||||
res.sendFile(reportHelpers.getReportOutputFile(report));
|
const file = reportHelpers.getReportOutputFile(report);
|
||||||
|
|
||||||
|
if (await fs.pathExists(file)) {
|
||||||
|
res.sendFile(file);
|
||||||
|
} else {
|
||||||
|
res.send('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
59
routes/subscriptions.js
Normal file
59
routes/subscriptions.js
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const passport = require('../lib/passport');
|
||||||
|
const shares = require('../models/shares');
|
||||||
|
const contextHelpers = require('../lib/context-helpers');
|
||||||
|
const router = require('../lib/router-async').create();
|
||||||
|
const subscriptions = require('../models/subscriptions');
|
||||||
|
const {castToInteger} = require('../lib/helpers');
|
||||||
|
const stringify = require('csv-stringify')
|
||||||
|
const fields = require('../models/fields');
|
||||||
|
const lists = require('../models/lists');
|
||||||
|
const moment = require('moment');
|
||||||
|
|
||||||
|
router.getAsync('/export/:listId/:segmentId', passport.loggedIn, async (req, res) => {
|
||||||
|
const listId = castToInteger(req.params.listId);
|
||||||
|
const segmentId = castToInteger(req.params.segmentId);
|
||||||
|
|
||||||
|
const flds = await fields.list(req.context, listId);
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{key: 'cid', header: 'cid'},
|
||||||
|
{key: 'hash_email', header: 'HASH_EMAIL'},
|
||||||
|
{key: 'email', header: 'EMAIL'},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const fld of flds) {
|
||||||
|
if (fld.column) {
|
||||||
|
columns.push({
|
||||||
|
key: fld.column,
|
||||||
|
header: fld.key
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = await lists.getById(req.context, listId);
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Disposition': `attachment;filename=subscriptions-${list.cid}-${segmentId}-${moment().toISOString()}.csv`,
|
||||||
|
'Content-Type': 'text/csv'
|
||||||
|
};
|
||||||
|
|
||||||
|
res.set(headers);
|
||||||
|
|
||||||
|
const stringifier = stringify({
|
||||||
|
columns,
|
||||||
|
header: true,
|
||||||
|
delimiter: ','
|
||||||
|
});
|
||||||
|
|
||||||
|
stringifier.pipe(res);
|
||||||
|
|
||||||
|
for await (const subscription of subscriptions.listIterator(req.context, listId, segmentId, false)) {
|
||||||
|
stringifier.write(subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
stringifier.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
|
@ -60,7 +60,7 @@ async function run() {
|
||||||
.select('id')
|
.select('id')
|
||||||
.first()) {
|
.first()) {
|
||||||
|
|
||||||
const rssCampaign = campaigns.getById(contextHelpers.getAdminContext(), rssCampaignIdRow.id);
|
const rssCampaign = await campaigns.getById(contextHelpers.getAdminContext(), rssCampaignIdRow.id, false);
|
||||||
|
|
||||||
let checkStatus = null;
|
let checkStatus = null;
|
||||||
|
|
||||||
|
@ -92,6 +92,7 @@ async function run() {
|
||||||
campaignData.rssEntry = entry;
|
campaignData.rssEntry = entry;
|
||||||
|
|
||||||
const campaign = {
|
const campaign = {
|
||||||
|
parent: rssCampaign.id,
|
||||||
type: CampaignType.RSS_ENTRY,
|
type: CampaignType.RSS_ENTRY,
|
||||||
source,
|
source,
|
||||||
name: entry.title || `RSS entry ${entry.guid.substr(0, 67)}`,
|
name: entry.title || `RSS entry ${entry.guid.substr(0, 67)}`,
|
||||||
|
@ -103,7 +104,7 @@ async function run() {
|
||||||
from_email_override: rssCampaign.from_email_override,
|
from_email_override: rssCampaign.from_email_override,
|
||||||
reply_to_override: rssCampaign.reply_to_override,
|
reply_to_override: rssCampaign.reply_to_override,
|
||||||
subject_override: rssCampaign.subject_override,
|
subject_override: rssCampaign.subject_override,
|
||||||
data: JSON.stringify(campaignData),
|
data: campaignData,
|
||||||
|
|
||||||
click_tracking_disabled: rssCampaign.click_tracking_disabled,
|
click_tracking_disabled: rssCampaign.click_tracking_disabled,
|
||||||
open_tracking_disabled: rssCampaign.open_tracking_disabled,
|
open_tracking_disabled: rssCampaign.open_tracking_disabled,
|
||||||
|
@ -126,8 +127,12 @@ async function run() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (added > 0) {
|
if (added > 0) {
|
||||||
checkStatus = util.format(_('Found %s new campaign messages from feed'), added);
|
checkStatus = util.format(_('Found %s new campaign messages from feed %s'), added, rssCampaign.id);
|
||||||
log.verbose('Feed', `Added ${added} new campaigns for ${rssCampaign.id}`);
|
log.verbose('Feed', `Found ${added} new campaigns messages from feed ${rssCampaign.id}`);
|
||||||
|
|
||||||
|
process.send({
|
||||||
|
type: 'entries-added'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
checkStatus = _('Found nothing new from the feed');
|
checkStatus = _('Found nothing new from the feed');
|
||||||
}
|
}
|
||||||
|
|
|
@ -177,7 +177,7 @@ async function _execImportRun(impt, handlers) {
|
||||||
lastId = rows[rows.length - 1].id;
|
lastId = rows[rows.length - 1].id;
|
||||||
|
|
||||||
await knex.transaction(async tx => {
|
await knex.transaction(async tx => {
|
||||||
const groupedFieldsMap = await subscriptions.getGroupedFieldsMap(tx, impt.list);
|
const groupedFieldsMap = await subscriptions.getGroupedFieldsMapTx(tx, impt.list);
|
||||||
|
|
||||||
let newRows = 0;
|
let newRows = 0;
|
||||||
|
|
||||||
|
|
|
@ -107,11 +107,12 @@ async function processCampaign(campaignId) {
|
||||||
const msgQueue = [];
|
const msgQueue = [];
|
||||||
messageQueue.set(campaignId, msgQueue);
|
messageQueue.set(campaignId, msgQueue);
|
||||||
|
|
||||||
|
try {
|
||||||
while (true) {
|
while (true) {
|
||||||
const cpg = await knex('campaigns').where('id', campaignId).first();
|
const cpg = await knex('campaigns').where('id', campaignId).first();
|
||||||
|
|
||||||
if (cpg.status === CampaignStatus.PAUSED) {
|
if (cpg.status === CampaignStatus.PAUSED) {
|
||||||
await finish();
|
messageQueue.delete(campaignId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +161,10 @@ async function processCampaign(campaignId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`)
|
||||||
|
log.verbose(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,7 @@ const mailstore = {
|
||||||
const server = new SMTPServer({
|
const server = new SMTPServer({
|
||||||
|
|
||||||
// log to console
|
// log to console
|
||||||
logger: config.testserver.logger,
|
logger: config.testServer.logger,
|
||||||
|
|
||||||
// not required but nice-to-have
|
// not required but nice-to-have
|
||||||
banner: 'Welcome to My Awesome SMTP Server',
|
banner: 'Welcome to My Awesome SMTP Server',
|
||||||
|
@ -55,8 +55,8 @@ const server = new SMTPServer({
|
||||||
|
|
||||||
// Setup authentication
|
// Setup authentication
|
||||||
onAuth: (auth, session, callback) => {
|
onAuth: (auth, session, callback) => {
|
||||||
let username = config.testserver.username;
|
let username = config.testServer.username;
|
||||||
let password = config.testserver.password;
|
let password = config.testServer.password;
|
||||||
|
|
||||||
// check username and password
|
// check username and password
|
||||||
if (auth.username === username && auth.password === password) {
|
if (auth.username === username && auth.password === password) {
|
||||||
|
@ -169,9 +169,9 @@ mailBoxServer.on('error', 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, () => {
|
||||||
log.info('Test SMTP', 'Server listening on port %s', config.testserver.port);
|
log.info('Test SMTP', 'Server listening on port %s', config.testServer.port);
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (received) {
|
if (received) {
|
||||||
|
@ -186,8 +186,8 @@ module.exports = callback => {
|
||||||
}
|
}
|
||||||
}, 60 * 1000);
|
}, 60 * 1000);
|
||||||
|
|
||||||
mailBoxServer.listen(config.testserver.mailboxserverport, config.testserver.host, () => {
|
mailBoxServer.listen(config.testServer.mailboxServerPort, config.testServer.host, () => {
|
||||||
log.info('Test SMTP', 'Mail Box Server listening on port %s', config.testserver.mailboxserverport);
|
log.info('Test SMTP', 'Mail Box Server listening on port %s', config.testServer.mailboxServerPort);
|
||||||
setImmediate(callback);
|
setImmediate(callback);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,8 @@ const { enforce } = require('../../../lib/helpers');
|
||||||
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../shared/triggers');
|
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../shared/triggers');
|
||||||
const { SubscriptionSource } = require('../../../shared/lists');
|
const { SubscriptionSource } = require('../../../shared/lists');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
|
const {DOMParser, XMLSerializer} = require('xmldom');
|
||||||
|
const log = require('../../../lib/log');
|
||||||
|
|
||||||
const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'user'];
|
const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'user'];
|
||||||
const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template'];
|
const shareableEntityTypes = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'namespace', 'send_configuration', 'mosaico_template'];
|
||||||
|
@ -250,9 +252,12 @@ async function migrateSubscriptions(knex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptionsStream = knex('subscription__' + list.id).stream();
|
let lastId = 0;
|
||||||
let subscription;
|
while (true) {
|
||||||
while ((subscription = subscriptionsStream.read()) != null) {
|
const rows = await knex('subscription__' + list.id).where('id', '>', lastId).orderBy('id', 'asc').limit(1000);
|
||||||
|
|
||||||
|
if (rows.length > 0) {
|
||||||
|
for await (const subscription of rows) {
|
||||||
subscription.hash_email = crypto.createHash('sha512').update(subscription.email).digest("base64");
|
subscription.hash_email = crypto.createHash('sha512').update(subscription.email).digest("base64");
|
||||||
subscription.source_email = subscription.imported ? SubscriptionSource.IMPORTED_V1 : SubscriptionSource.NOT_IMPORTED_V1;
|
subscription.source_email = subscription.imported ? SubscriptionSource.IMPORTED_V1 : SubscriptionSource.NOT_IMPORTED_V1;
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
|
@ -264,11 +269,19 @@ async function migrateSubscriptions(knex) {
|
||||||
await knex('subscription__' + list.id).where('id', subscription.id).update(subscription);
|
await knex('subscription__' + list.id).where('id', subscription.id).update(subscription);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastId = rows[rows.length - 1].id;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` MODIFY `hash_email` varchar(255) CHARACTER SET ascii NOT NULL');
|
await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` MODIFY `hash_email` varchar(255) CHARACTER SET ascii NOT NULL');
|
||||||
|
|
||||||
await knex.schema.table('subscription__' + list.id, table => {
|
await knex.schema.table('subscription__' + list.id, table => {
|
||||||
table.dropColumn('imported');
|
table.dropColumn('imported');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.verbose('Migration', 'Subscriptions for list ' + list.cid + ' complete');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -504,8 +517,8 @@ async function migrateSegments(knex) {
|
||||||
if (oldSettings.range) {
|
if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
|
||||||
rules.push({ type: 'lt', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'lt', column: oldRule.column, value: oldSettings.end });
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
|
@ -529,14 +542,14 @@ async function migrateSegments(knex) {
|
||||||
if (oldSettings.range) {
|
if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
|
||||||
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end });
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'ge', column: oldRule.column, value: oldSettings.start},
|
{ type: 'ge', column: oldRule.column, value: oldSettings.start },
|
||||||
{ type: 'le', column: oldRule.column, value: oldSettings.end}
|
{ type: 'le', column: oldRule.column, value: oldSettings.end }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -554,14 +567,14 @@ async function migrateSegments(knex) {
|
||||||
if (oldSettings.relativeRange) {
|
if (oldSettings.relativeRange) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start });
|
||||||
rules.push({ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end });
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start},
|
{ type: 'geTodayPlusDays', column: oldRule.column, value: oldSettings.start },
|
||||||
{ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end}
|
{ type: 'leTodayPlusDays', column: oldRule.column, value: oldSettings.end }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -574,14 +587,14 @@ async function migrateSegments(knex) {
|
||||||
} else if (oldSettings.range) {
|
} else if (oldSettings.range) {
|
||||||
if (oldSettings.start && oldSettings.end) {
|
if (oldSettings.start && oldSettings.end) {
|
||||||
if (type === 'all') {
|
if (type === 'all') {
|
||||||
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start});
|
rules.push({ type: 'ge', column: oldRule.column, value: oldSettings.start });
|
||||||
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end});
|
rules.push({ type: 'le', column: oldRule.column, value: oldSettings.end });
|
||||||
} else {
|
} else {
|
||||||
rules.push({
|
rules.push({
|
||||||
type: 'all',
|
type: 'all',
|
||||||
rules: [
|
rules: [
|
||||||
{ type: 'ge', column: oldRule.column, value: oldSettings.start},
|
{ type: 'ge', column: oldRule.column, value: oldSettings.start },
|
||||||
{ type: 'le', column: oldRule.column, value: oldSettings.end}
|
{ type: 'le', column: oldRule.column, value: oldSettings.end }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -596,7 +609,11 @@ async function migrateSegments(knex) {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'option':
|
case 'option':
|
||||||
rules.push({ type: 'eq', column: oldRule.column, value: oldSettings.value });
|
if (oldSettings.value) {
|
||||||
|
rules.push({ type: 'isTrue', column: oldRule.column });
|
||||||
|
} else {
|
||||||
|
rules.push({ type: 'isFalse', column: oldRule.column });
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown rule for column ${oldRule.column} with field type ${fieldType}`);
|
throw new Error(`Unknown rule for column ${oldRule.column} with field type ${fieldType}`);
|
||||||
|
@ -780,6 +797,51 @@ async function addFiles(knex) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function convertTemplateContent(type, html, data) {
|
||||||
|
if (type == 'summernote') {
|
||||||
|
type = 'ckeditor4';
|
||||||
|
data.source = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'mosaico') {
|
||||||
|
type = 'mosaicoWithFsTemplate';
|
||||||
|
data.mosaicoFsTemplate = data.template;
|
||||||
|
delete data.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'grapejs') {
|
||||||
|
type = 'grapesjs';
|
||||||
|
|
||||||
|
if (data.mjml) {
|
||||||
|
data.sourceType = 'mjml';
|
||||||
|
|
||||||
|
const serializer = new XMLSerializer();
|
||||||
|
const mjmlDoc = new DOMParser().parseFromString(data.mjml, 'text/xml');
|
||||||
|
|
||||||
|
const container = mjmlDoc.getElementsByTagName('mj-container')[0];
|
||||||
|
data.source = mjContainer = container ? serializer.serializeToString(container) : '<mj-container></mj-container>';
|
||||||
|
|
||||||
|
data.style = '';
|
||||||
|
delete data.mjml;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
data.sourceType = 'html';
|
||||||
|
data.source = data.html || html || '';
|
||||||
|
data.style = data.css;
|
||||||
|
delete data.css;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete data.template;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == 'codeeditor') {
|
||||||
|
data.sourceType = 'html';
|
||||||
|
data.source = html || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
async function migrateTemplates(knex) {
|
async function migrateTemplates(knex) {
|
||||||
await knex.schema.table('templates', table => {
|
await knex.schema.table('templates', table => {
|
||||||
table.text('data', 'longtext');
|
table.text('data', 'longtext');
|
||||||
|
@ -789,20 +851,16 @@ async function migrateTemplates(knex) {
|
||||||
const templates = await knex('templates');
|
const templates = await knex('templates');
|
||||||
|
|
||||||
for (const template of templates) {
|
for (const template of templates) {
|
||||||
let type = template.editor_name;
|
|
||||||
const data = JSON.parse(template.editor_data || '{}');
|
const data = JSON.parse(template.editor_data || '{}');
|
||||||
|
|
||||||
if (type == 'summernote') {
|
const type = await convertTemplateContent(template.editor_name, template.html, data);
|
||||||
type = 'ckeditor';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type == 'mosaico') {
|
await knex('templates').where('id', template.id).update({
|
||||||
type = 'mosaicoWithFsTemplate';
|
type,
|
||||||
data.mosaicoFsTemplate = data.template;
|
text: template.text || '',
|
||||||
delete data.template;
|
html: template.html || '',
|
||||||
}
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
await knex('templates').where('id', template.id).update({type, data: JSON.stringify(data)});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await knex.schema.table('templates', table => {
|
await knex.schema.table('templates', table => {
|
||||||
|
@ -953,8 +1011,10 @@ async function migrateCampaigns(knex) {
|
||||||
for (const campaign of campaigns) {
|
for (const campaign of campaigns) {
|
||||||
const data = {};
|
const data = {};
|
||||||
|
|
||||||
await knex.raw('INSERT INTO `campaign_messages` (`id`, `campaign`, `list`, `subscription`, `send_configuration`, `status`, `response`, `response_id`, `updated`, `created`) ' +
|
// IGNORE is here because the original table had a key based also on segment. We droped the distinction based on segmention in mailtrain v2,
|
||||||
'SELECT `id`, ' + campaign.id + ', `list`, `subscription`, ' + getSystemSendConfigurationId() + ', `status`, `response`, `response_id`, `updated`, `created` FROM `campaign__' + campaign.id + '`;');
|
// which means we can get some duplicates. Hopefully it's not such a big harm to ignore the duplicates.
|
||||||
|
await knex.raw('INSERT IGNORE INTO `campaign_messages` (`campaign`, `list`, `subscription`, `send_configuration`, `status`, `response`, `response_id`, `updated`, `created`) ' +
|
||||||
|
'SELECT ' + campaign.id + ', `list`, `subscription`, ' + getSystemSendConfigurationId() + ', `status`, `response`, `response_id`, `updated`, `created` FROM `campaign__' + campaign.id + '`;');
|
||||||
|
|
||||||
await knex.raw('INSERT INTO `campaign_links` (`campaign`, `list`, `subscription`, `link`, `ip`, `device_type`, `country`, `count`, `created`) ' +
|
await knex.raw('INSERT INTO `campaign_links` (`campaign`, `list`, `subscription`, `link`, `ip`, `device_type`, `country`, `count`, `created`) ' +
|
||||||
'SELECT ' + campaign.id + ', `list`, `subscriber`, `link`, `ip`, `device_type`, `country`, `count`, `created` FROM `campaign_tracker__' + campaign.id + '`;');
|
'SELECT ' + campaign.id + ', `list`, `subscriber`, `link`, `ip`, `device_type`, `country`, `count`, `created` FROM `campaign_tracker__' + campaign.id + '`;');
|
||||||
|
@ -964,31 +1024,22 @@ async function migrateCampaigns(knex) {
|
||||||
|
|
||||||
if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS || campaign.type === CampaignType.RSS_ENTRY || campaign.type === CampaignType.TRIGGERED) {
|
if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS || campaign.type === CampaignType.RSS_ENTRY || campaign.type === CampaignType.TRIGGERED) {
|
||||||
if (campaign.template) {
|
if (campaign.template) {
|
||||||
let editorType = campaign.editor_name;
|
|
||||||
const editorData = JSON.parse(campaign.editor_data || '{}');
|
const editorData = JSON.parse(campaign.editor_data || '{}');
|
||||||
|
const editorType = await convertTemplateContent(campaign.editor_name, campaign.html, editorData);
|
||||||
if (editorType === 'summernote') {
|
|
||||||
editorType = 'ckeditor';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editorType === 'mosaico') {
|
|
||||||
editorType = 'mosaicoWithFsTemplate';
|
|
||||||
editorData.mosaicoFsTemplate = editorData.template;
|
|
||||||
delete editorData.template;
|
|
||||||
}
|
|
||||||
|
|
||||||
campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE;
|
campaign.source = CampaignSource.CUSTOM_FROM_TEMPLATE;
|
||||||
data.sourceCustom = {
|
data.sourceCustom = {
|
||||||
type: editorType,
|
type: editorType,
|
||||||
data: editorData,
|
data: editorData,
|
||||||
html: campaign.html,
|
html: campaign.html_prepared || campaign.html || '',
|
||||||
text: campaign.text,
|
text: campaign.text || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
data.sourceTemplate = campaign.template;
|
data.sourceTemplate = campaign.template;
|
||||||
|
|
||||||
// For source === CampaignSource.TEMPLATE, the data is as follows:
|
// For source === CampaignSource.TEMPLATE, the data is as follows:
|
||||||
// data.sourceTemplate = <template id>
|
// data.sourceTemplate = <template id>
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
campaign.source = CampaignSource.URL;
|
campaign.source = CampaignSource.URL;
|
||||||
data.sourceUrl = campaign.source_url;
|
data.sourceUrl = campaign.source_url;
|
||||||
|
@ -1155,28 +1206,58 @@ async function migrateImporter(knex) {
|
||||||
|
|
||||||
exports.up = (knex, Promise) => (async() => {
|
exports.up = (knex, Promise) => (async() => {
|
||||||
await migrateBase(knex);
|
await migrateBase(knex);
|
||||||
|
log.verbose('Migration', 'Base complete')
|
||||||
await addNamespaces(knex);
|
await addNamespaces(knex);
|
||||||
|
log.verbose('Migration', 'Namespaces complete')
|
||||||
|
|
||||||
await migrateUsers(knex);
|
await migrateUsers(knex);
|
||||||
|
log.verbose('Migration', 'Users complete')
|
||||||
|
|
||||||
await migrateCustomForms(knex);
|
await migrateCustomForms(knex);
|
||||||
|
log.verbose('Migration', 'Custom forms complete')
|
||||||
|
|
||||||
await migrateCustomFields(knex);
|
await migrateCustomFields(knex);
|
||||||
|
log.verbose('Migration', 'Custom fields complete')
|
||||||
|
|
||||||
await migrateSubscriptions(knex);
|
await migrateSubscriptions(knex);
|
||||||
|
|
||||||
await migrateSegments(knex);
|
await migrateSegments(knex);
|
||||||
|
log.verbose('Migration', 'Segments complete')
|
||||||
|
|
||||||
await migrateReports(knex);
|
await migrateReports(knex);
|
||||||
|
log.verbose('Migration', 'Reports complete')
|
||||||
|
|
||||||
await migrateSettings(knex);
|
await migrateSettings(knex);
|
||||||
|
log.verbose('Migration', 'Settings complete')
|
||||||
|
|
||||||
await migrateTemplates(knex);
|
await migrateTemplates(knex);
|
||||||
|
log.verbose('Migration', 'Templates complete')
|
||||||
|
|
||||||
|
|
||||||
await addMosaicoTemplates(knex);
|
await addMosaicoTemplates(knex);
|
||||||
|
log.verbose('Migration', 'Mosaico templates complete')
|
||||||
|
|
||||||
await migrateCampaigns(knex);
|
await migrateCampaigns(knex);
|
||||||
|
log.verbose('Migration', 'Campaigns complete')
|
||||||
|
|
||||||
|
|
||||||
await addPermissions(knex);
|
await addPermissions(knex);
|
||||||
|
log.verbose('Migration', 'Permissions complete')
|
||||||
|
|
||||||
await addFiles(knex);
|
await addFiles(knex);
|
||||||
|
log.verbose('Migration', 'Files complete')
|
||||||
|
|
||||||
|
|
||||||
await migrateAttachments(knex);
|
await migrateAttachments(knex);
|
||||||
|
log.verbose('Migration', 'Attachments complete')
|
||||||
|
|
||||||
|
|
||||||
await migrateTriggers(knex);
|
await migrateTriggers(knex);
|
||||||
|
log.verbose('Migration', 'Trigger complete')
|
||||||
|
|
||||||
|
|
||||||
await migrateImporter(knex);
|
await migrateImporter(knex);
|
||||||
|
log.verbose('Migration', 'Importer complete')
|
||||||
})();
|
})();
|
||||||
|
|
||||||
exports.down = (knex, Promise) => (async() => {
|
exports.down = (knex, Promise) => (async() => {
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -26,7 +26,7 @@ By default e2e tests use `phantomjs`. If you want to use a different browser you
|
||||||
Then adjust your config:
|
Then adjust your config:
|
||||||
|
|
||||||
```
|
```
|
||||||
[seleniumwebdriver]
|
[seleniumWebDriver]
|
||||||
browser="firefox"
|
browser="firefox"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -27,9 +27,9 @@ port=3000
|
||||||
user="mailtrain_test"
|
user="mailtrain_test"
|
||||||
password="$MYSQL_PASSWORD"
|
password="$MYSQL_PASSWORD"
|
||||||
database="mailtrain_test"
|
database="mailtrain_test"
|
||||||
[testserver]
|
[testServer]
|
||||||
enabled=true
|
enabled=true
|
||||||
[seleniumwebdriver]
|
[seleniumWebDriver]
|
||||||
browser="phantomjs"
|
browser="phantomjs"
|
||||||
EOT
|
EOT
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,8 @@ const config = require('config');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
app: config,
|
app: config,
|
||||||
baseUrl: 'http://localhost:' + config.www.port,
|
baseUrl: 'http://localhost:' + config.www.publicPort,
|
||||||
mailUrl: 'http://localhost:' + config.testserver.mailboxserverport,
|
mailUrl: 'http://localhost:' + config.testServer.mailboxServerPort,
|
||||||
users: {
|
users: {
|
||||||
admin: {
|
admin: {
|
||||||
username: 'admin',
|
username: 'admin',
|
||||||
|
@ -74,10 +74,10 @@ module.exports = {
|
||||||
'service-url': 'http://localhost:' + config.www.publicPort + '/',
|
'service-url': 'http://localhost:' + config.www.publicPort + '/',
|
||||||
'admin-email': 'keep.admin@mailtrain.org',
|
'admin-email': 'keep.admin@mailtrain.org',
|
||||||
'default-homepage': 'https://mailtrain.org',
|
'default-homepage': 'https://mailtrain.org',
|
||||||
'smtp-hostname': config.testserver.host,
|
'smtp-hostname': config.testServer.host,
|
||||||
'smtp-port': config.testserver.port,
|
'smtp-port': config.testServer.port,
|
||||||
'smtp-encryption': 'NONE',
|
'smtp-encryption': 'NONE',
|
||||||
'smtp-user': config.testserver.username,
|
'smtp-user': config.testServer.username,
|
||||||
'smtp-pass': config.testserver.password
|
'smtp-pass': config.testServer.password
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -10,8 +10,8 @@ if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..',
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.app.testserver.enabled !== true) {
|
if (config.app.testServer.enabled !== true) {
|
||||||
log.error('e2e', 'This script only runs if the testserver is enabled. Check config/test.toml');
|
log.error('e2e', 'This script only runs if the testServer is enabled. Check config/test.toml');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ const config = require('./config');
|
||||||
const webdriver = require('selenium-webdriver');
|
const webdriver = require('selenium-webdriver');
|
||||||
|
|
||||||
const driver = new webdriver.Builder()
|
const driver = new webdriver.Builder()
|
||||||
.forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
|
.forBrowser(config.app.seleniumWebDriver.browser || 'phantomjs')
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
const failHandlerRunning = new WorkerCounter();
|
const failHandlerRunning = new WorkerCounter();
|
||||||
|
@ -96,7 +96,7 @@ function UseCaseReporter(runner) {
|
||||||
const info = `URL: ${currentUrl}`;
|
const info = `URL: ${currentUrl}`;
|
||||||
await fs.writeFile('last-failed-e2e-test.info', info);
|
await fs.writeFile('last-failed-e2e-test.info', info);
|
||||||
await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource());
|
await fs.writeFile('last-failed-e2e-test.html', await driver.getPageSource());
|
||||||
await fs.writeFile('last-failed-e2e-test.png', new Buffer(await driver.takeScreenshot(), 'base64'));
|
await fs.writeFile('last-failed-e2e-test.png', Buffer.from(await driver.takeScreenshot(), 'base64'));
|
||||||
failHandlerRunning.exit();
|
failHandlerRunning.exit();
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
@ -104,7 +104,7 @@ module.exports = (...extras) => Object.assign({
|
||||||
|
|
||||||
async saveScreenshot(destPath) {
|
async saveScreenshot(destPath) {
|
||||||
const pngData = await driver.takeScreenshot();
|
const pngData = await driver.takeScreenshot();
|
||||||
const buf = new Buffer(pngData, 'base64');
|
const buf = Buffer.from(pngData, 'base64');
|
||||||
await fs.writeFile(destPath, buf);
|
await fs.writeFile(destPath, buf);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
'use strict';
|
|
||||||
|
|
||||||
let nodemailer = require('nodemailer');
|
|
||||||
|
|
||||||
// This is a dummy test to ensure that nodeunit would not fail on 0 assertions
|
|
||||||
module.exports['Load nodemailer'] = function (test) {
|
|
||||||
let transport = nodemailer.createTransport({
|
|
||||||
streamTransport: true
|
|
||||||
});
|
|
||||||
test.ok(transport);
|
|
||||||
test.done();
|
|
||||||
};
|
|
Loading…
Reference in a new issue