- [RSS_ENTRY_IMAGE_URL]
+ {tg('RSS_ENTRY_IMAGE_URL')}
|
RSS entry image URL
diff --git a/client/src/templates/mosaico/CUD.js b/client/src/templates/mosaico/CUD.js
index 570e9cd2..9e387bba 100644
--- a/client/src/templates/mosaico/CUD.js
+++ b/client/src/templates/mosaico/CUD.js
@@ -18,13 +18,14 @@ import {
withFormErrorHandlers
} from '../../lib/form';
import {withErrorHandling} from '../../lib/error-handling';
-import {NamespaceSelect, validateNamespace} from '../../lib/namespace';
+import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../../lib/namespace';
import {DeleteModalDialog} from "../../lib/modals";
-
+import mailtrainConfig from 'mailtrainConfig';
import {getMJMLSample, getVersafix} from "../../../../shared/mosaico-templates";
import {getTemplateTypes, getTemplateTypesOrder} from "./helpers";
import {withComponentMixins} from "../../lib/decorator-helpers";
import styles from "../../lib/styles.scss";
+import {getTagLanguages} from "../helpers";
@withComponentMixins([
withTranslation,
@@ -38,6 +39,7 @@ export default class CUD extends Component {
super(props);
this.templateTypes = getTemplateTypes(props.t);
+ this.tagLanguages = getTagLanguages(props.t);
this.typeOptions = [];
for (const type of getTemplateTypesOrder()) {
@@ -55,7 +57,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
wizard: PropTypes.string,
- entity: PropTypes.object
+ entity: PropTypes.object,
+ permissions: PropTypes.object
}
getFormValuesMutator(data) {
@@ -63,8 +66,16 @@ export default class CUD extends Component {
}
submitFormValuesMutator(data) {
+ const wizard = this.props.wizard;
+
+ if (wizard === 'versafix') {
+ data.html = getVersafix(data.tag_language);
+ } else if (wizard === 'mjml-sample') {
+ data.mjml = getMJMLSample(data.tag_language);
+ }
+
this.templateTypes[data.type].beforeSave(this, data);
- return filterData(data, ['name', 'description', 'type', 'data', 'namespace']);
+ return filterData(data, ['name', 'description', 'type', 'tag_language', 'data', 'namespace']);
}
componentDidMount() {
@@ -78,26 +89,27 @@ export default class CUD extends Component {
this.populateFormValues({
name: '',
description: '',
- namespace: mailtrainConfig.user.namespace,
+ namespace: getDefaultNamespace(this.props.permissions),
type: 'html',
- html: getVersafix()
+ tag_language: mailtrainConfig.tagLanguages[0]
});
} else if (wizard === 'mjml-sample') {
this.populateFormValues({
name: '',
description: '',
- namespace: mailtrainConfig.user.namespace,
+ namespace: getDefaultNamespace(this.props.permissions),
type: 'mjml',
- mjml: getMJMLSample()
+ tag_language: mailtrainConfig.tagLanguages[0]
});
} else {
this.populateFormValues({
name: '',
description: '',
- namespace: mailtrainConfig.user.namespace,
+ namespace: getDefaultNamespace(this.props.permissions),
type: 'html',
+ tag_language: mailtrainConfig.tagLanguages[0],
html: ''
});
}
@@ -119,6 +131,12 @@ export default class CUD extends Component {
state.setIn(['type', 'error'], null);
}
+ if (!state.getIn(['tag_language', 'value'])) {
+ state.setIn(['tag_language', 'error'], t('Tag language must be selected'));
+ } else {
+ state.setIn(['tag_language', 'error'], null);
+ }
+
validateNamespace(t, state);
}
@@ -169,6 +187,11 @@ export default class CUD extends Component {
const typeKey = this.getFormValue('type');
+ const tagLanguageOptions = [];
+ for (const key of mailtrainConfig.tagLanguages) {
+ tagLanguageOptions.push({key, label: this.tagLanguages[key].name});
+ }
+
return (
{canDelete &&
@@ -194,6 +217,9 @@ export default class CUD extends Component {
:
}
+
+
+
{isEdit && typeKey && this.templateTypes[typeKey].getForm(this)}
diff --git a/client/src/templates/mosaico/List.js b/client/src/templates/mosaico/List.js
index b7a29ea7..b3faae6a 100644
--- a/client/src/templates/mosaico/List.js
+++ b/client/src/templates/mosaico/List.js
@@ -4,13 +4,14 @@ import React, {Component} from 'react';
import {withTranslation} from '../../lib/i18n';
import {ButtonDropdown, Icon} from '../../lib/bootstrap-components';
import {DropdownLink, requiresAuthenticatedUser, Title, Toolbar, withPageHelpers} from '../../lib/page';
-import {withAsyncErrorHandler, withErrorHandling} from '../../lib/error-handling';
+import {withErrorHandling} from '../../lib/error-handling';
import {Table} from '../../lib/table';
import moment from 'moment';
import {getTemplateTypes} from './helpers';
-import {checkPermissions} from "../../lib/permissions";
+import {getTagLanguages} from '../helpers';
import {tableAddDeleteButton, tableRestActionDialogInit, tableRestActionDialogRender} from "../../lib/modals";
import {withComponentMixins} from "../../lib/decorator-helpers";
+import PropTypes from 'prop-types';
@withComponentMixins([
@@ -24,43 +25,33 @@ export default class List extends Component {
super(props);
this.templateTypes = getTemplateTypes(props.t);
+ this.tagLanguages = getTagLanguages(props.t);
this.state = {};
tableRestActionDialogInit(this);
}
- @withAsyncErrorHandler
- async fetchPermissions() {
- const result = await checkPermissions({
- createMosaicoTemplate: {
- entityTypeId: 'namespace',
- requiredOperations: ['createMosaicoTemplate']
- }
- });
-
- this.setState({
- createPermitted: result.data.createMosaicoTemplate
- });
- }
-
- componentDidMount() {
- // noinspection JSIgnoredPromiseFromCall
- this.fetchPermissions();
+ static propTypes = {
+ permissions: PropTypes.object
}
render() {
const t = this.props.t;
+ const permissions = this.props.permissions;
+ const createPermitted = permissions.createMosaicoTemplate;
+
const columns = [
{ data: 1, title: t('name') },
{ data: 2, title: t('description') },
{ data: 3, title: t('type'), render: data => this.templateTypes[data].typeName },
- { data: 4, title: t('created'), render: data => moment(data).fromNow() },
- { data: 5, title: t('namespace') },
+ { data: 4, title: t('Tag language'), render: data => this.tagLanguages[data].name },
+ { data: 5, title: t('created'), render: data => moment(data).fromNow() },
+ { data: 6, title: t('namespace') },
{
actions: data => {
const actions = [];
- const perms = data[6];
+ const perms = data[7];
if (perms.includes('edit')) {
actions.push({
@@ -100,7 +91,7 @@ export default class List extends Component {
return (
{tableRestActionDialogRender(this)}
- {this.state.createPermitted &&
+ {createPermitted &&
{t('blank')}
diff --git a/client/src/templates/mosaico/helpers.js b/client/src/templates/mosaico/helpers.js
index 07b8321b..7f7a2f75 100644
--- a/client/src/templates/mosaico/helpers.js
+++ b/client/src/templates/mosaico/helpers.js
@@ -20,9 +20,7 @@ export function getTemplateTypes(t) {
return owner.getFormValue('mjml') || '';
}
- function generateHtmlFromMjml(owner) {
- const mjml = getMjml(owner);
-
+ function generateHtmlFromMjml(mjml) {
try {
const res = mjml2html(mjml);
return res.html;
@@ -88,7 +86,7 @@ export function getTemplateTypes(t) {
typeName: t('mjml'),
getForm: owner => (
<>
- generateHtmlFromMjml(owner)} onHide={() => setExportModalVisibility(owner, false)}/>
+ generateHtmlFromMjml(getMjml(owner))} onHide={() => setExportModalVisibility(owner, false)}/>
>
),
@@ -98,7 +96,7 @@ export function getTemplateTypes(t) {
beforeSave: (owner, data) => {
data.data = {
mjml: data.mjml,
- html: generateHtmlFromMjml(owner)
+ html: generateHtmlFromMjml(data.mjml)
};
clearBeforeSend(data);
diff --git a/client/src/templates/root.js b/client/src/templates/root.js
index ad440402..a31af293 100644
--- a/client/src/templates/root.js
+++ b/client/src/templates/root.js
@@ -9,14 +9,29 @@ import Files from "../lib/files";
import MosaicoCUD from './mosaico/CUD';
import MosaicoList from './mosaico/List';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
-
+import {namespaceCheckPermissions} from "../lib/namespace";
function getMenus(t) {
return {
'templates': {
title: t('templates'),
link: '/templates',
- panelComponent: TemplatesList,
+ checkPermissions: {
+ createTemplate: {
+ entityTypeId: 'namespace',
+ requiredOperations: ['createTemplate']
+ },
+ createMosaicoTemplate: {
+ entityTypeId: 'namespace',
+ requiredOperations: ['createMosaicoTemplate']
+ },
+ viewMosaicoTemplate: {
+ entityTypeId: 'mosaicoTemplate',
+ requiredOperations: ['view']
+ },
+ ...namespaceCheckPermissions('createTemplate')
+ },
+ panelRender: props => ,
children: {
':templateId([0-9]+)': {
title: resolved => t('templateName', {name: ellipsizeBreadcrumbLabel(resolved.template.name)}),
@@ -29,7 +44,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/templates/${params.templateId}/edit`,
visible: resolved => resolved.template.permissions.includes('edit'),
- panelRender: props =>
+ panelRender: props =>
},
files: {
title: t('files'),
@@ -47,12 +62,15 @@ function getMenus(t) {
},
create: {
title: t('create'),
- panelRender: props =>
+ panelRender: props =>
},
mosaico: {
title: t('mosaicoTemplates'),
link: '/templates/mosaico',
- panelComponent: MosaicoList,
+ checkPermissions: {
+ ...namespaceCheckPermissions('createMosaicoTemplate')
+ },
+ panelRender: props => ,
children: {
':mosaiceTemplateId([0-9]+)': {
title: resolved => t('mosaicoTemplateName', {name: ellipsizeBreadcrumbLabel(resolved.mosaicoTemplate.name)}),
@@ -65,7 +83,7 @@ function getMenus(t) {
title: t('edit'),
link: params => `/templates/mosaico/${params.mosaiceTemplateId}/edit`,
visible: resolved => resolved.mosaicoTemplate.permissions.includes('edit'),
- panelRender: props =>
+ panelRender: props =>
},
files: {
title: t('files'),
@@ -90,7 +108,7 @@ function getMenus(t) {
create: {
title: t('create'),
extraParams: [':wizard?'],
- panelRender: props =>
+ panelRender: props =>
}
}
}
diff --git a/client/src/users/CUD.js b/client/src/users/CUD.js
index 30cabed2..1db8b679 100644
--- a/client/src/users/CUD.js
+++ b/client/src/users/CUD.js
@@ -19,7 +19,7 @@ import {withErrorHandling} from '../lib/error-handling';
import interoperableErrors from '../../../shared/interoperable-errors';
import passwordValidator from '../../../shared/password-validator';
import mailtrainConfig from 'mailtrainConfig';
-import {NamespaceSelect, validateNamespace} from '../lib/namespace';
+import {getDefaultNamespace, NamespaceSelect, validateNamespace} from '../lib/namespace';
import {DeleteModalDialog} from "../lib/modals";
import {withComponentMixins} from "../lib/decorator-helpers";
@@ -49,7 +49,8 @@ export default class CUD extends Component {
static propTypes = {
action: PropTypes.string.isRequired,
- entity: PropTypes.object
+ entity: PropTypes.object,
+ permissions: PropTypes.object
}
getFormValuesMutator(data) {
@@ -71,7 +72,8 @@ export default class CUD extends Component {
email: '',
password: '',
password2: '',
- namespace: mailtrainConfig.user.namespace
+ namespace: getDefaultNamespace(this.props.permissions),
+ role: null
});
}
}
diff --git a/client/src/users/root.js b/client/src/users/root.js
index db3bf555..dac2f896 100644
--- a/client/src/users/root.js
+++ b/client/src/users/root.js
@@ -5,12 +5,17 @@ import CUD from './CUD';
import List from './List';
import UserShares from '../shares/UserShares';
import {ellipsizeBreadcrumbLabel} from "../lib/helpers";
+import {namespaceCheckPermissions} from "../lib/namespace";
+import MosaicoCUD from "../templates/mosaico/CUD";
function getMenus(t) {
return {
'users': {
title: t('users'),
link: '/users',
+ checkPermissions: {
+ ...namespaceCheckPermissions('manageUsers')
+ },
panelComponent: List,
children: {
':userId([0-9]+)': {
@@ -23,7 +28,7 @@ function getMenus(t) {
':action(edit|delete)': {
title: t('edit'),
link: params => `/users/${params.userId}/edit`,
- panelRender: props =>
+ panelRender: props =>
},
shares: {
title: t('shares'),
@@ -34,7 +39,7 @@ function getMenus(t) {
},
create: {
title: t('create'),
- panelRender: props =>
+ panelRender: props =>
},
}
}
diff --git a/client/static/subscription/form-input-style.css b/client/static/subscription/form-input-style.css
index a44d33df..c73c17ff 100644
--- a/client/static/subscription/form-input-style.css
+++ b/client/static/subscription/form-input-style.css
@@ -199,11 +199,10 @@ textarea {
/* --- Other ------------- */
-.help-block {
+.option-help-block {
display: block;
- font-size: .9em;
- line-height: 1;
- color: #999999;
+ margin-left: 3px;
+ margin-bottom: 4px;
}
form a {
diff --git a/locales/en-US-last-run/common.json b/locales/en-US-last-run/common.json
index dbc647da..dce14596 100644
--- a/locales/en-US-last-run/common.json
+++ b/locales/en-US-last-run/common.json
@@ -100,6 +100,7 @@
"customTemplateEditor": "Custom template editor",
"save": "Save",
"saveAndLeave": "Save and leave",
+ "copy": "Copy",
"saveAndGoToStatus": "Save and go to status",
"testSend": "Test send",
"createRegularCampaign": "Create Regular Campaign",
@@ -923,6 +924,7 @@
"editTemplate": "Edit Template",
"createTemplate": "Create Template",
"cloneFromAnExistingTemplate": "Clone from an existing template",
+ "cloneFromAnExistingCustomForms": "Clone from an existing custom forms",
"mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer",
diff --git a/locales/en-US/common.json b/locales/en-US/common.json
index dbc647da..6428b301 100644
--- a/locales/en-US/common.json
+++ b/locales/en-US/common.json
@@ -76,10 +76,10 @@
"forcesTheRssFeedCheckToImmediatelyCheck": "Forces the RSS feed check to immediately check the campaign with the given CID (in :campaignCid). It works only for RSS campaigns.",
"sendTransactionalEmail": "Send transactional email",
"sendSingleEmailByTemplateWithGiven": "Send single email by template with given templateId",
- "idOfConfigurationUsedToCreateMailer": "ID of configuration used to create mailer instance",
+ "idOfConfigurationUsedToCreateMailer": "ID of configuration used to create mailer instance. If omitted, the default system send configuration is used.",
"subject": "Subject",
"dataPassedToTemplateWhenCompilingWith": "Data passed to template when compiling with Handlebars",
- "mapOfTemplatesubjectVariablesToReplace": "Map of template/subject variables to replace",
+ "mapOfTemplatesubjectVariablesToReplace": "Map of template variables to replace",
"apiResponseIsAJsonStructureWithErrorAnd": "API response is a JSON structure with <1>error1> and <3>data3> properties. If the response <5>error5> has a value set then the request failed.",
"youNeedToDefineProperContentTypeWhen": "You need to define proper <1>Content-Type1> when making a request. You can either use <3>application/x-www-form-urlencoded3> for normal form data or <5>application/json5> for a JSON payload. Using <7>multipart/form-data7> is not supported.",
"emailMustNotBeEmpty-1": "Email must not be empty",
@@ -100,6 +100,7 @@
"customTemplateEditor": "Custom template editor",
"save": "Save",
"saveAndLeave": "Save and leave",
+ "copy": "Copy",
"saveAndGoToStatus": "Save and go to status",
"testSend": "Test send",
"createRegularCampaign": "Create Regular Campaign",
@@ -923,6 +924,7 @@
"editTemplate": "Edit Template",
"createTemplate": "Create Template",
"cloneFromAnExistingTemplate": "Clone from an existing template",
+ "cloneFromAnExistingCustomForms": "Clone from existing custom forms",
"mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer",
diff --git a/locales/es-ES/common.json b/locales/es-ES/common.json
index 5142299b..905d9d41 100644
--- a/locales/es-ES/common.json
+++ b/locales/es-ES/common.json
@@ -105,6 +105,7 @@
"save": "Save",
"saveAndLeave": "Save and leave",
"saveAndLeave - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and leave",
+ "copy": "Copiar",
"saveAndGoToStatus": "Save and go to status",
"saveAndGoToStatus - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and go to status",
"testSend": "Test send",
@@ -992,8 +993,8 @@
"templateDeleted": "Template deleted",
"editTemplate": "Edit Template",
"createTemplate": "Create Template",
- "cloneFromAnExistingTemplate": "Clone from an existing template",
- "cloneFromAnExistingTemplate - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing template",
+ "cloneFromAnExistingTemplate": "Clonar a partir de templates ya existentes",
+ "cloneFromAnExistingCustomForms": "Clonar a partir de formularios personalizados ya existentes",
"mosaico": "Mosaico",
"templateContentHtml": "Template content (HTML)",
"mosaicoTemplateDesigner": "Mosaico Template Designer",
diff --git a/locales/pt-BR/common.json b/locales/pt-BR/common.json
index 91baf919..43da6868 100644
--- a/locales/pt-BR/common.json
+++ b/locales/pt-BR/common.json
@@ -105,6 +105,8 @@
"save": "Salvar",
"saveAndLeave": "Salvar e sair",
"saveAndLeave - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and leave",
+ "copy": "Copia",
+ "copy - TODO: update line above and then delete this line to mark that the translation has been fixed": "Copy",
"saveAndGoToStatus": "Save and go to status",
"saveAndGoToStatus - TODO: update line above and then delete this line to mark that the translation has been fixed": "Save and go to status",
"testSend": "Testar envio",
@@ -994,6 +996,8 @@
"createTemplate": "Criar modelo",
"cloneFromAnExistingTemplate": "Clone from an existing template",
"cloneFromAnExistingTemplate - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing template",
+ "cloneFromAnExistingCustomForms": "Clone from an existing custom forms",
+ "cloneFromAnExistingCustomForms - TODO: update line above and then delete this line to mark that the translation has been fixed": "Clone from an existing custom forms",
"mosaico": "Mosaico",
"templateContentHtml": "Conteúdo do modelo (HTML)",
"mosaicoTemplateDesigner": "Editor do Modelo Mosaico",
diff --git a/mvis/ivis-core b/mvis/ivis-core
index ec89af43..052fbff6 160000
--- a/mvis/ivis-core
+++ b/mvis/ivis-core
@@ -1 +1 @@
-Subproject commit ec89af43120f95dcf7f14dc92a2b3811bf50d7e8
+Subproject commit 052fbff6e4eaba52eee2f984f2b8d947ebfa7298
diff --git a/server/app-builder.js b/server/app-builder.js
index 42105d55..bdadbea4 100644
--- a/server/app-builder.js
+++ b/server/app-builder.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./lib/config');
const log = require('./lib/log');
const express = require('express');
diff --git a/server/config/default.yaml b/server/config/default.yaml
index 59800d5c..39ffb95e 100644
--- a/server/config/default.yaml
+++ b/server/config/default.yaml
@@ -36,6 +36,11 @@ editors:
- ckeditor4
- codeeditor
+# Enabled tag languages
+tagLanguages:
+- simple # e.g. [FIRST_NAME] - this the style of merge tags found in Mailtrain v1
+- hbs # e.g. {{#if FIRST_NAME}}Hello {{firstName}}!{{else}}Hello!{{/if}} - this syntax uses Handlebars templating language (http://handlebarsjs.com)
+
# Default language to use
defaultLanguage: en-US
@@ -165,6 +170,23 @@ queue:
# How many parallel sender processes to spawn
processes: 2
+ # For how long (in seconds) to try to send an email before Mailtrain stops to trying. An email can normally
+ # be sent out almost immediately. However if the send configuration is not correct or the mail server is not reachable,
+ # Mailtrain will keep retrying until the email expires.
+ # Due to Mailtrain's internal timeouts, the values should be at least 60 seconds.
+ retention:
+ # Regular and RSS campaign. Once this expires, the campaign is considered finished. The remaining recipients
+ # are included in the set of those recipients to whom the message would be delivered if the campaign is again started.
+ campaign: 86400 # 1 day
+ # Triggered campaign. Once this expires, the message gets discarded.
+ triggered: 86400 # 1 day
+ # Test send (in campaign or template)
+ test: 300 # 5 minutes
+ # Subscription and password reset related emails
+ subscription: 300 # 5 minutes
+ # Transactional emails sent via API (i.e. /templates/:templateId/send)
+ apiTransactional: 3600 # 60 minutes
+
cors:
# Allow subscription widgets to be embedded
# origins: ['https://www.example.com']
@@ -235,7 +257,11 @@ seleniumWebDriver:
browser: phantomjs
-roles:
+# The section below defines the definition of roles (permissions) to be used when no "roles" section is provided
+# in custom config (typically production.yaml). If you want to extend rules provided below, add corresponding rules
+# in "defaultRoles" section in custom config. If you want to define roles from scratch, create "roles" section in
+# the custom config.
+defaultRoles:
global:
master:
name: Global Master
@@ -260,9 +286,9 @@ roles:
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createReportTemplate, createTemplate, createMosaicoTemplate, createSendConfiguration, createCampaign, manageUsers]
children:
sendConfiguration: [viewPublic, viewPrivate, edit, delete, share, sendWithoutOverrides, sendWithAllowedOverrides, sendWithAnyOverrides]
- list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
+ list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share]
- campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, fetchRss]
+ campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles]
report: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate: [view, edit, delete, share, execute]
@@ -275,9 +301,9 @@ roles:
permissions: [view, edit, delete, share, createNamespace, createList, createCustomForm, createReport, createTemplate, createMosaicoTemplate, createCampaign]
children:
sendConfiguration: [viewPublic, sendWithoutOverrides, sendWithAllowedOverrides]
- list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
+ list: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
customForm: [view, edit, delete, share]
- campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, fetchRss]
+ campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, fetchRss]
template: [view, edit, delete, share, viewFiles, manageFiles]
report: [view, edit, delete, share, execute, viewContent, viewOutput]
reportTemplate: [view, share, execute]
@@ -286,12 +312,24 @@ roles:
campaignsCreator:
name: Campaigns Creator
- description: In the respective namespace, the user has all permissions for templates and campaigns.
+ description: In the respective namespace, the user has all permissions to create and manage templates and campaigns. The user can also read public data about send configurations and use Mosaico templates in the namespace.
permissions: [view, createTemplate, createCampaign]
children:
sendConfiguration: [viewPublic]
- campaign: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, fetchRss]
- template: [view, edit, delete, share, viewFiles, manageFiles]
+ campaign: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss]
+ template: [view, edit, delete, viewFiles, manageFiles]
+ mosaicoTemplate: [view, viewFiles]
+ namespace: [view, createTemplate, createCampaign]
+
+ campaignsViewer:
+ name: Campaigns Viewer
+ description: In the respective namespace, the user has permissions to view campaigns and templates in order to be able to replicate them.
+ permissions: [view, createTemplate, createCampaign]
+ children:
+ campaign: [view, viewFiles, viewAttachments, viewTriggers]
+ template: [view, viewFiles]
+ mosaicoTemplate: [view, viewFiles]
+ namespace: [view]
sendConfiguration:
master:
@@ -301,17 +339,17 @@ roles:
campaignsCreator:
name: Campaigns Creator
description: The user can only use the send configuration in setting up a campaign. However, this gives no permission to send.
- permissions: [viewPublic]
+ permissions: [viewPublic, sendWithoutOverrides]
list:
master:
name: Master
description: All permissions
- permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports]
+ permissions: [view, edit, delete, share, viewFields, manageFields, viewSubscriptions, viewTestSubscriptions, manageSubscriptions, viewSegments, manageSegments, viewImports, manageImports, send, sendToTestUsers]
campaignsCreator:
name: Campaigns Creator
- description: The user can only use the list in setting up a campaign. However, this gives no permission to view subscriptions or to send to the list.
- permissions: [view, viewFields, viewSegments]
+ description: The user can only use the list in setting up a campaign and to send email to test users. This gives no permission to view subscriptions or to send to the whole list.
+ permissions: [view, viewFields, viewSegments, viewTestSubscriptions, sendToTestUsers]
customForm:
master:
@@ -323,11 +361,15 @@ roles:
master:
name: Master
description: All permissions
- permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, viewStats, manageMessages, fetchRss]
- campaignsCreator:
- name: Campaigns Creator
+ permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, send, sendToTestUsers, viewStats, manageMessages, fetchRss]
+ creator:
+ name: Creator
description: The user can setup the campaign but cannot send it.
- permissions: [view, edit, delete, share, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, fetchRss]
+ permissions: [view, edit, delete, viewFiles, manageFiles, viewAttachments, manageAttachments, viewTriggers, manageTriggers, sendToTestUsers, viewStats, fetchRss]
+ viewer:
+ name: Viewer
+ description: The user can view the campaign but cannot edit it or send it.
+ permissions: [view, viewFiles, viewAttachments, viewTriggers]
rssTrigger:
name: RSS Campaign Trigger
description: Allows triggering a fetch of an RSS campaign
@@ -338,6 +380,10 @@ roles:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles]
+ viewer:
+ name: Viewer
+ description: The user can view the template but cannot edit it.
+ permissions: [view, viewFiles]
report:
master:
@@ -356,5 +402,8 @@ roles:
name: Master
description: All permissions
permissions: [view, edit, delete, share, viewFiles, manageFiles]
-
+ viewer:
+ name: Viewer
+ description: The user can use the Mosaico template, but cannot edit it or delete it.
+ permissions: [view, viewFiles]
diff --git a/server/dbtest.js b/server/dbtest.js
deleted file mode 100644
index ec3ae585..00000000
--- a/server/dbtest.js
+++ /dev/null
@@ -1,32 +0,0 @@
-'use strict';
-
-const config = require('config');
-const knex = require('./lib/knex');
-const moment = require('moment');
-const shortid = require('shortid');
-
-async function run() {
-// const info = await knex('subscription__1').columnInfo();
-// console.log(info);
-
-// const ts = moment().toDate();
- const ts = new Date(Date.now());
- console.log(ts);
-
- const cid = shortid.generate();
-
- await knex('subscription__1')
- .insert({
- email: cid,
- cid,
- custom_date_mmddyy_rjkeojrzz: ts
- });
-
-
- const row = await knex('subscription__1').select(['id', 'created', 'custom_date_mmddyy_rjkeojrzz']).where('cid', cid).first();
-
-// const row = await knex('subscription__1').where('id', 2).first();
- console.log(row);
-}
-
-run();
\ No newline at end of file
diff --git a/server/index.js b/server/index.js
index 8ec45b4d..2fb88cc6 100644
--- a/server/index.js
+++ b/server/index.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./lib/config');
const log = require('./lib/log');
const appBuilder = require('./app-builder');
const translate = require('./lib/translate');
diff --git a/server/lib/builtin-zone-mta.js b/server/lib/builtin-zone-mta.js
index 06db0660..e25da829 100644
--- a/server/lib/builtin-zone-mta.js
+++ b/server/lib/builtin-zone-mta.js
@@ -1,20 +1,34 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const fork = require('./fork').fork;
const log = require('./log');
const path = require('path');
-const fs = require('fs-extra')
+const fs = require('fs-extra');
const crypto = require('crypto');
const bluebird = require('bluebird');
-let zoneMtaProcess;
+let zoneMtaProcess = null;
const zoneMtaDir = path.join(__dirname, '..', '..', 'zone-mta');
const zoneMtaBuiltingConfig = path.join(zoneMtaDir, 'config', 'builtin-zonemta.json');
const password = process.env.BUILTIN_ZONE_MTA_PASSWORD || crypto.randomBytes(20).toString('hex').toLowerCase();
+let restartCount = 0;
+let lastRestartCount = 0;
+
+let restartBackoffIdx = 0;
+const restartBackoff = [0, 30, 60, 300]; // in seconds
+
+setInterval(() => {
+ if (restartCount === lastRestartCount) {
+ restartBackoffIdx = 0;
+ }
+
+ lastRestartCount = restartCount;
+}, 300000 /* 5 mins */);
+
function getUsername() {
return 'mailtrain';
}
@@ -119,36 +133,58 @@ async function createConfig() {
await fs.writeFile(zoneMtaBuiltingConfig, JSON.stringify(cnf, null, 2));
}
+function restart(callback) {
+ if (zoneMtaProcess) return callback();
+
+ if (restartCount === 0) {
+ log.info('ZoneMTA', 'Starting built-in Zone MTA process');
+ } else {
+ log.info('ZoneMTA', `Restarting built-in Zone MTA process (restart count ${restartCount})`);
+ }
+
+ zoneMtaProcess = fork(
+ path.join(zoneMtaDir, 'index.js'),
+ ['--config=' + zoneMtaBuiltingConfig],
+ {
+ cwd: zoneMtaDir,
+ env: {NODE_ENV: process.env.NODE_ENV}
+ }
+ );
+
+ zoneMtaProcess.on('message', msg => {
+ if (msg) {
+ if (msg.type === 'zone-mta-started') {
+ log.info('ZoneMTA', 'ZoneMTA process started');
+
+ if (callback) {
+ return callback();
+ } else {
+ return;
+ }
+ }
+ }
+ });
+
+ zoneMtaProcess.on('close', (code, signal) => {
+ log.error('ZoneMTA', 'ZoneMTA process exited with code %s signal %s', code, signal);
+
+ zoneMtaProcess = null;
+ restartCount += 1;
+
+ const backoffTimeout = restartBackoff[restartBackoffIdx] * 1000;
+ if (restartBackoffIdx < restartBackoff.length - 1) {
+ restartBackoffIdx += 1;
+ }
+
+ setTimeout(restart, backoffTimeout);
+ });
+}
+
function spawn(callback) {
if (config.builtinZoneMTA.enabled) {
createConfig().then(() => {
- log.info('ZoneMTA', 'Starting built-in Zone MTA process');
-
- zoneMtaProcess = fork(
- path.join(zoneMtaDir, 'index.js'),
- ['--config=' + zoneMtaBuiltingConfig],
- {
- cwd: zoneMtaDir,
- env: {NODE_ENV: process.env.NODE_ENV}
- }
- );
-
- zoneMtaProcess.on('message', msg => {
- if (msg) {
- if (msg.type === 'zone-mta-started') {
- log.info('ZoneMTA', 'ZoneMTA process started');
- return callback();
- } else if (msg.type === 'entries-added') {
- senders.scheduleCheck();
- }
- }
- });
-
- zoneMtaProcess.on('close', (code, signal) => {
- log.error('ZoneMTA', 'ZoneMTA process exited with code %s signal %s', code, signal);
- });
-
+ restart(callback);
}).catch(err => callback(err));
} else {
diff --git a/server/lib/campaign-sender.js b/server/lib/campaign-sender.js
deleted file mode 100644
index a0d7a44c..00000000
--- a/server/lib/campaign-sender.js
+++ /dev/null
@@ -1,472 +0,0 @@
-'use strict';
-
-const config = require('config');
-const log = require('./log');
-const mailers = require('./mailers');
-const knex = require('./knex');
-const subscriptions = require('../models/subscriptions');
-const contextHelpers = require('./context-helpers');
-const campaigns = require('../models/campaigns');
-const templates = require('../models/templates');
-const lists = require('../models/lists');
-const fields = require('../models/fields');
-const sendConfigurations = require('../models/send-configurations');
-const links = require('../models/links');
-const {CampaignSource, CampaignType} = require('../../shared/campaigns');
-const {SubscriptionStatus} = require('../../shared/lists');
-const tools = require('./tools');
-const request = require('request-promise');
-const files = require('../models/files');
-const htmlToText = require('html-to-text');
-const {getPublicUrl} = require('./urls');
-const blacklist = require('../models/blacklist');
-const libmime = require('libmime');
-const shares = require('../models/shares');
-
-class CampaignSender {
- constructor() {
- }
-
- static async testSend(context, listCid, subscriptionCid, campaignId, sendConfigurationId, html, text) {
- let sendConfiguration, list, fieldsGrouped, campaign, subscriptionGrouped, useVerp, useVerpSenderHeader, mergeTags, attachments;
-
- await knex.transaction(async tx => {
- sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), sendConfigurationId, false, true);
- list = await lists.getByCidTx(tx, context, listCid);
- fieldsGrouped = await fields.listGroupedTx(tx, list.id);
-
- useVerp = config.verp.enabled && sendConfiguration.verp_hostname;
- useVerpSenderHeader = useVerp && !sendConfiguration.verp_disable_sender_header;
-
- subscriptionGrouped = await subscriptions.getByCid(context, list.id, subscriptionCid);
- mergeTags = fields.getMergeTags(fieldsGrouped, subscriptionGrouped);
-
- if (campaignId) {
- campaign = await campaigns.getByIdTx(tx, context, campaignId, false, campaigns.Content.WITHOUT_SOURCE_CUSTOM);
- await campaigns.enforceSendPermissionTx(tx, context, campaign);
- } else {
- await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides');
-
- // This is to fake the campaign for getMessageLinks, which is called inside formatMessage
- campaign = {
- cid: '[CAMPAIGN_ID]'
- };
- }
- });
-
- const encryptionKeys = [];
- for (const fld of fieldsGrouped) {
- if (fld.type === 'gpg' && mergeTags[fld.key]) {
- encryptionKeys.push(mergeTags[fld.key].trim());
- }
- }
-
- attachments = [];
- // replace data: images with embedded attachments
- html = html.replace(/( ]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
- const cid = shortid.generate() + '-attachments';
- attachments.push({
- path: dataUri,
- cid
- });
- return prefix + 'cid:' + cid;
- });
-
- html = tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true);
-
- text = (text || '').trim()
- ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text)
- : htmlToText.fromString(html, {wordwrap: 130});
-
-
- const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
-
- const getOverridable = key => {
- return sendConfiguration[key];
- };
-
- const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
-
- const mail = {
- from: {
- name: getOverridable('from_name'),
- address: getOverridable('from_email')
- },
- replyTo: getOverridable('reply_to'),
- xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
- to: {
- name: list.to_name === null ? undefined : tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false),
- address: subscriptionGrouped.email
- },
- sender: useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
-
- envelope: useVerp ? {
- from: campaignAddress + '@' + sendConfiguration.verp_hostname,
- to: subscriptionGrouped.email
- } : false,
-
- headers: {
- 'x-fbl': campaignAddress,
- // custom header for SparkPost
- 'x-msys-api': JSON.stringify({
- campaign_id: campaignAddress
- }),
- // custom header for SendGrid
- 'x-smtpapi': JSON.stringify({
- unique_args: {
- campaign_id: campaignAddress
- }
- }),
- // custom header for Mailgun
- 'x-mailgun-variables': JSON.stringify({
- campaign_id: campaignAddress
- }),
- 'List-ID': {
- prepared: true,
- value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
- }
- },
- list: {
- unsubscribe: null
- },
- subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
- html,
- text,
-
- attachments,
- encryptionKeys
- };
-
-
- let response;
- try {
- const info = await mailer.sendMassMail(mail);
- response = info.response || info.messageId;
- } catch (err) {
- response = err.response || err.message;
- }
-
- return response;
- }
-
-
- async init(settings) {
- this.listsById = new Map(); // listId -> list
- this.listsByCid = new Map(); // listCid -> list
- this.listsFieldsGrouped = new Map(); // listId -> fieldsGrouped
- this.attachments = [];
-
- await knex.transaction(async tx => {
- if (settings.campaignCid) {
- this.campaign = await campaigns.rawGetByTx(tx, 'cid', settings.campaignCid);
- } else {
- this.campaign = await campaigns.rawGetByTx(tx, 'id', settings.campaignId);
- }
-
- const campaign = this.campaign;
-
- this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, true);
-
- for (const listSpec of campaign.lists) {
- const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
- this.listsById.set(list.id, list);
- this.listsByCid.set(list.cid, list);
- this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
- }
-
- if (campaign.source === CampaignSource.TEMPLATE) {
- this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.data.sourceTemplate, false);
- }
-
- const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaign.id);
- for (const attachment of attachments) {
- this.attachments.push({
- filename: attachment.originalname,
- path: files.getFilePath('campaign', 'attachment', campaign.id, attachment.filename)
- });
- }
-
- this.useVerp = config.verp.enabled && this.sendConfiguration.verp_hostname;
- this.useVerpSenderHeader = this.useVerp && !this.sendConfiguration.verp_disable_sender_header;
- });
- }
-
- async _getMessage(campaign, list, subscriptionGrouped, mergeTags, replaceDataImgs) {
- let html = '';
- let text = '';
- let renderTags = false;
-
- if (campaign.source === CampaignSource.URL) {
- const form = tools.getMessageLinks(campaign, list, subscriptionGrouped);
- for (const key in mergeTags) {
- form[key] = mergeTags[key];
- }
-
- const response = await request.post({
- uri: campaign.sourceUrl,
- form,
- resolveWithFullResponse: true
- });
-
- if (response.statusCode !== 200) {
- throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`);
- }
-
- html = response.body;
- text = '';
- renderTags = false;
-
- } else if (campaign.source === CampaignSource.CUSTOM || campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN || campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE) {
- html = campaign.data.sourceCustom.html;
- text = campaign.data.sourceCustom.text;
- renderTags = true;
-
- } else if (campaign.source === CampaignSource.TEMPLATE) {
- const template = this.template;
- html = template.html;
- text = template.text;
- renderTags = true;
- }
-
- html = await links.updateLinks(campaign, list, subscriptionGrouped, mergeTags, html);
-
- const attachments = this.attachments.slice();
- if (replaceDataImgs) {
- // replace data: images with embedded attachments
- html = html.replace(/( ]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
- const cid = shortid.generate() + '-attachments';
- attachments.push({
- path: dataUri,
- cid
- });
- return prefix + 'cid:' + cid;
- });
- }
-
- html = renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, html, true) : html;
-
- text = (text || '').trim()
- ? (renderTags ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, text) : text)
- : htmlToText.fromString(html, {wordwrap: 130});
-
- return {
- html,
- text,
- attachments
- };
- }
-
- _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.imageUrl;
- tags['RSS_ENTRY_CUSTOM_TAGS'] = rssEntry.customTags;
- }
-
- return tags;
- }
-
- async getMessage(listCid, subscriptionCid) {
- const list = this.listsByCid.get(listCid);
- const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
- const flds = this.listsFieldsGrouped.get(list.id);
- const campaign = this.campaign;
- const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
-
- return await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, false);
- }
-
- async sendMessageByEmail(listId, email) {
- const subscriptionGrouped = await subscriptions.getByEmail(contextHelpers.getAdminContext(), listId, email);
- await this._sendMessage(listId, subscriptionGrouped);
- }
-
- async sendMessageBySubscriptionId(listId, subscriptionId) {
- const subscriptionGrouped = await subscriptions.getById(contextHelpers.getAdminContext(), listId, subscriptionId);
- await this._sendMessage(listId, subscriptionGrouped);
- }
-
- async _sendMessage(listId, subscriptionGrouped) {
- const email = subscriptionGrouped.email;
-
- if (await blacklist.isBlacklisted(email)) {
- return;
- }
-
- const list = this.listsById.get(listId);
- const flds = this.listsFieldsGrouped.get(list.id);
- const campaign = this.campaign;
-
- const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
-
- const encryptionKeys = [];
- for (const fld of flds) {
- if (fld.type === 'gpg' && mergeTags[fld.key]) {
- encryptionKeys.push(mergeTags[fld.key].trim());
- }
- }
-
- const sendConfiguration = this.sendConfiguration;
-
- const {html, text, attachments} = await this._getMessage(campaign, list, subscriptionGrouped, mergeTags, true);
-
- const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
-
- let listUnsubscribe = null;
- if (!list.listunsubscribe_disabled) {
- listUnsubscribe = campaign.unsubscribe_url
- ? tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, campaign.unsubscribe_url)
- : getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid);
- }
-
- const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
-
- await mailer.throttleWait();
-
- const getOverridable = key => {
- if (sendConfiguration[key + '_overridable'] && this.campaign[key + '_override'] !== null) {
- return campaign[key + '_override'] || '';
- } else {
- return sendConfiguration[key] || '';
- }
- };
-
- const mail = {
- from: {
- name: getOverridable('from_name'),
- address: getOverridable('from_email')
- },
- replyTo: getOverridable('reply_to'),
- xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
- to: {
- name: list.to_name === null ? undefined : tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, list.to_name, false),
- address: subscriptionGrouped.email
- },
- sender: this.useVerpSenderHeader ? campaignAddress + '@' + sendConfiguration.verp_hostname : false,
-
- envelope: this.useVerp ? {
- from: campaignAddress + '@' + sendConfiguration.verp_hostname,
- to: subscriptionGrouped.email
- } : false,
-
- headers: {
- 'x-fbl': campaignAddress,
- // custom header for SparkPost
- 'x-msys-api': JSON.stringify({
- campaign_id: campaignAddress
- }),
- // custom header for SendGrid
- 'x-smtpapi': JSON.stringify({
- unique_args: {
- campaign_id: campaignAddress
- }
- }),
- // custom header for Mailgun
- 'x-mailgun-variables': JSON.stringify({
- campaign_id: campaignAddress
- }),
- 'List-ID': {
- prepared: true,
- value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
- }
- },
- list: {
- unsubscribe: listUnsubscribe
- },
- subject: tools.formatMessage(campaign, list, subscriptionGrouped, mergeTags, getOverridable('subject'), false),
- html,
- text,
-
- attachments,
- encryptionKeys
- };
-
-
- let status;
- let response;
- let responseId = null;
- try {
- const info = await mailer.sendMassMail(mail);
- status = SubscriptionStatus.SUBSCRIBED;
-
- log.verbose('CampaignSender', `response: ${info.response} messageId: ${info.messageId}`);
-
- let match;
- if ((match = info.response.match(/^250 Message queued as ([0-9a-f]+)$/))) {
- /*
- ZoneMTA
- info.response: 250 Message queued as 1691ad7f7ae00080fd
- info.messageId:
- */
- response = info.response;
- responseId = match[1];
-
- } else if ((match = info.messageId.match(/^<([^>@]*)@.*amazonses\.com>$/))) {
- /*
- AWS SES
- info.response: 0102016ad2244c0a-955492f2-9194-4cd1-bef9-70a45906a5a7-000000
- info.messageId: <0102016ad2244c0a-955492f2-9194-4cd1-bef9-70a45906a5a7-000000@eu-west-1.amazonses.com>
- */
- response = info.response;
- responseId = match[1];
-
- } else if (info.response.match(/^250 OK$/) && (match = info.messageId.match(/^<([^>]*)>$/))) {
- /*
- Postal Mail Server
- info.response: 250 OK
- info.messageId: (postal messageId)
- */
- response = info.response;
- responseId = match[1];
-
- } else {
- /*
- Fallback - Mailtrain v1 behavior
- */
- response = info.response || info.messageId;
- responseId = response.split(/\s+/).pop();
- }
-
-
- await knex('campaigns').where('id', campaign.id).increment('delivered');
- } catch (err) {
- status = SubscriptionStatus.BOUNCED;
- response = err.response || err.message;
- await knex('campaigns').where('id', campaign.id).increment('delivered').increment('bounced');
- }
-
-
- const now = new Date();
-
- if (campaign.type === CampaignType.REGULAR || campaign.type === CampaignType.RSS_ENTRY) {
- await knex('campaign_messages').insert({
- campaign: this.campaign.id,
- list: list.id,
- subscription: subscriptionGrouped.id,
- send_configuration: sendConfiguration.id,
- status,
- response,
- response_id: responseId,
- updated: now
- });
-
- } else if (campaign.type = CampaignType.TRIGGERED) {
- await knex('queued')
- .where({
- campaign: this.campaign.id,
- list: list.id,
- subscription: subscriptionGrouped.id
- })
- .del();
- }
- }
-}
-
-module.exports = CampaignSender;
\ No newline at end of file
diff --git a/server/lib/client-helpers.js b/server/lib/client-helpers.js
index ebf0c3ca..c00a9389 100644
--- a/server/lib/client-helpers.js
+++ b/server/lib/client-helpers.js
@@ -1,7 +1,7 @@
'use strict';
const passport = require('./passport');
-const config = require('config');
+const config = require('./config');
const forms = require('../models/forms');
const shares = require('../models/shares');
const urls = require('./urls');
@@ -44,6 +44,7 @@ async function getAuthenticatedConfig(context) {
},
globalPermissions,
editors: config.editors,
+ tagLanguages: config.tagLanguages,
mosaico: config.mosaico,
verpEnabled: config.verp.enabled,
reportsEnabled: config.reports.enabled,
diff --git a/server/lib/config.js b/server/lib/config.js
new file mode 100644
index 00000000..d7840072
--- /dev/null
+++ b/server/lib/config.js
@@ -0,0 +1,9 @@
+"use strict";
+
+const config = require('config');
+
+if (!config.roles) {
+ config.roles = config.defaultRoles;
+}
+
+module.exports = config;
\ No newline at end of file
diff --git a/server/lib/dbcheck.js b/server/lib/dbcheck.js
index 86dbafaa..7efb66fa 100644
--- a/server/lib/dbcheck.js
+++ b/server/lib/dbcheck.js
@@ -4,7 +4,7 @@
This module handles Mailtrain database initialization and upgrades
*/
-const config = require('config');
+const config = require('./config');
const mysql = require('mysql');
const log = require('./log');
const fs = require('fs');
@@ -12,7 +12,7 @@ const pathlib = require('path');
const Handlebars = require('handlebars');
const bluebird = require('bluebird');
-const highestLegacySchemaVersion = 33;
+const highestLegacySchemaVersion = 34;
const mysqlConfig = {
multipleStatements: true
diff --git a/server/lib/dependency-helpers.js b/server/lib/dependency-helpers.js
index 03ff1e39..9aaba342 100644
--- a/server/lib/dependency-helpers.js
+++ b/server/lib/dependency-helpers.js
@@ -4,7 +4,6 @@ const knex = require('./knex');
const interoperableErrors = require('../../shared/interoperable-errors');
const entitySettings = require('./entity-settings');
const shares = require('../models/shares');
-const { enforce } = require('./helpers');
const defaultNoOfDependenciesReported = 20;
@@ -21,7 +20,7 @@ async function ensureNoDependencies(tx, context, id, depSpecs) {
if (depSpec.query) {
rows = await depSpec.query(tx).limit(defaultNoOfDependenciesReported + 1);
} else if (depSpec.column) {
- rows = await tx(entityType.entitiesTable).where(depSpec.column, id).select(['id', 'name']).limit(defaultNoOfDependenciesReported + 1);
+ rows = await tx(entityType.entitiesTable).where(depSpec.column, id).forShare().select(['id', 'name']).limit(defaultNoOfDependenciesReported + 1);
} else if (depSpec.rows) {
rows = await depSpec.rows(tx, defaultNoOfDependenciesReported + 1)
}
@@ -32,16 +31,17 @@ async function ensureNoDependencies(tx, context, id, depSpecs) {
break;
}
- if (await shares.checkEntityPermissionTx(tx, context, depSpec.entityTypeId, row.id, 'view')) {
+ if (depSpec.viewPermission && await shares.checkEntityPermissionTx(tx, context, depSpec.viewPermission.entityTypeId, depSpec.viewPermission.entityId, depSpec.viewPermission.requiredOperations)) {
deps.push({
entityTypeId: depSpec.entityTypeId,
name: row.name,
link: entityType.clientLink(row.id)
});
- } else {
+ } else if (!depSpec.viewPermission && await shares.checkEntityPermissionTx(tx, context, depSpec.entityTypeId, row.id, 'view')) {
deps.push({
entityTypeId: depSpec.entityTypeId,
- id: row.id
+ name: row.name,
+ link: entityType.clientLink(row.id)
});
}
}
diff --git a/server/lib/entity-settings.js b/server/lib/entity-settings.js
index cf23e724..3bb34927 100644
--- a/server/lib/entity-settings.js
+++ b/server/lib/entity-settings.js
@@ -44,6 +44,7 @@ const entityTypes = {
},
attachment: {
table: 'files_campaign_attachment',
+ inUseTable: 'files_campaign_attachment_usage',
permissions: {
view: 'viewAttachments',
manage: 'manageAttachments'
diff --git a/server/lib/helpers.js b/server/lib/helpers.js
index 2f87df96..8a61ff7b 100644
--- a/server/lib/helpers.js
+++ b/server/lib/helpers.js
@@ -1,10 +1,14 @@
'use strict';
+const crypto = require('crypto');
+
module.exports = {
enforce,
cleanupFromPost,
filterObject,
- castToInteger
+ castToInteger,
+ normalizeEmail,
+ hashEmail
};
function enforce(condition, message) {
@@ -28,12 +32,30 @@ function filterObject(obj, allowedKeys) {
return result;
}
-function castToInteger(id) {
+function castToInteger(id, msg) {
const val = parseInt(id);
if (!Number.isInteger(val)) {
- throw new Error('Invalid id');
+ throw new Error(msg || 'Invalid id');
}
return val;
-}
\ No newline at end of file
+}
+
+function normalizeEmail(email) {
+ const emailParts = email.split(/@/);
+
+ if (emailParts.length !== 2) {
+ return email;
+ }
+
+ const username = emailParts[0];
+ const domain = emailParts[1].toLowerCase();
+
+ return username + '@' + domain;
+}
+
+function hashEmail(email) {
+ return crypto.createHash('sha512').update(normalizeEmail(email)).digest("base64");
+}
+
diff --git a/server/lib/knex.js b/server/lib/knex.js
index 045b73cb..43cc7194 100644
--- a/server/lib/knex.js
+++ b/server/lib/knex.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const path = require('path');
const knexConstructor = require('knex');
@@ -23,6 +23,12 @@ const knex = require('knex')({
//, debug: true
});
+/*
+This is to enable logging on mysql side:
+SET GLOBAL general_log = 'ON';
+SET GLOBAL general_log_file = '/tmp/mysql-all.log';
+*/
+
module.exports = knex;
diff --git a/server/lib/log.js b/server/lib/log.js
index f8429067..845bab77 100644
--- a/server/lib/log.js
+++ b/server/lib/log.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const log = require('npmlog');
log.level = config.log.level;
diff --git a/server/lib/mailers.js b/server/lib/mailers.js
index 7880ce1a..8c0100a9 100644
--- a/server/lib/mailers.js
+++ b/server/lib/mailers.js
@@ -1,10 +1,8 @@
'use strict';
const log = require('./log');
-const config = require('config');
+const config = require('./config');
-const Handlebars = require('handlebars');
-const util = require('util');
const nodemailer = require('nodemailer');
const aws = require('aws-sdk');
const openpgpEncrypt = require('nodemailer-openpgp').openpgpEncrypt;
@@ -14,13 +12,21 @@ const builtinZoneMta = require('./builtin-zone-mta');
const contextHelpers = require('./context-helpers');
const settings = require('../models/settings');
-const tools = require('./tools');
-const htmlToText = require('html-to-text');
const bluebird = require('bluebird');
const transports = new Map();
+class SendConfigurationError extends Error {
+ constructor(sendConfigurationId, ...args) {
+ super(...args);
+ this.sendConfigurationId = sendConfigurationId;
+ Error.captureStackTrace(this, SendConfigurationError);
+ }
+}
+
+
+
async function getOrCreateMailer(sendConfigurationId) {
let sendConfiguration;
@@ -73,62 +79,26 @@ function _addDkimKeys(transport, mail) {
async function _sendMail(transport, mail, template) {
_addDkimKeys(transport, mail);
- let tryCount = 0;
- const trySend = (callback) => {
- tryCount++;
- transport.sendMail(mail, (err, info) => {
- if (err) {
- log.error('Mail', err);
- if (err.responseCode && err.responseCode >= 400 && err.responseCode < 500 && tryCount <= 5) {
- // temporary error, try again
- log.verbose('Mail', 'Retrying after %s sec. ...', tryCount);
- return setTimeout(trySend, tryCount * 1000);
- }
- return callback(err);
- }
- return callback(null, info);
- });
- };
+ try {
+ return await transport.sendMailAsync(mail);
- const trySendAsync = bluebird.promisify(trySend);
- return await trySendAsync();
+ } catch (err) {
+ if ( (err.responseCode && err.responseCode >= 400 && err.responseCode < 500) ||
+ (err.code === 'ECONNECTION' && err.errno === 'ECONNREFUSED')
+ ) {
+ throw new SendConfigurationError(transport.mailer.sendConfiguration.id, 'Cannot connect to service specified by send configuration ' + transport.mailer.sendConfiguration.id);
+ }
+
+ throw err;
+ }
}
-async function _sendTransactionalMail(transport, mail, template) {
- const sendConfiguration = transport.mailer.sendConfiguration;
-
+async function _sendTransactionalMail(transport, mail) {
if (!mail.headers) {
mail.headers = {};
}
mail.headers['X-Sending-Zone'] = 'transactional';
- mail.from = {
- name: sendConfiguration.from_name,
- address: sendConfiguration.from_email
- };
-
- const htmlRenderer = await tools.getTemplate(template.html, template.locale);
-
- if (htmlRenderer) {
- mail.html = htmlRenderer(template.data || {});
- }
-
- const preparedHtml = await tools.prepareHtml(mail.html);
-
- if (preparedHtml) {
- mail.html = preparedHtml;
- }
-
- const textRenderer = await tools.getTemplate(template.text, template.locale);
-
- if (textRenderer) {
- mail.text = textRenderer(template.data || {});
- } else if (mail.html) {
- mail.text = htmlToText.fromString(mail.html, {
- wordwrap: 130
- });
- }
-
return await _sendMail(transport, mail);
}
@@ -218,6 +188,7 @@ async function _createTransport(sendConfiguration) {
}
const transport = nodemailer.createTransport(transportOptions, config.nodemailer);
+ transport.sendMailAsync = bluebird.promisify(transport.sendMail.bind(transport));
transport.use('stream', openpgpEncrypt({
signingKey: configItems.pgpPrivateKey,
@@ -263,7 +234,7 @@ async function _createTransport(sendConfiguration) {
transport.mailer = {
sendConfiguration,
throttleWait: bluebird.promisify(throttleWait),
- sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail, template),
+ sendTransactionalMail: async (mail) => await _sendTransactionalMail(transport, mail),
sendMassMail: async (mail, template) => await _sendMail(transport, mail)
};
@@ -281,3 +252,4 @@ class MailerError extends Error {
module.exports.getOrCreateMailer = getOrCreateMailer;
module.exports.invalidateMailer = invalidateMailer;
module.exports.MailerError = MailerError;
+module.exports.SendConfigurationError = SendConfigurationError;
\ No newline at end of file
diff --git a/server/lib/message-sender.js b/server/lib/message-sender.js
new file mode 100644
index 00000000..ec90eefd
--- /dev/null
+++ b/server/lib/message-sender.js
@@ -0,0 +1,746 @@
+'use strict';
+
+const config = require('./config');
+const log = require('./log');
+const mailers = require('./mailers');
+const knex = require('./knex');
+const subscriptions = require('../models/subscriptions');
+const contextHelpers = require('./context-helpers');
+const campaigns = require('../models/campaigns');
+const templates = require('../models/templates');
+const lists = require('../models/lists');
+const fields = require('../models/fields');
+const sendConfigurations = require('../models/send-configurations');
+const links = require('../models/links');
+const {CampaignSource, CampaignType} = require('../../shared/campaigns');
+const {toNameTagLangauge} = require('../../shared/lists');
+const {CampaignMessageStatus} = require('../../shared/campaigns');
+const tools = require('./tools');
+const htmlToText = require('html-to-text');
+const request = require('request-promise');
+const files = require('../models/files');
+const {getPublicUrl} = require('./urls');
+const blacklist = require('../models/blacklist');
+const libmime = require('libmime');
+const { enforce, hashEmail } = require('./helpers');
+const senders = require('./senders');
+
+const MessageType = {
+ REGULAR: 0,
+ TRIGGERED: 1,
+ TEST: 2,
+ SUBSCRIPTION: 3,
+ API_TRANSACTIONAL: 4
+
+};
+
+class MessageSender {
+ constructor() {
+ }
+
+ /*
+ Accepted combinations of settings:
+
+ Option #1
+ - settings.type in [MessageType.REGULAR, MessageType.TRIGGERED, MessageType.TEST]
+ - campaignCid / campaignId
+ - listId / listCid [optional if campaign is provided]
+ - sendConfigurationId [optional if campaign is provided]
+ - attachments [optional]
+ - renderedHtml + renderedText / html + text + tagLanguage [optional if campaign is provided]
+ - subject [optional if campaign is provided]
+
+ Option #2
+ - settings.type in [MessageType.SUBSCRIPTION, MessageType.API_TRANSACTIONAL]
+ - sendConfigurationId
+ - attachments [optional]
+ - renderedHtml + renderedText / html + text + tagLanguage
+ - subject
+ */
+ async _init(settings) {
+ this.type = settings.type;
+
+ this.listsById = new Map(); // listId -> list
+ this.listsByCid = new Map(); // listCid -> list
+ this.listsFieldsGrouped = new Map(); // listId -> fieldsGrouped
+
+ await knex.transaction(async tx => {
+ if (this.type === MessageType.REGULAR || this.type === MessageType.TRIGGERED || this.type === MessageType.TEST) {
+ this.isMassMail = true;
+
+ if (settings.campaignCid) {
+ this.campaign = await campaigns.rawGetByTx(tx, 'cid', settings.campaignCid);
+ } else if (settings.campaignId) {
+ this.campaign = await campaigns.rawGetByTx(tx, 'id', settings.campaignId);
+ }
+
+ if (settings.sendConfigurationId) {
+ this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), settings.sendConfigurationId, false, true);
+ } else if (this.campaign && this.campaign.send_configuration) {
+ this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.send_configuration, false, true);
+ } else {
+ enforce(false);
+ }
+
+ this.useVerp = config.verp.enabled && this.sendConfiguration.verp_hostname;
+ this.useVerpSenderHeader = this.useVerp && !this.sendConfiguration.verp_disable_sender_header;
+
+
+ if (settings.listId) {
+ const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), settings.listId);
+ this.listsById.set(list.id, list);
+ this.listsByCid.set(list.cid, list);
+ this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
+
+ } else if (settings.listCid) {
+ const list = await lists.getByCidTx(tx, contextHelpers.getAdminContext(), settings.listCid);
+ this.listsById.set(list.id, list);
+ this.listsByCid.set(list.cid, list);
+ this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
+
+ } else if (this.campaign && this.campaign.lists) {
+ for (const listSpec of this.campaign.lists) {
+ const list = await lists.getByIdTx(tx, contextHelpers.getAdminContext(), listSpec.list);
+ this.listsById.set(list.id, list);
+ this.listsByCid.set(list.cid, list);
+ this.listsFieldsGrouped.set(list.id, await fields.listGroupedTx(tx, list.id));
+ }
+ }
+
+ } else if (this.type === MessageType.SUBSCRIPTION || this.type === MessageType.API_TRANSACTIONAL) {
+ this.isMassMail = false;
+ this.sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), settings.sendConfigurationId, false, true);
+
+ } else {
+ enforce(false);
+ }
+
+
+ if (settings.attachments) {
+ this.attachments = settings.attachments;
+
+ } else if (this.campaign && this.campaign.id) {
+ const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', this.campaign.id);
+
+ this.attachments = [];
+ for (const attachment of attachments) {
+ this.attachments.push({
+ filename: attachment.originalname,
+ path: files.getFilePath('campaign', 'attachment', this.campaign.id, attachment.filename)
+ });
+ }
+
+ } else {
+ this.attachments = [];
+ }
+
+ if (settings.renderedHtml !== undefined) {
+ this.renderedHtml = settings.renderedHtml;
+ this.renderedText = settings.renderedText;
+
+ } else if (settings.html !== undefined) {
+ this.html = settings.html;
+ this.text = settings.text;
+ this.tagLanguage = settings.tagLanguage;
+
+ } else if (this.campaign && this.campaign.source === CampaignSource.TEMPLATE) {
+ this.template = await templates.getByIdTx(tx, contextHelpers.getAdminContext(), this.campaign.data.sourceTemplate, false);
+ this.html = this.template.html;
+ this.text = this.template.text;
+ this.tagLanguage = this.template.tag_language;
+
+ } else if (this.campaign && (this.campaign.source === CampaignSource.CUSTOM || this.campaign.source === CampaignSource.CUSTOM_FROM_TEMPLATE || this.campaign.source === CampaignSource.CUSTOM_FROM_CAMPAIGN)) {
+ this.html = this.campaign.data.sourceCustom.html;
+ this.text = this.campaign.data.sourceCustom.text;
+ this.tagLanguage = this.campaign.data.sourceCustom.tag_language;
+ }
+
+ enforce(this.renderedHtml || (this.campaign && this.campaign.source === CampaignSource.URL) || this.tagLanguage);
+
+ if (settings.subject !== undefined) {
+ this.subject = settings.subject;
+ } else if (this.campaign && this.campaign.subject !== undefined) {
+ this.subject = this.campaign.subject;
+ } else {
+ enforce(false);
+ }
+ });
+ }
+
+ async _getMessage(mergeTags, list, subscriptionGrouped, replaceDataImgs) {
+ let html = '';
+ let text = '';
+ let renderTags = false;
+ const campaign = this.campaign;
+
+
+ if (this.renderedHtml !== undefined) {
+ html = this.renderedHtml;
+ text = this.renderedText;
+ renderTags = false;
+
+ } else if (this.html !== undefined) {
+ enforce(mergeTags);
+
+ html = this.html;
+ text = this.text;
+ renderTags = true;
+
+ } else if (campaign && campaign.source === CampaignSource.URL) {
+ const form = tools.getMessageLinks(campaign, list, subscriptionGrouped);
+ for (const key in mergeTags) {
+ form[key] = mergeTags[key];
+ }
+
+ const response = await request.post({
+ uri: campaign.sourceUrl,
+ form,
+ resolveWithFullResponse: true
+ });
+
+ if (response.statusCode !== 200) {
+ throw new Error(`Received status code ${httpResponse.statusCode} from ${campaign.sourceUrl}`);
+ }
+
+ html = response.body;
+ text = '';
+ renderTags = false;
+ }
+
+ const attachments = this.attachments.slice();
+ if (replaceDataImgs) {
+ // replace data: images with embedded attachments
+ html = html.replace(/( ]* src\s*=[\s"']*)(data:[^"'>\s]+)/gi, (match, prefix, dataUri) => {
+ const cid = shortid.generate() + '-attachments';
+ attachments.push({
+ path: dataUri,
+ cid
+ });
+ return prefix + 'cid:' + cid;
+ });
+ }
+
+
+ if (renderTags) {
+ if (this.campaign) {
+ html = await links.updateLinks(html, this.tagLanguage, mergeTags, campaign, list, subscriptionGrouped);
+ }
+
+ // When no list and subscriptionGrouped is provided, formatCampaignTemplate works the same way as formatTemplate
+ html = tools.formatCampaignTemplate(html, this.tagLanguage, mergeTags, true, campaign, list, subscriptionGrouped);
+ }
+
+ const generateText = !!(text || '').trim();
+ if (generateText) {
+ text = htmlToText.fromString(html, {wordwrap: 130});
+ } else {
+ // When no list and subscriptionGrouped is provided, formatCampaignTemplate works the same way as formatTemplate
+ text = tools.formatCampaignTemplate(text, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped)
+ }
+
+ return {
+ html,
+ text,
+ attachments
+ };
+ }
+
+ _getExtraTags(campaign) {
+ const tags = {};
+
+ if (campaign && 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.imageUrl;
+ tags['RSS_ENTRY_CUSTOM_TAGS'] = rssEntry.customTags;
+ }
+
+ return tags;
+ }
+
+ async initByCampaignId(campaignId) {
+ await this._init({type: MessageType.REGULAR, campaignId});
+ }
+
+
+ /*
+ Accepted combinations of subData:
+
+ Option #1
+ - listId
+ - subscriptionId
+ - mergeTags [optional, used only when campaign / html+text is provided]
+
+ Option #2:
+ - to ... email / { name, address }
+ - encryptionKeys [optional]
+ - mergeTags [used only when campaign / html+text is provided]
+ */
+ async _sendMessage(subData) {
+ let to, email;
+ let envelope = false;
+ let sender = false;
+ let headers = {};
+ let listHeader = false;
+ let encryptionKeys = [];
+ let subject;
+ let message;
+
+ let subscriptionGrouped, list; // May be undefined
+ const campaign = this.campaign; // May be undefined
+
+ let mergeTags = subData.mergeTags;
+
+ if (subData.listId) {
+ let listId;
+
+ if (subData.subscriptionId) {
+ listId = subData.listId;
+ subscriptionGrouped = await subscriptions.getById(contextHelpers.getAdminContext(), listId, subData.subscriptionId);
+ }
+
+ list = this.listsById.get(listId);
+ email = subscriptionGrouped.email;
+
+ const flds = this.listsFieldsGrouped.get(list.id);
+
+ if (!mergeTags) {
+ mergeTags = fields.getMergeTags(flds, subscriptionGrouped, this._getExtraTags(campaign));
+ }
+
+ for (const fld of flds) {
+ if (fld.type === 'gpg' && mergeTags[fld.key]) {
+ encryptionKeys.push(mergeTags[fld.key].trim());
+ }
+ }
+
+ message = await this._getMessage(mergeTags, list, subscriptionGrouped, true);
+
+ let listUnsubscribe = null;
+ if (!list.listunsubscribe_disabled) {
+ listUnsubscribe = campaign && campaign.unsubscribe_url
+ ? tools.formatCampaignTemplate(campaign.unsubscribe_url, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped)
+ : getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscriptionGrouped.cid);
+ }
+
+ to = {
+ name: list.to_name === null ? undefined : tools.formatCampaignTemplate(list.to_name, toNameTagLangauge, mergeTags, false, campaign, list, subscriptionGrouped),
+ address: subscriptionGrouped.email
+ };
+
+ subject = this.subject;
+
+ if (this.tagLanguage) {
+ subject = tools.formatCampaignTemplate(this.subject, this.tagLanguage, mergeTags, false, campaign, list, subscriptionGrouped);
+ }
+
+ headers = {
+ 'List-ID': {
+ prepared: true,
+ value: libmime.encodeWords(list.name) + ' <' + list.cid + '.' + getPublicUrl() + '>'
+ }
+ };
+
+ if (campaign) {
+ const campaignAddress = [campaign.cid, list.cid, subscriptionGrouped.cid].join('.');
+
+ if (this.useVerp) {
+ envelope = {
+ from: campaignAddress + '@' + sendConfiguration.verp_hostname,
+ to: subscriptionGrouped.email
+ };
+ }
+
+ if (this.useVerpSenderHeader) {
+ sender = campaignAddress + '@' + sendConfiguration.verp_hostname;
+ }
+
+ headers['x-fbl'] = campaignAddress;
+ headers['x-msys-api'] = JSON.stringify({
+ campaign_id: campaignAddress
+ });
+ headers['x-smtpapi'] = JSON.stringify({
+ unique_args: {
+ campaign_id: campaignAddress
+ }
+ });
+ headers['x-mailgun-variables'] = JSON.stringify({
+ campaign_id: campaignAddress
+ });
+ }
+
+ listHeader = {
+ unsubscribe: listUnsubscribe
+ };
+
+ } else if (subData.to) {
+ to = subData.to;
+ email = to.address;
+ subject = this.subject;
+ encryptionKeys = subData.encryptionKeys;
+ message = await this._getMessage(mergeTags);
+ }
+
+ if (await blacklist.isBlacklisted(email)) {
+ return;
+ }
+
+ const sendConfiguration = this.sendConfiguration;
+ const mailer = await mailers.getOrCreateMailer(sendConfiguration.id);
+
+ await mailer.throttleWait();
+
+ const getOverridable = key => {
+ if (campaign && sendConfiguration[key + '_overridable'] && campaign[key + '_override'] !== null) {
+ return campaign[key + '_override'] || '';
+ } else {
+ return sendConfiguration[key] || '';
+ }
+ };
+
+ const mail = {
+ from: {
+ name: getOverridable('from_name'),
+ address: getOverridable('from_email')
+ },
+ replyTo: getOverridable('reply_to'),
+ xMailer: sendConfiguration.x_mailer ? sendConfiguration.x_mailer : false,
+ to,
+ sender,
+ envelope,
+ headers,
+ list: listHeader,
+ subject,
+ html: message.html,
+ text: message.text,
+ attachments: message.attachments,
+ encryptionKeys
+ };
+
+
+ let response;
+ let responseId = null;
+
+ const info = this.isMassMail ? await mailer.sendMassMail(mail) : await mailer.sendTransactionalMail(mail);
+
+ log.verbose('MessageSender', `response: ${info.response} messageId: ${info.messageId}`);
+
+ let match;
+ if ((match = info.response.match(/^250 Message queued as ([0-9a-f]+)$/))) {
+ /*
+ ZoneMTA
+ info.response: 250 Message queued as 1691ad7f7ae00080fd
+ info.messageId:
+ */
+ response = info.response;
+ responseId = match[1];
+
+ } else if ((match = info.messageId.match(/^<([^>@]*)@.*amazonses\.com>$/))) {
+ /*
+ AWS SES
+ info.response: 0102016ad2244c0a-955492f2-9194-4cd1-bef9-70a45906a5a7-000000
+ info.messageId: <0102016ad2244c0a-955492f2-9194-4cd1-bef9-70a45906a5a7-000000@eu-west-1.amazonses.com>
+ */
+ response = info.response;
+ responseId = match[1];
+
+ } else if (info.response.match(/^250 OK$/) && (match = info.messageId.match(/^<([^>]*)>$/))) {
+ /*
+ Postal Mail Server
+ info.response: 250 OK
+ info.messageId: (postal messageId)
+ */
+ response = info.response;
+ responseId = match[1];
+
+ } else {
+ /*
+ Fallback - Mailtrain v1 behavior
+ */
+ response = info.response || info.messageId;
+ responseId = response.split(/\s+/).pop();
+ }
+
+
+ const result = {
+ response,
+ responseId: responseId,
+ list,
+ subscriptionGrouped,
+ email
+ };
+
+ return result;
+ }
+
+ async sendRegularCampaignMessage(campaignMessage) {
+ enforce(this.type === MessageType.REGULAR);
+
+ // We set the campaign_message to SENT before the message is actually sent. This is to avoid multiple delivery
+ // if by chance we run out of disk space and couldn't change status in the database after the message has been sent out
+ await knex('campaign_messages')
+ .where({id: campaignMessage.id})
+ .update({
+ status: CampaignMessageStatus.SENT,
+ updated: new Date()
+ });
+
+ let result;
+ try {
+ result = await this._sendMessage({listId: campaignMessage.list, subscriptionId: campaignMessage.subscription});
+ } catch (err) {
+ await knex('campaign_messages')
+ .where({id: campaignMessage.id})
+ .update({
+ status: CampaignMessageStatus.SCHEDULED,
+ updated: new Date()
+ });
+
+ throw err;
+ }
+
+ enforce(result.list);
+ enforce(result.subscriptionGrouped);
+
+ await knex('campaign_messages')
+ .where({id: campaignMessage.id})
+ .update({
+ response: result.response,
+ response_id: result.responseId,
+ updated: new Date()
+ });
+
+ await knex('campaigns').where('id', this.campaign.id).increment('delivered');
+ }
+}
+
+
+async function sendQueuedMessage(queuedMessage) {
+ const messageType = queuedMessage.type;
+
+ enforce(messageType === MessageType.TRIGGERED || messageType === MessageType.TEST || messageType === MessageType.SUBSCRIPTION || messageType === MessageType.API_TRANSACTIONAL);
+
+ const msgData = queuedMessage.data;
+
+ const cs = new MessageSender();
+ await cs._init({
+ type: messageType,
+ campaignId: msgData.campaignId,
+ listId: msgData.listId,
+ sendConfigurationId: queuedMessage.send_configuration,
+ attachments: msgData.attachments,
+ html: msgData.html,
+ text: msgData.text,
+ subject: msgData.subject,
+ tagLanguage: msgData.tagLanguage,
+ renderedHtml: msgData.renderedHtml,
+ renderedText: msgData.renderedText
+ });
+
+ const campaign = cs.campaign;
+
+ await knex('queued')
+ .where({id: queuedMessage.id})
+ .del();
+
+ let result;
+ try {
+ result = await cs._sendMessage({
+ subscriptionId: msgData.subscriptionId,
+ listId: msgData.listId,
+ to: msgData.to,
+ encryptionKeys: msgData.encryptionKeys
+ });
+ } catch (err) {
+ await knex.insert({
+ id: queuedMessage.id,
+ send_configuration: queuedMessage.send_configuration,
+ type: queuedMessage.type,
+ data: JSON.stringify(queuedMessage.data)
+ });
+
+ throw err;
+ }
+
+ if (messageType === MessageType.TRIGGERED) {
+ await knex('campaigns').where('id', campaign.id).increment('delivered');
+ }
+
+ if (campaign && messageType === MessageType.TEST) {
+ enforce(result.list);
+ enforce(result.subscriptionGrouped);
+
+ try {
+ // Insert an entry to test_messages. This allows us to remember test sends to lists that are not
+ // listed in the campaign - see the check in getMessage
+ await knex('test_messages').insert({
+ campaign: campaign.id,
+ list: result.list.id,
+ subscription: result.subscriptionGrouped.id
+ });
+ } catch (err) {
+ if (err.code === 'ER_DUP_ENTRY') {
+ // The entry is already there, so we can ignore this error
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ if (msgData.attachments) {
+ for (const attachment of msgData.attachments) {
+ if (attachment.id) { // This means that it is an attachment recorded in table files_campaign_attachment
+ try {
+ // We ignore any errors here because we already sent the message. Thus we have to mark it as completed to avoid sending it again.
+ await knex.transaction(async tx => {
+ await files.unlockTx(tx, 'campaign', 'attachment', attachment.id);
+ });
+ } catch (err) {
+ log.error('MessageSender', `Error when unlocking attachment ${attachment.id} for ${result.email} (queuedId: ${queuedMessage.id})`);
+ log.verbose(err.stack);
+ }
+ }
+ }
+ }
+}
+
+async function dropQueuedMessage(queuedMessage) {
+ await knex('queued')
+ .where({id: queuedMessage.id})
+ .del();
+}
+
+
+
+async function queueCampaignMessageTx(tx, sendConfigurationId, listId, subscriptionId, messageType, messageData) {
+ enforce(messageType === MessageType.TRIGGERED || messageType === MessageType.TEST);
+
+ const msgData = {...messageData};
+
+ if (msgData.attachments) {
+ for (const attachment of messageData.attachments) {
+ await files.lockTx(tx,'campaign', 'attachment', attachment.id);
+ }
+ }
+
+ msgData.listId = listId;
+ msgData.subscriptionId = subscriptionId;
+
+ await tx('queued').insert({
+ send_configuration: sendConfigurationId,
+ type: messageType,
+ data: JSON.stringify(msgData)
+ });
+}
+
+async function queueAPITransactionalMessage(sendConfigurationId, email, subject, html, text, tagLanguage, mergeTags, attachments) {
+ const msgData = {
+ to: {
+ address: email
+ },
+ html,
+ text,
+ tagLanguage,
+ subject,
+ mergeTags,
+ attachments
+ };
+
+ await tx('queued').insert({
+ send_configuration: sendConfigurationId,
+ type: MessageType.API_TRANSACTIONAL,
+ data: JSON.stringify(msgData)
+ });
+}
+
+async function queueSubscriptionMessage(sendConfigurationId, to, subject, encryptionKeys, template) {
+ let html, text;
+
+ const htmlRenderer = await tools.getTemplate(template.html, template.locale);
+ if (htmlRenderer) {
+ html = htmlRenderer(template.data || {});
+
+ if (html) {
+ html = await tools.prepareHtml(html);
+ }
+ }
+
+ const textRenderer = await tools.getTemplate(template.text, template.locale);
+ if (textRenderer) {
+ text = textRenderer(template.data || {});
+ } else if (html) {
+ text = htmlToText.fromString(html, {
+ wordwrap: 130
+ });
+ }
+
+ const msgData = {
+ renderedHtml: html,
+ renderedText: text,
+ to,
+ subject,
+ encryptionKeys
+ };
+
+ await knex('queued').insert({
+ send_configuration: sendConfigurationId,
+ type: MessageType.SUBSCRIPTION,
+ data: JSON.stringify(msgData)
+ });
+
+ senders.scheduleCheck();
+}
+
+async function getMessage(campaignCid, listCid, subscriptionCid) {
+ const cs = new MessageSender();
+ await cs._init({type: MessageType.REGULAR, campaignCid, listCid});
+
+ const campaign = cs.campaign;
+ const list = cs.listsByCid.get(listCid);
+
+ const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid);
+
+ let listOk = false;
+
+ for (const listSpec of campaign.lists) {
+ if (list.id === listSpec.list) {
+ // This means we send to a list that is associated with the campaign
+ listOk = true;
+ break;
+ }
+ }
+
+ if (!listOk) {
+ const row = await knex('test_messages').where({
+ campaign: campaign.id,
+ list: list.id,
+ subscription: subscriptionGrouped.id
+ }).first();
+
+ if (row) {
+ listOk = true;
+ }
+ }
+
+ if (!listOk) {
+ throw new Error('Message not found');
+ }
+
+ const flds = cs.listsFieldsGrouped.get(list.id);
+ const mergeTags = fields.getMergeTags(flds, subscriptionGrouped, cs._getExtraTags(campaign));
+
+ return await cs._getMessage(mergeTags, list, subscriptionGrouped, false);
+}
+
+module.exports.MessageSender = MessageSender;
+module.exports.MessageType = MessageType;
+module.exports.sendQueuedMessage = sendQueuedMessage;
+module.exports.queueCampaignMessageTx = queueCampaignMessageTx;
+module.exports.queueSubscriptionMessage = queueSubscriptionMessage;
+module.exports.dropQueuedMessage = dropQueuedMessage;
+module.exports.getMessage = getMessage;
+module.exports.queueAPITransactionalMessage = queueAPITransactionalMessage;
\ No newline at end of file
diff --git a/server/lib/passport.js b/server/lib/passport.js
index d8e10122..8b1eaae5 100644
--- a/server/lib/passport.js
+++ b/server/lib/passport.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const log = require('./log');
const util = require('util');
diff --git a/server/lib/privilege-helpers.js b/server/lib/privilege-helpers.js
index 67d52be9..3b27e459 100644
--- a/server/lib/privilege-helpers.js
+++ b/server/lib/privilege-helpers.js
@@ -1,7 +1,7 @@
'use strict';
const log = require('./log');
-const config = require('config');
+const config = require('./config');
const fs = require('fs-extra-promise');
diff --git a/server/lib/senders.js b/server/lib/senders.js
index 7ea15a28..476603b5 100644
--- a/server/lib/senders.js
+++ b/server/lib/senders.js
@@ -15,6 +15,7 @@ function spawn(callback) {
log.verbose('Senders', 'Spawning master sender process');
knex('campaigns').where('status', CampaignStatus.SENDING).update({status: CampaignStatus.SCHEDULED})
+ .then(() => knex('campaigns').where('status', CampaignStatus.PAUSING).update({status: CampaignStatus.PAUSED}))
.then(() => {
senderProcess = fork(path.join(__dirname, '..', 'services', 'sender-master.js'), [], {
cwd: path.join(__dirname, '..'),
diff --git a/server/lib/subscription-mail-helpers.js b/server/lib/subscription-mail-helpers.js
index ad7d0202..2d511e55 100644
--- a/server/lib/subscription-mail-helpers.js
+++ b/server/lib/subscription-mail-helpers.js
@@ -5,11 +5,11 @@ const fields = require('../models/fields');
const settings = require('../models/settings');
const {getTrustedUrl, getPublicUrl} = require('./urls');
const { tUI, tMark } = require('./translate');
-const util = require('util');
const contextHelpers = require('./context-helpers');
-const {getFieldColumn} = require('../../shared/lists');
+const {getFieldColumn, toNameTagLangauge} = require('../../shared/lists');
const forms = require('../models/forms');
-const mailers = require('./mailers');
+const messageSender = require('./message-sender');
+const tools = require('./tools');
module.exports = {
sendAlreadySubscribed,
@@ -65,38 +65,13 @@ async function sendUnsubscriptionConfirmed(locale, list, email, subscription) {
await _sendMail(list, email, 'unsubscription_confirmed', locale, tMark('listUnsubscriptionConfirmed'), relativeUrls, subscription);
}
-function getDisplayName(flds, subscription) {
- let firstName, lastName, name;
-
- for (const fld of flds) {
- if (fld.key === 'FIRST_NAME') {
- firstName = subscription[fld.column];
- }
-
- if (fld.key === 'LAST_NAME') {
- lastName = subscription[fld.column];
- }
-
- if (fld.key === 'NAME') {
- name = subscription[fld.column];
- }
- }
-
- if (name) {
- return name;
- } else if (firstName && lastName) {
- return firstName + ' ' + lastName;
- } else if (lastName) {
- return lastName;
- } else if (firstName) {
- return firstName;
- } else {
- return '';
- }
-}
-
async function _sendMail(list, email, template, locale, subjectKey, relativeUrls, subscription) {
- const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
+ subscription = {
+ ...subscription,
+ email
+ };
+
+ const flds = await fields.listGrouped(contextHelpers.getAdminContext(), list.id);
const encryptionKeys = [];
for (const fld of flds) {
@@ -137,21 +112,24 @@ async function _sendMail(list, email, template, locale, subjectKey, relativeUrls
}
try {
+ const mergeTags = fields.getMergeTags(flds, subscription);
+
if (list.send_configuration) {
- const mailer = await mailers.getOrCreateMailer(list.send_configuration);
- await mailer.sendTransactionalMail({
- to: {
- name: getDisplayName(flds, subscription),
+ await messageSender.queueSubscriptionMessage(
+ list.send_configuration,
+ {
+ name: list.to_name === null ? undefined : tools.formatTemplate(list.to_name, toNameTagLangauge, mergeTags, false),
address: email
},
- subject: tUI(subjectKey, locale, { list: list.name }),
- encryptionKeys
- }, {
- html,
- text,
- locale,
- data
- });
+ tUI(subjectKey, locale, { list: list.name }),
+ encryptionKeys,
+ {
+ html,
+ text,
+ locale,
+ data
+ }
+ );
} else {
log.warn('Subscription', `Not sending email for list id:${list.id} because not send configuration is set.`);
}
diff --git a/server/lib/template-sender.js b/server/lib/template-sender.js
deleted file mode 100644
index 5fda749f..00000000
--- a/server/lib/template-sender.js
+++ /dev/null
@@ -1,88 +0,0 @@
-'use strict';
-
-const mailers = require('./mailers');
-const tools = require('./tools');
-const templates = require('../models/templates');
-const { getMergeTagsForBases } = require('../../shared/templates');
-const { getTrustedUrl, getSandboxUrl, getPublicUrl } = require('../lib/urls');
-
-class TemplateSender {
- constructor(options) {
- this.defaultOptions = {
- maxMails: 100,
- ...options
- };
- }
-
- async send(params) {
- const options = { ...this.defaultOptions, ...params };
- this._validateMailOptions(options);
-
- const [mailer, template] = await Promise.all([
- mailers.getOrCreateMailer(
- options.sendConfigurationId
- ),
- templates.getById(
- options.context,
- options.templateId,
- false
- )
- ]);
-
- const variables = {
- EMAIL: options.email,
- ...getMergeTagsForBases(getTrustedUrl(), getSandboxUrl(), getPublicUrl()),
- ...options.variables
- };
-
- const html = tools.formatTemplate(
- template.html,
- null,
- variables,
- true
- );
- const subject = tools.formatTemplate(
- options.subject || template.description || template.name,
- variables
- );
- return mailer.sendTransactionalMail(
- {
- to: options.email,
- subject,
- attachments: options.attachments || []
- },
- {
- html: { template: html },
- data: options.data,
- locale: options.locale
- }
- );
- }
-
- _validateMailOptions(options) {
- let { context, email, locale, templateId } = options;
-
- if (!templateId) {
- throw new Error('Missing templateId');
- }
- if (!context) {
- throw new Error('Missing context');
- }
- if (!email || email.length === 0) {
- throw new Error('Missing email');
- }
- if (typeof email === 'string') {
- email = email.split(',');
- }
- if (email.length > options.maxMails) {
- throw new Error(
- `Cannot send more than ${options.maxMails} emails at once`
- );
- }
- if (!locale) {
- throw new Error('Missing locale');
- }
- }
-}
-
-module.exports = TemplateSender;
diff --git a/server/lib/tools.js b/server/lib/tools.js
index 8fb0c67b..e0ac497c 100644
--- a/server/lib/tools.js
+++ b/server/lib/tools.js
@@ -1,12 +1,9 @@
'use strict';
-const util = require('util');
const isemail = require('isemail');
const path = require('path');
const {getPublicUrl} = require('./urls');
-
-const bluebird = require('bluebird');
-
+const {enforce} = require('./helpers');
const hasher = require('node-object-hash')();
const mjml2html = require('mjml');
@@ -21,6 +18,8 @@ const fs = require('fs-extra');
const { JSDOM } = require('jsdom');
const { tUI, tLog, getLangCodeFromExpressLocale } = require('./translate');
+const {TagLanguages} = require('../../shared/templates');
+
const templates = new Map();
@@ -42,9 +41,7 @@ async function getLocalizedFile(basePath, fileName, language) {
}
async function getTemplate(template, locale) {
- if (!template) {
- return false;
- }
+ enforce(template);
const key = getLangCodeFromExpressLocale(locale) + ':' + ((typeof template === 'object') ? hasher.hash(template) : template);
@@ -127,7 +124,7 @@ async function validateEmail(address) {
function validateEmailGetMessage(result, address, language) {
let t;
- if (language) {
+ if (language !== undefined) {
t = (key, args) => tUI(key, language, args);
} else {
t = (key, args) => tLog(key, args);
@@ -147,23 +144,18 @@ function validateEmailGetMessage(result, address, language) {
}
}
-function formatMessage(campaign, list, subscription, mergeTags, message, isHTML) {
+function formatCampaignTemplate(source, tagLanguage, mergeTags, isHTML, campaign, list, subscription) {
const links = getMessageLinks(campaign, list, subscription);
- return formatTemplate(message, links, mergeTags, isHTML);
+ mergeTags = {...mergeTags, ...links};
+ return formatTemplate(source, tagLanguage, mergeTags, isHTML);
}
-function formatTemplate(template, links, mergeTags, isHTML) {
- if (!links && !mergeTags) { return template; }
+function _formatTemplateSimple(source, mergeTags, isHTML) {
+ if (!mergeTags) { return source; }
const getValue = fullKey => {
const keys = (fullKey || '').split('.');
- if (links && links.hasOwnProperty(keys[0])) {
- return links[keys[0]];
- }
-
- if (!mergeTags) { return false; }
-
let value = mergeTags;
while (keys.length > 0) {
let key = keys.shift();
@@ -181,7 +173,7 @@ function formatTemplate(template, links, mergeTags, isHTML) {
}) : (containsHTML ? htmlToText.fromString(value) : value);
};
- return template.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
+ return source.replace(/\[([a-z0-9_.]+)(?:\/([^\]]+))?\]/ig, (match, identifier, fallback) => {
let value = getValue(identifier);
if (value === false) {
return match;
@@ -191,11 +183,22 @@ function formatTemplate(template, links, mergeTags, isHTML) {
});
}
-async function prepareHtml(html) {
- if (!(html || '').toString().trim()) {
- return false;
- }
+function _formatTemplateHbs(source, mergeTags, isHTML) {
+ const renderer = hbs.handlebars.compile(source);
+ const options = {};
+ return renderer(mergeTags, options);
+}
+
+function formatTemplate(source, tagLanguage, mergeTags, isHTML) {
+ if (tagLanguage === TagLanguages.SIMPLE) {
+ return _formatTemplateSimple(source, mergeTags, isHTML)
+ } else if (tagLanguage === TagLanguages.HBS) {
+ return _formatTemplateHbs(source, mergeTags, isHTML)
+ }
+}
+
+async function prepareHtml(html) {
const { window } = new JSDOM(html);
const head = window.document.querySelector('head');
@@ -221,14 +224,26 @@ async function prepareHtml(html) {
}
function getMessageLinks(campaign, list, subscription) {
- return {
- LINK_UNSUBSCRIBE: getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid),
- LINK_PREFERENCES: getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid),
- LINK_BROWSER: getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid),
- CAMPAIGN_ID: campaign.cid,
- LIST_ID: list.cid,
- SUBSCRIPTION_ID: subscription.cid
- };
+ const result = {};
+
+ if (list && subscription) {
+ if (campaign) {
+ result.LINK_UNSUBSCRIBE = getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid + '?c=' + campaign.cid);
+ result.LINK_BROWSER = getPublicUrl('/archive/' + campaign.cid + '/' + list.cid + '/' + subscription.cid);
+ } else {
+ result.LINK_UNSUBSCRIBE = getPublicUrl('/subscription/' + list.cid + '/unsubscribe/' + subscription.cid);
+ }
+
+ result.LINK_PREFERENCES = getPublicUrl('/subscription/' + list.cid + '/manage/' + subscription.cid);
+ result.LIST_ID = list.cid;
+ result.SUBSCRIPTION_ID = subscription.cid;
+ }
+
+ if (campaign) {
+ result.CAMPAIGN_ID = campaign.cid;
+ }
+
+ return result;
}
module.exports = {
@@ -237,7 +252,7 @@ module.exports = {
getTemplate,
prepareHtml,
getMessageLinks,
- formatMessage,
+ formatCampaignTemplate,
formatTemplate
};
diff --git a/server/lib/translate.js b/server/lib/translate.js
index 0189d983..ba272355 100644
--- a/server/lib/translate.js
+++ b/server/lib/translate.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const i18n = require("i18next");
const fs = require('fs');
const path = require('path');
diff --git a/server/lib/urls.js b/server/lib/urls.js
index b3c5ee22..4e6f7077 100644
--- a/server/lib/urls.js
+++ b/server/lib/urls.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('./config');
const urllib = require('url');
const {anonymousRestrictedAccessToken} = require('../../shared/urls');
const {getLangCodeFromExpressLocale} = require('./translate');
diff --git a/server/models/blacklist.js b/server/models/blacklist.js
index 95f92460..8dca9c17 100644
--- a/server/models/blacklist.js
+++ b/server/models/blacklist.js
@@ -41,16 +41,17 @@ async function search(context, offset, limit, search) {
async function add(context, email) {
enforce(email, 'Email has to be set');
- return await knex.transaction(async tx => {
- shares.enforceGlobalPermission(context, 'manageBlacklist');
-
- const existing = await tx('blacklist').where('email', email).first();
- if (!existing) {
- await tx('blacklist').insert({email});
- }
+ shares.enforceGlobalPermission(context, 'manageBlacklist');
+ try {
+ await knex('blacklist').insert({email});
await activityLog.logBlacklistActivity(BlacklistActivityType.ADD, email);
- });
+ } catch (err) {
+ if (err.code === 'ER_DUP_ENTRY') {
+ } else {
+ throw err;
+ }
+ }
}
async function remove(context, email) {
diff --git a/server/models/campaigns.js b/server/models/campaigns.js
index 483ee32f..2e6aaa8f 100644
--- a/server/models/campaigns.js
+++ b/server/models/campaigns.js
@@ -10,23 +10,26 @@ const shares = require('./shares');
const namespaceHelpers = require('../lib/namespace-helpers');
const files = require('./files');
const templates = require('./templates');
-const { CampaignStatus, CampaignSource, CampaignType, getSendConfigurationPermissionRequiredForSend } = require('../../shared/campaigns');
+const { allTagLanguages } = require('../../shared/templates');
+const { CampaignMessageStatus, CampaignStatus, CampaignSource, CampaignType, getSendConfigurationPermissionRequiredForSend } = require('../../shared/campaigns');
const sendConfigurations = require('./send-configurations');
const triggers = require('./triggers');
const {SubscriptionStatus} = require('../../shared/lists');
const subscriptions = require('./subscriptions');
const segments = require('./segments');
const senders = require('../lib/senders');
-const {LinkId} = require('./links');
+const links = require('./links');
const feedcheck = require('../lib/feedcheck');
const contextHelpers = require('../lib/context-helpers');
const {convertFileURLs} = require('../lib/campaign-content');
+const messageSender = require('../lib/message-sender');
+const lists = require('./lists');
const {EntityActivityType, CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
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', 'data', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe_url'];
const allowedKeysCreate = new Set(['type', 'source', ...allowedKeysCommon]);
const allowedKeysCreateRssEntry = new Set(['type', 'source', 'parent', ...allowedKeysCommon]);
@@ -168,7 +171,7 @@ async function listTestUsersDTAjax(context, campaignId, params) {
let subsQry;
if (subsQrys.length === 1) {
- const subsUnionSql = '(' + subsQrys[0].sql + ') as `test_subscriptions`'
+ const subsUnionSql = '(' + subsQrys[0].sql + ') as `test_subscriptions`';
subsQry = knex.raw(subsUnionSql, subsQrys[0].bindings);
} else {
@@ -182,7 +185,7 @@ async function listTestUsersDTAjax(context, campaignId, params) {
return await dtHelpers.ajaxListWithPermissionsTx(
tx,
context,
- [{ entityTypeId: 'list', requiredOperations: ['viewSubscriptions'], column: 'subs.list_id' }],
+ [{ entityTypeId: 'list', requiredOperations: ['viewTestSubscriptions'], column: 'subs.list_id' }],
params,
builder => {
return builder.from(function () {
@@ -296,7 +299,7 @@ async function listOpensDTAjax(context, campaignId, params) {
return this.from('campaign_links')
.where('campaign_links.campaign', campaignId)
.where('campaign_links.list', cpgList.list)
- .where('campaign_links.link', LinkId.OPEN)
+ .where('campaign_links.link', links.LinkId.OPEN)
.as('related_campaign_links');
},
'related_campaign_links.subscription', subsTable + '.id')
@@ -336,13 +339,18 @@ async function getTrackingSettingsByCidTx(tx, cid) {
return entity;
}
+async function lockByIdTx(tx, id) {
+ // This locks the entry for update
+ await tx('campaigns').where('id', id).forUpdate();
+}
+
async function rawGetByTx(tx, key, id) {
const entity = await tx('campaigns').where('campaigns.' + key, id)
.leftJoin('campaign_lists', 'campaigns.id', 'campaign_lists.campaign')
.groupBy('campaigns.id')
.select([
'campaigns.id', 'campaigns.cid', 'campaigns.name', 'campaigns.description', 'campaigns.namespace', 'campaigns.status', 'campaigns.type', 'campaigns.source',
- 'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject_override',
+ 'campaigns.send_configuration', 'campaigns.from_name_override', 'campaigns.from_email_override', 'campaigns.reply_to_override', 'campaigns.subject',
'campaigns.data', 'campaigns.click_tracking_disabled', 'campaigns.open_tracking_disabled', 'campaigns.unsubscribe_url', 'campaigns.scheduled',
'campaigns.delivered', 'campaigns.unsubscribed', 'campaigns.bounced', 'campaigns.complained', 'campaigns.blacklisted', 'campaigns.opened', 'campaigns.clicks',
knex.raw(`GROUP_CONCAT(CONCAT_WS(\':\', campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \';\') as lists`)
@@ -382,11 +390,13 @@ async function getByIdTx(tx, context, id, withPermissions = true, content = Cont
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
- const unsentQryGen = await getSubscribersQueryGeneratorTx(tx, id);
- if (unsentQryGen) {
- const res = await unsentQryGen(tx).count('* AS subscriptionsToSend').first();
- entity.subscriptionsToSend = res.subscriptionsToSend;
- }
+ const totalRes = await tx('campaign_messages')
+ .where({campaign: id})
+ .whereIn('status', [CampaignMessageStatus.SCHEDULED, CampaignMessageStatus.SENT,
+ CampaignMessageStatus.COMPLAINED, CampaignMessageStatus.UNSUBSCRIBED, CampaignMessageStatus.BOUNCED])
+ .count('* as count').first();
+
+ entity.total = totalRes.count;
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
delete entity.data.sourceCustom;
@@ -443,6 +453,10 @@ async function _validateAndPreprocess(tx, context, entity, isCreate, content) {
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', entity.send_configuration, 'viewPublic');
}
+
+ if ((isCreate && entity.source === CampaignSource.CUSTOM) || (content === Content.ONLY_SOURCE_CUSTOM)) {
+ enforce(allTagLanguages.includes(entity.data.sourceCustom.tag_language), `Invalid tag language '${entity.data.sourceCustom.tag_language}'`);
+ }
}
async function _createTx(tx, context, entity, content) {
@@ -460,6 +474,7 @@ async function _createTx(tx, context, entity, content) {
entity.data.sourceCustom = {
type: template.type,
+ tag_language: template.tag_language,
data: template.data,
html: template.html,
text: template.text
@@ -557,7 +572,7 @@ async function updateWithConsistencyCheck(context, entity, content) {
} else if (content === Content.WITHOUT_SOURCE_CUSTOM) {
filteredEntity.data.sourceCustom = existing.data.sourceCustom;
- await namespaceHelpers.validateMove(context, filteredEntity, existing, 'campaign', 'createCampaign', 'delete'); // XXX TB - try with entity
+ await namespaceHelpers.validateMove(context, entity, existing, 'campaign', 'createCampaign', 'delete');
} else if (content === Content.ONLY_SOURCE_CUSTOM) {
const data = existing.data;
@@ -632,21 +647,32 @@ async function remove(context, id) {
});
}
-async function enforceSendPermissionTx(tx, context, campaignId) {
+async function enforceSendPermissionTx(tx, context, campaignOrCampaignId, isToTestUsers, listId) {
let campaign;
- if (typeof campaignId === 'object') {
- campaign = campaignId;
+ if (typeof campaignOrCampaignId === 'object') {
+ campaign = campaignOrCampaignId;
} else {
- campaign = await getByIdTx(tx, context, campaignId, false);
+ campaign = await getByIdTx(tx, context, campaignOrCampaignId, false);
}
const sendConfiguration = await sendConfigurations.getByIdTx(tx, contextHelpers.getAdminContext(), campaign.send_configuration, false, false);
- const requiredPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
+ const requiredSendConfigurationPermission = getSendConfigurationPermissionRequiredForSend(campaign, sendConfiguration);
+ await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredSendConfigurationPermission);
- await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', campaign.send_configuration, requiredPermission);
- await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaign.id, 'send');
+ const requiredListAndCampaignPermission = isToTestUsers ? 'sendToTestUsers' : 'send';
+
+ await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaign.id, requiredListAndCampaignPermission);
+
+ if (listId) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, requiredListAndCampaignPermission);
+
+ } else {
+ for (const listIds of campaign.lists) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listIds.list, requiredListAndCampaignPermission);
+ }
+ }
}
@@ -675,7 +701,7 @@ async function getMessageByCid(messageCid, withVerpHostname = false) { // withVe
.where(subscrTblName + '.cid', subscriptionCid)
.where('campaigns.cid', campaignCid)
.select([
- 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
+ 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.hash_email', 'campaign_messages.status'
])
.first();
@@ -692,55 +718,45 @@ async function getMessageByCid(messageCid, withVerpHostname = false) { // withVe
}
async function getMessageByResponseId(responseId) {
- return await knex.transaction(async tx => {
- const message = await tx('campaign_messages')
- .where('campaign_messages.response_id', responseId)
- .select([
- 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
- ])
- .first();
-
- return message;
- });
+ return await knex('campaign_messages')
+ .where('campaign_messages.response_id', responseId)
+ .select([
+ 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.hash_email', 'campaign_messages.status'
+ ])
+ .first();
}
-const statusFieldMapping = {
- [SubscriptionStatus.UNSUBSCRIBED]: 'unsubscribed',
- [SubscriptionStatus.BOUNCED]: 'bounced',
- [SubscriptionStatus.COMPLAINED]: 'complained'
-};
+const statusFieldMapping = new Map();
+statusFieldMapping.set(CampaignMessageStatus.UNSUBSCRIBED, 'unsubscribed');
+statusFieldMapping.set(CampaignMessageStatus.BOUNCED, 'bounced');
+statusFieldMapping.set(CampaignMessageStatus.COMPLAINED, 'complained');
-async function _changeStatusByMessageTx(tx, context, message, subscriptionStatus) {
- enforce(subscriptionStatus !== SubscriptionStatus.SUBSCRIBED);
+async function _changeStatusByMessageTx(tx, context, message, campaignMessageStatus) {
+ enforce(statusFieldMapping.has(campaignMessageStatus));
- if (message.status === SubscriptionStatus.SUBSCRIBED) {
+ if (message.status === SubscriptionStatus.SENT) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', message.campaign, 'manageMessages');
- if (!subscriptionStatus in statusFieldMapping) {
- throw new Error('Unrecognized message status');
- }
-
- const statusField = statusFieldMapping[subscriptionStatus];
+ const statusField = statusFieldMapping.get(campaignMessageStatus);
await tx('campaigns').increment(statusField, 1).where('id', message.campaign);
await tx('campaign_messages')
.where('id', message.id)
.update({
- status: subscriptionStatus,
+ status: campaignMessageStatus,
updated: knex.fn.now()
});
}
}
-async function changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaignCid, listId, subscriptionId, subscriptionStatus) {
- const campaign = await tx('campaigns').where('cid', campaignCid);
+async function changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaignCid, listId, subscriptionId, campaignMessageStatus) {
const message = await tx('campaign_messages')
.innerJoin('campaigns', 'campaign_messages.campaign', 'campaigns.id')
.where('campaigns.cid', campaignCid)
.where({subscription: subscriptionId, list: listId})
.select([
- 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
+ 'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.hash_email', 'campaign_messages.status'
])
.first();
@@ -748,17 +764,23 @@ async function changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaig
throw new Error('Invalid campaign.');
}
- await _changeStatusByMessageTx(tx, context, message, subscriptionStatus);
+ await _changeStatusByMessageTx(tx, context, message, campaignMessageStatus);
}
-async function changeStatusByMessage(context, message, subscriptionStatus, updateSubscription) {
+
+const campaignMessageStatusToSubscriptionStatusMapping = new Map();
+campaignMessageStatusToSubscriptionStatusMapping.set(CampaignMessageStatus.BOUNCED, SubscriptionStatus.BOUNCED);
+campaignMessageStatusToSubscriptionStatusMapping.set(CampaignMessageStatus.UNSUBSCRIBED, SubscriptionStatus.UNSUBSCRIBED);
+campaignMessageStatusToSubscriptionStatusMapping.set(CampaignMessageStatus.COMPLAINED, SubscriptionStatus.COMPLAINED);
+
+async function changeStatusByMessage(context, message, campaignMessageStatus, updateSubscription) {
await knex.transaction(async tx => {
if (updateSubscription) {
- await subscriptions.changeStatusTx(tx, context, message.list, message.subscription, subscriptionStatus);
+ enforce(campaignMessageStatusToSubscriptionStatusMapping.has(campaignMessageStatus));
+ await subscriptions.changeStatusTx(tx, context, message.list, message.subscription, campaignMessageStatusToSubscriptionStatusMapping.get(campaignMessageStatus));
}
- await _changeStatusByMessageTx(tx, context, message, subscriptionStatus);
-
+ await _changeStatusByMessageTx(tx, context, message, campaignMessageStatus);
});
}
@@ -773,93 +795,85 @@ async function updateMessageResponse(context, message, response, responseId) {
});
}
-async function getSubscribersQueryGeneratorTx(tx, campaignId) {
- /*
- This is supposed to produce queries like this:
+async function prepareCampaignMessages(campaignId) {
+ const campaign = await getById(contextHelpers.getAdminContext(), campaignId, false);
- select ... from `campaign_lists` inner join (
- select `email`, min(`campaign_list_id`) as `campaign_list_id`, max(`sent`) as `sent` from (
- (select `subscription__2`.`email`, 8 AS campaign_list_id, related_campaign_messages.id IS NOT NULL AS sent from `subscription__2` left join
- (select * from `campaign_messages` where `campaign_messages`.`campaign` = 1 and `campaign_messages`.`list` = 2)
- as `related_campaign_messages` on `related_campaign_messages`.`subscription` = `subscription__2`.`id` where `subscription__2`.`status` = 1)
- UNION ALL
- (select `subscription__1`.`email`, 9 AS campaign_list_id, related_campaign_messages.id IS NOT NULL AS sent from `subscription__1` left join
- (select * from `campaign_messages` where `campaign_messages`.`campaign` = 1 and `campaign_messages`.`list` = 1)
- as `related_campaign_messages` on `related_campaign_messages`.`subscription` = `subscription__1`.`id` where `subscription__1`.`status` = 1)
- ) as `pending_subscriptions_all` where `sent` = false group by `email`)
- as `pending_subscriptions` on `campaign_lists`.`id` = `pending_subscriptions`.`campaign_list_id` where `campaign_lists`.`campaign` = '1'
+ await knex('campaign_messages').where({campaign: campaignId, status: CampaignMessageStatus.SCHEDULED}).del();
- This was too much for Knex, so we partially construct these queries directly as strings;
- */
-
- const subsQrys = [];
- const cpgLists = await tx('campaign_lists').where('campaign', campaignId);
-
- for (const cpgList of cpgLists) {
- const addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {};
+ for (const cpgList of campaign.lists) {
+ let addSegmentQuery;
+ await knex.transaction(async tx => {
+ addSegmentQuery = cpgList.segment ? await segments.getQueryGeneratorTx(tx, cpgList.list, cpgList.segment) : () => {};
+ });
const subsTable = subscriptions.getSubscriptionTableName(cpgList.list);
- const sqlQry = knex.from(subsTable)
- .leftJoin(
- function () {
- return this.from('campaign_messages')
- .where('campaign_messages.campaign', campaignId)
- .where('campaign_messages.list', cpgList.list)
- .as('related_campaign_messages');
- },
- 'related_campaign_messages.subscription', subsTable + '.id')
+ const subsQry = knex.from(subsTable)
.where(subsTable + '.status', SubscriptionStatus.SUBSCRIBED)
.where(function() {
addSegmentQuery(this);
})
- .select([subsTable + '.email', knex.raw('? AS campaign_list_id', [cpgList.id]), knex.raw('related_campaign_messages.id IS NOT NULL AS sent')])
+ .select([
+ 'hash_email',
+ 'id',
+ knex.raw('? AS campaign', [campaign.id]),
+ knex.raw('? AS list', [cpgList.list]),
+ knex.raw('? AS send_configuration', [campaign.send_configuration]),
+ knex.raw('? AS status', [CampaignMessageStatus.SCHEDULED])
+ ])
.toSQL().toNative();
- subsQrys.push(sqlQry);
- }
-
- if (subsQrys.length > 0) {
- let subsQry;
- const unsentWhere = ' where `sent` = false';
-
- if (subsQrys.length === 1) {
- const subsUnionSql = '(select `email`, `campaign_list_id`, `sent` from (' + subsQrys[0].sql + ') as `pending_subscriptions_all`' + unsentWhere + ') as `pending_subscriptions`'
- subsQry = knex.raw(subsUnionSql, subsQrys[0].bindings);
-
- } else {
- const subsUnionSql = '(select `email`, min(`campaign_list_id`) as `campaign_list_id`, max(`sent`) as `sent` from (' +
- subsQrys.map(qry => '(' + qry.sql + ')').join(' UNION ALL ') +
- ') as `pending_subscriptions_all`' + unsentWhere + ' group by `email`) as `pending_subscriptions`';
- const subsUnionBindings = Array.prototype.concat(...subsQrys.map(qry => qry.bindings));
- subsQry = knex.raw(subsUnionSql, subsUnionBindings);
- }
-
- return knx => knx.from('campaign_lists')
- .where('campaign_lists.campaign', campaignId)
- .innerJoin(subsQry, 'campaign_lists.id', 'pending_subscriptions.campaign_list_id');
-
- } else {
- return null;
+ await knex.raw('INSERT IGNORE INTO `campaign_messages` (`hash_email`, `subscription`, `campaign`, `list`, `send_configuration`, `status`) ' + subsQry.sql, subsQry.bindings);
}
}
-async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, scheduled = null) {
+async function _changeStatus(context, campaignId, permittedCurrentStates, newState, invalidStateMessage, extraData) {
await knex.transaction(async tx => {
- const entity = await tx('campaigns').where('id', campaignId).first();
- if (!entity) {
- throw new interoperableErrors.NotFoundError();
- }
+ // This is quite inefficient because it selects the same row 3 times. However as status is changed
+ // rather infrequently, we keep it this way for simplicity
+ await lockByIdTx(tx, campaignId);
- await enforceSendPermissionTx(tx, context, entity);
+ const entity = await getByIdTx(tx, context, campaignId, false);
+
+ await enforceSendPermissionTx(tx, context, entity, false);
if (!permittedCurrentStates.includes(entity.status)) {
throw new interoperableErrors.InvalidStateError(invalidStateMessage);
}
- await tx('campaigns').where('id', campaignId).update({
- status: newState,
- scheduled
- });
+ if (Array.isArray(newState)) {
+ const newStateIdx = permittedCurrentStates.indexOf(entity.status);
+ enforce(newStateIdx != -1);
+ newState = newState[newStateIdx];
+ }
+
+ const updateData = {
+ status: newState
+ };
+
+ if (!extraData) {
+ updateData.scheduled = null;
+ updateData.start_at = null;
+ } else {
+ const startAt = extraData.startAt;
+
+ // If campaign is started without "scheduled" specified, startAt === null
+ updateData.scheduled = startAt;
+ if (!startAt || startAt.valueOf() < Date.now()) {
+ updateData.start_at = new Date();
+ } else {
+ updateData.start_at = startAt;
+ }
+
+ const timezone = extraData.timezone;
+ if (timezone) {
+ updateData.data = JSON.stringify({
+ ...entity.data,
+ timezone
+ });
+ }
+ }
+
+ await tx('campaigns').where('id', campaignId).update(updateData);
await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: newState});
});
@@ -868,22 +882,23 @@ async function _changeStatus(context, campaignId, permittedCurrentStates, newSta
}
-async function start(context, campaignId, startAt) {
- await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.SCHEDULED, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE or PAUSED state', startAt);
+async function start(context, campaignId, extraData) {
+ await _changeStatus(context, campaignId, [CampaignStatus.IDLE, CampaignStatus.SCHEDULED, CampaignStatus.PAUSED, CampaignStatus.FINISHED], CampaignStatus.SCHEDULED, 'Cannot start campaign until it is in IDLE, PAUSED, or FINISHED state', extraData);
}
async function stop(context, campaignId) {
- await _changeStatus(context, campaignId, [CampaignStatus.SCHEDULED], CampaignStatus.PAUSED, 'Cannot stop campaign until it is in SCHEDULED state');
+ await _changeStatus(context, campaignId, [CampaignStatus.SCHEDULED, CampaignStatus.SENDING], [CampaignStatus.PAUSED, CampaignStatus.PAUSING], 'Cannot stop campaign until it is in SCHEDULED or SENDING state');
}
async function reset(context, campaignId) {
await knex.transaction(async tx => {
+ // This is quite inefficient because it selects the same row 3 times. However as RESET is
+ // going to be called rather infrequently, we keep it this way for simplicity
+ await lockByIdTx(tx, campaignId);
+
await shares.enforceEntityPermissionTx(tx, context, 'campaign', campaignId, 'send');
const entity = await tx('campaigns').where('id', campaignId).first();
- if (!entity) {
- throw new interoperableErrors.NotFoundError();
- }
if (entity.status !== CampaignStatus.FINISHED && entity.status !== CampaignStatus.PAUSED) {
throw new interoperableErrors.InvalidStateError('Cannot reset campaign until it is FINISHED or PAUSED state');
@@ -919,8 +934,8 @@ async function getStatisticsOpened(context, id) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'viewStats');
- const devices = await tx('campaign_links').where('campaign', id).where('link', LinkId.OPEN).groupBy('device_type').select('device_type AS key').count('* as count');
- const countries = await tx('campaign_links').where('campaign', id).where('link', LinkId.OPEN).groupBy('country').select('country AS key').count('* as count');
+ const devices = await tx('campaign_links').where('campaign', id).where('link', links.LinkId.OPEN).groupBy('device_type').select('device_type AS key').count('* as count');
+ const countries = await tx('campaign_links').where('campaign', id).where('link', links.LinkId.OPEN).groupBy('country').select('country AS key').count('* as count');
return {
devices,
@@ -944,6 +959,105 @@ async function fetchRssCampaign(context, cid) {
});
}
+async function testSend(context, data) {
+ // Though it's a bit counter-intuitive, this handles also test sends of a template (i.e. without any campaign id)
+
+ await knex.transaction(async tx => {
+ const processSubscriber = async (sendConfigurationId, listId, subscriptionId, messageData) => {
+ await messageSender.queueCampaignMessageTx(tx, sendConfigurationId, listId, subscriptionId, messageSender.MessageType.TEST, messageData);
+
+ await activityLog.logEntityActivity('campaign', CampaignActivityType.TEST_SEND, campaignId, {list: listId, subscription: subscriptionId});
+ };
+
+ const campaignId = data.campaignId;
+
+ if (campaignId) { // This means we are sending a campaign
+ /*
+ Data coming from the client:
+ - html, text
+ - subjectPrepend, subjectAppend
+ - listCid, subscriptionCid
+ - listId, segmentId
+ */
+
+ const campaign = await getByIdTx(tx, context, campaignId, false);
+ const sendConfigurationId = campaign.send_configuration;
+
+ const messageData = {
+ campaignId: campaignId,
+ subject: data.subjectPrepend + campaign.subject + data.subjectAppend,
+ html: data.html, // The html, text and tagLanguage may be undefined
+ text: data.text,
+ tagLanguage: data.tagLanguage,
+ attachments: []
+ };
+
+ const attachments = await files.listTx(tx, contextHelpers.getAdminContext(), 'campaign', 'attachment', campaignId);
+ for (const attachment of attachments) {
+ messageData.attachments.push({
+ filename: attachment.originalname,
+ path: files.getFilePath('campaign', 'attachment', campaign.id, attachment.filename),
+ id: attachment.id
+ });
+ }
+
+
+ let listId = data.listId;
+ if (!listId && data.listCid) {
+ const list = await lists.getByCidTx(tx, context, data.listCid);
+ listId = list.id;
+ }
+
+ const segmentId = data.segmentId;
+
+ if (listId) {
+ await enforceSendPermissionTx(tx, context, campaign, true, listId);
+
+ if (data.subscriptionCid) {
+ const subscriber = await subscriptions.getByCidTx(tx, context, listId, data.subscriptionCid, true, true);
+ await processSubscriber(sendConfigurationId, listId, subscriber.id, messageData);
+
+ } else {
+ const subscribers = await subscriptions.listTestUsersTx(tx, context, listId, segmentId);
+ for (const subscriber of subscribers) {
+ await processSubscriber(sendConfigurationId, listId, subscriber.id, messageData);
+ }
+ }
+
+ } else {
+ for (const lstSeg of campaign.lists) {
+ await enforceSendPermissionTx(tx, context, campaign, true, lstSeg.list);
+
+ const subscribers = await subscriptions.listTestUsersTx(tx, context, lstSeg.list, segmentId);
+ for (const subscriber of subscribers) {
+ await processSubscriber(sendConfigurationId, lstSeg.list, subscriber.id, messageData);
+ }
+ }
+ }
+
+ } else { // This means we are sending a template
+ /*
+ Data coming from the client:
+ - html, text
+ - listCid, subscriptionCid, sendConfigurationId
+ */
+
+ const messageData = {
+ subject: 'Test',
+ html: data.html,
+ text: data.text,
+ tagLanguage: data.tagLanguage
+ };
+
+ const list = await lists.getByCidTx(tx, context, data.listCid);
+ const subscriber = await subscriptions.getByCidTx(tx, context, list.id, data.subscriptionCid, true, true);
+ await processSubscriber(data.sendConfigurationId, list.id, subscriber.id, messageData);
+ }
+ });
+
+ senders.scheduleCheck();
+}
+
module.exports.Content = Content;
module.exports.hash = hash;
@@ -974,7 +1088,7 @@ module.exports.changeStatusByCampaignCidAndSubscriptionIdTx = changeStatusByCamp
module.exports.changeStatusByMessage = changeStatusByMessage;
module.exports.updateMessageResponse = updateMessageResponse;
-module.exports.getSubscribersQueryGeneratorTx = getSubscribersQueryGeneratorTx;
+module.exports.prepareCampaignMessages = prepareCampaignMessages;
module.exports.start = start;
module.exports.stop = stop;
@@ -986,4 +1100,6 @@ module.exports.rawGetByTx = rawGetByTx;
module.exports.getTrackingSettingsByCidTx = getTrackingSettingsByCidTx;
module.exports.getStatisticsOpened = getStatisticsOpened;
-module.exports.fetchRssCampaign = fetchRssCampaign;
\ No newline at end of file
+module.exports.fetchRssCampaign = fetchRssCampaign;
+
+module.exports.testSend = testSend;
\ No newline at end of file
diff --git a/server/models/confirmations.js b/server/models/confirmations.js
index b3312bcb..f14dfd38 100644
--- a/server/models/confirmations.js
+++ b/server/models/confirmations.js
@@ -21,7 +21,7 @@ async function addConfirmation(listId, action, ip, data) {
*/
async function takeConfirmation(cid) {
return await knex.transaction(async tx => {
- const entry = await tx('confirmations').select(['cid', 'list', 'action', 'ip', 'data']).where('cid', cid).first();
+ const entry = await tx('confirmations').select(['cid', 'list', 'action', 'ip', 'data']).where('cid', cid).forUpdate().first();
if (!entry) {
return false;
diff --git a/server/models/fields.js b/server/models/fields.js
index d7bc7943..faac8bd7 100644
--- a/server/models/fields.js
+++ b/server/models/fields.js
@@ -2,7 +2,6 @@
const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
-const slugify = require('slugify');
const { enforce, filterObject } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
@@ -20,8 +19,8 @@ const {ListActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
-const allowedKeysCreate = new Set(['name', 'key', 'default_value', 'type', 'group', 'settings']);
-const allowedKeysUpdate = new Set(['name', 'key', 'default_value', 'group', 'settings']);
+const allowedKeysCreate = new Set(['name', 'help', 'key', 'default_value', 'type', 'group', 'settings']);
+const allowedKeysUpdate = new Set(['name', 'help', 'key', 'default_value', 'group', 'settings']);
const hashKeys = allowedKeysCreate;
const fieldTypes = {};
@@ -304,7 +303,7 @@ async function getById(context, listId, id) {
}
async function listTx(tx, listId) {
- return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'key', 'column', 'settings', 'group', 'default_value', 'order_list', 'order_subscribe', 'order_manage']).orderBy(knex.raw('-order_list'), 'desc').orderBy('id', 'asc');
+ return await tx('custom_fields').where({list: listId}).select(['id', 'name', 'type', 'help', 'key', 'column', 'settings', 'group', 'default_value', 'order_list', 'order_subscribe', 'order_manage']).orderBy(knex.raw('-order_list'), 'desc').orderBy('id', 'asc');
}
async function list(context, listId) {
@@ -543,7 +542,7 @@ async function createTx(tx, context, listId, entity) {
let columnName;
if (!fieldType.grouped) {
- columnName = ('custom_' + slugify(entity.name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
+ columnName = ('custom_' + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_');
}
const filteredEntity = filterObject(entity, allowedKeysCreate);
@@ -661,10 +660,11 @@ function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes group
const entry = {
name: fld.name,
key: fld.key,
+ help: fld.help,
field: fld,
[type.getHbsType(fld)]: true,
order_subscribe: fld.order_subscribe,
- order_manage: fld.order_manage,
+ order_manage: fld.order_manage
};
if (!type.grouped && !type.enumerated) {
@@ -688,6 +688,7 @@ function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes group
options.push({
key: opt.key,
name: opt.name,
+ help: opt.help,
value: isEnabled
});
}
@@ -702,6 +703,7 @@ function forHbsWithFieldsGrouped(fieldsGrouped, subscription) { // assumes group
options.push({
key: opt.key,
name: opt.label,
+ help: opt.help,
value: value === opt.key
});
}
diff --git a/server/models/files.js b/server/models/files.js
index 2ceaf928..01afe512 100644
--- a/server/models/files.js
+++ b/server/models/files.js
@@ -45,7 +45,7 @@ async function listDTAjax(context, type, subType, entityId, params) {
await shares.enforceEntityPermission(context, type, entityId, getFilesPermission(type, subType, 'view'));
return await dtHelpers.ajaxList(
params,
- builder => builder.from(getFilesTable(type, subType)).where({entity: entityId}),
+ builder => builder.from(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}),
['id', 'originalname', 'filename', 'size', 'created']
);
}
@@ -53,7 +53,7 @@ async function listDTAjax(context, type, subType, entityId, params) {
async function listTx(tx, context, type, subType, entityId) {
enforceTypePermitted(type, subType);
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
- return await tx(getFilesTable(type, subType)).where({entity: entityId}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
+ return await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}).select(['id', 'originalname', 'filename', 'size', 'created']).orderBy('originalname', 'asc');
}
async function list(context, type, subType, entityId) {
@@ -65,7 +65,7 @@ async function list(context, type, subType, entityId) {
async function getFileById(context, type, subType, id) {
enforceTypePermitted(type, subType);
const file = await knex.transaction(async tx => {
- const file = await tx(getFilesTable(type, subType)).where('id', id).first();
+ const file = await tx(getFilesTable(type, subType)).where({id: id, delete_pending: false}).first();
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'view'));
return file;
});
@@ -85,7 +85,7 @@ async function _getFileBy(context, type, subType, entityId, key, value) {
enforceTypePermitted(type, subType);
const file = await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'view'));
- const file = await tx(getFilesTable(type, subType)).where({entity: entityId, [key]: value}).first();
+ const file = await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false, [key]: value}).first();
return file;
});
@@ -155,7 +155,7 @@ async function createFiles(context, type, subType, entityId, files, replacementB
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, type, entityId, getFilesPermission(type, subType, 'manage'));
- const existingNamesRows = await tx(getFilesTable(type, subType)).where('entity', entityId).select(['id', 'filename', 'originalname']);
+ const existingNamesRows = await tx(getFilesTable(type, subType)).where({entity: entityId, delete_pending: false}).select(['id', 'filename', 'originalname']);
const existingNameSet = new Set();
for (const row of existingNamesRows) {
@@ -275,18 +275,49 @@ async function createFiles(context, type, subType, entityId, files, replacementB
}
}
+async function lockTx(tx, type, subType, id) {
+ enforceTypePermitted(type, subType);
+ const filesTableName = getFilesTable(type, subType);
+ await tx(filesTableName).where('id', id).increment('lock_count');
+}
+
+async function unlockTx(tx, type, subType, id) {
+ enforceTypePermitted(type, subType);
+
+ const filesTableName = getFilesTable(type, subType);
+ const file = await tx(filesTableName).where('id', id).first();
+
+ enforce(file, `File ${id} not found`);
+ enforce(file.lock_count > 0, `Corrupted lock count at file ${id}`);
+
+ if (file.lock_count === 1 && file.delete_pending) {
+ await tx(filesTableName).where('id', id).del();
+
+ const filePath = getFilePath(type, subType, file.entity, file.filename);
+ await fs.removeAsync(filePath);
+
+ } else {
+ await tx(filesTableName).where('id', id).update({lock_count: file.lock_count - 1});
+ }
+}
+
async function removeFile(context, type, subType, id) {
enforceTypePermitted(type, subType);
- const file = await knex.transaction(async tx => {
- const file = await tx(getFilesTable(type, subType)).where('id', id).select('entity', 'filename').first();
+ await knex.transaction(async tx => {
+ const filesTableName = getFilesTable(type, subType);
+ const file = await tx(filesTableName).where('id', id).first();
await shares.enforceEntityPermissionTx(tx, context, type, file.entity, getFilesPermission(type, subType, 'manage'));
- await tx(getFilesTable(type, subType)).where('id', id).del();
- return {filename: file.filename, entity: file.entity};
- });
- const filePath = getFilePath(type, subType, file.entity, file.filename);
- await fs.removeAsync(filePath);
+ if (!file.lock_count) {
+ await tx(filesTableName).where('id', file.id).del();
+
+ const filePath = getFilePath(type, subType, file.entity, file.filename);
+ await fs.removeAsync(filePath);
+ } else {
+ await tx(filesTableName).where('id', file.id).update({delete_pending: true});
+ }
+ });
}
async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toType, toSubType, toEntityId) {
@@ -296,7 +327,7 @@ async function copyAllTx(tx, context, fromType, fromSubType, fromEntityId, toTyp
enforceTypePermitted(toType, toSubType);
await shares.enforceEntityPermissionTx(tx, context, toType, toEntityId, getFilesPermission(toType, toSubType, 'manage'));
- const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId});
+ const rows = await tx(getFilesTable(fromType, fromSubType)).where({entity: fromEntityId, delete_pending: false});
for (const row of rows) {
const fromFilePath = getFilePath(fromType, fromSubType, fromEntityId, row.filename);
const toFilePath = getFilePath(toType, toSubType, toEntityId, row.filename);
@@ -339,4 +370,6 @@ module.exports.getFileUrl = getFileUrl;
module.exports.getFilePath = getFilePath;
module.exports.copyAllTx = copyAllTx;
module.exports.removeAllTx = removeAllTx;
+module.exports.lockTx = lockTx;
+module.exports.unlockTx = unlockTx;
module.exports.ReplacementBehavior = ReplacementBehavior;
diff --git a/server/models/forms.js b/server/models/forms.js
index 1bfade5e..8eac9cae 100644
--- a/server/models/forms.js
+++ b/server/models/forms.js
@@ -70,7 +70,7 @@ async function listDTAjax(context, params) {
}
-async function _getById(tx, id) {
+async function _getByIdTx(tx, id) {
const entity = await tx('custom_forms').where('id', id).first();
if (!entity) {
@@ -86,17 +86,20 @@ async function _getById(tx, id) {
return entity;
}
+async function getByIdTx(tx, context, id, withPermissions = true) {
+ await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
+ const entity = await _getByIdTx(tx, id);
+
+ if (withPermissions) {
+ entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id);
+ }
+
+ return entity;
+}
async function getById(context, id, withPermissions = true) {
return await knex.transaction(async tx => {
- await shares.enforceEntityPermissionTx(tx, context, 'customForm', id, 'view');
- const entity = await _getById(tx, id);
-
- if (withPermissions) {
- entity.permissions = await shares.getPermissionsTx(tx, context, 'customForm', id);
- }
-
- return entity;
+ return await getByIdTx(tx, context, id, withPermissions);
});
}
@@ -122,6 +125,17 @@ async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createCustomForm');
+ if (entity.fromExistingEntity) {
+ const existing = await getByIdTx(tx, context, entity.existingEntity, false);
+
+ entity.layout = existing.layout;
+ entity.form_input_style = existing.form_input_style;
+
+ for (const key of allowedFormKeys) {
+ entity[key] = existing[key];
+ }
+ }
+
await namespaceHelpers.validateEntity(tx, entity);
const form = filterObject(entity, allowedFormKeys);
@@ -147,7 +161,7 @@ async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'customForm', entity.id, 'edit');
- const existing = await _getById(tx, entity.id);
+ const existing = await _getByIdTx(tx, entity.id);
const existingHash = hash(existing);
if (existingHash !== entity.originalHash) {
@@ -219,7 +233,7 @@ async function getDefaultCustomFormValues() {
// TODO - this could run in the browser too - move to shared
function checkForMjmlErrors(form) {
- let testLayout = '{{{body}}}';
+ let testLayout = '{{{body}}}';
let hasMjmlError = (template, layout = testLayout) => {
let source = layout.replace(/\{\{\{body\}\}\}/g, template);
@@ -283,6 +297,7 @@ function checkForMjmlErrors(form) {
module.exports.listDTAjax = listDTAjax;
module.exports.hash = hash;
module.exports.getById = getById;
+module.exports.getByIdTx = getByIdTx;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;
diff --git a/server/models/links.js b/server/models/links.js
index 9c6acc0d..30922b00 100644
--- a/server/models/links.js
+++ b/server/models/links.js
@@ -11,6 +11,7 @@ const he = require('he');
const { getPublicUrl } = require('../lib/urls');
const tools = require('../lib/tools');
const shortid = require('shortid');
+const {enforce} = require('../lib/helpers');
const LinkId = {
OPEN: -1,
@@ -103,16 +104,16 @@ async function countLink(remoteIp, userAgent, campaignCid, listCid, subscription
}
async function addOrGet(campaignId, url) {
- return await knex.transaction(async tx => {
- const link = await tx('links').select(['id', 'cid']).where({
- campaign: campaignId,
- url
- }).first();
+ const link = await knex('links').select(['id', 'cid']).where({
+ campaign: campaignId,
+ url
+ }).first();
- if (!link) {
- let cid = shortid.generate();
+ if (!link) {
+ let cid = shortid.generate();
- const ids = await tx('links').insert({
+ try {
+ const ids = await knex('links').insert({
campaign: campaignId,
cid,
url
@@ -122,16 +123,27 @@ async function addOrGet(campaignId, url) {
id: ids[0],
cid
};
- } else {
- return link;
+ } catch (err) {
+ if (err.code === 'ER_DUP_ENTRY') {
+ const link = await knex('links').select(['id', 'cid']).where({
+ campaign: campaignId,
+ url
+ }).first();
+
+ enforce(link);
+ return link;
+ }
}
- });
+
+ } else {
+ return link;
+ }
}
-async function updateLinks(campaign, list, subscription, mergeTags, message) {
- if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !message || !message.trim()) {
+async function updateLinks(source, tagLanguage, mergeTags, campaign, list, subscription) {
+ if ((campaign.open_tracking_disabled && campaign.click_tracking_disabled) || !source || !source.trim()) {
// tracking is disabled, do not modify the message
- return message;
+ return source;
}
// insert tracking image
@@ -139,12 +151,12 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
let inserted = false;
const imgUrl = getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}`);
const img = ' ';
- message = message.replace(/<\/body\b/i, match => {
+ source = source.replace(/<\/body\b/i, match => {
inserted = true;
return img + match;
});
if (!inserted) {
- message = message + img;
+ source = source + img;
}
}
@@ -153,7 +165,7 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
const urlsToBeReplaced = new Set();
- message.replace(re, (match, prefix, encodedUrl) => {
+ source.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true});
urlsToBeReplaced.add(url);
});
@@ -161,19 +173,19 @@ async function updateLinks(campaign, list, subscription, mergeTags, message) {
const urls = new Map(); // url -> {id, cid} (as returned by add)
for (const url of urlsToBeReplaced) {
// url might include variables, need to rewrite those just as we do with message content
- const expanedUrl = tools.formatMessage(campaign, list, subscription, mergeTags, url);
+ const expanedUrl = tools.formatCampaignTemplate(url, tagLanguage, mergeTags, false, campaign, list, subscription);
const link = await addOrGet(campaign.id, expanedUrl);
urls.set(url, link);
}
- message = message.replace(re, (match, prefix, encodedUrl) => {
+ source = source.replace(re, (match, prefix, encodedUrl) => {
const url = he.decode(encodedUrl, {isAttributeValue: true});
const link = urls.get(url);
return prefix + (link ? getPublicUrl(`/links/${campaign.cid}/${list.cid}/${subscription.cid}/${link.cid}`) : url);
});
}
- return message;
+ return source;
}
module.exports.LinkId = LinkId;
diff --git a/server/models/mosaico-templates.js b/server/models/mosaico-templates.js
index 4f3f5df9..fb05ed64 100644
--- a/server/models/mosaico-templates.js
+++ b/server/models/mosaico-templates.js
@@ -9,8 +9,9 @@ const namespaceHelpers = require('../lib/namespace-helpers');
const shares = require('./shares');
const files = require('./files');
const dependencyHelpers = require('../lib/dependency-helpers');
+const { allTagLanguages } = require('../../shared/templates');
-const allowedKeys = new Set(['name', 'description', 'type', 'data', 'namespace']);
+const allowedKeys = new Set(['name', 'description', 'type', 'tag_language', 'data', 'namespace']);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
@@ -32,11 +33,25 @@ async function listDTAjax(context, params) {
[{ entityTypeId: 'mosaicoTemplate', requiredOperations: ['view'] }],
params,
builder => builder.from('mosaico_templates').innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace'),
- [ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.created', 'namespaces.name' ]
+ [ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.tag_language', 'mosaico_templates.created', 'namespaces.name' ]
+ );
+}
+
+async function listByTagLanguageDTAjax(context, tagLanguage, params) {
+ return await dtHelpers.ajaxListWithPermissions(
+ context,
+ [{ entityTypeId: 'mosaicoTemplate', requiredOperations: ['view'] }],
+ params,
+ builder => builder.from('mosaico_templates')
+ .innerJoin('namespaces', 'namespaces.id', 'mosaico_templates.namespace')
+ .where('mosaico_templates.tag_language', tagLanguage),
+ [ 'mosaico_templates.id', 'mosaico_templates.name', 'mosaico_templates.description', 'mosaico_templates.type', 'mosaico_templates.tag_language', 'mosaico_templates.created', 'namespaces.name' ]
);
}
async function _validateAndPreprocess(tx, entity) {
+ enforce(allTagLanguages.includes(entity.tag_language), `Invalid tag language '${entity.tag_language}'`);
+
entity.data = JSON.stringify(entity.data);
await namespaceHelpers.validateEntity(tx, entity);
}
@@ -58,7 +73,6 @@ async function create(context, entity) {
async function updateWithConsistencyCheck(context, entity) {
await knex.transaction(async tx => {
- await shares.enforceGlobalPermission(context, 'createJavascriptWithROAccess');
await shares.enforceEntityPermissionTx(tx, context, 'mosaicoTemplate', entity.id, 'edit');
const existing = await tx('mosaico_templates').where('id', entity.id).first();
@@ -119,6 +133,7 @@ async function remove(context, id) {
module.exports.hash = hash;
module.exports.getById = getById;
module.exports.listDTAjax = listDTAjax;
+module.exports.listByTagLanguageDTAjax = listByTagLanguageDTAjax;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;
diff --git a/server/models/namespaces.js b/server/models/namespaces.js
index 77d5cd91..b1343874 100644
--- a/server/models/namespaces.js
+++ b/server/models/namespaces.js
@@ -226,7 +226,16 @@ async function remove(context, id) {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', id, 'delete');
const entityTypesWithNamespace = Object.keys(entitySettings.getEntityTypes());
- await dependencyHelpers.ensureNoDependencies(tx, context, id, entityTypesWithNamespace.map(entityTypeId => ({ entityTypeId: entityTypeId, column: 'namespace' })));
+
+ const depSpecs = entityTypesWithNamespace.map(entityTypeId => {
+ if (entityTypeId === 'user') {
+ return { entityTypeId: entityTypeId, column: 'namespace', viewPermission: {entityTypeId: 'namespace', entityId: id, requiredOperations: 'manageUsers'} };
+ } else {
+ return { entityTypeId: entityTypeId, column: 'namespace' };
+ }
+ });
+
+ await dependencyHelpers.ensureNoDependencies(tx, context, id, depSpecs);
await tx('namespaces').where('id', id).del();
});
diff --git a/server/models/send-configurations.js b/server/models/send-configurations.js
index 7e0ca860..7fa4c909 100644
--- a/server/models/send-configurations.js
+++ b/server/models/send-configurations.js
@@ -14,7 +14,7 @@ const mailers = require('../lib/mailers');
const senders = require('../lib/senders');
const dependencyHelpers = require('../lib/dependency-helpers');
-const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable', 'x_mailer', 'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']);
+const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'x_mailer', 'verp_hostname', 'verp_disable_sender_header', 'mailer_type', 'mailer_settings', 'namespace']);
const allowedMailerTypes = new Set(Object.values(MailerType));
@@ -75,7 +75,7 @@ async function _getByTx(tx, context, key, id, withPermissions, withPrivateData)
entity.mailer_settings = JSON.parse(entity.mailer_settings);
} else {
entity = await tx('send_configurations').where(key, id).select(
- ['id', 'name', 'cid', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable', 'subject', 'subject_overridable']
+ ['id', 'name', 'cid', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'reply_to', 'reply_to_overridable']
).first();
if (!entity) {
diff --git a/server/models/shares.js b/server/models/shares.js
index 976129bd..ce974cf8 100644
--- a/server/models/shares.js
+++ b/server/models/shares.js
@@ -1,7 +1,7 @@
'use strict';
const knex = require('../lib/knex');
-const config = require('config');
+const config = require('../lib/config');
const { enforce, castToInteger } = require('../lib/helpers');
const dtHelpers = require('../lib/dt-helpers');
const entitySettings = require('../lib/entity-settings');
@@ -171,7 +171,6 @@ async function rebuildPermissionsTx(tx, restriction) {
}
}
-
// Reset root, own and shared namespaces shares as per the user roles
const usersAutoSharesQry = tx('users')
.select(['users.id', 'users.role', 'users.namespace']);
diff --git a/server/models/subscriptions.js b/server/models/subscriptions.js
index f9276073..2e90be43 100644
--- a/server/models/subscriptions.js
+++ b/server/models/subscriptions.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
const shortid = require('shortid');
@@ -9,16 +9,18 @@ const interoperableErrors = require('../../shared/interoperable-errors');
const shares = require('./shares');
const fields = require('./fields');
const { SubscriptionSource, SubscriptionStatus, getFieldColumn } = require('../../shared/lists');
+const { CampaignMessageStatus } = require('../../shared/campaigns');
const segments = require('./segments');
-const { enforce, filterObject } = require('../lib/helpers');
+const { enforce, filterObject, hashEmail, normalizeEmail } = require('../lib/helpers');
const moment = require('moment');
const { formatDate, formatBirthday } = require('../../shared/date');
-const crypto = require('crypto');
const campaigns = require('./campaigns');
const lists = require('./lists');
const allowedKeysBase = new Set(['email', 'tz', 'is_test', 'status']);
+const TEST_USERS_LIST_LIMIT = 1000;
+
const fieldTypes = {};
const Cardinality = {
@@ -82,7 +84,6 @@ fieldTypes.option = {
};
-
function getSubscriptionTableName(listId) {
return `subscription__${listId}`;
}
@@ -200,11 +201,15 @@ async function hashByList(listId, entity) {
});
}
-async function _getByTx(tx, context, listId, key, value, grouped) {
- await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
+async function _getByTx(tx, context, listId, key, value, grouped, isTest) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, isTest ? 'viewTestSubscriptions' : 'viewSubscriptions');
const entity = await tx(getSubscriptionTableName(listId)).where(key, value).first();
+ if (isTest && (!entity || !entity.is_test)) {
+ shares.throwPermissionDenied();
+ }
+
if (!entity) {
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
}
@@ -218,26 +223,31 @@ async function _getByTx(tx, context, listId, key, value, grouped) {
return entity;
}
-async function _getBy(context, listId, key, value, grouped) {
+async function _getBy(context, listId, key, value, grouped, isTest) {
return await knex.transaction(async tx => {
- return _getByTx(tx, context, listId, key, value, grouped);
+ return _getByTx(tx, context, listId, key, value, grouped, isTest);
});
}
async function getById(context, listId, id, grouped = true) {
- return await _getBy(context, listId, 'id', id, grouped);
+ return await _getBy(context, listId, 'id', id, grouped, false);
}
async function getByEmail(context, listId, email, grouped = true) {
- return await _getBy(context, listId, 'email', email, grouped);
+ const result = await _getBy(context, listId, 'hash_email', hashEmail(email), grouped, false);
+ if (result.email === null) {
+ throw new interoperableErrors.NotFoundError('Subscription not found in this list');
+ }
+ enforce(normalizeEmail(email) === normalizeEmail(result.email));
+ return result;
}
-async function getByCid(context, listId, cid, grouped = true) {
- return await _getBy(context, listId, 'cid', cid, grouped);
+async function getByCid(context, listId, cid, grouped = true, isTest = true) {
+ return await _getBy(context, listId, 'cid', cid, grouped, isTest);
}
-async function getByCidTx(tx, context, listId, cid, grouped = true) {
- return await _getByTx(tx, context, listId, 'cid', cid, grouped);
+async function getByCidTx(tx, context, listId, cid, grouped = true, isTest = true) {
+ return await _getByTx(tx, context, listId, 'cid', cid, grouped, isTest);
}
async function listDTAjax(context, listId, segmentId, params) {
@@ -352,7 +362,7 @@ async function listDTAjax(context, listId, segmentId, params) {
async function listTestUsersDTAjax(context, listCid, params) {
return await knex.transaction(async tx => {
const list = await lists.getByCidTx(tx, context, listCid);
- await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'viewSubscriptions');
+ await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'viewTestSubscriptions');
const listTable = getSubscriptionTableName(list.id);
@@ -409,6 +419,32 @@ async function list(context, listId, grouped, offset, limit) {
});
}
+async function listTestUsersTx(tx, context, listId, segmentId, grouped) {
+ await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'viewSubscriptions');
+
+ let entitiesQry = tx(getSubscriptionTableName(listId)).orderBy('id', 'asc').where('is_test', true).limit(TEST_USERS_LIST_LIMIT);
+
+ if (segmentId) {
+ const addSegmentQuery = await segments.getQueryGeneratorTx(tx, listId, segmentId);
+
+ entitiesQry = entitiesQry.where(function() {
+ addSegmentQuery(this);
+ });
+ }
+
+ const entities = await entitiesQry;
+
+ if (grouped) {
+ const groupedFieldsMap = await getGroupedFieldsMapTx(tx, listId);
+
+ for (const entity of entities) {
+ groupSubscription(groupedFieldsMap, entity);
+ }
+ }
+
+ return entities;
+}
+
// 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;
@@ -457,7 +493,7 @@ async function serverValidate(context, listId, data) {
await shares.enforceEntityPermissionTx(tx, context, 'list', listId, 'manageSubscriptions');
if (data.email) {
- const existingKeyQuery = tx(getSubscriptionTableName(listId)).where('email', data.email);
+ const existingKeyQuery = tx(getSubscriptionTableName(listId)).where('hash_email', hashEmail(data.email)).whereNotNull('email');
if (data.id) {
existingKeyQuery.whereNot('id', data.id);
@@ -476,7 +512,7 @@ async function serverValidate(context, listId, data) {
async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta, isCreate) {
enforce(entity.email, 'Email must be set');
- const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('hash_email', hashEmail(entity.email));
+ const existingWithKeyQuery = tx(getSubscriptionTableName(listId)).where('hash_email', hashEmail(entity.email)).forUpdate();
if (!isCreate) {
existingWithKeyQuery.whereNot('id', entity.id);
@@ -510,10 +546,6 @@ async function _validateAndPreprocess(tx, listId, groupedFieldsMap, entity, meta
}
}
-function hashEmail(email) {
- return crypto.createHash('sha512').update(email).digest("base64");
-}
-
function updateSourcesAndHashEmail(subscription, source, groupedFieldsMap) {
if ('email' in subscription) {
subscription.hash_email = hashEmail(subscription.email);
@@ -755,7 +787,7 @@ async function unsubscribeByCidAndGet(context, listId, subscriptionCid, campaign
const existing = await tx(getSubscriptionTableName(listId)).where('cid', subscriptionCid).first();
if (campaignCid) {
- await campaigns.changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaignCid, listId, existing.id, SubscriptionStatus.UNSUBSCRIBED);
+ await campaigns.changeStatusByCampaignCidAndSubscriptionIdTx(tx, context, campaignCid, listId, existing.id, CampaignMessageStatus.UNSUBSCRIBED);
}
return await _unsubscribeExistingAndGetTx(tx, context, listId, existing);
@@ -813,7 +845,7 @@ async function updateManaged(context, listId, cid, entity) {
for (const key in groupedFieldsMap) {
const fld = groupedFieldsMap[key];
- if (fld.order_manage) {
+ if (fld.order_manage !== null) {
update[key] = entity[key];
}
@@ -836,7 +868,7 @@ async function getListsWithEmail(context, email) {
for (const list of lsts) {
await shares.enforceEntityPermissionTx(tx, context, 'list', list.id, 'viewSubscriptions');
- const entity = await tx(getSubscriptionTableName(list.id)).where('email', email).first();
+ const entity = await tx(getSubscriptionTableName(list.id)).where('hash_email', hashEmail(email)).whereNotNull('email').first();
if (entity) {
result.push(list);
}
@@ -855,6 +887,7 @@ module.exports.getByEmail = getByEmail;
module.exports.list = list;
module.exports.listIterator = listIterator;
module.exports.listDTAjax = listDTAjax;
+module.exports.listTestUsersTx = listTestUsersTx;
module.exports.listTestUsersDTAjax = listTestUsersDTAjax;
module.exports.serverValidate = serverValidate;
module.exports.create = create;
@@ -871,4 +904,4 @@ module.exports.updateAddressAndGet = updateAddressAndGet;
module.exports.updateManaged = updateManaged;
module.exports.getListsWithEmail = getListsWithEmail;
module.exports.changeStatusTx = changeStatusTx;
-module.exports.purgeSensitiveData = purgeSensitiveData;
\ No newline at end of file
+module.exports.purgeSensitiveData = purgeSensitiveData;
diff --git a/server/models/templates.js b/server/models/templates.js
index b18b7186..68d34c28 100644
--- a/server/models/templates.js
+++ b/server/models/templates.js
@@ -7,12 +7,13 @@ const dtHelpers = require('../lib/dt-helpers');
const interoperableErrors = require('../../shared/interoperable-errors');
const namespaceHelpers = require('../lib/namespace-helpers');
const shares = require('./shares');
-const reports = require('./reports');
const files = require('./files');
const dependencyHelpers = require('../lib/dependency-helpers');
const {convertFileURLs} = require('../lib/campaign-content');
+const { allTagLanguages } = require('../../shared/templates');
+const messageSender = require('../lib/message-sender');
-const allowedKeys = new Set(['name', 'description', 'type', 'data', 'html', 'text', 'namespace']);
+const allowedKeys = new Set(['name', 'description', 'type', 'tag_language', 'data', 'html', 'text', 'namespace']);
function hash(entity) {
return hasher.hash(filterObject(entity, allowedKeys));
@@ -48,7 +49,7 @@ async function _listDTAjax(context, namespaceId, params) {
}
return builder;
},
- [ 'templates.id', 'templates.name', 'templates.description', 'templates.type', 'templates.created', 'namespaces.name' ]
+ [ 'templates.id', 'templates.name', 'templates.description', 'templates.type', 'templates.tag_language', 'templates.created', 'namespaces.name' ]
);
}
@@ -63,6 +64,8 @@ async function listByNamespaceDTAjax(context, namespaceId, params) {
async function _validateAndPreprocess(tx, entity) {
await namespaceHelpers.validateEntity(tx, entity);
+ enforce(allTagLanguages.includes(entity.tag_language), `Invalid tag language '${entity.tag_language}'`);
+
// We don't check contents of the "data" because it is processed solely on the client. The client generates the HTML code we use when sending out campaigns.
entity.data = JSON.stringify(entity.data);
@@ -72,13 +75,14 @@ async function create(context, entity) {
return await knex.transaction(async tx => {
await shares.enforceEntityPermissionTx(tx, context, 'namespace', entity.namespace, 'createTemplate');
- if (entity.fromSourceTemplate) {
- const template = await getByIdTx(tx, context, entity.sourceTemplate, false);
+ if (entity.fromExistingEntity) {
+ const existing = await getByIdTx(tx, context, entity.existingEntity, false);
- entity.type = template.type;
- entity.data = template.data;
- entity.html = template.html;
- entity.text = template.text;
+ entity.type = existing.type;
+ entity.tag_language = existing.tag_language;
+ entity.data = existing.data;
+ entity.html = existing.html;
+ entity.text = existing.text;
}
await _validateAndPreprocess(tx, entity);
@@ -88,10 +92,10 @@ async function create(context, entity) {
await shares.rebuildPermissionsTx(tx, { entityTypeId: 'template', entityId: id });
- if (entity.fromSourceTemplate) {
- await files.copyAllTx(tx, context, 'template', 'file', entity.sourceTemplate, 'template', 'file', id);
+ if (entity.fromExistingEntity) {
+ await files.copyAllTx(tx, context, 'template', 'file', entity.existingEntity, 'template', 'file', id);
- convertFileURLs(entity, 'template', entity.sourceTemplate, 'template', id);
+ convertFileURLs(entity, 'template', entity.existingEntity, 'template', id);
await tx('templates').update(filterObject(entity, allowedKeys)).where('id', id);
}
@@ -145,6 +149,17 @@ async function remove(context, id) {
});
}
+async function sendAsTransactionalEmail(context, templateId, sendConfigurationId, emails, subject, mergeTags, attachments) {
+ const template = await getById(context, templateId, false);
+
+ await shares.enforceEntityPermission(context, 'sendConfiguration', sendConfigurationId, 'sendWithoutOverrides');
+
+ for (const email of emails) {
+ await messageSender.queueAPITransactionalMessage(sendConfigurationId, email, subject, template.html, template.text, template.tag_language, {...mergeTags, EMAIL: email }, attachments);
+ }
+}
+
+
module.exports.hash = hash;
module.exports.getByIdTx = getByIdTx;
module.exports.getById = getById;
@@ -153,3 +168,4 @@ module.exports.listByNamespaceDTAjax = listByNamespaceDTAjax;
module.exports.create = create;
module.exports.updateWithConsistencyCheck = updateWithConsistencyCheck;
module.exports.remove = remove;
+module.exports.sendAsTransactionalEmail = sendAsTransactionalEmail;
diff --git a/server/models/triggers.js b/server/models/triggers.js
index 5a02de7b..b8434e24 100644
--- a/server/models/triggers.js
+++ b/server/models/triggers.js
@@ -65,7 +65,7 @@ async function _validateAndPreprocess(tx, context, campaignId, entity) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', entity.source_campaign, 'view');
}
- await campaigns.enforceSendPermissionTx(tx, context, campaignId);
+ await campaigns.enforceSendPermissionTx(tx, context, campaignId, false);
}
async function create(context, campaignId, entity) {
diff --git a/server/models/users.js b/server/models/users.js
index 5b7da1bd..6ff8626e 100644
--- a/server/models/users.js
+++ b/server/models/users.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const knex = require('../lib/knex');
const hasher = require('node-object-hash')();
const { enforce, filterObject } = require('../lib/helpers');
@@ -12,6 +12,8 @@ const crypto = require('crypto');
const settings = require('./settings');
const {getTrustedUrl} = require('../lib/urls');
const { tUI } = require('../lib/translate');
+const messageSender = require('../lib/message-sender');
+const {getSystemSendConfigurationId} = require('../../shared/send-configurations');
const bluebird = require('bluebird');
@@ -19,8 +21,6 @@ const bcrypt = require('bcrypt-nodejs');
const bcryptHash = bluebird.promisify(bcrypt.hash.bind(bcrypt));
const bcryptCompare = bluebird.promisify(bcrypt.compare.bind(bcrypt));
-const mailers = require('../lib/mailers');
-
const passport = require('../lib/passport');
const namespaceHelpers = require('../lib/namespace-helpers');
@@ -125,8 +125,6 @@ async function listDTAjax(context, params) {
async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) {
enforce(await tools.validateEmail(entity.email) === 0, 'Invalid email');
- await namespaceHelpers.validateEntity(tx, entity);
-
const otherUserWithSameEmailQuery = tx('users').where('email', entity.email);
if (entity.id) {
otherUserWithSameEmailQuery.andWhereNot('id', entity.id);
@@ -138,6 +136,9 @@ async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) {
if (!isOwnAccount) {
+ await namespaceHelpers.validateEntity(tx, entity);
+ enforce(entity.role in config.roles.global, 'Unknown role');
+
const otherUserWithSameUsernameQuery = tx('users').where('username', entity.username);
if (!isCreate) {
otherUserWithSameUsernameQuery.andWhereNot('id', entity.id);
@@ -148,8 +149,6 @@ async function _validateAndPreprocess(tx, entity, isCreate, isOwnAccount) {
}
}
- enforce(entity.role in config.roles.global, 'Unknown role');
-
enforce(!isCreate || entity.password.length > 0, 'Password not set');
if (entity.password) {
@@ -297,26 +296,31 @@ async function resetAccessToken(userId) {
async function sendPasswordReset(locale, usernameOrEmail) {
enforce(passport.isAuthMethodLocal, 'Local user management is required');
+ const resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, '');
+ let user;
+
await knex.transaction(async tx => {
- const user = await tx('users').where('username', usernameOrEmail).orWhere('email', usernameOrEmail).select(['id', 'username', 'email', 'name']).first();
+ user = await tx('users').where('username', usernameOrEmail).orWhere('email', usernameOrEmail).select(['id', 'username', 'email', 'name']).forUpdate().first();
if (user) {
- const resetToken = crypto.randomBytes(16).toString('base64').replace(/[^a-z0-9]/gi, '');
-
await tx('users').where('id', user.id).update({
reset_token: resetToken,
reset_expire: new Date(Date.now() + 60 * 60 * 1000)
});
+ }
+ // We intentionally silently ignore the situation when user is not found. This is not to reveal if a user exists in the system.
- const { adminEmail } = await settings.get(contextHelpers.getAdminContext(), ['adminEmail']);
+ });
- const mailer = await mailers.getOrCreateMailer();
- await mailer.sendTransactionalMail({
- to: {
- address: user.email
- },
- subject: tUI('mailerPasswordChangeRequest', locale)
- }, {
+ if (user) {
+ await messageSender.queueSubscriptionMessage(
+ getSystemSendConfigurationId(),
+ {
+ address: user.email
+ },
+ tUI('mailerPasswordChangeRequest', locale),
+ null,
+ {
html: 'users/password-reset-html.hbs',
text: 'users/password-reset-text.hbs',
locale,
@@ -326,10 +330,9 @@ async function sendPasswordReset(locale, usernameOrEmail) {
name: user.name,
confirmUrl: getTrustedUrl(`login/reset/${encodeURIComponent(user.username)}/${encodeURIComponent(resetToken)}`)
}
- });
- }
- // We intentionally silently ignore the situation when user is not found. This is not to reveal if a user exists in the system.
- });
+ }
+ );
+ }
}
async function isPasswordResetTokenValid(username, resetToken) {
diff --git a/server/package-lock.json b/server/package-lock.json
index 87f610b2..5f689350 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -5,22 +5,23 @@
"requires": true,
"dependencies": {
"@babel/code-frame": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
- "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
+ "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
+ "dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
},
"@babel/generator": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.0.tgz",
- "integrity": "sha512-/v5I+a1jhGSKLgZDcmAUZ4K/VePi43eRkUs3yePW1HB1iANOD5tqJXwGSG4BZhSksP8J9ejSlwGeTiiOFZOrXQ==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz",
+ "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==",
"dev": true,
"requires": {
- "@babel/types": "^7.4.0",
+ "@babel/types": "^7.5.5",
"jsesc": "^2.5.1",
- "lodash": "^4.17.11",
+ "lodash": "^4.17.13",
"source-map": "^0.5.0",
"trim-right": "^1.0.1"
},
@@ -54,18 +55,19 @@
}
},
"@babel/helper-split-export-declaration": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.0.tgz",
- "integrity": "sha512-7Cuc6JZiYShaZnybDmfwhY4UYHzI6rlqhWjaIqbsJGsIqPimEYy5uh3akSRLMg65LSdSEnJ8a8/bWQN6u2oMGw==",
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
+ "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
"dev": true,
"requires": {
- "@babel/types": "^7.4.0"
+ "@babel/types": "^7.4.4"
}
},
"@babel/highlight": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
- "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
+ "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
+ "dev": true,
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
@@ -76,6 +78,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -84,6 +87,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -94,6 +98,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@@ -101,9 +106,9 @@
}
},
"@babel/parser": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.3.tgz",
- "integrity": "sha512-gxpEUhTS1sGA63EGQGuA+WESPR/6tz6ng7tSHFCmaTJK/cGK8y37cBTspX+U2xCAue2IQVvF6Z0oigmjwD8YGQ==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz",
+ "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==",
"dev": true
},
"@babel/polyfill": {
@@ -123,31 +128,31 @@
}
},
"@babel/template": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.0.tgz",
- "integrity": "sha512-SOWwxxClTTh5NdbbYZ0BmaBVzxzTh2tO/TeLTbF6MO6EzVhHTnff8CdBXx3mEtazFBoysmEM6GU/wF+SuSx4Fw==",
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz",
+ "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
- "@babel/parser": "^7.4.0",
- "@babel/types": "^7.4.0"
+ "@babel/parser": "^7.4.4",
+ "@babel/types": "^7.4.4"
}
},
"@babel/traverse": {
- "version": "7.4.3",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.3.tgz",
- "integrity": "sha512-HmA01qrtaCwwJWpSKpA948cBvU5BrmviAief/b3AVw936DtcdsTexlbyzNuDnthwhOQ37xshn7hvQaEQk7ISYQ==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz",
+ "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==",
"dev": true,
"requires": {
- "@babel/code-frame": "^7.0.0",
- "@babel/generator": "^7.4.0",
+ "@babel/code-frame": "^7.5.5",
+ "@babel/generator": "^7.5.5",
"@babel/helper-function-name": "^7.1.0",
- "@babel/helper-split-export-declaration": "^7.4.0",
- "@babel/parser": "^7.4.3",
- "@babel/types": "^7.4.0",
+ "@babel/helper-split-export-declaration": "^7.4.4",
+ "@babel/parser": "^7.5.5",
+ "@babel/types": "^7.5.5",
"debug": "^4.1.0",
"globals": "^11.1.0",
- "lodash": "^4.17.11"
+ "lodash": "^4.17.13"
},
"dependencies": {
"debug": {
@@ -160,29 +165,24 @@
}
},
"ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
}
}
},
"@babel/types": {
- "version": "7.4.0",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.0.tgz",
- "integrity": "sha512-aPvkXyU2SPOnztlgo8n9cEiXW755mgyvueUPcpStqdzoSPm0fjO0vQBjLkt3JKJW7ufikfcnMTTPsN1xaTsBPA==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz",
+ "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
- "lodash": "^4.17.11",
+ "lodash": "^4.17.13",
"to-fast-properties": "^2.0.0"
}
},
- "@mattiasbuelens/web-streams-polyfill": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/@mattiasbuelens/web-streams-polyfill/-/web-streams-polyfill-0.3.2.tgz",
- "integrity": "sha512-ANZvP8lC9IXiaPM3rwM8BGMbFIZbbj0goZT/xP2IA95UIZjEToyHXT/k8G0MmSAnxKRMh5E6oLVE6jmOt5zZ/g=="
- },
"@types/bluebird": {
"version": "3.5.26",
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.26.tgz",
@@ -193,32 +193,32 @@
"resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz",
"integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w=="
},
- "@types/form-data": {
+ "@types/feedparser": {
"version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz",
- "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
- "requires": {
- "@types/node": "*"
- }
- },
- "@types/node": {
- "version": "11.13.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-11.13.4.tgz",
- "integrity": "sha512-+rabAZZ3Yn7tF/XPGHupKIL5EcAbrLxnTr/hgQICxbeuAfWtT0UZSfULE+ndusckBItcv4o6ZeOJplQikVcLvQ=="
- },
- "@types/node-feedparser": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/@types/node-feedparser/-/node-feedparser-2.2.1.tgz",
- "integrity": "sha512-OvFOOoaYDJDXVS1tZ4Yf44298HjIdUYy0AgdvPObkoFFAxN+AGcg1bxKs7E/sq9F0rpvwEo6Y1lyTwriFdP2bw==",
+ "resolved": "https://registry.npmjs.org/@types/feedparser/-/feedparser-2.2.1.tgz",
+ "integrity": "sha512-Up/CPV8zqm3QS39kCN98dMbJGPBGqOB5z+6aNht7MkqT1ZSzWwPbcwsMifASUevn+DIvssPgxwCQsLBE0j8syQ==",
"requires": {
"@types/node": "*",
"@types/sax": "*"
}
},
+ "@types/form-data": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.5.0.tgz",
+ "integrity": "sha512-23/wYiuckYYtFpL+4RPWiWmRQH2BjFuqCUi2+N3amB1a1Drv+i/byTrGvlLwRVLFNAZbwpbQ7JvTK+VCAPMbcg==",
+ "requires": {
+ "form-data": "*"
+ }
+ },
+ "@types/node": {
+ "version": "12.6.8",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz",
+ "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg=="
+ },
"@types/request": {
- "version": "2.47.1",
- "resolved": "https://registry.npmjs.org/@types/request/-/request-2.47.1.tgz",
- "integrity": "sha512-TV3XLvDjQbIeVxJ1Z3oCTDk/KuYwwcNKVwz2YaT0F5u86Prgc4syDAp6P96rkTQQ4bIdh+VswQIC9zS6NjY7/g==",
+ "version": "2.48.1",
+ "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.1.tgz",
+ "integrity": "sha512-ZgEZ1TiD+KGA9LiAAPPJL68Id2UWfeSO62ijSXZjFJArVV+2pKcsVHmrcu+1oiE3q6eDGiFiSolRc4JHoerBBg==",
"requires": {
"@types/caseless": "*",
"@types/form-data": "*",
@@ -227,9 +227,9 @@
}
},
"@types/sax": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.0.1.tgz",
- "integrity": "sha512-5O70hTAMd9zEOoHiDJ6lk/WvqQgH+aIqU6zkvPLKIl6WJkQeHecHRUWomkjQzAAYPG346nDNus7y724FzTwTKQ==",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.0.tgz",
+ "integrity": "sha512-D8ef/GGUjiHuUOiXV6tkJw6Zq2Sm8vcBScJSvj+monDI5YncJ6M3oNIXR7EtmWPVqJw0jsZF2ARN/X5gvGmQSA==",
"requires": {
"@types/node": "*"
}
@@ -275,7 +275,8 @@
"acorn-jsx": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz",
- "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg=="
+ "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==",
+ "dev": true
},
"acorn-walk": {
"version": "6.1.1",
@@ -306,10 +307,16 @@
"uri-js": "^4.2.2"
}
},
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
+ },
"ansi-escapes": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz",
- "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ=="
+ "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==",
+ "dev": true
},
"ansi-regex": {
"version": "2.1.1",
@@ -322,94 +329,12 @@
"integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
},
"anymatch": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz",
- "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==",
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.0.3.tgz",
+ "integrity": "sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g==",
"requires": {
- "micromatch": "^2.1.5",
- "normalize-path": "^2.0.0"
- },
- "dependencies": {
- "arr-diff": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz",
- "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=",
- "requires": {
- "arr-flatten": "^1.0.1"
- }
- },
- "array-unique": {
- "version": "0.2.1",
- "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz",
- "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM="
- },
- "braces": {
- "version": "1.8.5",
- "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz",
- "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=",
- "requires": {
- "expand-range": "^1.8.1",
- "preserve": "^0.2.0",
- "repeat-element": "^1.1.2"
- }
- },
- "expand-brackets": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz",
- "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=",
- "requires": {
- "is-posix-bracket": "^0.1.0"
- }
- },
- "extglob": {
- "version": "0.3.2",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz",
- "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=",
- "requires": {
- "is-extglob": "^1.0.0"
- }
- },
- "is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
- },
- "is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
- "requires": {
- "is-extglob": "^1.0.0"
- }
- },
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "requires": {
- "is-buffer": "^1.1.5"
- }
- },
- "micromatch": {
- "version": "2.3.11",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz",
- "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=",
- "requires": {
- "arr-diff": "^2.0.0",
- "array-unique": "^0.2.1",
- "braces": "^1.8.2",
- "expand-brackets": "^0.1.4",
- "extglob": "^0.3.1",
- "filename-regex": "^2.0.0",
- "is-extglob": "^1.0.0",
- "is-glob": "^2.0.1",
- "kind-of": "^3.0.2",
- "normalize-path": "^2.0.1",
- "object.omit": "^2.0.0",
- "parse-glob": "^3.0.4",
- "regex-cache": "^0.4.2"
- }
- }
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
}
},
"append-field": {
@@ -435,6 +360,7 @@
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
@@ -542,18 +468,14 @@
"astral-regex": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz",
- "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="
+ "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
+ "dev": true
},
"async": {
"version": "0.1.22",
"resolved": "https://registry.npmjs.org/async/-/async-0.1.22.tgz",
"integrity": "sha1-D8GqoIig4+8Ovi2IMbqw3PiEUGE="
},
- "async-each": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.2.tgz",
- "integrity": "sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg=="
- },
"async-limiter": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz",
@@ -570,9 +492,9 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg=="
},
"aws-sdk": {
- "version": "2.440.0",
- "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.440.0.tgz",
- "integrity": "sha512-/sXI7pZggvFMr2J9qCpGgE2XO/4uBErRVSGoHTR3PkGWvf352w+HebnuGdRKK9D3lnGuGMnEY8w9IS44LUKsxw==",
+ "version": "2.497.0",
+ "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.497.0.tgz",
+ "integrity": "sha512-SCAVfWqj0qe22Lj6ZHMP/4autc9x3GqgWTj3YrE3VleSuql62xhyCC9gSYfbm7wo8Puqx0+kEnKYyyXR7Z98QA==",
"requires": {
"buffer": "4.9.1",
"events": "1.1.1",
@@ -596,9 +518,9 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
"babel-eslint": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.1.tgz",
- "integrity": "sha512-z7OT1iNV+TjOwHNLLyJk+HN+YVWX+CLE6fPD2SymJZOZQBs+QIexFjhm4keGTm8MW9xr4EC9Q0PbaLB24V5GoQ==",
+ "version": "10.0.2",
+ "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.0.2.tgz",
+ "integrity": "sha512-UdsurWPtgiPgpJ06ryUnuaSXC2s0WoSZnQmEpbAH65XZSdwowgN5MvyP7e88nW07FYXv72erVtpBkxyDVKhH1Q==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
@@ -607,18 +529,6 @@
"@babel/types": "^7.0.0",
"eslint-scope": "3.7.1",
"eslint-visitor-keys": "^1.0.0"
- },
- "dependencies": {
- "eslint-scope": {
- "version": "3.7.1",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
- "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
- "dev": true,
- "requires": {
- "esrecurse": "^4.1.0",
- "estraverse": "^4.1.1"
- }
- }
}
},
"babel-runtime": {
@@ -731,14 +641,14 @@
}
},
"binary-extensions": {
- "version": "1.13.1",
- "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz",
- "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.0.0.tgz",
+ "integrity": "sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow=="
},
"bluebird": {
- "version": "3.5.4",
- "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
- "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw=="
+ "version": "3.5.5",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz",
+ "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w=="
},
"bn.js": {
"version": "4.11.8",
@@ -746,20 +656,40 @@
"integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
"body-parser": {
- "version": "1.18.3",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
- "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=",
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
- "bytes": "3.0.0",
+ "bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
- "http-errors": "~1.6.3",
- "iconv-lite": "0.4.23",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
- "qs": "6.5.2",
- "raw-body": "2.3.3",
- "type-is": "~1.6.16"
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ }
}
},
"boolbase": {
@@ -828,11 +758,6 @@
}
}
},
- "brorand": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
- "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
- },
"browser-process-hrtime": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz",
@@ -920,7 +845,8 @@
"callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
- "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true
},
"camel-case": {
"version": "3.0.0",
@@ -1001,7 +927,8 @@
"chardet": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
- "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
+ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==",
+ "dev": true
},
"check-error": {
"version": "1.0.2",
@@ -1033,32 +960,47 @@
}
},
"chokidar": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz",
- "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.0.2.tgz",
+ "integrity": "sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA==",
"requires": {
- "anymatch": "^1.3.0",
- "async-each": "^1.0.0",
- "fsevents": "^1.0.0",
- "glob-parent": "^2.0.0",
- "inherits": "^2.0.1",
- "is-binary-path": "^1.0.0",
- "is-glob": "^2.0.0",
- "path-is-absolute": "^1.0.0",
- "readdirp": "^2.0.0"
+ "anymatch": "^3.0.1",
+ "braces": "^3.0.2",
+ "fsevents": "^2.0.6",
+ "glob-parent": "^5.0.0",
+ "is-binary-path": "^2.1.0",
+ "is-glob": "^4.0.1",
+ "normalize-path": "^3.0.0",
+ "readdirp": "^3.1.1"
},
"dependencies": {
- "is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
- },
- "is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"requires": {
- "is-extglob": "^1.0.0"
+ "fill-range": "^7.0.1"
+ }
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
}
}
}
@@ -1096,6 +1038,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
"integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
+ "dev": true,
"requires": {
"restore-cursor": "^2.0.0"
}
@@ -1103,7 +1046,8 @@
"cli-width": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
- "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=",
+ "dev": true
},
"cliui": {
"version": "3.2.0",
@@ -1159,6 +1103,7 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
"requires": {
"color-name": "1.1.3"
}
@@ -1166,7 +1111,8 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
- "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
},
"colorette": {
"version": "1.0.7",
@@ -1235,9 +1181,9 @@
}
},
"config": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/config/-/config-3.1.0.tgz",
- "integrity": "sha512-t6oDeNQbsIWa+D/KF4959TANzjSHLv1BA/hvL8tHEA3OUSWgBXELKaONSI6nr9oanbKs0DXonjOWLcrtZ3yTAA==",
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/config/-/config-3.2.2.tgz",
+ "integrity": "sha512-rOsfIOAcG82AWouK4/vBS/OKz3UPl2T/kP0irExmXJJOoWg2CmdfPLdx56bCoMUMFNh+7soQkQWCUC8DyemiwQ==",
"requires": {
"json5": "^1.0.1"
}
@@ -1257,9 +1203,9 @@
"integrity": "sha1-2GMPJtlaf4UfmVax6MxnMvO2qjA="
},
"connect-redis": {
- "version": "3.4.1",
- "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.1.tgz",
- "integrity": "sha512-oXNcpLg/PJ6G4gbhyGwrQK9mUQTKYa2aEnOH9kWIxbNUjIFPqUmzz75RdLp5JTPSjrBVcz+9ll4sSxfvlW0ZLA==",
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-3.4.2.tgz",
+ "integrity": "sha512-ozA1Z0GDnsCJECfNyNJOqPuW3Fk43fUbKC65Sa/V9hkCBNtXsFU2xtTOVsQGUsflpywuJMgGOV4xrnKzIPFqvA==",
"requires": {
"debug": "^4.1.1",
"redis": "^2.8.0"
@@ -1274,9 +1220,9 @@
}
},
"ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
@@ -1286,9 +1232,12 @@
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"content-disposition": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
- "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
+ "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
},
"content-type": {
"version": "1.0.4",
@@ -1338,29 +1287,6 @@
"vary": "^1"
}
},
- "cross-env": {
- "version": "5.2.0",
- "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz",
- "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==",
- "requires": {
- "cross-spawn": "^6.0.5",
- "is-windows": "^1.0.0"
- },
- "dependencies": {
- "cross-spawn": {
- "version": "6.0.5",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
- "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
- "requires": {
- "nice-try": "^1.0.4",
- "path-key": "^2.0.1",
- "semver": "^5.5.0",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- }
- }
- },
"cross-spawn": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz",
@@ -1376,13 +1302,13 @@
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig=="
},
"csrf": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.0.6.tgz",
- "integrity": "sha1-thEg3c7q/JHnbtUxO7XAsmZ7cQo=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/csrf/-/csrf-3.1.0.tgz",
+ "integrity": "sha512-uTqEnCvWRk042asU6JtapDTcJeeailFy4ydOQS28bj1hcLnYRiqi8SsD2jS412AY1I/4qdOwWZun774iqywf9w==",
"requires": {
"rndm": "1.2.0",
- "tsscmp": "1.0.5",
- "uid-safe": "2.1.4"
+ "tsscmp": "1.0.6",
+ "uid-safe": "2.1.5"
}
},
"css-select": {
@@ -1426,37 +1352,20 @@
}
},
"csurf": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.9.0.tgz",
- "integrity": "sha1-SdLGkl/87Ht95VlZfBU/pTM2QTM=",
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/csurf/-/csurf-1.10.0.tgz",
+ "integrity": "sha512-fh725p0R83wA5JukCik5hdEko/LizW/Vl7pkKDa1WJUVCosg141mqaAWCScB+nkEaRMFMGbutHMOr6oBNc/j9A==",
"requires": {
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
- "csrf": "~3.0.3",
- "http-errors": "~1.5.0"
- },
- "dependencies": {
- "http-errors": {
- "version": "1.5.1",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.5.1.tgz",
- "integrity": "sha1-eIwNLB3iyBuebowBhDtrl+uSB1A=",
- "requires": {
- "inherits": "2.0.3",
- "setprototypeof": "1.0.2",
- "statuses": ">= 1.3.1 < 2"
- }
- },
- "setprototypeof": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.2.tgz",
- "integrity": "sha1-gaVSFB7BBLiOic44MQOtXGZWTQg="
- }
+ "csrf": "3.1.0",
+ "http-errors": "~1.7.2"
}
},
"csv-parse": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.4.1.tgz",
- "integrity": "sha512-uFe5phPfmwBXSPWz5GYHeaEc2Oezn2kY5iLIvG1sJjc32Y4GU7T/b/uX5ffZh4CBDWwJQjwAuxrDEdl3Z5Qv+g=="
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.4.3.tgz",
+ "integrity": "sha512-TiLGAy14FPJ7/yB+Gn6RgSxoZLpf6pJTRkGqmCt9t/SGVwubrXjbUWtEw39RlKB6hDHzbdjLyBZaysQ0Ji6p/w=="
},
"csv-stringify": {
"version": "5.3.0",
@@ -1662,6 +1571,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
"requires": {
"esutils": "^2.0.2"
}
@@ -1697,9 +1607,9 @@
}
},
"dompurify": {
- "version": "1.0.10",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-1.0.10.tgz",
- "integrity": "sha512-huhl3DSWX5LaA7jDtnj3XQdJgWW1wYouNW7N0drGzQa4vEUSVWyeFN+Atx6HP4r5cang6oQytMom6I4yhGJj5g=="
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-1.0.11.tgz",
+ "integrity": "sha512-XywCTXZtc/qCX3iprD1pIklRVk/uhl8BKpkTxr+ZyMVUzSUg7wkQXRBp/euJ5J5moa1QvfpvaPQVP71z1O59dQ=="
},
"domutils": {
"version": "1.7.0",
@@ -1753,7 +1663,8 @@
"emoji-regex": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
- "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
},
"encodeurl": {
"version": "1.0.2",
@@ -1813,6 +1724,7 @@
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-5.16.0.tgz",
"integrity": "sha512-S3Rz11i7c8AA5JPv7xAH+dOyq/Cu/VXHiHXBPOU1k/JAM5dXqQPt3qcrhpHSorXmrpu2g0gkIBVXAqCpzfoZIg==",
+ "dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"ajv": "^6.9.1",
@@ -1855,12 +1767,14 @@
"ansi-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
- "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=",
+ "dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -1869,6 +1783,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -1879,6 +1794,7 @@
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "dev": true,
"requires": {
"nice-try": "^1.0.4",
"path-key": "^2.0.1",
@@ -1891,19 +1807,32 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+ "dev": true,
"requires": {
"ms": "^2.1.1"
}
},
+ "eslint-scope": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
+ "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
"ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
},
"strip-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
"integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dev": true,
"requires": {
"ansi-regex": "^3.0.0"
}
@@ -1912,6 +1841,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@@ -1925,28 +1855,35 @@
"dev": true
},
"eslint-scope": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
- "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==",
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.1.tgz",
+ "integrity": "sha1-PWPD7f2gLgbgGkUq2IyqzHzctug=",
+ "dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
},
"eslint-utils": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
- "integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q=="
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz",
+ "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^1.0.0"
+ }
},
"eslint-visitor-keys": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
- "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ=="
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
+ "dev": true
},
"espree": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-5.0.1.tgz",
"integrity": "sha512-qWAZcWh4XE/RwzLJejfcofscgMc9CamR6Tn1+XRXNzrvUSSbiAjGOI/fggztjIi7y9VLPqnICMIPiGyr8JaZ0A==",
+ "dev": true,
"requires": {
"acorn": "^6.0.7",
"acorn-jsx": "^5.0.0",
@@ -1962,6 +1899,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
"integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "dev": true,
"requires": {
"estraverse": "^4.0.0"
}
@@ -1970,6 +1908,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
"integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "dev": true,
"requires": {
"estraverse": "^4.1.0"
}
@@ -2069,52 +2008,6 @@
}
}
},
- "expand-range": {
- "version": "1.8.2",
- "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz",
- "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=",
- "requires": {
- "fill-range": "^2.1.0"
- },
- "dependencies": {
- "fill-range": {
- "version": "2.2.4",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz",
- "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==",
- "requires": {
- "is-number": "^2.1.0",
- "isobject": "^2.0.0",
- "randomatic": "^3.0.0",
- "repeat-element": "^1.1.2",
- "repeat-string": "^1.5.2"
- }
- },
- "is-number": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz",
- "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=",
- "requires": {
- "kind-of": "^3.0.2"
- }
- },
- "isobject": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz",
- "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=",
- "requires": {
- "isarray": "1.0.0"
- }
- },
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
- },
"expand-tilde": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
@@ -2124,46 +2017,83 @@
}
},
"express": {
- "version": "4.16.4",
- "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz",
- "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==",
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"requires": {
- "accepts": "~1.3.5",
+ "accepts": "~1.3.7",
"array-flatten": "1.1.1",
- "body-parser": "1.18.3",
- "content-disposition": "0.5.2",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
"content-type": "~1.0.4",
- "cookie": "0.3.1",
+ "cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
- "finalhandler": "1.1.1",
+ "finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
- "parseurl": "~1.3.2",
+ "parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
- "proxy-addr": "~2.0.4",
- "qs": "6.5.2",
- "range-parser": "~1.2.0",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
- "send": "0.16.2",
- "serve-static": "1.13.2",
- "setprototypeof": "1.1.0",
- "statuses": "~1.4.0",
- "type-is": "~1.6.16",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
- "statuses": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
- "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "mime-db": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+ },
+ "mime-types": {
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
}
}
},
@@ -2176,16 +2106,16 @@
}
},
"express-session": {
- "version": "1.16.1",
- "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.1.tgz",
- "integrity": "sha512-pWvUL8Tl5jUy1MLH7DhgUlpoKeVPUTe+y6WQD9YhcN0C5qAhsh4a8feVjiUXo3TFhIy191YGZ4tewW9edbl2xQ==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.16.2.tgz",
+ "integrity": "sha512-oy0sRsdw6n93E9wpCNWKRnSsxYnSDX9Dnr9mhZgqUEEorzcq5nshGYSZ4ZReHFhKQ80WI5iVUUSPW7u3GaKauw==",
"requires": {
"cookie": "0.3.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
- "parseurl": "~1.3.2",
+ "parseurl": "~1.3.3",
"safe-buffer": "5.1.2",
"uid-safe": "~2.1.5"
},
@@ -2195,13 +2125,10 @@
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
},
- "uid-safe": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
- "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
- "requires": {
- "random-bytes": "~1.0.0"
- }
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
}
}
},
@@ -2230,9 +2157,10 @@
}
},
"external-editor": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.0.3.tgz",
- "integrity": "sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
+ "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==",
+ "dev": true,
"requires": {
"chardet": "^0.7.0",
"iconv-lite": "^0.4.24",
@@ -2243,6 +2171,7 @@
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "dev": true,
"requires": {
"safer-buffer": ">= 2.1.2 < 3"
}
@@ -2378,12 +2307,12 @@
}
},
"feedparser-promised": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/feedparser-promised/-/feedparser-promised-2.0.0.tgz",
- "integrity": "sha512-pSnGjKZlfqRHPDZezA/LNNgGygy2h6ofK8q2tVow9oa/mnHMKUGiASDj3Go1/1HEdm5PlJ/afg3iDA0T1PJO8g==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/feedparser-promised/-/feedparser-promised-2.0.1.tgz",
+ "integrity": "sha512-N0qUzf7V8H5E2WppixoeLQRDYlZiR5e/jXXXtUBlb7uPL09LulB9whP6iRmzVG2S9cyjO4tbwbYfirKhy02fCw==",
"requires": {
- "@types/node-feedparser": "2.2.1",
- "@types/request": "2.47.1",
+ "@types/feedparser": "2.2.1",
+ "@types/request": "2.48.1",
"feedparser": "2.2.9",
"request": "2.88.0"
}
@@ -2392,6 +2321,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
"integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "dev": true,
"requires": {
"escape-string-regexp": "^1.0.5"
}
@@ -2400,15 +2330,11 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz",
"integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==",
+ "dev": true,
"requires": {
"flat-cache": "^2.0.1"
}
},
- "filename-regex": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
- "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY="
- },
"fill-range": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
@@ -2431,23 +2357,23 @@
}
},
"finalhandler": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
- "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
"requires": {
"debug": "2.6.9",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"on-finished": "~2.3.0",
- "parseurl": "~1.3.2",
- "statuses": "~1.4.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
"unpipe": "~1.0.0"
},
"dependencies": {
- "statuses": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
- "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
}
}
},
@@ -2491,6 +2417,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz",
"integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==",
+ "dev": true,
"requires": {
"flatted": "^2.0.0",
"rimraf": "2.6.3",
@@ -2501,6 +2428,7 @@
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
"requires": {
"glob": "^7.1.3"
}
@@ -2508,9 +2436,10 @@
}
},
"flatted": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.0.tgz",
- "integrity": "sha512-R+H8IZclI8AAkSBRQJLVOsxwAoHd6WC40b4QTNWIjzAa6BXOBfQcM587MXDTVPeYaopFNWHUFLx7eNmHDSxMWg=="
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz",
+ "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==",
+ "dev": true
},
"for-in": {
"version": "1.0.2",
@@ -2607,480 +2536,10 @@
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fsevents": {
- "version": "1.2.7",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.7.tgz",
- "integrity": "sha512-Pxm6sI2MeBD7RdD12RYsqaP0nMiwx8eZBXCa6z2L+mRHm2DYrOYwihmhjpkdjUHwQhslWQjRpEgNq4XvBmaAuw==",
- "optional": true,
- "requires": {
- "nan": "^2.9.2",
- "node-pre-gyp": "^0.10.0"
- },
- "dependencies": {
- "abbrev": {
- "version": "1.1.1",
- "bundled": true,
- "optional": true
- },
- "ansi-regex": {
- "version": "2.1.1",
- "bundled": true
- },
- "aproba": {
- "version": "1.2.0",
- "bundled": true,
- "optional": true
- },
- "are-we-there-yet": {
- "version": "1.1.5",
- "bundled": true,
- "optional": true,
- "requires": {
- "delegates": "^1.0.0",
- "readable-stream": "^2.0.6"
- }
- },
- "balanced-match": {
- "version": "1.0.0",
- "bundled": true,
- "optional": true
- },
- "brace-expansion": {
- "version": "1.1.11",
- "bundled": true,
- "optional": true,
- "requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
- }
- },
- "chownr": {
- "version": "1.1.1",
- "bundled": true,
- "optional": true
- },
- "code-point-at": {
- "version": "1.1.0",
- "bundled": true,
- "optional": true
- },
- "concat-map": {
- "version": "0.0.1",
- "bundled": true,
- "optional": true
- },
- "console-control-strings": {
- "version": "1.1.0",
- "bundled": true,
- "optional": true
- },
- "core-util-is": {
- "version": "1.0.2",
- "bundled": true,
- "optional": true
- },
- "debug": {
- "version": "2.6.9",
- "bundled": true,
- "optional": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "deep-extend": {
- "version": "0.6.0",
- "bundled": true,
- "optional": true
- },
- "delegates": {
- "version": "1.0.0",
- "bundled": true,
- "optional": true
- },
- "detect-libc": {
- "version": "1.0.3",
- "bundled": true,
- "optional": true
- },
- "fs-minipass": {
- "version": "1.2.5",
- "bundled": true,
- "optional": true,
- "requires": {
- "minipass": "^2.2.1"
- }
- },
- "fs.realpath": {
- "version": "1.0.0",
- "bundled": true,
- "optional": true
- },
- "gauge": {
- "version": "2.7.4",
- "bundled": true,
- "optional": true,
- "requires": {
- "aproba": "^1.0.3",
- "console-control-strings": "^1.0.0",
- "has-unicode": "^2.0.0",
- "object-assign": "^4.1.0",
- "signal-exit": "^3.0.0",
- "string-width": "^1.0.1",
- "strip-ansi": "^3.0.1",
- "wide-align": "^1.1.0"
- }
- },
- "glob": {
- "version": "7.1.3",
- "bundled": true,
- "optional": true,
- "requires": {
- "fs.realpath": "^1.0.0",
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "^3.0.4",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
- }
- },
- "has-unicode": {
- "version": "2.0.1",
- "bundled": true,
- "optional": true
- },
- "iconv-lite": {
- "version": "0.4.24",
- "bundled": true,
- "optional": true,
- "requires": {
- "safer-buffer": ">= 2.1.2 < 3"
- }
- },
- "ignore-walk": {
- "version": "3.0.1",
- "bundled": true,
- "optional": true,
- "requires": {
- "minimatch": "^3.0.4"
- }
- },
- "inflight": {
- "version": "1.0.6",
- "bundled": true,
- "optional": true,
- "requires": {
- "once": "^1.3.0",
- "wrappy": "1"
- }
- },
- "inherits": {
- "version": "2.0.3",
- "bundled": true,
- "optional": true
- },
- "ini": {
- "version": "1.3.5",
- "bundled": true,
- "optional": true
- },
- "is-fullwidth-code-point": {
- "version": "1.0.0",
- "bundled": true,
- "optional": true,
- "requires": {
- "number-is-nan": "^1.0.0"
- }
- },
- "isarray": {
- "version": "1.0.0",
- "bundled": true,
- "optional": true
- },
- "minimatch": {
- "version": "3.0.4",
- "bundled": true,
- "optional": true,
- "requires": {
- "brace-expansion": "^1.1.7"
- }
- },
- "minimist": {
- "version": "0.0.8",
- "bundled": true,
- "optional": true
- },
- "minipass": {
- "version": "2.3.5",
- "bundled": true,
- "optional": true,
- "requires": {
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.0"
- }
- },
- "minizlib": {
- "version": "1.2.1",
- "bundled": true,
- "optional": true,
- "requires": {
- "minipass": "^2.2.1"
- }
- },
- "mkdirp": {
- "version": "0.5.1",
- "bundled": true,
- "optional": true,
- "requires": {
- "minimist": "0.0.8"
- }
- },
- "ms": {
- "version": "2.0.0",
- "bundled": true,
- "optional": true
- },
- "needle": {
- "version": "2.2.4",
- "bundled": true,
- "optional": true,
- "requires": {
- "debug": "^2.1.2",
- "iconv-lite": "^0.4.4",
- "sax": "^1.2.4"
- }
- },
- "node-pre-gyp": {
- "version": "0.10.3",
- "bundled": true,
- "optional": true,
- "requires": {
- "detect-libc": "^1.0.2",
- "mkdirp": "^0.5.1",
- "needle": "^2.2.1",
- "nopt": "^4.0.1",
- "npm-packlist": "^1.1.6",
- "npmlog": "^4.0.2",
- "rc": "^1.2.7",
- "rimraf": "^2.6.1",
- "semver": "^5.3.0",
- "tar": "^4"
- }
- },
- "nopt": {
- "version": "4.0.1",
- "bundled": true,
- "optional": true,
- "requires": {
- "abbrev": "1",
- "osenv": "^0.1.4"
- }
- },
- "npm-bundled": {
- "version": "1.0.5",
- "bundled": true,
- "optional": true
- },
- "npm-packlist": {
- "version": "1.2.0",
- "bundled": true,
- "optional": true,
- "requires": {
- "ignore-walk": "^3.0.1",
- "npm-bundled": "^1.0.1"
- }
- },
- "npmlog": {
- "version": "4.1.2",
- "bundled": true,
- "optional": true,
- "requires": {
- "are-we-there-yet": "~1.1.2",
- "console-control-strings": "~1.1.0",
- "gauge": "~2.7.3",
- "set-blocking": "~2.0.0"
- }
- },
- "number-is-nan": {
- "version": "1.0.1",
- "bundled": true,
- "optional": true
- },
- "object-assign": {
- "version": "4.1.1",
- "bundled": true,
- "optional": true
- },
- "once": {
- "version": "1.4.0",
- "bundled": true,
- "optional": true,
- "requires": {
- "wrappy": "1"
- }
- },
- "os-homedir": {
- "version": "1.0.2",
- "bundled": true,
- "optional": true
- },
- "os-tmpdir": {
- "version": "1.0.2",
- "bundled": true,
- "optional": true
- },
- "osenv": {
- "version": "0.1.5",
- "bundled": true,
- "optional": true,
- "requires": {
- "os-homedir": "^1.0.0",
- "os-tmpdir": "^1.0.0"
- }
- },
- "path-is-absolute": {
- "version": "1.0.1",
- "bundled": true,
- "optional": true
- },
- "process-nextick-args": {
- "version": "2.0.0",
- "bundled": true,
- "optional": true
- },
- "rc": {
- "version": "1.2.8",
- "bundled": true,
- "optional": true,
- "requires": {
- "deep-extend": "^0.6.0",
- "ini": "~1.3.0",
- "minimist": "^1.2.0",
- "strip-json-comments": "~2.0.1"
- },
- "dependencies": {
- "minimist": {
- "version": "1.2.0",
- "bundled": true,
- "optional": true
- }
- }
- },
- "readable-stream": {
- "version": "2.3.6",
- "bundled": true,
- "optional": true,
- "requires": {
- "core-util-is": "~1.0.0",
- "inherits": "~2.0.3",
- "isarray": "~1.0.0",
- "process-nextick-args": "~2.0.0",
- "safe-buffer": "~5.1.1",
- "string_decoder": "~1.1.1",
- "util-deprecate": "~1.0.1"
- }
- },
- "rimraf": {
- "version": "2.6.3",
- "bundled": true,
- "optional": true,
- "requires": {
- "glob": "^7.1.3"
- }
- },
- "safe-buffer": {
- "version": "5.1.2",
- "bundled": true
- },
- "safer-buffer": {
- "version": "2.1.2",
- "bundled": true,
- "optional": true
- },
- "sax": {
- "version": "1.2.4",
- "bundled": true,
- "optional": true
- },
- "semver": {
- "version": "5.6.0",
- "bundled": true,
- "optional": true
- },
- "set-blocking": {
- "version": "2.0.0",
- "bundled": true,
- "optional": true
- },
- "signal-exit": {
- "version": "3.0.2",
- "bundled": true,
- "optional": true
- },
- "string-width": {
- "version": "1.0.2",
- "bundled": true,
- "optional": true,
- "requires": {
- "code-point-at": "^1.0.0",
- "is-fullwidth-code-point": "^1.0.0",
- "strip-ansi": "^3.0.0"
- }
- },
- "string_decoder": {
- "version": "1.1.1",
- "bundled": true,
- "optional": true,
- "requires": {
- "safe-buffer": "~5.1.0"
- }
- },
- "strip-ansi": {
- "version": "3.0.1",
- "bundled": true,
- "requires": {
- "ansi-regex": "^2.0.0"
- }
- },
- "strip-json-comments": {
- "version": "2.0.1",
- "bundled": true,
- "optional": true
- },
- "tar": {
- "version": "4.4.8",
- "bundled": true,
- "optional": true,
- "requires": {
- "chownr": "^1.1.1",
- "fs-minipass": "^1.2.5",
- "minipass": "^2.3.4",
- "minizlib": "^1.1.1",
- "mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.2",
- "yallist": "^3.0.2"
- }
- },
- "util-deprecate": {
- "version": "1.0.2",
- "bundled": true,
- "optional": true
- },
- "wide-align": {
- "version": "1.1.3",
- "bundled": true,
- "optional": true,
- "requires": {
- "string-width": "^1.0.2 || 2"
- }
- },
- "wrappy": {
- "version": "1.0.2",
- "bundled": true
- },
- "yallist": {
- "version": "3.0.3",
- "bundled": true
- }
- }
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.0.7.tgz",
+ "integrity": "sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ==",
+ "optional": true
},
"fstream": {
"version": "0.1.31",
@@ -3106,7 +2565,8 @@
"functional-red-black-tree": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
- "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
+ "dev": true
},
"gauge": {
"version": "2.7.4",
@@ -3222,51 +2682,12 @@
"path-is-absolute": "^1.0.0"
}
},
- "glob-base": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz",
- "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=",
- "requires": {
- "glob-parent": "^2.0.0",
- "is-glob": "^2.0.0"
- },
- "dependencies": {
- "is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
- },
- "is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
- "requires": {
- "is-extglob": "^1.0.0"
- }
- }
- }
- },
"glob-parent": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz",
- "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=",
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz",
+ "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==",
"requires": {
- "is-glob": "^2.0.0"
- },
- "dependencies": {
- "is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
- },
- "is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
- "requires": {
- "is-extglob": "^1.0.0"
- }
- }
+ "is-glob": "^4.0.1"
}
},
"global-modules": {
@@ -3292,9 +2713,10 @@
}
},
"globals": {
- "version": "11.11.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.11.0.tgz",
- "integrity": "sha512-WHq43gS+6ufNOEqlrDBxVEbb8ntfXrfAUU2ZOpCxrBdGKW3gyv8mCxAfIBD0DroPKGrJ2eSsXsLtY9MPntsyTw=="
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true
},
"gm": {
"version": "1.23.1",
@@ -3327,11 +2749,6 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
"integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
},
- "graceful-readlink": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
- "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU="
- },
"growl": {
"version": "1.10.5",
"resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
@@ -3514,13 +2931,13 @@
}
},
"grunt-eslint": {
- "version": "21.0.0",
- "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-21.0.0.tgz",
- "integrity": "sha512-HJocD9P35lpCvy6pPPCTgzBavzckrT1nt7lpqV55Vy8E6LQJv4RortXoH1jJTYhO5DYY7RPATv7Uc4383PUYqQ==",
+ "version": "21.1.0",
+ "resolved": "https://registry.npmjs.org/grunt-eslint/-/grunt-eslint-21.1.0.tgz",
+ "integrity": "sha512-TN1C4BV947eUB/XrROUQ/QFTufWgH6wdfaxhlfmpjE70bFTp5q+Q2LgIZ5Y//+Rn1BWrXmm44sxegijNN6WR/A==",
"dev": true,
"requires": {
"chalk": "^2.1.0",
- "eslint": "^5.0.0"
+ "eslint": "^5.16.0"
},
"dependencies": {
"ansi-styles": {
@@ -3680,7 +3097,8 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
- "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
},
"has-unicode": {
"version": "2.0.1",
@@ -3770,16 +3188,6 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
- "hmac-drbg": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
- "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
- "requires": {
- "hash.js": "^1.0.3",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.1"
- }
- },
"homedir-polyfill": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
@@ -3881,14 +3289,15 @@
}
},
"http-errors": {
- "version": "1.6.3",
- "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
- "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
- "setprototypeof": "1.1.0",
- "statuses": ">= 1.4.0 < 2"
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
}
},
"http-signature": {
@@ -3927,7 +3336,8 @@
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
- "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg=="
+ "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==",
+ "dev": true
},
"image-size": {
"version": "0.6.3",
@@ -3941,9 +3351,10 @@
"dev": true
},
"import-fresh": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.0.0.tgz",
- "integrity": "sha512-pOnA9tfM3Uwics+SaBLCNyZZZbK+4PTu0OPZtLlMIrv17EdBoC15S9Kn8ckJ9TZTyKb3ywNE5y1yeDxxGA7nTQ==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz",
+ "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==",
+ "dev": true,
"requires": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
@@ -3983,9 +3394,10 @@
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"inquirer": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.3.1.tgz",
- "integrity": "sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA==",
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz",
+ "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==",
+ "dev": true,
"requires": {
"ansi-escapes": "^3.2.0",
"chalk": "^2.4.2",
@@ -3993,7 +3405,7 @@
"cli-width": "^2.0.0",
"external-editor": "^3.0.3",
"figures": "^2.0.0",
- "lodash": "^4.17.11",
+ "lodash": "^4.17.12",
"mute-stream": "0.0.7",
"run-async": "^2.2.0",
"rxjs": "^6.4.0",
@@ -4005,12 +3417,14 @@
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
},
"ansi-styles": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -4019,6 +3433,7 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@@ -4029,6 +3444,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@@ -4037,6 +3453,7 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@@ -4054,9 +3471,9 @@
"integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY="
},
"ipaddr.js": {
- "version": "1.8.0",
- "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
- "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+ "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
},
"ipv6-normalize": {
"version": "1.0.1",
@@ -4096,11 +3513,11 @@
"integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
},
"is-binary-path": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz",
- "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=",
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"requires": {
- "binary-extensions": "^1.0.0"
+ "binary-extensions": "^2.0.0"
}
},
"is-buffer": {
@@ -4143,19 +3560,6 @@
}
}
},
- "is-dotfile": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz",
- "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE="
- },
- "is-equal-shallow": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz",
- "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=",
- "requires": {
- "is-primitive": "^2.0.0"
- }
- },
"is-extendable": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
@@ -4214,20 +3618,11 @@
"isobject": "^3.0.1"
}
},
- "is-posix-bracket": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz",
- "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q="
- },
- "is-primitive": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz",
- "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU="
- },
"is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
- "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=",
+ "dev": true
},
"is-relative": {
"version": "1.0.0",
@@ -4307,14 +3702,14 @@
"integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc="
},
"js-beautify": {
- "version": "1.9.1",
- "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.9.1.tgz",
- "integrity": "sha512-oxxvVZdOdUfzk8IOLBF2XUZvl2GoBEfA+b0of4u2EBY/46NlXasi8JdFvazA5lCrf9/lQhTjyVy2QCUW7iq0MQ==",
+ "version": "1.10.1",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.1.tgz",
+ "integrity": "sha512-4y8SHOIRC+/YQ2gs3zJEKBUraQerq49FJYyXRpdzUGYQzCq8q9xtIh0YXial1S5KmonVui4aiUb6XaGyjE51XA==",
"requires": {
"config-chain": "^1.1.12",
- "editorconfig": "^0.15.2",
+ "editorconfig": "^0.15.3",
"glob": "^7.1.3",
- "mkdirp": "~0.5.0",
+ "mkdirp": "~0.5.1",
"nopt": "~4.0.1"
}
},
@@ -4340,6 +3735,7 @@
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
@@ -4348,7 +3744,8 @@
"esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
- "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
}
}
},
@@ -4425,7 +3822,8 @@
"json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
@@ -4594,15 +3992,23 @@
"integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY="
},
"libmime": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/libmime/-/libmime-4.0.1.tgz",
- "integrity": "sha512-mGgJLRkpkMxZZYE7ncVXokgKfi5ePrIB1H3W/Bv3GbkVnFydIHTsPrfAVW0edxalQHmFfqDMU9W45PidCLG6DA==",
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/libmime/-/libmime-4.1.3.tgz",
+ "integrity": "sha512-vCEJD6jfrqf5yLhI9ZmB3NqrefZIr9e/uKQ2eL/U21wMqGDqOyka/NVpW5q93QFoD+gKgDrnoxLd8Ch1/9Mu2g==",
"requires": {
- "iconv-lite": "0.4.23",
+ "iconv-lite": "0.4.24",
"libbase64": "1.0.3",
"libqp": "1.1.0"
},
"dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
"libbase64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.0.3.tgz",
@@ -4668,9 +4074,9 @@
}
},
"lodash": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash.assign": {
"version": "4.2.0",
@@ -4723,9 +4129,9 @@
"integrity": "sha1-dx7Hg540c9nEzeKLGTlMNWL09tM="
},
"lodash.merge": {
- "version": "4.6.1",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.1.tgz",
- "integrity": "sha512-AOYza4+Hf5z1/0Hztxpm2/xiPZgi/cjMqdnKTUWTBSKchJlxXXuUSxCCl8rJlf4g6yww/j6mA8nC8Hw/EZWxKQ=="
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
},
"lodash.pick": {
"version": "4.4.0",
@@ -4795,30 +4201,29 @@
}
},
"mailparser": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-2.6.0.tgz",
- "integrity": "sha512-eiDxFmcKKk0mGi6HtL4KFkFrrzFyAzBIoZfieZQL1q4J31vUISdUfEsEg+VD2GDxWepyOVqXCK5o+JWlfAWl6w==",
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/mailparser/-/mailparser-2.7.1.tgz",
+ "integrity": "sha512-qAyDPuyd0ygTM3V9yzxVilYyRt0mpjLmp6OSzBPjwMZYX1PVDOoGEyUgDtyCDoEgC5fqslpXpWCI6t7RN3i3fw==",
"requires": {
- "eslint": "^5.15.3",
"he": "1.2.0",
- "html-to-text": "5.0.0",
+ "html-to-text": "5.1.1",
"iconv-lite": "0.4.24",
- "libmime": "4.0.1",
+ "libmime": "4.1.1",
"linkify-it": "2.1.0",
- "mailsplit": "4.3.0",
- "nodemailer": "6.0.0",
+ "mailsplit": "4.4.1",
+ "nodemailer": "6.1.1",
"tlds": "1.203.1"
},
"dependencies": {
"html-to-text": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-5.0.0.tgz",
- "integrity": "sha512-47Z+6ePABRjrvzt7OyTH3V7uiDjNUmAS4HryHYnKd6t2iYi5h7r8fpRrQK6VkGLC6Bjm+Ucya3tT21IMb4iKOQ==",
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-5.1.1.tgz",
+ "integrity": "sha512-Bci6bD/JIfZSvG4s0gW/9mMKwBRoe/1RWLxUME/d6WUSZCdY7T60bssf/jFf7EYXRyqU4P5xdClVqiYU0/ypdA==",
"requires": {
"he": "^1.2.0",
"htmlparser2": "^3.10.1",
"lodash": "^4.17.11",
- "optimist": "^0.6.1"
+ "minimist": "^1.2.0"
}
},
"iconv-lite": {
@@ -4829,27 +4234,60 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
- "nodemailer": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.0.0.tgz",
- "integrity": "sha512-PMQJyLhoNAMoBU1hEh5aaUkpa/tcDNwzS7s7zow/myKfoEoZewMxUuWZqQ5yjYsAnvE484KSkYH5s6iPvcjhCg=="
- }
- }
- },
- "mailsplit": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-4.3.0.tgz",
- "integrity": "sha512-yZm3NFB3w92QlmP1FZCcYYyL4KtklhMr4wnfEERo0JWwlZiGgO9/07uLs1oJe/X7eHJvuR1lV/xdcLZlWkBj9A==",
- "requires": {
- "libbase64": "1.0.3",
- "libmime": "4.0.1",
- "libqp": "1.1.0"
- },
- "dependencies": {
"libbase64": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.0.3.tgz",
"integrity": "sha512-ULQZAATVGTAgVNwP61R+MbbSGNBy1tVzWupB9kbE6p+VccWd+J+ICXgOwQic5Yqagzpu+oPZ8sI7yXdWJnPPkA=="
+ },
+ "libmime": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/libmime/-/libmime-4.1.1.tgz",
+ "integrity": "sha512-HkOfBSj+l7pBOOucEgiI6PdbgHa8ljv+1rARzW743HQ51UP8gabMlcA2wAF3Dg1aeuMjHZ+LzAPYxM52IZsyGA==",
+ "requires": {
+ "iconv-lite": "0.4.24",
+ "libbase64": "1.0.3",
+ "libqp": "1.1.0"
+ }
+ },
+ "nodemailer": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.1.1.tgz",
+ "integrity": "sha512-/x5MRIh56VyuuhLfcz+DL2SlBARpZpgQIf2A4Ao4hMb69MHSgDIMPwYmFwesGT1lkRDZ0eBSoym5+JoIZ3N+cQ=="
+ }
+ }
+ },
+ "mailsplit": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/mailsplit/-/mailsplit-4.4.1.tgz",
+ "integrity": "sha512-AmWLEHQAg/zbNb1MdrPQS9VOzysHaU9IuoQV9kGU5fgjM5RCbgqVkZzp0+DhPep8sj8iHfbWkl16Nb1PbNlTYg==",
+ "requires": {
+ "libbase64": "1.0.3",
+ "libmime": "4.1.1",
+ "libqp": "1.1.0"
+ },
+ "dependencies": {
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "libbase64": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-1.0.3.tgz",
+ "integrity": "sha512-ULQZAATVGTAgVNwP61R+MbbSGNBy1tVzWupB9kbE6p+VccWd+J+ICXgOwQic5Yqagzpu+oPZ8sI7yXdWJnPPkA=="
+ },
+ "libmime": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/libmime/-/libmime-4.1.1.tgz",
+ "integrity": "sha512-HkOfBSj+l7pBOOucEgiI6PdbgHa8ljv+1rARzW743HQ51UP8gabMlcA2wAF3Dg1aeuMjHZ+LzAPYxM52IZsyGA==",
+ "requires": {
+ "iconv-lite": "0.4.24",
+ "libbase64": "1.0.3",
+ "libqp": "1.1.0"
+ }
}
}
},
@@ -4880,11 +4318,6 @@
"object-visit": "^1.0.0"
}
},
- "math-random": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
- "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A=="
- },
"media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -5032,9 +4465,9 @@
}
},
"mime": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
- "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"mime-db": {
"version": "1.38.0",
@@ -5064,11 +4497,6 @@
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
},
- "minimalistic-crypto-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
- "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
- },
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
@@ -5083,9 +4511,9 @@
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"mixin-deep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
- "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"requires": {
"for-in": "^1.0.2",
"is-extendable": "^1.0.1"
@@ -5102,404 +4530,343 @@
}
},
"mjml": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.3.1.tgz",
- "integrity": "sha512-hUCdCodoWcLGaRifSCFuLo32ZSlh70kM44wUzEd3iXpaCfgaCen9YfykL4wSMkDogA1llMd2yWggRUVc8V2R+A==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml/-/mjml-4.4.0.tgz",
+ "integrity": "sha512-7K892HAovb+Q7ZTng0LaU9stuxwpfVWwyvKgO4EDTCshLP8bB94gcScX8bIWpLtw1O1Wgu1eclqH3G7DyJGl+Q==",
"requires": {
- "cross-env": "^5.1.4",
- "mjml-accordion": "4.3.1",
- "mjml-body": "4.3.1",
- "mjml-button": "4.3.1",
- "mjml-carousel": "4.3.1",
- "mjml-cli": "4.3.1",
- "mjml-column": "4.3.1",
- "mjml-core": "4.3.1",
- "mjml-divider": "4.3.1",
- "mjml-group": "4.3.1",
- "mjml-head": "4.3.1",
- "mjml-head-attributes": "4.3.1",
- "mjml-head-breakpoint": "4.3.1",
- "mjml-head-font": "4.3.1",
- "mjml-head-preview": "4.3.1",
- "mjml-head-style": "4.3.1",
- "mjml-head-title": "4.3.1",
- "mjml-hero": "4.3.1",
- "mjml-image": "4.3.1",
- "mjml-migrate": "4.3.1",
- "mjml-navbar": "4.3.1",
- "mjml-raw": "4.3.1",
- "mjml-section": "4.3.1",
- "mjml-social": "4.3.1",
- "mjml-spacer": "4.3.1",
- "mjml-table": "4.3.1",
- "mjml-text": "4.3.1",
- "mjml-validator": "4.3.0",
- "mjml-wrapper": "4.3.1"
+ "mjml-accordion": "4.4.0",
+ "mjml-body": "4.4.0",
+ "mjml-button": "4.4.0",
+ "mjml-carousel": "4.4.0",
+ "mjml-cli": "4.4.0",
+ "mjml-column": "4.4.0",
+ "mjml-core": "4.4.0",
+ "mjml-divider": "4.4.0",
+ "mjml-group": "4.4.0",
+ "mjml-head": "4.4.0",
+ "mjml-head-attributes": "4.4.0",
+ "mjml-head-breakpoint": "4.4.0",
+ "mjml-head-font": "4.4.0",
+ "mjml-head-preview": "4.4.0",
+ "mjml-head-style": "4.4.0",
+ "mjml-head-title": "4.4.0",
+ "mjml-hero": "4.4.0",
+ "mjml-image": "4.4.0",
+ "mjml-migrate": "4.4.0",
+ "mjml-navbar": "4.4.0",
+ "mjml-raw": "4.4.0",
+ "mjml-section": "4.4.0",
+ "mjml-social": "4.4.0",
+ "mjml-spacer": "4.4.0",
+ "mjml-table": "4.4.0",
+ "mjml-text": "4.4.0",
+ "mjml-validator": "4.4.0",
+ "mjml-wrapper": "4.4.0"
}
},
"mjml-accordion": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.3.1.tgz",
- "integrity": "sha512-utnvo+4DnKcZVkuxkaAWFdbNNHDkQmmi+ft8JempxWsJeNy0GWRl2fj0p62r2ODKRWPCNtqnKzEIM0y7X3HeyA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-accordion/-/mjml-accordion-4.4.0.tgz",
+ "integrity": "sha512-0i4/uxOGLXWDkdxSxImkp9FOSXopZC2Bt/kGB8e/YxpznTwU31pCEWMZxyL9DjRHSdNRxwpMl6hdXNKCfgojmg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-body": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.3.1.tgz",
- "integrity": "sha512-b0RjN8a6+nBUvWyNPaF12wt4U4mzVlBernOphjllB1N7lk5hFe7NVVHM2/n3s3eEuXoperxESaKLMCI+tuvi2Q==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-body/-/mjml-body-4.4.0.tgz",
+ "integrity": "sha512-tVEbA/NJiIAo4DLFljxEnCSJyq/W29Z9E6l39+CCYLw7AznqoEZ5W88hGiQkMDDDXeY2euiFpDioS3ID4KWEPA==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-button": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.3.1.tgz",
- "integrity": "sha512-6/Wswyn1qFTHvTmzgYfBB3mAlz2mQ+cF5hyFjUUN4FTulO4EonROjshVmSMP1DK3dO3hp9FjgB/AR/daRFOxEg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-button/-/mjml-button-4.4.0.tgz",
+ "integrity": "sha512-I9G2cjTEMAkrM0yf4rj+pt9mTVy1G7VnTsHruH1cD1D3umL4KSHoslH9VYzg7fv8SrdJqIOwpJOjQO7XE2mIOg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-carousel": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.3.1.tgz",
- "integrity": "sha512-RF9F46xj8FbkFeUNPy2ElnzZD2SDdfRJ3MDqBjOd5NNCdoTyRzMtPojL+bkgaBfWcjeM2m/gPJp7BYppj1RTPg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-carousel/-/mjml-carousel-4.4.0.tgz",
+ "integrity": "sha512-6vwqqUQjq9jA7coQcQYUy/ODdeogRytJ58KbWljblPCL8+LIrV6wAhyeniutX0u111IeXjeAZBN5Li+aBgl9Bw==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-cli": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.3.1.tgz",
- "integrity": "sha512-CXBs4IyOgr6lhhe0TfMp358N9vsj1tNfpS3PZdG5nxoymQc1QDCk4E3N8RMLzoz64YfanGV+oEu1VycgAJ8QmQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-cli/-/mjml-cli-4.4.0.tgz",
+ "integrity": "sha512-Q9pnl4SKhUuLJouXCGdh8NhJuAw+K15CW33tb4M+ZQhdPDQAkxY/LaNp3+vtA5juhk7R5SI7PoVPMQGfs1W6IA==",
"requires": {
"babel-runtime": "^6.26.0",
- "chokidar": "^1.6.1",
- "cross-env": "^5.1.4",
+ "chokidar": "^3.0.0",
"glob": "^7.1.1",
"lodash": "^4.17.4",
- "mjml-core": "4.3.1",
- "mjml-migrate": "4.3.1",
- "mjml-parser-xml": "4.3.1",
- "mjml-validator": "4.3.0",
+ "mjml-core": "4.4.0",
+ "mjml-migrate": "4.4.0",
+ "mjml-parser-xml": "4.4.0",
+ "mjml-validator": "4.4.0",
"yargs": "^8.0.2"
}
},
"mjml-column": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.3.1.tgz",
- "integrity": "sha512-ZLQfifTiR5LgeHYITYXBEAm8uChj/fF0rFYUOiCRAkkILk1/h1WnvYrwDvGFhGHWanPuO868SleHmM53L+EaoA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-column/-/mjml-column-4.4.0.tgz",
+ "integrity": "sha512-x2Jt2VHIyHUwI3pg0J1uAXvL+rd8VwO9Qf0g0FYVGkLBxxHJLqSBC6pVHyVzGYL5GDiy5lYVqwQ7nw5c2C3mgA==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-core": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.3.1.tgz",
- "integrity": "sha512-Iq1KxLcyJRpPKcXCUujm+3MdfS1RFLTmHg8hg2xucdgVtB7pd4ky6QHQOfpJaoW8H8icv9oIj9XOkiJgo3hTsQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-core/-/mjml-core-4.4.0.tgz",
+ "integrity": "sha512-pvSzyK7uMl5aft+XT65OovKhe0hUvPrZM/T0byrrBZ3wq8LDlotnKgGHnjCfByFoM3higfmdAKpxBsTJfXfaGQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"html-minifier": "^3.5.3",
"js-beautify": "^1.6.14",
- "juice": "^4.1.0",
+ "juice": "^5.2.0",
"lodash": "^4.17.2",
- "mjml-migrate": "4.3.1",
- "mjml-parser-xml": "4.3.1",
- "mjml-validator": "4.3.0"
- },
- "dependencies": {
- "cross-spawn": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
- "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
- "requires": {
- "lru-cache": "^4.0.1",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
- "deep-extend": {
- "version": "0.5.1",
- "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz",
- "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w=="
- },
- "juice": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/juice/-/juice-4.3.2.tgz",
- "integrity": "sha512-3Qym/RnFoCGa9qrDz6xn4zRnohgI6G87xKWZV+/seF3dYpaVqNS1HijsDef+elGhytRY79RIboOzk0hucLtx6g==",
- "requires": {
- "cheerio": "^0.22.0",
- "commander": "^2.15.1",
- "cross-spawn": "^5.1.0",
- "deep-extend": "^0.5.1",
- "mensch": "^0.3.3",
- "slick": "^1.12.2",
- "web-resource-inliner": "^4.2.1"
- }
- }
+ "mjml-migrate": "4.4.0",
+ "mjml-parser-xml": "4.4.0",
+ "mjml-validator": "4.4.0"
}
},
"mjml-divider": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.3.1.tgz",
- "integrity": "sha512-yWs1nBvBR6YiAR345VZHZUxHYOjZx0CpUYsK5+DWmUrs4qKcMs7kZoOiQ/1ESvRtJS59TLsaw5ZzTswCII4Ppw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-divider/-/mjml-divider-4.4.0.tgz",
+ "integrity": "sha512-GLuH7cEIA2s4hNL/8At//bO55mHMAWh88CtOlzbXr1vtCM4IUCajObrRESLyWcev72TDkuOZ6SvgZFW181RqkQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-group": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.3.1.tgz",
- "integrity": "sha512-JiMi8kTvsh70+DrqVUSLt9eXfxgbHGHQnW+BI1dp9dY+aJac6Mu1mk1oeZWiwsQFyRiofT7Laxd8Vm4KAwSphw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-group/-/mjml-group-4.4.0.tgz",
+ "integrity": "sha512-5uyblUpfWhAhRVwBC1H2zR5pRS7LgQjmN7X/empItq5uSvKy13LCrNTQ55s9MtWbmtnwYcnBI01s1P1HaJeQwg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.3.1.tgz",
- "integrity": "sha512-3sg3PTcbNt2x3e90+C0ryfKJYAEiGpVXOLBpBld39Hnkvl1LNwMw42NZPoHWbV1bU/rS/gM9D6e4Jy9OCYSCfQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head/-/mjml-head-4.4.0.tgz",
+ "integrity": "sha512-OANkaQH2T3pXfjFSxQRDBR8qVZZkVeGPgdIJxv6M1p6LjBDsehXKbPaJ3tNMT25XGNHzL30cwcFfephrvQjSWg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-attributes": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.3.1.tgz",
- "integrity": "sha512-ZWyXxRolnu2I41CHuCXyXkKSiGbYf5s5iy70vyIFBoa6Dz9Gm31DMk97cMW2Nodd2UEGMju2pa2m+ElMcPcS2A==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-attributes/-/mjml-head-attributes-4.4.0.tgz",
+ "integrity": "sha512-irWt/yMO190y+HqbmFLfv+ae81jpljUj+OzQAagIDr14I4i1WXLAUkwwiNVHLAyHx+4dqcNQKDJUKN0foT12Xw==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-breakpoint": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.3.1.tgz",
- "integrity": "sha512-Ij6+FIAZKwSbwEseVXMeu9fjP3h2f4EutHyjyH90tExkaIvx9a1Rc7oJA3jpjTbUIkIjLRecEk0XGxgGhn5mdw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-breakpoint/-/mjml-head-breakpoint-4.4.0.tgz",
+ "integrity": "sha512-U3VdPpZ5xf6qDT8Tjb+Zwyfr4y96FVltjtgVhs6VNReOtYuerELUmc6BVdLDQRxj4g1Ep36nkwoER7XfFzoU5A==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-font": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.3.1.tgz",
- "integrity": "sha512-T4aOt2d6nsb63UrHHCPeijmeaWUDSEDDV2n14HQlEkCLwz2nbFRfopEQZakdVf8oa3eWvS82wCdPPq83NNCAdg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-font/-/mjml-head-font-4.4.0.tgz",
+ "integrity": "sha512-OLfg3vKBn4Lu8QHZ+8xrU5EiqQEdi685XasCmzLH7KZ6X40iqxQFiEzs8/mHLW9msLlA1NmJCM6GZtZpIMdDdQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-preview": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.3.1.tgz",
- "integrity": "sha512-Fc8SpGUKU+RNAdbKSuDL0S4hePr8zYlvNR5FC3lUN9U4lomrE7/i8KBZbJdoZPRuZH4gKyv1BlT5Td1xkyWKdQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-preview/-/mjml-head-preview-4.4.0.tgz",
+ "integrity": "sha512-rt2RgygJEgzGErGUev9SVshp1Uhsi5T//phOTVFxarB0xAin55nMhyYPHq1N+Wf1+67mRSv4n0oS3zCGpVj+ew==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-style": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.3.1.tgz",
- "integrity": "sha512-qohC5W5Ur9Y4kqNcNP1dRr5sDeb59tEkYoS4M8leQutW8w1Aq5X0jq+/JNDAQB4FylYCpXIy3rO2vjPUI68X9A==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-style/-/mjml-head-style-4.4.0.tgz",
+ "integrity": "sha512-PiXDjLzKMzEViJTpYylF9RJd1ImRLMkZxPPgHGAVxFHv9cw9A7erxf1Pf6hTjXs6bng8PWfw+B9DBi7DKbpBCA==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-head-title": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.3.1.tgz",
- "integrity": "sha512-spzZNmhfLL+AmT7w3m0UBMG2VpMv3fOX6B0nlAeDCnv5VWLgO51UvhATWYqccHX12Ee5Fnng4N0lB5TRmSF8mw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-head-title/-/mjml-head-title-4.4.0.tgz",
+ "integrity": "sha512-6nkEAdBQjw7BnJ8qJOEOes7pQZH8trAq47Pvp0CZWmEdVEhe+JJLUyj+OHtMVKOM62Qw/0hISifX3J90Wbeoeg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-hero": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.3.1.tgz",
- "integrity": "sha512-jyheCr5M7b6XEzG6Ripn/tF789yo9gfT0zZnO9gVbJWTlwpHC6BGac8UaBHguc3kFtzoi85n5Ob1D0j/qbCOcA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-hero/-/mjml-hero-4.4.0.tgz",
+ "integrity": "sha512-2z4jLEDaIOA6Lga0XNVgfNL4yQQ2RPhPOV0foUBNhU/jVsHcTpjfw5luybFnPrNHMPw36IUcRzWE3L106l1YJQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-image": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.3.1.tgz",
- "integrity": "sha512-IYsywUHN2RKnj1QUf8WnNz93tNgL2/p1sUdnhtZ9ccObPfpSxDc1BkkOVo6PLf2sADTuposNHGajB4lqV45Oyg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-image/-/mjml-image-4.4.0.tgz",
+ "integrity": "sha512-zgjaI1o2fY6pkEa6DKwZ09ofsNOfLhMedeI1WpJKACWJcuRvcyKTC7bfcG/F0BVkxVIh8LeoD79cCWwv1hZIOw==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-migrate": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.3.1.tgz",
- "integrity": "sha512-NLbk4YW4ht+v79qNtgirMD53EFq2qwBaHDZmETu5V+i7xgshiQv+lskA5tZpe4qtetIHHpCPUJC3hyt39gzAzg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-migrate/-/mjml-migrate-4.4.0.tgz",
+ "integrity": "sha512-n4scacdm7nk3o3JCKotmwmFWQdMwvScG854Kxn8Xm9tWMre3f+k4yxWYLxDvbtqg4AmWnNEDhWsIb55g1GEphw==",
"requires": {
"babel-runtime": "^6.26.0",
"commander": "^2.11.0",
- "cross-env": "^5.1.4",
"js-beautify": "^1.6.14",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1",
- "mjml-parser-xml": "4.3.1"
+ "mjml-core": "4.4.0",
+ "mjml-parser-xml": "4.4.0"
}
},
"mjml-navbar": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.3.1.tgz",
- "integrity": "sha512-XvMh8u+UZGvcBFsiR+z5OEiiWnf+xqx0ytIqoYLlAtETyJlnBBrYuSSD5iBCQhWdx7lwzKLyzguQ1nmOcuvd7Q==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-navbar/-/mjml-navbar-4.4.0.tgz",
+ "integrity": "sha512-FmuD2JAJx5lZNP63/9aLWMG+DnMkPlSML0X4sk1QmMzAMH9JbHXlyDtxyHrLssZRAqyycahTx6s2YUOpR0F5yA==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-parser-xml": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.3.1.tgz",
- "integrity": "sha512-dcKrPOzCeDL/yEsu21OYkLodAxQA60ergM9GY2RGIHcTSUz4rcB92oBwS0HmUImiD60ByddcYCHNJxredRDBSQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-parser-xml/-/mjml-parser-xml-4.4.0.tgz",
+ "integrity": "sha512-VrFr2IoLHrKh1UydKfo45EokSa4F90zfYESxvPbF/s5dew5gyam0HuyZSQQY6B5aAj7p1mE8/x0OpL2OLivqBQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"htmlparser2": "^3.9.2",
"lodash": "^4.17.2"
}
},
"mjml-raw": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.3.1.tgz",
- "integrity": "sha512-tyZRYgyZxZcmY7Q4uJ4BfUABTO3H6wYZhch/XUZ7ZVK9aFnYqv5WxQNY64ZA9jlVmy26ZonzJ/nEC00tFgY+hw==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-raw/-/mjml-raw-4.4.0.tgz",
+ "integrity": "sha512-/CI9xSJG0ygISNr/rNiur4CjtSvTSehGoRXRMQx0oio7icw/4PxeagppdDuidnGEaDlMWSrh9xSJeMQMt6wO9A==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-section": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.3.1.tgz",
- "integrity": "sha512-ZVSneZXOfYoGFp45KItDyBRzFxdwr+410h4Vk3KByHZF5976PKBOjtAAoGkBmhsEacCpWNtP2dKm5AuycM8wNQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-section/-/mjml-section-4.4.0.tgz",
+ "integrity": "sha512-FOHLwWFwIxHE6yd/hysB3S63+3KhtgRqq//wdoEU93m+/8nQqas3SBudL8huWIYdPKAcfCvKwmi54IeS0XED5A==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-social": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.3.1.tgz",
- "integrity": "sha512-ICW5mlGsPKYKtXF+33I3HQy8H2ab9pY7OUM8JmwIB9XMnvfQTLBf5uSrMRaRjoQ09sXXsyzuR4mpaQs+7khxIg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-social/-/mjml-social-4.4.0.tgz",
+ "integrity": "sha512-6Hk61kGRBTy6JqFD63UPXFMzjEE14qk0fNCINtiRPOVlInzi0C+tga3f9hNH0LUHMQtJC2V1M87syi5gWhubEA==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-spacer": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.3.1.tgz",
- "integrity": "sha512-/x0SDTqfkJzPjPj7ORxJgvd11+0Ht2GM7537MbSpxSUsSeRLaTRA92xn9/qRk5LKm0c+vOXwYWRAvtMUsSv2NQ==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-spacer/-/mjml-spacer-4.4.0.tgz",
+ "integrity": "sha512-eVYNKkvMgdUmY914okg07hfi6VYJnlAsF2YtwyfjU7PZyX2hqeDETZXyaIZjZ7vp9IiXblbnTVYn5fQ05dmq+g==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-table": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.3.1.tgz",
- "integrity": "sha512-n1ZEYHvZhWvaehz//q2W6LQMbX3Sqd8uWu8ux4cP6WrRnmSDG7gCdcRwxrhSMjfzgP9ZXtT9NahkqfFTKSkJfg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-table/-/mjml-table-4.4.0.tgz",
+ "integrity": "sha512-bwBvthYpjTw3HZ+UTE0dB/4G4Hjr3nIlgcKUUohu9PyA8BnkZBAfS0N7l/HYRqO+o6t9FIZlFplq+U98EnOTLw==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-text": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.3.1.tgz",
- "integrity": "sha512-z1H7xg8ugqWwMv69B/naGGoLI9C1jHpGsw4IFpB814qpA7FPtKWxgwz33fKdL9tju3mv+TBOqzYTbxuaQjTGtA==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-text/-/mjml-text-4.4.0.tgz",
+ "integrity": "sha512-gyR5mFHDFu60WrJENh/Ctta925XrVrpgPrAm3+m3UWId11zVUtEeE1T3Xij5tVfKQpzXrqYnCZRPPwYSlMjKxg==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1"
+ "mjml-core": "4.4.0"
}
},
"mjml-validator": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.3.0.tgz",
- "integrity": "sha512-SWfsBHt4gC1zz+kjf3FWZgM9d9tdZj0VO1e0BK0Y1uqshGf192d9vMIKkqrtHI9sMkIl1Y6+/XJEjDBcbkKOvg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-validator/-/mjml-validator-4.4.0.tgz",
+ "integrity": "sha512-htKvNAjfhLgPZHTVAQENRc/sMlOKYGCRn7pvSGi1N96VhDf2pB5/kgyLJMjT2u1yT0hXID+6P9/HKVNW2zlUQQ==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
"warning": "^3.0.0"
}
},
"mjml-wrapper": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.3.1.tgz",
- "integrity": "sha512-wmfa3A+0BrjneqV/vxGgGDMm0AswWFEJkJ1kg2Qh4hxM4duaQJd9C/STy8PuTQ+IVMadGld0O1bi9bOEeKhVEg==",
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/mjml-wrapper/-/mjml-wrapper-4.4.0.tgz",
+ "integrity": "sha512-6Cg+WVmardwjO+204ZEC7hno5q4kDIdk9WeL7jka5tMp3fPhtUOr38fAMFDfnxiIZhI/c0WYSdTA5MGN5MQaNw==",
"requires": {
"babel-runtime": "^6.26.0",
- "cross-env": "^5.1.4",
"lodash": "^4.17.2",
- "mjml-core": "4.3.1",
- "mjml-section": "4.3.1"
+ "mjml-core": "4.4.0",
+ "mjml-section": "4.4.0"
}
},
"mkdirp": {
@@ -5588,9 +4955,9 @@
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
},
"moment-timezone": {
- "version": "0.5.25",
- "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.25.tgz",
- "integrity": "sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw==",
+ "version": "0.5.26",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.26.tgz",
+ "integrity": "sha512-sFP4cgEKTCymBBKgoxZjYzlSovC20Y6J7y3nanDc5RoBIXKlZhoYwBoZGe3flwU6A372AcRwScH8KiwV6zjy1g==",
"requires": {
"moment": ">= 2.9.0"
}
@@ -5618,9 +4985,9 @@
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"multer": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.1.tgz",
- "integrity": "sha512-zzOLNRxzszwd+61JFuAo0fxdQfvku12aNJgnla0AQ+hHxFmfc/B7jBVuPr5Rmvu46Jze/iJrFpSOsD7afO8SDw==",
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz",
+ "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==",
"requires": {
"append-field": "^1.0.0",
"busboy": "^0.2.11",
@@ -5635,7 +5002,8 @@
"mute-stream": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
- "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
+ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=",
+ "dev": true
},
"mysql": {
"version": "2.17.1",
@@ -5685,7 +5053,8 @@
"natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=",
+ "dev": true
},
"negotiator": {
"version": "0.6.1",
@@ -5697,11 +5066,6 @@
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.0.tgz",
"integrity": "sha512-MFh0d/Wa7vkKO3Y3LlacqAEeHK0mckVqzDieUKTT+KGxi+zIpeVsFxymkIiRpbpDziHc290Xr9A1O4Om7otoRA=="
},
- "net": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/net/-/net-1.0.2.tgz",
- "integrity": "sha1-0XV+yaf7I3HYPPR1XOPifhCCk4g="
- },
"nice-try": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
@@ -5743,20 +5107,53 @@
}
},
"node-mocks-http": {
- "version": "1.7.3",
- "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.7.3.tgz",
- "integrity": "sha512-wayzLNhEroH3lJj113pFKQ1cd1GKG1mXoZR1HcKp/o9a9lTGGgVY/hYeLajiIFr/z4tXFKOdfJickqqihBtn9g==",
+ "version": "1.7.6",
+ "resolved": "https://registry.npmjs.org/node-mocks-http/-/node-mocks-http-1.7.6.tgz",
+ "integrity": "sha512-ZWbZ5HEEAoVZbAYM8KHezx0v66Te3klg/yhAmdJJ0ULWQAkSqPStEzqSjONj4zRZOrTWqsHnI6nHeJxw46gj6Q==",
"requires": {
- "accepts": "^1.3.5",
+ "accepts": "^1.3.7",
"depd": "^1.1.0",
"fresh": "^0.5.2",
"merge-descriptors": "^1.0.1",
"methods": "^1.1.2",
"mime": "^1.3.4",
- "net": "^1.0.2",
- "parseurl": "^1.3.1",
+ "parseurl": "^1.3.3",
"range-parser": "^1.2.0",
- "type-is": "^1.6.16"
+ "type-is": "^1.6.18"
+ },
+ "dependencies": {
+ "accepts": {
+ "version": "1.3.7",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz",
+ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==",
+ "requires": {
+ "mime-types": "~2.1.24",
+ "negotiator": "0.6.2"
+ }
+ },
+ "mime-db": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+ },
+ "mime-types": {
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ }
}
},
"node-object-hash": {
@@ -5802,21 +5199,37 @@
"ieee754": "^1.1.4"
}
},
+ "commander": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
+ "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
+ "requires": {
+ "graceful-readlink": ">= 1.0.0"
+ },
+ "dependencies": {
+ "graceful-readlink": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz",
+ "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU="
+ }
+ }
+ },
"compressjs": {
"version": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
- "from": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9"
+ "from": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
+ "requires": {
+ "amdefine": "~1.0.0",
+ "commander": "~2.8.1"
+ }
},
"elliptic": {
"version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"from": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
"requires": {
"bn.js": "^4.4.0",
- "brorand": "^1.0.1",
"hash.js": "^1.0.0",
- "hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.0"
+ "minimalistic-assert": "^1.0.0"
}
},
"openpgp": {
@@ -5829,6 +5242,8 @@
"asn1.js": "^5.0.0",
"bn.js": "^4.11.8",
"buffer": "^5.0.8",
+ "compressjs": "github:openpgpjs/compressjs",
+ "elliptic": "github:openpgpjs/elliptic",
"hash.js": "^1.1.3",
"node-fetch": "^1.7.3",
"node-localstorage": "~1.3.0",
@@ -5838,30 +5253,15 @@
"dependencies": {
"compressjs": {
"version": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
- "from": "github:openpgpjs/compressjs#bfbb371a34d1750afa34bfa49156461acdab79a9",
+ "from": "github:openpgpjs/compressjs",
"requires": {
"amdefine": "~1.0.0",
"commander": "~2.8.1"
- },
- "dependencies": {
- "amdefine": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
- "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
- },
- "commander": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
- "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
- "requires": {
- "graceful-readlink": ">= 1.0.0"
- }
- }
}
},
"elliptic": {
- "version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
- "from": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
+ "version": "github:openpgpjs/elliptic#1beea74833b48bd5698ed079c75fd21f0eb70b1c",
+ "from": "github:openpgpjs/elliptic",
"requires": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
@@ -5870,6 +5270,28 @@
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
+ },
+ "dependencies": {
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+ }
}
}
}
@@ -5897,12 +5319,9 @@
}
},
"normalize-path": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
- "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
- "requires": {
- "remove-trailing-separator": "^1.0.1"
- }
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
},
"npm-run-path": {
"version": "2.0.2",
@@ -6007,25 +5426,6 @@
"make-iterator": "^1.0.0"
}
},
- "object.omit": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz",
- "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=",
- "requires": {
- "for-own": "^0.1.4",
- "is-extendable": "^0.1.1"
- },
- "dependencies": {
- "for-own": {
- "version": "0.1.5",
- "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz",
- "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=",
- "requires": {
- "for-in": "^1.0.1"
- }
- }
- }
- },
"object.pick": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz",
@@ -6059,60 +5459,25 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
"integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "dev": true,
"requires": {
"mimic-fn": "^1.0.0"
}
},
"openpgp": {
- "version": "4.4.10",
- "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-4.4.10.tgz",
- "integrity": "sha512-OdDK5+tVApsVFMmrbzVtLKlZHWOjy5LDQCbVOkun9jMXNhaJ+eX6DArN6BpPs+liVztqBzCqmTKgZ+tvmUNtqg==",
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-4.5.5.tgz",
+ "integrity": "sha512-naG9hi3EIBXYtl8FplXbfYKQCmO7JUwAFBOlejyaLi4XSmFvTtAUSxB5vTchsw7tBbejYZj01dhgqhtYRDLfRg==",
"requires": {
- "@mattiasbuelens/web-streams-polyfill": "^0.3.1",
- "address-rfc2822": "^2.0.3",
- "asmcrypto.js": "github:openpgpjs/asmcrypto#6e4e407b9b8ae317925a9e677cc7b4de3e447e83",
"asn1.js": "^5.0.0",
- "bn.js": "^4.11.8",
- "buffer": "^5.0.8",
- "elliptic": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
- "hash.js": "^1.1.3",
"node-fetch": "^2.1.2",
- "node-localstorage": "~1.3.0",
- "pako": "^1.0.6",
- "seek-bzip": "github:openpgpjs/seek-bzip#3aca608ffedc055a1da1d898ecb244804ef32209",
- "web-stream-tools": "github:openpgpjs/web-stream-tools#84a497715c9df271a673f8616318264ab42ab3cc"
+ "node-localstorage": "~1.3.0"
},
"dependencies": {
- "asmcrypto.js": {
- "version": "github:openpgpjs/asmcrypto#6e4e407b9b8ae317925a9e677cc7b4de3e447e83",
- "from": "github:openpgpjs/asmcrypto#6e4e407b9b8ae317925a9e677cc7b4de3e447e83"
- },
- "buffer": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
- "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
- "requires": {
- "base64-js": "^1.0.2",
- "ieee754": "^1.1.4"
- }
- },
- "elliptic": {
- "version": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
- "from": "github:openpgpjs/elliptic#ad81845f693effa5b4b6d07db2e82112de222f48",
- "requires": {
- "bn.js": "^4.4.0",
- "brorand": "^1.0.1",
- "hash.js": "^1.0.0",
- "hmac-drbg": "^1.0.0",
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0",
- "minimalistic-crypto-utils": "^1.0.0"
- }
- },
"node-fetch": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz",
- "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA=="
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
+ "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
}
}
},
@@ -6229,6 +5594,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
"requires": {
"callsites": "^3.0.0"
}
@@ -6243,32 +5609,6 @@
"path-root": "^0.1.1"
}
},
- "parse-glob": {
- "version": "3.0.4",
- "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz",
- "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=",
- "requires": {
- "glob-base": "^0.3.0",
- "is-dotfile": "^1.0.0",
- "is-extglob": "^1.0.0",
- "is-glob": "^2.0.0"
- },
- "dependencies": {
- "is-extglob": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
- "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA="
- },
- "is-glob": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
- "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
- "requires": {
- "is-extglob": "^1.0.0"
- }
- }
- }
- },
"parse-json": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
@@ -6332,7 +5672,8 @@
"path-is-inside": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
- "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=",
+ "dev": true
},
"path-key": {
"version": "2.0.1",
@@ -6442,6 +5783,11 @@
}
}
},
+ "picomatch": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.0.7.tgz",
+ "integrity": "sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA=="
+ },
"pify": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
@@ -6495,11 +5841,6 @@
"underscore": "^1.8.3"
}
},
- "preserve": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz",
- "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks="
- },
"process-nextick-args": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
@@ -6508,7 +5849,8 @@
"progress": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
- "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="
+ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==",
+ "dev": true
},
"promise": {
"version": "1.3.0",
@@ -6531,12 +5873,12 @@
"integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk="
},
"proxy-addr": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
- "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
+ "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
"requires": {
"forwarded": "~0.1.2",
- "ipaddr.js": "1.8.0"
+ "ipaddr.js": "1.9.0"
}
},
"pseudomap": {
@@ -6578,37 +5920,35 @@
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha1-T2ih3Arli9P7lYSMMDJNt11kNgs="
},
- "randomatic": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz",
- "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==",
- "requires": {
- "is-number": "^4.0.0",
- "kind-of": "^6.0.0",
- "math-random": "^1.0.1"
- },
- "dependencies": {
- "is-number": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz",
- "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ=="
- }
- }
- },
"range-parser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
- "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
},
"raw-body": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz",
- "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
- "bytes": "3.0.0",
- "http-errors": "1.6.3",
- "iconv-lite": "0.4.23",
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
"unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ }
}
},
"read-pkg": {
@@ -6645,13 +5985,11 @@
}
},
"readdirp": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
- "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.1.1.tgz",
+ "integrity": "sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ==",
"requires": {
- "graceful-fs": "^4.1.11",
- "micromatch": "^3.1.10",
- "readable-stream": "^2.0.2"
+ "picomatch": "^2.0.4"
}
},
"rechoir": {
@@ -6683,9 +6021,9 @@
}
},
"redis-commands": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
- "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw=="
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.5.0.tgz",
+ "integrity": "sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg=="
},
"redis-parser": {
"version": "2.6.0",
@@ -6697,14 +6035,6 @@
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
},
- "regex-cache": {
- "version": "0.4.4",
- "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz",
- "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==",
- "requires": {
- "is-equal-shallow": "^0.1.3"
- }
- },
"regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@@ -6717,18 +6047,14 @@
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
- "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw=="
+ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
+ "dev": true
},
"relateurl": {
"version": "0.2.7",
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
"integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk="
},
- "remove-trailing-separator": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
- "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8="
- },
"repeat-element": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz",
@@ -6843,7 +6169,8 @@
"resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
- "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true
},
"resolve-url": {
"version": "0.2.1",
@@ -6854,6 +6181,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
"integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "dev": true,
"requires": {
"onetime": "^2.0.0",
"signal-exit": "^3.0.2"
@@ -6889,6 +6217,7 @@
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
"integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "dev": true,
"requires": {
"is-promise": "^2.1.0"
}
@@ -6899,9 +6228,10 @@
"integrity": "sha1-mghOe4YLF7/zAVuSxnpqM2GRUTo="
},
"rxjs": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.4.0.tgz",
- "integrity": "sha512-Z9Yfa11F6B9Sg/BK9MnqnQ+aQYicPLtilXBp2yUtDt2JRCE0h26d33EnfO3ZxoNxG0T92OUucP3Ct7cpfkdFfw==",
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
+ "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
+ "dev": true,
"requires": {
"tslib": "^1.9.0"
}
@@ -6937,23 +6267,6 @@
"xmlchars": "^1.3.1"
}
},
- "seek-bzip": {
- "version": "github:openpgpjs/seek-bzip#3aca608ffedc055a1da1d898ecb244804ef32209",
- "from": "github:openpgpjs/seek-bzip#3aca608ffedc055a1da1d898ecb244804ef32209",
- "requires": {
- "commander": "~2.8.1"
- },
- "dependencies": {
- "commander": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz",
- "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=",
- "requires": {
- "graceful-readlink": ">= 1.0.0"
- }
- }
- }
- },
"selenium-webdriver": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/selenium-webdriver/-/selenium-webdriver-3.6.0.tgz",
@@ -6992,9 +6305,9 @@
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
},
"send": {
- "version": "0.16.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
- "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@@ -7003,18 +6316,18 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
- "http-errors": "~1.6.2",
- "mime": "1.4.1",
- "ms": "2.0.0",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
"on-finished": "~2.3.0",
- "range-parser": "~1.2.0",
- "statuses": "~1.4.0"
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
},
"dependencies": {
- "statuses": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
- "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
@@ -7043,14 +6356,21 @@
}
},
"serve-static": {
- "version": "1.13.2",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
- "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
- "parseurl": "~1.3.2",
- "send": "0.16.2"
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ },
+ "dependencies": {
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ }
}
},
"set-blocking": {
@@ -7065,9 +6385,9 @@
"dev": true
},
"set-value": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
- "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"requires": {
"extend-shallow": "^2.0.1",
"is-extendable": "^0.1.1",
@@ -7086,9 +6406,9 @@
}
},
"setprototypeof": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
- "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"shebang-command": {
"version": "1.2.0",
@@ -7125,6 +6445,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz",
"integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==",
+ "dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"astral-regex": "^1.0.0",
@@ -7135,6 +6456,7 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@@ -7336,7 +6658,8 @@
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
- "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
},
"sqlstring": {
"version": "2.3.1",
@@ -7460,7 +6783,8 @@
"strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
- "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
+ "dev": true
},
"supports-color": {
"version": "2.0.0",
@@ -7473,25 +6797,40 @@
"integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY="
},
"table": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/table/-/table-5.2.3.tgz",
- "integrity": "sha512-N2RsDAMvDLvYwFcwbPyF3VmVSSkuF+G1e+8inhBLtHpvwXGw4QRPEZhihQNeEN0i1up6/f6ObCJXNdlRG3YVyQ==",
+ "version": "5.4.4",
+ "resolved": "https://registry.npmjs.org/table/-/table-5.4.4.tgz",
+ "integrity": "sha512-IIfEAUx5QlODLblLrGTTLJA7Tk0iLSGBvgY8essPRVNGHAzThujww1YqHLs6h3HfTg55h++RzLHH5Xw/rfv+mg==",
+ "dev": true,
"requires": {
- "ajv": "^6.9.1",
- "lodash": "^4.17.11",
+ "ajv": "^6.10.2",
+ "lodash": "^4.17.14",
"slice-ansi": "^2.1.0",
"string-width": "^3.0.0"
},
"dependencies": {
+ "ajv": {
+ "version": "6.10.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz",
+ "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==",
+ "dev": true,
+ "requires": {
+ "fast-deep-equal": "^2.0.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ }
+ },
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
- "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
+ "dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
@@ -7502,6 +6841,7 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
@@ -7516,7 +6856,8 @@
"text-table": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
- "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ="
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=",
+ "dev": true
},
"throttleit": {
"version": "1.0.0",
@@ -7527,7 +6868,8 @@
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
- "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
+ "dev": true
},
"tildify": {
"version": "1.2.0",
@@ -7551,12 +6893,30 @@
}
},
"tmp-promise": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.0.5.tgz",
- "integrity": "sha512-hOabTz9Tp49wCozFwuJe5ISrOqkECm6kzw66XTP23DuzNU7QS/KiZq5LC9Y7QSy8f1rPSLy4bKaViP0OwGI1cA==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/tmp-promise/-/tmp-promise-1.1.0.tgz",
+ "integrity": "sha512-8+Ah9aB1IRXCnIOxXZ0uFozV1nMU5xiu7hhFVUSxZ3bYu+psD4TzagCzVbexUCgNNGJnsmNDQlS4nG3mTyoNkw==",
"requires": {
"bluebird": "^3.5.0",
- "tmp": "0.0.33"
+ "tmp": "0.1.0"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "tmp": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz",
+ "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==",
+ "requires": {
+ "rimraf": "^2.6.3"
+ }
+ }
}
},
"to-fast-properties": {
@@ -7603,6 +6963,11 @@
"repeat-string": "^1.6.1"
}
},
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
"toml": {
"version": "2.3.6",
"resolved": "https://registry.npmjs.org/toml/-/toml-2.3.6.tgz",
@@ -7662,14 +7027,15 @@
"integrity": "sha1-NEiaLKwMCcHMEO2RugEVlNQzO+I="
},
"tslib": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
+ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
+ "dev": true
},
"tsscmp": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.5.tgz",
- "integrity": "sha1-fcSjOvcVgatDN9qR2FylQn69mpc="
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz",
+ "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA=="
},
"tunnel-agent": {
"version": "0.6.0",
@@ -7699,12 +7065,27 @@
"dev": true
},
"type-is": {
- "version": "1.6.16",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
- "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"requires": {
"media-typer": "0.3.0",
- "mime-types": "~2.1.18"
+ "mime-types": "~2.1.24"
+ },
+ "dependencies": {
+ "mime-db": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+ },
+ "mime-types": {
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ }
}
},
"typedarray": {
@@ -7728,9 +7109,9 @@
}
},
"uid-safe": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.4.tgz",
- "integrity": "sha1-Otbzg2jG1MjHXsF2I/t5qh0HHYE=",
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
+ "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"requires": {
"random-bytes": "~1.0.0"
}
@@ -7756,35 +7137,14 @@
}
},
"union-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
- "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
- "set-value": "^0.4.3"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "set-value": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
- "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.1",
- "to-object-path": "^0.3.0"
- }
- }
+ "set-value": "^2.0.1"
}
},
"universalify": {
@@ -8009,10 +7369,6 @@
}
}
},
- "web-stream-tools": {
- "version": "github:openpgpjs/web-stream-tools#84a497715c9df271a673f8616318264ab42ab3cc",
- "from": "github:openpgpjs/web-stream-tools#84a497715c9df271a673f8616318264ab42ab3cc"
- },
"webidl-conversions": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz",
@@ -8115,6 +7471,7 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz",
"integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==",
+ "dev": true,
"requires": {
"mkdirp": "^0.5.1"
}
diff --git a/server/package.json b/server/package.json
index a77e6ebb..7def6b8c 100644
--- a/server/package.json
+++ b/server/package.json
@@ -26,13 +26,13 @@
"node": ">=10.0.0"
},
"devDependencies": {
- "babel-eslint": "^10.0.1",
+ "babel-eslint": "^10.0.2",
"chai": "^4.2.0",
"eslint-config-nodemailer": "^1.2.0",
"grunt": "^1.0.3",
"grunt-cli": "^1.3.2",
- "grunt-eslint": "^21.0.0",
- "lodash": "^4.17.11",
+ "grunt-eslint": "^21.1.0",
+ "lodash": "^4.17.15",
"mocha": "^5.2.0",
"phantomjs-prebuilt": "^2.1.16",
"selenium-webdriver": "^3.6.0",
@@ -42,31 +42,31 @@
"posix": "^4.1.2"
},
"dependencies": {
- "aws-sdk": "^2.440.0",
+ "aws-sdk": "^2.497.0",
"bcrypt-nodejs": "0.0.3",
- "bluebird": "^3.5.3",
- "body-parser": "^1.18.3",
+ "bluebird": "^3.5.5",
+ "body-parser": "^1.19.0",
"bounce-handler": "7.3.2-fork.3",
"capitalize": "^2.0.0",
"compression": "^1.7.3",
- "config": "^3.0.1",
+ "config": "^3.2.2",
"connect-flash": "^0.1.1",
- "connect-redis": "^3.4.0",
+ "connect-redis": "^3.4.2",
"cookie-parser": "^1.4.3",
"cors": "^2.8.5",
"crypto": "^1.0.1",
- "csurf": "^1.9.0",
- "csv-parse": "^4.4.1",
+ "csurf": "^1.10.0",
+ "csv-parse": "^4.4.3",
"csv-stringify": "^5.1.2",
"device": "^0.3.9",
- "dompurify": "^1.0.8",
+ "dompurify": "^1.0.11",
"escape-html": "^1.0.3",
"escape-string-regexp": "^1.0.5",
- "express": "^4.16.4",
+ "express": "^4.17.1",
"express-locale": "^1.0.5",
- "express-session": "^1.15.6",
+ "express-session": "^1.16.2",
"faker": "^4.1.0",
- "feedparser-promised": "^2.0.0",
+ "feedparser-promised": "^2.0.1",
"fs-extra": "^7.0.1",
"fs-extra-promise": "^1.0.1",
"geoip-ultralight": "^0.1.5",
@@ -82,23 +82,23 @@
"juice": "^5.2.0",
"klaw-sync": "^6.0.0",
"knex": "^0.16.5",
- "libmime": "^4.0.1",
- "mailparser": "^2.4.3",
+ "libmime": "^4.1.3",
+ "mailparser": "^2.7.1",
"memory-cache": "^0.2.0",
- "mjml": "^4.3.0",
+ "mjml": "^4.4.0",
"moment": "^2.23.0",
- "moment-timezone": "^0.5.25",
+ "moment-timezone": "^0.5.26",
"morgan": "^1.9.1",
- "multer": "^1.4.1",
+ "multer": "^1.4.2",
"mysql": "^2.17.1",
"node-ipc": "^9.1.1",
- "node-mocks-http": "^1.7.3",
+ "node-mocks-http": "^1.7.6",
"node-object-hash": "^1.4.1",
"nodeify": "^1.0.1",
"nodemailer": "^5.0.0",
"nodemailer-openpgp": "^1.2.0",
"npmlog": "^4.1.2",
- "openpgp": "^4.4.3",
+ "openpgp": "^4.5.5",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"premailer-api": "^1.0.4",
@@ -108,7 +108,7 @@
"shortid": "^2.2.14",
"slugify": "^1.3.4",
"smtp-server": "^3.4.7",
- "tmp-promise": "^1.0.5",
+ "tmp-promise": "^1.1.0",
"toml": "^2.3.3",
"try-require": "^1.2.1",
"xmldom": "^0.1.27"
diff --git a/server/routes/api.js b/server/routes/api.js
index 943f1fac..52d1c800 100644
--- a/server/routes/api.js
+++ b/server/routes/api.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const lists = require('../models/lists');
const tools = require('../lib/tools');
const blacklist = require('../models/blacklist');
@@ -16,8 +16,10 @@ const contextHelpers = require('../lib/context-helpers');
const shares = require('../models/shares');
const slugify = require('slugify');
const passport = require('../lib/passport');
-const TemplateSender = require('../lib/template-sender');
+const templates = require('../models/templates');
const campaigns = require('../models/campaigns');
+const {castToInteger} = require('../lib/helpers');
+const {getSystemSendConfigurationId} = require('../../shared/send-configurations');
class APIError extends Error {
constructor(msg, status) {
@@ -42,7 +44,7 @@ router.postAsync('/subscribe/:listCid', passport.loggedIn, async (req, res) => {
const emailErr = await tools.validateEmail(input.EMAIL);
if (emailErr) {
- const errMsg = tools.validateEmailGetMessage(emailErr, input.email);
+ const errMsg = tools.validateEmailGetMessage(emailErr, input.email, null);
log.error('API', errMsg);
throw new APIError(errMsg, 400);
}
@@ -236,7 +238,7 @@ router.postAsync('/blacklist/add', passport.loggedIn, async (req, res) => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
- throw new Error('EMAIL argument is required');
+ throw new APIError('EMAIL argument is required', 400);
}
await blacklist.add(req.context, input.EMAIL);
@@ -253,7 +255,7 @@ router.postAsync('/blacklist/delete', passport.loggedIn, async (req, res) => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
- throw new Error('EMAIL argument is required');
+ throw new APIError('EMAIL argument is required', 400);
}
await blacklist.remove(req.oontext, input.EMAIL);
@@ -288,33 +290,34 @@ router.getAsync('/rss/fetch/:campaignCid', passport.loggedIn, async (req, res) =
router.postAsync('/templates/:templateId/send', async (req, res) => {
const input = {};
- Object.keys(req.body).forEach(key => {
- input[
- (key || '')
- .toString()
- .trim()
- .toUpperCase()
- ] = req.body[key] || '';
- });
- try {
- const templateSender = new TemplateSender({
- context: req.context,
- locale: req.locale,
- templateId: req.params.templateId
- });
- const info = await templateSender.send({
- data: input.DATA,
- email: input.EMAIL,
- sendConfigurationId: input.SEND_CONFIGURATION_ID,
- subject: input.SUBJECT,
- variables: input.VARIABLES,
- attachments: input.ATTACHMENTS || []
- });
- res.status(200).json({ data: info });
- } catch (e) {
- throw new APIError(e.message, 400);
+ for (const key in req.body) {
+ const sanitizedKey = key.toString().trim().toUpperCase();
+ input[sanitizedKey] = req.body[key] || '';
}
+
+ const templateId = castToInteger(req.params.templateId, 'Invalid template ID');
+
+ let sendConfigurationId;
+ if (!('SEND_CONFIGURATION_ID' in input)) {
+ sendConfigurationId = getSystemSendConfigurationId();
+ } else {
+ sendConfigurationId = castToInteger(input.SEND_CONFIGURATION_ID, 'Invalid send configuration ID');
+ }
+
+ if (!input.EMAIL || input.EMAIL === 0) {
+ throw new APIError('Missing email(s)', 400);
+ }
+
+ const emails = input.EMAIL.split(',');
+ const mergeTags = input.TAGS || {};
+ const subject = input.SUBJECT || '';
+ const attachments = input.ATTACHMENTS || [];
+
+
+ const result = await templates.sendAsTransactionalEmail(req.context, templateId, sendConfigurationId, emails, subject, mergeTags, attachments);
+
+ res.json({ data: result });
});
module.exports = router;
diff --git a/server/routes/archive.js b/server/routes/archive.js
index 5e77de78..f975a097 100644
--- a/server/routes/archive.js
+++ b/server/routes/archive.js
@@ -1,13 +1,11 @@
'use strict';
const router = require('../lib/router-async').create();
-const CampaignSender = require('../lib/campaign-sender');
+const messageSender = require('../lib/message-sender');
router.get('/:campaign/:list/:subscription', (req, res, next) => {
- const cs = new CampaignSender();
- cs.init({campaignCid: req.params.campaign})
- .then(() => cs.getMessage(req.params.list, req.params.subscription))
+ messageSender.getMessage(req.params.campaign, req.params.list, req.params.subscription)
.then(result => {
const {html} = result;
diff --git a/server/routes/links.js b/server/routes/links.js
index 3cd43528..897fc2c6 100644
--- a/server/routes/links.js
+++ b/server/routes/links.js
@@ -1,7 +1,7 @@
'use strict';
const log = require('../lib/log');
-const config = require('config');
+const config = require('../lib/config');
const router = require('../lib/router-async').create();
const links = require('../models/links');
const interoperableErrors = require('../../shared/interoperable-errors');
diff --git a/server/routes/rest/campaigns.js b/server/routes/rest/campaigns.js
index 77f5a083..0d1cf657 100644
--- a/server/routes/rest/campaigns.js
+++ b/server/routes/rest/campaigns.js
@@ -74,11 +74,13 @@ router.deleteAsync('/campaigns/:campaignId', passport.loggedIn, passport.csrfPro
});
router.postAsync('/campaign-start/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
- return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), null));
+ return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), {startAt: null}));
});
-router.postAsync('/campaign-start-at/:campaignId/:dateTime', passport.loggedIn, passport.csrfProtection, async (req, res) => {
- return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), new Date(Number.parseInt(req.params.dateTime))));
+router.postAsync('/campaign-start-at/:campaignId', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ const startAt = new Date(req.body.startAt);
+ const timezone = req.body.timezone;
+ return res.json(await campaigns.start(req.context, castToInteger(req.params.campaignId), {startAt, timezone}));
});
@@ -114,6 +116,12 @@ router.postAsync('/campaigns-link-clicks-table/:campaignId', passport.loggedIn,
return res.json(await campaigns.listLinkClicksDTAjax(req.context, castToInteger(req.params.campaignId), req.body));
});
+router.postAsync('/campaign-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
+ const data = req.body;
+ const result = await campaigns.testSend(req.context, data);
+ return res.json(result);
+});
+
module.exports = router;
\ No newline at end of file
diff --git a/server/routes/rest/mosaico-templates.js b/server/routes/rest/mosaico-templates.js
index e55cae54..52bd2357 100644
--- a/server/routes/rest/mosaico-templates.js
+++ b/server/routes/rest/mosaico-templates.js
@@ -33,4 +33,9 @@ router.postAsync('/mosaico-templates-table', passport.loggedIn, async (req, res)
return res.json(await mosaicoTemplates.listDTAjax(req.context, req.body));
});
+router.postAsync('/mosaico-templates-by-tag-language-table/:tagLanguage', passport.loggedIn, async (req, res) => {
+ return res.json(await mosaicoTemplates.listByTagLanguageDTAjax(req.context, req.params.tagLanguage, req.body));
+});
+
+
module.exports = router;
\ No newline at end of file
diff --git a/server/routes/rest/shares.js b/server/routes/rest/shares.js
index d1a2bdf6..24057fe8 100644
--- a/server/routes/rest/shares.js
+++ b/server/routes/rest/shares.js
@@ -35,12 +35,13 @@ router.putAsync('/shares', passport.loggedIn, async (req, res) => {
Accepts format:
{
XXX1: {
- entityTypeId: ...
+ entityTypeId: ...,
requiredOperations: [ ... ]
},
XXX2: {
- entityTypeId: ...
+ entityTypeId: ...,
+ entityId: ...,
requiredOperations: [ ... ]
}
}
diff --git a/server/routes/rest/templates.js b/server/routes/rest/templates.js
index 67f666a6..ee7ec252 100644
--- a/server/routes/rest/templates.js
+++ b/server/routes/rest/templates.js
@@ -5,7 +5,6 @@ const templates = require('../../models/templates');
const router = require('../../lib/router-async').create();
const {castToInteger} = require('../../lib/helpers');
-const CampaignSender = require('../../lib/campaign-sender');
router.getAsync('/templates/:templateId', passport.loggedIn, async (req, res) => {
@@ -39,10 +38,4 @@ router.postAsync('/templates-by-namespace-table/:namespaceId', passport.loggedIn
return res.json(await templates.listByNamespaceDTAjax(req.context, castToInteger(req.params.namespaceId), req.body));
});
-router.postAsync('/template-test-send', passport.loggedIn, passport.csrfProtection, async (req, res) => {
- const data = req.body;
- const result = await CampaignSender.testSend(req.context, data.listCid, data.subscriptionCid, data.campaignId, data.sendConfigurationId, data.html, data.text);
- return res.json(result);
-});
-
module.exports = router;
\ No newline at end of file
diff --git a/server/routes/sandboxed-mosaico.js b/server/routes/sandboxed-mosaico.js
index 001a3129..98f4854b 100644
--- a/server/routes/sandboxed-mosaico.js
+++ b/server/routes/sandboxed-mosaico.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const path = require('path');
const express = require('express');
const routerFactory = require('../lib/router-async');
diff --git a/server/routes/subscription.js b/server/routes/subscription.js
index 258f3164..36b2d1f8 100644
--- a/server/routes/subscription.js
+++ b/server/routes/subscription.js
@@ -1,7 +1,7 @@
'use strict';
const log = require('../lib/log');
-const config = require('config');
+const config = require('../lib/config');
const router = require('../lib/router-async').create();
const confirmations = require('../models/confirmations');
const subscriptions = require('../models/subscriptions');
@@ -249,7 +249,7 @@ router.postAsync('/:cid/subscribe', passport.parseForm, corsOrCsrfProtection, as
const emailErr = await tools.validateEmail(email);
if (emailErr) {
- const errMsg = tools.validateEmailGetMessage(emailErr, email);
+ const errMsg = tools.validateEmailGetMessage(emailErr, email, req.locale);
if (req.xhr) {
throw new Error(errMsg);
@@ -457,7 +457,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
} else {
const emailErr = await tools.validateEmail(emailNew);
if (emailErr) {
- const errMsg = tools.validateEmailGetMessage(emailErr, email);
+ const errMsg = tools.validateEmailGetMessage(emailErr, email, req.locale);
req.flash('danger', errMsg);
diff --git a/server/routes/webhooks.js b/server/routes/webhooks.js
index 0304c832..2dcdbe6f 100644
--- a/server/routes/webhooks.js
+++ b/server/routes/webhooks.js
@@ -5,7 +5,7 @@ const request = require('request-promise');
const campaigns = require('../models/campaigns');
const sendConfigurations = require('../models/send-configurations');
const contextHelpers = require('../lib/context-helpers');
-const {SubscriptionStatus} = require('../../shared/lists');
+const {CampaignMessageStatus} = require('../../shared/campaigns');
const {MailerType} = require('../../shared/send-configurations');
const log = require('../lib/log');
const multer = require('multer');
@@ -44,13 +44,13 @@ router.postAsync('/aws', async (req, res) => {
switch (req.body.Message.notificationType) {
case 'Bounce':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, req.body.Message.bounce.bounceType === 'Permanent');
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, req.body.Message.bounce.bounceType === 'Permanent');
log.verbose('AWS', 'Marked message %s as bounced', req.body.Message.mail.messageId);
break;
case 'Complaint':
if (req.body.Message.complaint) {
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.COMPLAINED, true);
log.verbose('AWS', 'Marked message %s as complaint', req.body.Message.mail.messageId);
}
break;
@@ -93,17 +93,17 @@ router.postAsync('/sparkpost', async (req, res) => {
switch (evt.type) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, [1, 10, 25, 30, 50].indexOf(Number(evt.bounce_class)) >= 0);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, [1, 10, 25, 30, 50].indexOf(Number(evt.bounce_class)) >= 0);
log.verbose('Sparkpost', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'spam_complaint':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.COMPLAINED, true);
log.verbose('Sparkpost', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'link_unsubscribe':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.UNSUBSCRIBED, true);
log.verbose('Sparkpost', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
@@ -134,18 +134,18 @@ router.postAsync('/sendgrid', async (req, res) => {
switch (evt.event) {
case 'bounce':
// https://support.sparkpost.com/customer/portal/articles/1929896
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, true);
log.verbose('Sendgrid', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'spamreport':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.COMPLAINED, true);
log.verbose('Sendgrid', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'group_unsubscribe':
case 'unsubscribe':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.UNSUBSCRIBED, true);
log.verbose('Sendgrid', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
@@ -167,17 +167,17 @@ router.postAsync('/mailgun', uploads.any(), async (req, res) => {
if (message) {
switch (evt.event) {
case 'bounced':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, true);
log.verbose('Mailgun', 'Marked message %s as bounced', evt.campaign_id);
break;
case 'complained':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.COMPLAINED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.COMPLAINED, true);
log.verbose('Mailgun', 'Marked message %s as complaint', evt.campaign_id);
break;
case 'unsubscribed':
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.UNSUBSCRIBED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.UNSUBSCRIBED, true);
log.verbose('Mailgun', 'Marked message %s as unsubscribed', evt.campaign_id);
break;
}
@@ -199,7 +199,7 @@ router.postAsync('/zone-mta', async (req, res) => {
const message = await campaigns.getMessageByResponseId(req.body.id);
if (message) {
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message (campaign:%s, list:%s, subscription:%s) as bounced', message.campaign, message.list, message.subscription);
}
}
@@ -265,7 +265,7 @@ router.postAsync('/postal', async (req, res) => {
if (req.body.payload.message && req.body.payload.message.message_id) {
const message = await campaigns.getMessageByResponseId(req.body.payload.message.message_id);
if (message) {
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, req.body.payload.status === 'HardFail');
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, req.body.payload.status === 'HardFail');
log.verbose('Postal', 'Marked message %s as bounced', req.body.payload.message.message_id);
}
}
@@ -275,7 +275,7 @@ router.postAsync('/postal', async (req, res) => {
if (req.body.payload.original_message && req.body.payload.original_message.message_id) {
const message = await campaigns.getMessageByResponseId(req.body.payload.original_message.message_id);
if (message) {
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, true);
log.verbose('Postal', 'Marked message %s as bounced', req.body.payload.original_message.message_id);
}
}
diff --git a/server/services/executor.js b/server/services/executor.js
index f772d0d5..19b74a7a 100644
--- a/server/services/executor.js
+++ b/server/services/executor.js
@@ -4,7 +4,7 @@
that can chroot.
*/
-const config = require('config');
+const config = require('../lib/config');
const reportHelpers = require('../lib/report-helpers');
const fork = require('../lib/fork').fork;
const path = require('path');
diff --git a/server/services/feedcheck.js b/server/services/feedcheck.js
index 170b65d7..d97284b0 100644
--- a/server/services/feedcheck.js
+++ b/server/services/feedcheck.js
@@ -1,11 +1,10 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const log = require('../lib/log');
const knex = require('../lib/knex');
const feedparser = require('feedparser-promised');
const { CampaignType, CampaignStatus, CampaignSource } = require('../../shared/campaigns');
-const util = require('util');
const campaigns = require('../models/campaigns');
const contextHelpers = require('../lib/context-helpers');
require('../lib/fork');
@@ -112,7 +111,7 @@ async function run() {
from_name_override: rssCampaign.from_name_override,
from_email_override: rssCampaign.from_email_override,
reply_to_override: rssCampaign.reply_to_override,
- subject_override: rssCampaign.subject_override,
+ subject: rssCampaign.subject,
data: campaignData,
click_tracking_disabled: rssCampaign.click_tracking_disabled,
diff --git a/server/services/gdpr-cleanup.js b/server/services/gdpr-cleanup.js
index 9c532ca6..5f9bc68b 100644
--- a/server/services/gdpr-cleanup.js
+++ b/server/services/gdpr-cleanup.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const log = require('../lib/log');
const knex = require('../lib/knex');
const subscriptions = require('../models/subscriptions');
diff --git a/server/services/importer.js b/server/services/importer.js
index 48aa8a8c..049ed8e6 100644
--- a/server/services/importer.js
+++ b/server/services/importer.js
@@ -1,6 +1,6 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const knex = require('../lib/knex');
const path = require('path');
const log = require('../lib/log');
diff --git a/server/services/postfix-bounce-server.js b/server/services/postfix-bounce-server.js
index f2596c66..1123cfe5 100644
--- a/server/services/postfix-bounce-server.js
+++ b/server/services/postfix-bounce-server.js
@@ -1,11 +1,11 @@
'use strict';
const log = require('../lib/log');
-const config = require('config');
+const config = require('../lib/config');
const net = require('net');
const campaigns = require('../models/campaigns');
const contextHelpers = require('../lib/context-helpers');
-const { SubscriptionStatus } = require('../../shared/lists');
+const { CampaignMessageStatus } = require('../../shared/campaigns');
const bluebird = require('bluebird');
const seenIds = new Set();
@@ -66,7 +66,7 @@ async function readNextChunks() {
campaigns.updateMessageResponse(contextHelpers.getAdminContext(), message, queued, queuedAs);
log.verbose('POSTFIXBOUNCE', 'Successfully changed message queueId to %s', queuedAs);
} else {
- campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
+ campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, CampaignMessageStatus.BOUNCED, true);
log.verbose('POSTFIXBOUNCE', 'Marked message %s as bounced', queueId);
}
diff --git a/server/services/sender-master.js b/server/services/sender-master.js
index 1bec702d..db7c8b84 100644
--- a/server/services/sender-master.js
+++ b/server/services/sender-master.js
@@ -1,199 +1,395 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const fork = require('../lib/fork').fork;
const log = require('../lib/log');
const path = require('path');
const knex = require('../lib/knex');
-const {CampaignStatus, CampaignType} = require('../../shared/campaigns');
-const { enforce } = require('../lib/helpers');
+const {CampaignStatus, CampaignType, CampaignMessageStatus} = require('../../shared/campaigns');
const campaigns = require('../models/campaigns');
const builtinZoneMta = require('../lib/builtin-zone-mta');
const {CampaignActivityType} = require('../../shared/activity-log');
const activityLog = require('../lib/activity-log');
+const {MessageType} = require('../lib/message-sender');
require('../lib/fork');
+class Notifications {
+ constructor() {
+ this.conts = new Map();
+ }
+
+ notify(id) {
+ const cont = this.conts.get(id);
+ if (cont) {
+ for (const cb of cont) {
+ setImmediate(cb);
+ }
+ this.conts.delete(id);
+ }
+ }
+
+ async waitFor(id) {
+ let cont = this.conts.get(id);
+ if (!cont) {
+ cont = [];
+ }
+
+ const notified = new Promise(resolve => {
+ cont.push(resolve);
+ });
+
+ this.conts.set(id, cont);
+
+ await notified;
+ }
+}
+
+const notifier = new Notifications();
let messageTid = 0;
const workerProcesses = new Map();
+const workersCount = config.queue.processes;
const idleWorkers = [];
let campaignSchedulerRunning = false;
let queuedSchedulerRunning = false;
-let workerSchedulerRunning = false;
-const campaignsCheckPeriod = 5 * 1000;
+const checkPeriod = 30 * 1000;
const retrieveBatchSize = 1000;
-const workerBatchSize = 100;
+const workerBatchSize = 10;
-const messageQueue = new Map(); // campaignId -> [{listId, email}]
-const messageQueueCont = new Map(); // campaignId -> next batch callback
-const campaignFinishCont = new Map(); // campaignId -> worker finished callback
+const sendConfigurationIdByCampaignId = new Map(); // campaignId -> sendConfigurationId
+const sendConfigurationStatuses = new Map(); // sendConfigurationId -> {retryCount, postponeTill}
-const workAssignment = new Map(); // workerId -> { campaignId, subscribers: [{listId, email}] }
+const sendConfigurationMessageQueue = new Map(); // sendConfigurationId -> [queuedMessage]
+const campaignMessageQueue = new Map(); // campaignId -> [campaignMessage]
-let workerSchedulerCont = null;
-let queuedLastId = 0;
+const workAssignment = new Map(); // workerId -> { type: WorkAssignmentType.CAMPAIGN, campaignId, messages: [campaignMessage] / { type: WorkAssignmentType.QUEUED, sendConfigurationId, messages: [queuedMessage] }
+
+const WorkAssignmentType = {
+ CAMPAIGN: 0,
+ QUEUED: 1
+};
+
+const retryBackoff = [10, 20, 30, 30, 60, 60, 120, 120, 300]; // in seconds
+
+function getSendConfigurationStatus(sendConfigurationId) {
+ let status = sendConfigurationStatuses.get(sendConfigurationId);
+ if (!status) {
+ status = {
+ retryCount: 0,
+ postponeTill: 0
+ };
+
+ sendConfigurationStatuses.set(sendConfigurationId, status);
+ }
+
+ return status;
+}
+
+function setSendConfigurationRetryCount(sendConfigurationStatus, newRetryCount) {
+ sendConfigurationStatus.retryCount = newRetryCount;
+
+ let next = 0;
+ if (newRetryCount > 0) {
+ let backoff;
+ if (newRetryCount > retryBackoff.length) {
+ backoff = retryBackoff[retryBackoff.length - 1];
+ } else {
+ backoff = retryBackoff[newRetryCount - 1];
+ }
+
+ next = Date.now() + backoff * 1000;
+ setTimeout(scheduleCheck, backoff * 1000);
+ }
+
+ sendConfigurationStatus.postponeTill = next;
+}
+
+function isSendConfigurationPostponed(sendConfigurationId) {
+ const now = Date.now();
+ const sendConfigurationStatus = getSendConfigurationStatus(sendConfigurationId);
+ return sendConfigurationStatus.postponeTill > now;
+}
+
+function getPostponedSendConfigurationIds() {
+ const result = [];
+ const now = Date.now();
+
+ for (const entry of sendConfigurationStatuses.entries()) {
+ if (entry[1].postponeTill > now) {
+ result.push(entry[0]);
+ }
+ }
+
+ return result;
+}
-function messagesProcessed(workerId) {
+function getExpirationThresholds() {
+ const now = Date.now();
+
+ return {
+ [MessageType.TRIGGERED]: {
+ threshold: now - config.queue.retention.triggered * 1000,
+ title: 'triggered campaign'
+ },
+ [MessageType.TEST]: {
+ threshold: now - config.queue.retention.test * 1000,
+ title: 'test campaign'
+ },
+ [MessageType.SUBSCRIPTION]: {
+ threshold: now - config.queue.retention.subscription * 1000,
+ title: 'subscription and password-related'
+ },
+ [MessageType.API_TRANSACTIONAL]: {
+ threshold: now - config.queue.retention.apiTransactional * 1000,
+ title: 'transactional (API)'
+ }
+ };
+}
+
+
+function messagesProcessed(workerId, withErrors) {
const wa = workAssignment.get(workerId);
+ const sendConfigurationStatus = getSendConfigurationStatus(wa.sendConfigurationId);
+ if (withErrors) {
+ if (sendConfigurationStatus.retryCount === wa.sendConfigurationRetryCount) { // This is to avoid multiple increments when more workers simultaneously fail to send messages ot the same send configuration
+ setSendConfigurationRetryCount(sendConfigurationStatus, sendConfigurationStatus.retryCount + 1);
+ }
+ } else {
+ setSendConfigurationRetryCount(sendConfigurationStatus, 0);
+ }
+
+
workAssignment.delete(workerId);
idleWorkers.push(workerId);
- if (workerSchedulerCont) {
- const cont = workerSchedulerCont;
- setImmediate(workerSchedulerCont);
- workerSchedulerCont = null;
- }
-
- if (campaignFinishCont.has(wa.campaignId)) {
- setImmediate(campaignFinishCont.get(wa.campaignId));
- campaignFinishCont.delete(wa.campaignId);
- }
+ notifier.notify('workerFinished');
}
-async function scheduleWorkers() {
+async function workersLoop() {
async function getAvailableWorker() {
- if (idleWorkers.length > 0) {
- return idleWorkers.shift();
-
- } else {
- const workerAvailable = new Promise(resolve => {
- workerSchedulerCont = resolve;
- });
-
- await workerAvailable;
- return idleWorkers.shift();
+ while (idleWorkers.length === 0) {
+ await notifier.waitFor('workerFinished');
}
+
+ return idleWorkers.shift();
}
-
- if (workerSchedulerRunning) {
- return;
+ function cancelWorker(workerId) {
+ idleWorkers.push(workerId);
}
- workerSchedulerRunning = true;
- let workerId = await getAvailableWorker();
+ function selectNextTask() {
+ const allocationMap = new Map();
+ const allocation = [];
- let keepLooping = true;
+ function initAllocation(waType, attrName, queues, workerMsg, getSendConfigurationId, getQueueEmptyEvent) {
+ for (const id of queues.keys()) {
+ const sendConfigurationId = getSendConfigurationId(id);
+ const key = attrName + ':' + id;
- while (keepLooping) {
- keepLooping = false;
+ const queue = queues.get(id);
- for (const campaignId of messageQueue.keys()) {
- const queue = messageQueue.get(campaignId);
+ const postponed = isSendConfigurationPostponed(sendConfigurationId);
- if (queue.length > 0) {
- const subscribers = queue.splice(0, workerBatchSize);
- workAssignment.set(workerId, {campaignId, subscribers});
+ const task = {
+ type: waType,
+ id,
+ existingWorkers: 0,
+ isValid: queue.length > 0 && !postponed,
+ queue,
+ workerMsg,
+ attrName,
+ getQueueEmptyEvent,
+ sendConfigurationId
+ };
- if (queue.length === 0 && messageQueueCont.has(campaignId)) {
- setImmediate(messageQueueCont.get(campaignId));
- messageQueueCont.delete(campaignId);
+ allocationMap.set(key, task);
+ allocation.push(task);
+
+ if (postponed && queue.length > 0) {
+ queue.splice(0);
+ notifier.notify(task.getQueueEmptyEvent(task));
}
+ }
- sendToWorker(workerId, 'process-messages', {
- campaignId,
- subscribers
- });
- workerId = await getAvailableWorker();
-
- keepLooping = true;
+ for (const wa of workAssignment.values()) {
+ if (wa.type === waType) {
+ const key = attrName + ':' + wa[attrName];
+ const task = allocationMap.get(key);
+ task.existingWorkers += 1;
+ }
}
}
+
+ initAllocation(
+ WorkAssignmentType.QUEUED,
+ 'sendConfigurationId',
+ sendConfigurationMessageQueue,
+ 'process-queued-messages',
+ id => id,
+ task => `sendConfigurationMessageQueueEmpty:${task.id}`
+ );
+
+ initAllocation(
+ WorkAssignmentType.CAMPAIGN,
+ 'campaignId',
+ campaignMessageQueue,
+ 'process-campaign-messages',
+ id => sendConfigurationIdByCampaignId.get(id),
+ task => `campaignMessageQueueEmpty:${task.id}`
+ );
+
+ let minTask = null;
+ let minExistingWorkers;
+
+ for (const task of allocation) {
+ if (task.isValid && (minTask === null || minExistingWorkers > task.existingWorkers)) {
+ minTask = task;
+ minExistingWorkers = task.existingWorkers;
+ }
+ }
+
+ return minTask;
}
- idleWorkers.push(workerId);
- workerSchedulerRunning = false;
+ while (true) {
+ const workerId = await getAvailableWorker();
+ const task = selectNextTask();
+
+ if (task) {
+ const attrName = task.attrName;
+ const sendConfigurationId = task.sendConfigurationId;
+ const sendConfigurationStatus = getSendConfigurationStatus(sendConfigurationId);
+ const sendConfigurationRetryCount = sendConfigurationStatus.retryCount;
+
+ const queue = task.queue;
+
+ const messages = queue.splice(0, workerBatchSize);
+ workAssignment.set(workerId, {
+ type: task.type,
+ [attrName]: task.id,
+ sendConfigurationId,
+ sendConfigurationRetryCount,
+ messages
+ });
+
+ if (queue.length === 0) {
+ notifier.notify(task.getQueueEmptyEvent(task));
+ }
+
+ sendToWorker(workerId, task.workerMsg, {
+ [attrName]: task.id,
+ messages
+ });
+
+ } else {
+ cancelWorker(workerId);
+ await notifier.waitFor('workAvailable');
+ }
+ }
}
async function processCampaign(campaignId) {
- async function finish() {
+ const msgQueue = campaignMessageQueue.get(campaignId);
+
+ const isCompleted = () => {
+ if (msgQueue.length > 0) return false;
+
let workerRunning = false;
+
for (const wa of workAssignment.values()) {
- if (wa.campaignId === campaignId) {
+ if (wa.type === WorkAssignmentType.CAMPAIGN && wa.campaignId === campaignId) {
workerRunning = true;
}
}
- if (workerRunning) {
- const workerFinished = new Promise(resolve => {
- campaignFinishCont.set(campaignId, resolve);
- });
+ return !workerRunning;
+ };
- await workerFinished;
- setImmediate(finish);
+ async function finish(clearMsgQueue, newStatus) {
+ if (clearMsgQueue) {
+ msgQueue.splice(0);
}
- await knex('campaigns').where('id', campaignId).update({status: CampaignStatus.FINISHED});
- await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: CampaignStatus.FINISHED});
+ while (!isCompleted()) {
+ await notifier.waitFor('workerFinished');
+ }
- messageQueue.delete(campaignId);
+ if (newStatus) {
+ campaignMessageQueue.delete(campaignId);
+
+ await knex('campaigns').where('id', campaignId).update({status: newStatus});
+ await activityLog.logEntityActivity('campaign', CampaignActivityType.STATUS_CHANGE, campaignId, {status: newStatus});
+ }
}
- const msgQueue = [];
- messageQueue.set(campaignId, msgQueue);
try {
+ await campaigns.prepareCampaignMessages(campaignId);
+
while (true) {
const cpg = await knex('campaigns').where('id', campaignId).first();
- if (cpg.status === CampaignStatus.PAUSED) {
- messageQueue.delete(campaignId);
- return;
+ if (cpg.status === CampaignStatus.PAUSING) {
+ return await finish(true, CampaignStatus.PAUSED);
}
- let qryGen;
- await knex.transaction(async tx => {
- qryGen = await campaigns.getSubscribersQueryGeneratorTx(tx, campaignId);
- });
+ const expirationThreshold = Date.now() - config.queue.retention.campaign * 1000;
+ if (cpg.start_at && cpg.start_at.valueOf() < expirationThreshold) {
+ return await finish(true, CampaignStatus.FINISHED);
+ }
- if (qryGen) {
- let subscribersInProcessing = [...msgQueue];
- for (const wa of workAssignment.values()) {
- if (wa.campaignId === campaignId) {
- subscribersInProcessing = subscribersInProcessing.concat(wa.subscribers);
- }
+ sendConfigurationIdByCampaignId.set(cpg.id, cpg.send_configuration);
+
+ if (isSendConfigurationPostponed(cpg.send_configuration)) {
+ // postpone campaign if its send configuration is problematic
+ return await finish(true, CampaignStatus.SCHEDULED);
+ }
+
+ let messagesInProcessing = [...msgQueue];
+ for (const wa of workAssignment.values()) {
+ if (wa.type === WorkAssignmentType.CAMPAIGN && wa.campaignId === campaignId) {
+ messagesInProcessing = messagesInProcessing.concat(wa.messages);
+ }
+ }
+
+ const subs = await knex('campaign_messages')
+ .where({status: CampaignMessageStatus.SCHEDULED})
+ .whereNotIn('hash_email', messagesInProcessing.map(x => x.hash_email))
+ .limit(retrieveBatchSize);
+
+ if (subs.length === 0) {
+ if (isCompleted()) {
+ return await finish(false, CampaignStatus.FINISHED);
+
+ } else {
+ await finish(false);
+
+ // At this point, there might be messages that re-appeared because sending failed.
+ continue;
}
- const qry = qryGen(knex)
- .whereNotIn('pending_subscriptions.email', subscribersInProcessing.map(x => x.email))
- .select(['pending_subscriptions.email', 'campaign_lists.list'])
- .limit(retrieveBatchSize);
- const subs = await qry;
+ }
- if (subs.length === 0) {
- await finish();
- return;
- }
+ for (const sub of subs) {
+ msgQueue.push(sub);
+ }
- for (const sub of subs) {
- msgQueue.push({
- listId: sub.list,
- email: sub.email
- });
- }
+ notifier.notify('workAvailable');
- const nextBatchNeeded = new Promise(resolve => {
- messageQueueCont.set(campaignId, resolve);
- });
-
- setImmediate(scheduleWorkers);
-
- await nextBatchNeeded;
-
- } else {
- await finish();
- return;
+ while (msgQueue.length > 0) {
+ await notifier.waitFor(`campaignMessageQueueEmpty:${campaignId}`);
}
}
} catch (err) {
- log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`)
+ log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`);
log.verbose(err.stack);
}
}
@@ -207,15 +403,45 @@ async function scheduleCampaigns() {
campaignSchedulerRunning = true;
try {
+ // Finish old campaigns
+ const nowDate = new Date();
+ const now = nowDate.valueOf();
+
+ const expirationThreshold = new Date(now - config.queue.retention.campaign * 1000);
+ const expiredCampaigns = await knex('campaigns')
+ .whereIn('campaigns.type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
+ .whereIn('campaigns.status', [CampaignStatus.SCHEDULED, CampaignStatus.PAUSED])
+ .where('campaigns.start_at', '<', expirationThreshold)
+ .update({status: CampaignStatus.FINISHED});
+
+ // Empty message queues for PAUSING campaigns. A pausing campaign typically waits for campaignMessageQueueEmpty before it can check for PAUSING
+ // We speed this up by discarding messages in the message queue of the campaign.
+ const pausingCampaigns = await knex('campaigns')
+ .whereIn('campaigns.type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
+ .where('campaigns.status', CampaignStatus.PAUSING)
+ .select(['id'])
+ .forUpdate();
+
+ for (const cpg of pausingCampaigns) {
+ const campaignId = cpg.id;
+ const queue = campaignMessageQueue.get(campaignId);
+ queue.splice(0);
+ notifier.notify(`campaignMessageQueueEmpty:${campaignId}`);
+ }
+
+
while (true) {
let campaignId = 0;
+ const postponedSendConfigurationIds = getPostponedSendConfigurationIds();
await knex.transaction(async tx => {
const scheduledCampaign = await tx('campaigns')
.whereIn('campaigns.type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
+ .whereNotIn('campaigns.send_configuration', postponedSendConfigurationIds)
.where('campaigns.status', CampaignStatus.SCHEDULED)
- .where(qry => qry.whereNull('campaigns.scheduled').orWhere('campaigns.scheduled', '<=', new Date()))
+ .where('campaigns.start_at', '<=', nowDate)
.select(['id'])
+ .forUpdate()
.first();
if (scheduledCampaign) {
@@ -226,6 +452,8 @@ async function scheduleCampaigns() {
});
if (campaignId) {
+ campaignMessageQueue.set(campaignId, []);
+
// noinspection JSIgnoredPromiseFromCall
processCampaign(campaignId);
@@ -234,16 +462,118 @@ async function scheduleCampaigns() {
}
}
} catch (err) {
- log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`)
+ log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`);
log.verbose(err.stack);
}
-
campaignSchedulerRunning = false;
}
-async function processQueued() {
+async function processQueuedBySendConfiguration(sendConfigurationId) {
+ const msgQueue = sendConfigurationMessageQueue.get(sendConfigurationId);
+
+ const isCompleted = () => {
+ if (msgQueue.length > 0) return false;
+
+ let workerRunning = false;
+
+ for (const wa of workAssignment.values()) {
+ if (wa.type === WorkAssignmentType.QUEUED && wa.sendConfigurationId === sendConfigurationId) {
+ workerRunning = true;
+ }
+ }
+
+ return !workerRunning;
+ };
+
+ async function finish(clearMsgQueue, deleteMsgQueue) {
+ if (clearMsgQueue) {
+ msgQueue.splice(0);
+ }
+
+ while (!isCompleted()) {
+ await notifier.waitFor('workerFinished');
+ }
+
+ if (deleteMsgQueue) {
+ sendConfigurationMessageQueue.delete(sendConfigurationId);
+ }
+ }
+
+
+ try {
+ while (true) {
+ if (isSendConfigurationPostponed(sendConfigurationId)) {
+ return await finish(true, true);
+ }
+
+ let messagesInProcessing = [...msgQueue];
+ for (const wa of workAssignment.values()) {
+ if (wa.type === WorkAssignmentType.QUEUED && wa.sendConfigurationId === sendConfigurationId) {
+ messagesInProcessing = messagesInProcessing.concat(wa.messages);
+ }
+ }
+
+ const messageIdsInProcessing = messagesInProcessing.map(x => x.id);
+
+ const rows = await knex('queued')
+ .orderByRaw(`FIELD(type, ${MessageType.TRIGGERED}, ${MessageType.API_TRANSACTIONAL}, ${MessageType.TEST}, ${MessageType.SUBSCRIPTION}) DESC, id ASC`) // This orders messages in the following order MessageType.SUBSCRIPTION, MessageType.TEST, MessageType.API_TRANSACTIONAL and MessageType.TRIGGERED
+ .where('send_configuration', sendConfigurationId)
+ .whereNotIn('id', messageIdsInProcessing)
+ .limit(retrieveBatchSize);
+
+ if (rows.length === 0) {
+ if (isCompleted()) {
+ return await finish(false, true);
+
+ } else {
+ await finish(false, false);
+
+ // At this point, there might be new messages in the queued that could belong to us. Thus we have to try again instead for returning.
+ continue;
+ }
+ }
+
+ const expirationThresholds = getExpirationThresholds();
+ const expirationCounters = {};
+ for (const type in expirationThresholds) {
+ expirationCounters[type] = 0;
+ }
+
+ for (const row of rows) {
+ const expirationThreshold = expirationThresholds[row.type];
+
+ if (row.created < expirationThreshold.threshold) {
+ expirationCounters[row.type] += 1;
+ await knex('queued').where('id', row.id).del();
+
+ } else {
+ row.data = JSON.parse(row.data);
+ msgQueue.push(row);
+ }
+ }
+
+ for (const type in expirationThresholds) {
+ const expirationThreshold = expirationThresholds[type];
+ if (expirationCounters[type] > 0) {
+ log.warn('Senders', `Discarded ${expirationCounters[type]} expired ${expirationThreshold.title} message(s).`);
+ }
+ }
+
+ notifier.notify('workAvailable');
+
+ while (msgQueue.length > 0) {
+ await notifier.waitFor(`sendConfigurationMessageQueueEmpty:${sendConfigurationId}`);
+ }
+ }
+ } catch (err) {
+ log.error('Senders', `Sending queued messages for send configuration ${sendConfigurationId} failed with error: ${err.message}`);
+ log.verbose(err.stack);
+ }
+}
+
+async function scheduleQueued() {
if (queuedSchedulerRunning) {
return;
}
@@ -251,35 +581,39 @@ async function processQueued() {
queuedSchedulerRunning = true;
try {
- while (true) {
- const rows = await knex('queued')
- .orderBy('id', 'asc')
- .where('id', '>', queuedLastId)
- .limit(retrieveBatchSize);
+ const sendConfigurationsIdsInProcessing = [...sendConfigurationMessageQueue.keys()];
+ const postponedSendConfigurationIds = getPostponedSendConfigurationIds();
- if (rows.length === 0) {
- break;
+ // prune old messages
+ const expirationThresholds = getExpirationThresholds();
+ for (const type in expirationThresholds) {
+ const expirationThreshold = expirationThresholds[type];
+
+ const expiredCount = await knex('queued')
+ .whereNotIn('send_configuration', sendConfigurationsIdsInProcessing)
+ .where('type', type)
+ .where('created', '<', expirationThreshold.threshold)
+ .del();
+
+ if (expiredCount) {
+ log.warn('Senders', `Discarded ${expiredCount} expired ${expirationThreshold.title} message(s).`);
}
+ }
- for (const row of rows) {
- let msgQueue = messageQueue.get(row.campaign);
- if (!msgQueue) {
- msgQueue = [];
- messageQueue.set(row.campaign, msgQueue);
- }
+ const rows = await knex('queued')
+ .whereNotIn('send_configuration', [...sendConfigurationsIdsInProcessing, ...postponedSendConfigurationIds])
+ .groupBy('send_configuration')
+ .select(['send_configuration']);
- msgQueue.push({
- listId: row.list,
- subscriptionId: row.subscription
- });
- }
+ for (const row of rows) {
+ const sendConfigurationId = row.send_configuration;
+ sendConfigurationMessageQueue.set(sendConfigurationId, []);
- queuedLastId = rows[rows.length - 1].id;
-
- setImmediate(scheduleWorkers);
+ // noinspection JSIgnoredPromiseFromCall
+ processQueuedBySendConfiguration(sendConfigurationId);
}
} catch (err) {
- log.error('Senders', `Processing queued messages failed with error: ${err.message}`)
+ log.error('Senders', `Scheduling queued messages failed with error: ${err.message}`);
log.verbose(err.stack);
}
@@ -306,7 +640,7 @@ async function spawnWorker(workerId) {
return resolve();
} else if (msg.type === 'messages-processed') {
- messagesProcessed(workerId);
+ messagesProcessed(workerId, msg.data.withErrors);
}
}
@@ -332,20 +666,26 @@ function sendToWorker(workerId, msgType, data) {
}
-function periodicCampaignsCheck() {
+function scheduleCheck() {
// noinspection JSIgnoredPromiseFromCall
scheduleCampaigns();
// noinspection JSIgnoredPromiseFromCall
- processQueued();
-
- setTimeout(periodicCampaignsCheck, campaignsCheckPeriod);
+ scheduleQueued();
}
+function periodicCheck() {
+ // noinspection JSIgnoredPromiseFromCall
+ scheduleCheck();
+
+ setTimeout(periodicCheck, checkPeriod);
+}
+
+
async function init() {
const spawnWorkerFutures = [];
let workerId;
- for (workerId = 0; workerId < config.queue.processes; workerId++) {
+ for (workerId = 0; workerId < workersCount; workerId++) {
spawnWorkerFutures.push(spawnWorker(workerId));
}
@@ -357,9 +697,18 @@ async function init() {
if (type === 'schedule-check') {
// noinspection JSIgnoredPromiseFromCall
- scheduleCampaigns();
+ scheduleCheck();
} else if (type === 'reload-config') {
+ const sendConfigurationStatus = getSendConfigurationStatus(msg.data.sendConfigurationId);
+ if (sendConfigurationStatus.retryCount > 0) {
+ const sendConfigurationStatus = getSendConfigurationStatus(msg.data.sendConfigurationId)
+ setSendConfigurationRetryCount(sendConfigurationStatus, 0);
+
+ // noinspection JSIgnoredPromiseFromCall
+ scheduleCheck();
+ }
+
for (const workerId of workerProcesses.keys()) {
sendToWorker(workerId, 'reload-config', msg.data);
}
@@ -375,7 +724,9 @@ async function init() {
type: 'master-sender-started'
});
- periodicCampaignsCheck();
+ periodicCheck();
+
+ setImmediate(workersLoop);
}
// noinspection JSIgnoredPromiseFromCall
diff --git a/server/services/sender-worker.js b/server/services/sender-worker.js
index cbe7750f..f0667ee6 100644
--- a/server/services/sender-worker.js
+++ b/server/services/sender-worker.js
@@ -1,16 +1,15 @@
'use strict';
-const config = require('config');
+const config = require('../lib/config');
const log = require('../lib/log');
const mailers = require('../lib/mailers');
-const CampaignSender = require('../lib/campaign-sender');
-const {enforce} = require('../lib/helpers');
+const messageSender = require('../lib/message-sender');
require('../lib/fork');
const workerId = Number.parseInt(process.argv[2]);
let running = false;
-async function processMessages(campaignId, subscribers) {
+async function processCampaignMessages(campaignId, messages) {
if (running) {
log.error('Senders', `Worker ${workerId} assigned work while working`);
return;
@@ -18,36 +17,91 @@ async function processMessages(campaignId, subscribers) {
running = true;
- const cs = new CampaignSender();
- await cs.init({campaignId})
+ const cs = new messageSender.MessageSender();
+ await cs.initByCampaignId(campaignId);
- for (const subData of subscribers) {
+ let withErrors = false;
+
+ for (const campaignMessage of messages) {
try {
- if (subData.email) {
- await cs.sendMessageByEmail(subData.listId, subData.email);
+ await cs.sendRegularCampaignMessage(campaignMessage);
- } else if (subData.subscriptionId) {
- await cs.sendMessageBySubscriptionId(subData.listId, subData.subscriptionId);
+ log.verbose('Senders', 'Message sent and status updated for %s:%s', campaignMessage.list, campaignMessage.subscription);
+ } catch (err) {
+
+ if (err instanceof mailers.SendConfigurationError) {
+ log.error('Senders', `Sending message to ${campaignMessage.list}:${campaignMessage.subscription} failed with error: ${err.message}. Will retry the message if within retention interval.`);
+ withErrors = true;
+ break;
} else {
- enforce(false);
+ log.error('Senders', `Sending message to ${campaignMessage.list}:${campaignMessage.subscription} failed with error: ${err.message}.`);
+ log.verbose(err.stack);
}
-
- log.verbose('Senders', 'Message sent and status updated for %s:%s', subData.listId, subData.email || subData.subscriptionId);
- } catch (err) {
- log.error('Senders', `Sending message to ${subData.listId}:${subData.email} failed with error: ${err.message}`)
- log.verbose(err.stack);
}
}
running = false;
- sendToMaster('messages-processed');
+ sendToMaster('messages-processed', { withErrors });
}
-function sendToMaster(msgType) {
+async function processQueuedMessages(sendConfigurationId, messages) {
+ if (running) {
+ log.error('Senders', `Worker ${workerId} assigned work while working`);
+ return;
+ }
+
+ running = true;
+
+ let withErrors = false;
+
+ for (const queuedMessage of messages) {
+
+ const msgData = queuedMessage.data;
+ let target = '';
+ if (msgData.listId && msgData.subscriptionId) {
+ target = `${msgData.listId}:${msgData.subscriptionId}`;
+ } else if (msgData.to) {
+ if (msgData.to.name && msgData.to.address) {
+ target = `${msgData.to.name} <${msgData.to.address}>`;
+ } else if (msgData.to.address) {
+ target = msgData.to.address;
+ } else {
+ target = msgData.to.toString();
+ }
+ }
+
+ try {
+ await messageSender.sendQueuedMessage(queuedMessage);
+ log.verbose('Senders', `Message sent and status updated for ${target}`);
+ } catch (err) {
+ if (err instanceof mailers.SendConfigurationError) {
+ log.error('Senders', `Sending message to ${target} failed with error: ${err.message}. Will retry the message if within retention interval.`);
+ withErrors = true;
+ break;
+ } else {
+ log.error('Senders', `Sending message to ${target} failed with error: ${err.message}. Dropping the message.`);
+ log.verbose(err.stack);
+
+ try {
+ await messageSender.dropQueuedMessage(queuedMessage);
+ } catch (err) {
+ log.error(err.stack);
+ }
+ }
+ }
+ }
+
+ running = false;
+
+ sendToMaster('messages-processed', { withErrors });
+}
+
+function sendToMaster(msgType, data) {
process.send({
- type: msgType
+ type: msgType,
+ data
});
}
@@ -58,11 +112,14 @@ process.on('message', msg => {
if (type === 'reload-config') {
mailers.invalidateMailer(msg.data.sendConfigurationId);
- } else if (type === 'process-messages') {
+ } else if (type === 'process-campaign-messages') {
// noinspection JSIgnoredPromiseFromCall
- processMessages(msg.data.campaignId, msg.data.subscribers)
- }
+ processCampaignMessages(msg.data.campaignId, msg.data.messages)
+ } else if (type === 'process-queued-messages') {
+ // noinspection JSIgnoredPromiseFromCall
+ processQueuedMessages(msg.data.sendConfigurationId, msg.data.messages)
+ }
}
});
diff --git a/server/services/test-server.js b/server/services/test-server.js
index 37529120..7cc208d6 100644
--- a/server/services/test-server.js
+++ b/server/services/test-server.js
@@ -1,7 +1,7 @@
'use strict';
const log = require('../lib/log');
-const config = require('config');
+const config = require('../lib/config');
const crypto = require('crypto');
const humanize = require('humanize');
const http = require('http');
diff --git a/server/services/triggers.js b/server/services/triggers.js
index a3f11de9..0eb58ee3 100644
--- a/server/services/triggers.js
+++ b/server/services/triggers.js
@@ -10,6 +10,7 @@ const { Entity, Event } = require('../../shared/triggers');
const { SubscriptionStatus } = require('../../shared/lists');
const links = require('../models/links');
const contextHelpers = require('../lib/context-helpers');
+const messageSender = require('../lib/message-sender');
const triggerCheckPeriod = 30 * 1000;
const triggerFirePeriod = 120 * 1000;
@@ -151,12 +152,13 @@ async function run() {
subscription: subscriber.id
});
- await tx('queued').insert({
- campaign: campaign.id,
- list: cpgList.list,
- subscription: subscriber.id,
- trigger: trigger.id
- });
+ await messageSender.queueCampaignMessageTx(tx,
+ campaign.send_configuration, cpgList.list, subscriber.id, messageSender.MessageType.TRIGGERED,
+ {
+ campaignId: campaign.id,
+ triggerId: trigger.id
+ }
+ );
await tx('triggers').increment('count').where('id', trigger.id);
diff --git a/server/services/verp-server.js b/server/services/verp-server.js
index d264795d..847e81b2 100644
--- a/server/services/verp-server.js
+++ b/server/services/verp-server.js
@@ -2,11 +2,11 @@
const { nodeifyFunction, nodeifyPromise } = require('../lib/nodeify');
const log = require('../lib/log');
-const config = require('config');
+const config = require('../lib/config');
const {MailerError} = require('../lib/mailers');
const campaigns = require('../models/campaigns');
const contextHelpers = require('../lib/context-helpers');
-const {SubscriptionStatus} = require('../../shared/lists');
+const {CampaignMessageStatus} = require('../../shared/campaigns');
const bluebird = require('bluebird');
const BounceHandler = require('bounce-handler').BounceHandler;
@@ -56,7 +56,7 @@ function onData(stream, session, callback) {
if (!bounceResult || ['failed', 'transient'].indexOf(bounceResult.action) < 0) {
return 'Message accepted';
} else {
- await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), session.message, SubscriptionStatus.BOUNCED, bounceResult.action === 'failed');
+ await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), session.message, CampaignMessageStatus.BOUNCED, bounceResult.action === 'failed');
log.verbose('VERP', 'Marked message (campaign:%s, list:%s, subscription:%s) as unsubscribed', session.message.campaign, session.message.list, session.message.subscription);
}
};
diff --git a/server/setup/knex/migrations/20170506102634_v1_to_v2.js b/server/setup/knex/migrations/20170506102634_v1_to_v2.js
index 05369236..d64a40c6 100644
--- a/server/setup/knex/migrations/20170506102634_v1_to_v2.js
+++ b/server/setup/knex/migrations/20170506102634_v1_to_v2.js
@@ -2,13 +2,13 @@ const { CampaignSource, CampaignType} = require('../../../../shared/campaigns');
const files = require('../../../models/files');
const contextHelpers = require('../../../lib/context-helpers');
const mosaicoTemplates = require('../../../../shared/mosaico-templates');
+const {TagLanguages} = require('../../../../shared/templates');
const {getGlobalNamespaceId} = require('../../../../shared/namespaces');
const {getAdminId} = require('../../../../shared/users');
const { MailerType, ZoneMTAType, getSystemSendConfigurationId, getSystemSendConfigurationCid } = require('../../../../shared/send-configurations');
-const { enforce } = require('../../../lib/helpers');
+const { enforce, hashEmail} = require('../../../lib/helpers');
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../../shared/triggers');
const { SubscriptionSource } = require('../../../../shared/lists');
-const crypto = require('crypto');
const {DOMParser, XMLSerializer} = require('xmldom');
const log = require('../../../lib/log');
@@ -78,7 +78,7 @@ async function migrateBase(knex) {
// The original Mailtrain migration is executed before this one. So here we check whether the original migration
// ended where it should have and we take it from there.
const row = await knex('settings').where({key: 'db_schema_version'}).first('value');
- if (!row || Number(row.value) !== 33) {
+ if (!row || Number(row.value) !== 34) {
throw new Error('Unsupported DB schema version: ' + row.value);
}
@@ -270,7 +270,7 @@ async function migrateSubscriptions(knex) {
if (rows.length > 0) {
for await (const subscription of rows) {
- subscription.hash_email = crypto.createHash('sha512').update(subscription.email).digest("base64");
+ subscription.hash_email = hashEmail(subscription.email);
subscription.source_email = subscription.imported ? SubscriptionSource.IMPORTED_V1 : SubscriptionSource.NOT_IMPORTED_V1;
for (const field of fields) {
if (field.column != null) {
@@ -416,6 +416,7 @@ async function migrateCustomFields(knex) {
});
await knex.schema.table('custom_fields', table => {
+ table.renameColumn('description', 'help');
table.dropColumn('visible');
});
@@ -753,11 +754,11 @@ async function migrateSettings(knex) {
if (settings.dkimApiKey) {
mailer_type = MailerType.ZONE_MTA;
- mailer_settings.dkimApiKey = settings.dkimApiKey;
+ mailer_settings.dkimApiKey = settings.dkimApiKey || '';
mailer_settings.zoneMtaType = ZoneMTAType.WITH_HTTP_CONF;
- mailer_settings.dkimDomain = settings.dkimDomain;
- mailer_settings.dkimSelector = settings.dkimSelector;
- mailer_settings.dkimPrivateKey = settings.dkimPrivateKey;
+ mailer_settings.dkimDomain = settings.dkimDomain || '';
+ mailer_settings.dkimSelector = settings.dkimSelector || '';
+ mailer_settings.dkimPrivateKey = settings.dkimPrivateKey || '';
}
}
@@ -777,7 +778,7 @@ async function migrateSettings(knex) {
verp_hostname: settings.verpUse ? settings.verpHostname : null,
mailer_type,
mailer_settings: JSON.stringify(mailer_settings),
- x_mailer: settings.x_mailer,
+ x_mailer: settings.x_mailer || '',
namespace: getGlobalNamespaceId()
});
@@ -810,7 +811,7 @@ async function addFiles(knex) {
table.string('mimetype');
table.integer('size');
table.timestamp('created').defaultTo(knex.fn.now());
- table.index(['entity', 'originalname'])
+ table.index(['entity', 'originalname']);
});
}
}
@@ -905,7 +906,7 @@ async function addMosaicoTemplates(knex) {
type: 'html',
namespace: 1,
data: JSON.stringify({
- html: mosaicoTemplates.getVersafix()
+ html: mosaicoTemplates.getVersafix(TagLanguages.SIMPLE)
})
};
diff --git a/server/setup/knex/migrations/20190615000000_generalization_of_queued_and_file_locking.js b/server/setup/knex/migrations/20190615000000_generalization_of_queued_and_file_locking.js
new file mode 100644
index 00000000..e479e653
--- /dev/null
+++ b/server/setup/knex/migrations/20190615000000_generalization_of_queued_and_file_locking.js
@@ -0,0 +1,63 @@
+const entityTypesWithFiles = {
+ campaign: {
+ file: 'files_campaign_file',
+ attachment: 'files_campaign_attachment',
+ },
+ template: {
+ file: 'files_template_file'
+ },
+ mosaico_template: {
+ file: 'files_mosaico_template_file',
+ block: 'files_mosaico_template_block'
+ }
+};
+
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('queued', table => {
+ table.integer('send_configuration').unsigned().notNullable();
+ table.integer('type').unsigned().notNullable(); // The values come from message-sender.js:MessageType
+ table.text('data', 'longtext');
+ });
+
+ const queued = await knex('queued')
+ .leftJoin('campaigns', 'queued.campaign', 'campaigns.id')
+ .select(['queued.id', 'queued.trigger', 'queued.campaign', 'campaigns.send_configuration']);
+
+ for (const queuedEntry of queued) {
+ const data = {};
+
+ if (queued.trigger) {
+ data.triggerId = queuedEntry.trigger;
+ data.campaignId = queuedEntry.campaign;
+ }
+
+ knex('queued')
+ .where('id', queuedEntry.id)
+ .update({
+ send_configuration: queuedEntry.send_configuration,
+ data: JSON.stringify(data)
+ });
+ }
+
+ await knex.schema.table('queued', table => {
+ table.dropColumn('trigger');
+ table.dropColumn('campaign');
+ });
+
+
+ for (const type in entityTypesWithFiles) {
+ const typeEntry = entityTypesWithFiles[type];
+
+ for (const subType in typeEntry) {
+ const subTypeEntry = typeEntry[subType];
+
+ await knex.schema.table(subTypeEntry, table => {
+ table.boolean('delete_pending').notNullable().defaultTo(false);
+ table.integer('lock_count').notNullable().defaultTo(0);
+ });
+ }
+ }
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190616000000_drop_subject_in_send_configurations.js b/server/setup/knex/migrations/20190616000000_drop_subject_in_send_configurations.js
new file mode 100644
index 00000000..862a8bad
--- /dev/null
+++ b/server/setup/knex/migrations/20190616000000_drop_subject_in_send_configurations.js
@@ -0,0 +1,15 @@
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('send_configurations', table => {
+ table.dropColumn('subject');
+ table.dropColumn('subject_overridable');
+ });
+
+ await knex.schema.table('campaigns', table => {
+ table.renameColumn('subject_override', 'subject');
+ });
+
+ await knex('campaigns').whereNull('subject').update('subject', '');
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190629000000_add_start_at_to_campaigns.js b/server/setup/knex/migrations/20190629000000_add_start_at_to_campaigns.js
new file mode 100644
index 00000000..593885b9
--- /dev/null
+++ b/server/setup/knex/migrations/20190629000000_add_start_at_to_campaigns.js
@@ -0,0 +1,26 @@
+const { CampaignType, CampaignStatus } = require('../../../../shared/campaigns');
+
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('campaigns', table => {
+ table.timestamp('start_at').nullable().defaultTo(null);
+ });
+
+ await knex('campaigns')
+ .whereIn('type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
+ .whereIn('status', [CampaignStatus.SCHEDULED, CampaignStatus.SENDING, CampaignStatus.PAUSING, CampaignStatus.PAUSED])
+ .whereNotNull('scheduled')
+ .update({
+ start_at: knex.raw('scheduled')
+ });
+
+ await knex('campaigns')
+ .whereIn('type', [CampaignType.REGULAR, CampaignType.RSS_ENTRY])
+ .whereIn('status', [CampaignStatus.SCHEDULED, CampaignStatus.SENDING, CampaignStatus.PAUSING, CampaignStatus.PAUSED])
+ .whereNull('scheduled')
+ .update({
+ start_at: new Date()
+ });
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190629170000_generalization_of_queued.js b/server/setup/knex/migrations/20190629170000_generalization_of_queued.js
new file mode 100644
index 00000000..86be168c
--- /dev/null
+++ b/server/setup/knex/migrations/20190629170000_generalization_of_queued.js
@@ -0,0 +1,25 @@
+exports.up = (knex, Promise) => (async() => {
+ const queued = await knex('queued');
+
+ for (const queuedEntry of queued) {
+ const data = JSON.parse(queuedEntry.data);
+
+ data.listId = queuedEntry.list;
+ data.subscriptionId = queuedEntry.subscription;
+
+ await knex('queued')
+ .where('id', queuedEntry.id)
+ .update({
+ data: JSON.stringify(data)
+ });
+ }
+
+ await knex.schema.table('queued', table => {
+ table.dropColumn('list');
+ table.dropColumn('subscription');
+ });
+
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190630210000_tag_language.js b/server/setup/knex/migrations/20190630210000_tag_language.js
new file mode 100644
index 00000000..4feddca7
--- /dev/null
+++ b/server/setup/knex/migrations/20190630210000_tag_language.js
@@ -0,0 +1,41 @@
+const { CampaignSource } = require('../../../../shared/campaigns');
+const { TagLanguages } = require('../../../../shared/templates');
+
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.table('templates', table => {
+ table.string('tag_language', 48);
+ });
+
+ await knex('templates').update({
+ tag_language: 'simple'
+ });
+
+ await knex.schema.table('templates', table => {
+ table.string('tag_language', 48).notNullable().index().alter();
+ });
+
+
+ await knex.schema.table('mosaico_templates', table => {
+ table.string('tag_language', 48);
+ });
+
+ await knex('mosaico_templates').update({
+ tag_language: TagLanguages.SIMPLE
+ });
+
+ await knex.schema.table('mosaico_templates', table => {
+ table.string('tag_language', 48).notNullable().index().alter();
+ });
+
+ const rows = await knex('campaigns').whereIn('source', [CampaignSource.CUSTOM, CampaignSource.CUSTOM_FROM_CAMPAIGN, CampaignSource.CUSTOM_FROM_TEMPLATE]);
+ for (const row of rows) {
+ const data = JSON.parse(row.data);
+
+ data.sourceCustom.tag_language = TagLanguages.SIMPLE;
+
+ await knex('campaigns').where('id', row.id).update({data: JSON.stringify(data)});
+ }
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190705220000_test_messages.js b/server/setup/knex/migrations/20190705220000_test_messages.js
new file mode 100644
index 00000000..112b0aa6
--- /dev/null
+++ b/server/setup/knex/migrations/20190705220000_test_messages.js
@@ -0,0 +1,14 @@
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.raw('CREATE TABLE `test_messages` (\n' +
+ ' `id` int(10) unsigned NOT NULL AUTO_INCREMENT,\n' +
+ ' `campaign` int(10) unsigned NOT NULL,\n' +
+ ' `list` int(10) unsigned NOT NULL,\n' +
+ ' `subscription` int(10) unsigned NOT NULL,\n' +
+ ' `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n' +
+ ' PRIMARY KEY (`id`),\n' +
+ ' UNIQUE KEY `cls` (`campaign`, `list`, `subscription`)\n' +
+ ') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;\n');
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190722110000_hash_email.js b/server/setup/knex/migrations/20190722110000_hash_email.js
new file mode 100644
index 00000000..bf022a5b
--- /dev/null
+++ b/server/setup/knex/migrations/20190722110000_hash_email.js
@@ -0,0 +1,17 @@
+exports.up = (knex, Promise) => (async() => {
+ await knex.schema.raw('ALTER TABLE `campaign_messages` ADD `hash_email` char(88) CHARACTER SET ascii');
+ await knex.schema.raw('ALTER TABLE `campaign_messages` ADD UNIQUE KEY `campaign_hash_email` (`campaign`, `hash_email`)');
+ await knex.schema.raw('ALTER TABLE `campaign_messages` DROP KEY `created`');
+ await knex.schema.raw('ALTER TABLE `campaign_links` DROP KEY `created_index`');
+
+ const lists = await knex('lists');
+ for (const list of lists) {
+ await knex.schema.raw('ALTER TABLE `subscription__' + list.id + '` MODIFY `hash_email` char(88) CHARACTER SET ascii');
+ await knex.raw('update `campaign_messages` inner join `subscription__' + list.id + '` on `campaign_messages`.`list`=' + list.id + ' and `campaign_messages`.`subscription`=`subscription__' + list.id + '`.`id` set `campaign_messages`.`hash_email`=`subscription__' + list.id + '`.`hash_email`');
+ }
+
+ await knex('campaign_messages').whereNull('hash_email').del();
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190722150000_ensure_help_column_in_custom_fields.js b/server/setup/knex/migrations/20190722150000_ensure_help_column_in_custom_fields.js
new file mode 100644
index 00000000..f2ccbb2a
--- /dev/null
+++ b/server/setup/knex/migrations/20190722150000_ensure_help_column_in_custom_fields.js
@@ -0,0 +1,15 @@
+exports.up = (knex, Promise) => (async() => {
+ // This is to provide upgrade path to stable to those that already have beta installed.
+ try {
+ await knex.schema.raw('ALTER TABLE `custom_fields` ADD COLUMN `help` text AFTER `name`');
+ } catch (err) {
+ if (err.code === 'ER_DUP_FIELDNAME') {
+ // The field is already there, so we can ignore this error
+ } else {
+ throw err;
+ }
+ }
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js b/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js
new file mode 100644
index 00000000..c1fc42eb
--- /dev/null
+++ b/server/setup/knex/migrations/20190726150000_shorten_field_column_names.js
@@ -0,0 +1,21 @@
+const shortid = require('shortid');
+
+exports.up = (knex, Promise) => (async() => {
+ const fields = await knex('custom_fields').whereNotNull('column');
+
+ for (const field of fields) {
+ const listId = field.list;
+ const oldName = field.column;
+ const newName = ('custom_' + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '_');
+
+ await knex('custom_fields').where('id', field.id).update('column', newName);
+
+ await knex.schema.table('subscription__' + listId, table => {
+ table.renameColumn(oldName, newName);
+ table.renameColumn('source_' + oldName, 'source_' + newName);
+ });
+ }
+})();
+
+exports.down = (knex, Promise) => (async() => {
+})();
diff --git a/server/setup/sql/drop.js b/server/setup/sql/drop.js
index 3e972fb3..3b603cfc 100644
--- a/server/setup/sql/drop.js
+++ b/server/setup/sql/drop.js
@@ -13,8 +13,8 @@ if (process.env.NODE_ENV === 'production') {
process.exit(1);
}
-if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
- log.error('sqldrop', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
+if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.yaml'))) {
+ log.error('sqldrop', 'This script only runs in test if config/test.yaml (i.e. a dedicated test database) is present');
process.exit(1);
}
diff --git a/server/setup/sql/init.js b/server/setup/sql/init.js
index c654b451..2093b401 100644
--- a/server/setup/sql/init.js
+++ b/server/setup/sql/init.js
@@ -12,8 +12,8 @@ if (process.env.NODE_ENV === 'production') {
process.exit(1);
}
-if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
- log.error('sqlinit', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
+if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.yaml'))) {
+ log.error('sqlinit', 'This script only runs in test if config/test.yaml (i.e. a dedicated test database) is present');
process.exit(1);
}
diff --git a/server/setup/sql/upgrade-00034.sql b/server/setup/sql/upgrade-00034.sql
new file mode 100644
index 00000000..41ac21db
--- /dev/null
+++ b/server/setup/sql/upgrade-00034.sql
@@ -0,0 +1,11 @@
+# Header section
+# Define incrementing schema version number
+SET @schema_version = '34';
+
+# Add template field for group elements
+ALTER TABLE `custom_fields` ADD COLUMN `description` text AFTER `name`;
+
+# Footer section
+LOCK TABLES `settings` WRITE;
+INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
+UNLOCK TABLES;
\ No newline at end of file
diff --git a/server/test/e2e/lib/config.js b/server/test/e2e/lib/config.js
index 301e8a24..410ab99f 100644
--- a/server/test/e2e/lib/config.js
+++ b/server/test/e2e/lib/config.js
@@ -1,10 +1,11 @@
'use strict';
-const config = require('server/test/e2e/lib/config');
+const config = require('../../../lib/config');
module.exports = {
app: config,
- baseUrl: 'http://localhost:' + config.www.publicPort,
+ baseTrustedUrl: 'http://localhost:' + config.www.trustedPort,
+ basePublicUrl: 'http://localhost:' + config.www.publicPort,
mailUrl: 'http://localhost:' + config.testServer.mailboxServerPort,
users: {
admin: {
diff --git a/server/test/e2e/lib/exit-unless-test.js b/server/test/e2e/lib/exit-unless-test.js
index 1bd7527e..e829fea5 100644
--- a/server/test/e2e/lib/exit-unless-test.js
+++ b/server/test/e2e/lib/exit-unless-test.js
@@ -5,17 +5,13 @@ const log = require('npmlog');
const path = require('path');
const fs = require('fs');
-if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..', '..', '..', 'config', 'test.toml'))) {
- log.error('e2e', 'This script only runs in test and config/test.toml (i.e. a dedicated test database) is present');
+if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..', '..', '..', 'config', 'test.yaml'))) {
+ log.error('e2e', 'This script only runs in test and config/test.yaml (i.e. a dedicated test database) is present');
process.exit(1);
}
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.yaml');
process.exit(1);
}
-if (config.app.www.port !== 3000) {
- log.error('e2e', 'This script requires Mailtrain to be running on port 3000. Check config/test.toml');
- process.exit(1);
-}
diff --git a/server/test/e2e/lib/page.js b/server/test/e2e/lib/page.js
index 34755cef..ccd2ff0b 100644
--- a/server/test/e2e/lib/page.js
+++ b/server/test/e2e/lib/page.js
@@ -8,7 +8,7 @@ const driver = require('./mocha-e2e').driver;
const url = require('url');
const UrlPattern = require('url-pattern');
-const waitTimeout = 10000;
+const waitTimeout = 20000;
module.exports = (...extras) => Object.assign({
elements: {},
@@ -21,7 +21,8 @@ module.exports = (...extras) => Object.assign({
const elem = await driver.findElement(By.css(this.elements[key]));
const linkUrl = await elem.getAttribute('href');
- const linkPath = url.parse(linkUrl).path;
+ const parsedUrl = url.parse(linkUrl);
+ const linkPath = parsedUrl.pathname;
const urlPattern = new UrlPattern(this.links[key]);
diff --git a/server/test/e2e/lib/web.js b/server/test/e2e/lib/web.js
index 9e345219..893f5480 100644
--- a/server/test/e2e/lib/web.js
+++ b/server/test/e2e/lib/web.js
@@ -23,7 +23,7 @@ module.exports = (...extras) => page({
if (parsedUrl.host) {
absolutePath = path;
} else {
- absolutePath = config.baseUrl + path;
+ absolutePath = this.baseUrl + path;
}
await driver.navigate().to(absolutePath);
@@ -37,8 +37,8 @@ module.exports = (...extras) => page({
const currentUrl = url.parse(await driver.getCurrentUrl());
const urlPattern = new UrlPattern(desiredUrl);
const params = urlPattern.match(currentUrl.pathname);
- if (!params || config.baseUrl !== `${currentUrl.protocol}//${currentUrl.host}`) {
- throw new Error(`Unexpected URL. Expecting ${config.baseUrl}${this.url} got ${currentUrl.protocol}//${currentUrl.host}/${currentUrl.pathname}`);
+ if (!params || this.baseUrl !== `${currentUrl.protocol}//${currentUrl.host}`) {
+ throw new Error(`Unexpected URL. Expecting ${this.baseUrl}${this.url} got ${currentUrl.protocol}//${currentUrl.host}/${currentUrl.pathname}`);
}
this.params = params;
diff --git a/server/test/e2e/page-objects/home.js b/server/test/e2e/page-objects/home.js
index 3e33af24..025f3bd0 100644
--- a/server/test/e2e/page-objects/home.js
+++ b/server/test/e2e/page-objects/home.js
@@ -1,7 +1,9 @@
'use strict';
+const config = require('../lib/config');
const web = require('../lib/web');
module.exports = web({
+ baseUrl: config.baseTrustedUrl,
url: '/'
});
diff --git a/server/test/e2e/page-objects/subscription.js b/server/test/e2e/page-objects/subscription.js
index c97b3cbd..8f8ca047 100644
--- a/server/test/e2e/page-objects/subscription.js
+++ b/server/test/e2e/page-objects/subscription.js
@@ -50,6 +50,7 @@ const fieldHelpers = list => ({
module.exports = list => ({
webSubscribe: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}`,
elementsToWaitFor: ['form'],
textsToWaitFor: ['Subscribe to list'],
@@ -63,6 +64,7 @@ module.exports = list => ({
}, fieldHelpers(list)),
webSubscribeAfterPost: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/subscribe`,
elementsToWaitFor: ['form'],
textsToWaitFor: ['Subscribe to list'],
@@ -76,11 +78,13 @@ module.exports = list => ({
}, fieldHelpers(list)),
webSubscribeNonPublic: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}`,
textsToWaitFor: ['Permission denied'],
}),
webConfirmSubscriptionNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/confirm-subscription-notice`,
textsToWaitFor: ['We need to confirm your email address']
}),
@@ -107,6 +111,7 @@ module.exports = list => ({
}),
webSubscribedNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/subscribed-notice`,
textsToWaitFor: ['Subscription Confirmed']
}),
@@ -125,6 +130,7 @@ module.exports = list => ({
}),
webManage: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/manage/:ucid`,
elementsToWaitFor: ['form'],
textsToWaitFor: ['Update Your Preferences'],
@@ -142,6 +148,7 @@ module.exports = list => ({
}, fieldHelpers(list)),
webManageAddress: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/manage-address/:ucid`,
elementsToWaitFor: ['form'],
textsToWaitFor: ['Update Your Email Address'],
@@ -162,11 +169,13 @@ module.exports = list => ({
}),
webUpdatedNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/updated-notice`,
textsToWaitFor: ['Profile Updated']
}),
webUnsubscribedNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/unsubscribed-notice`,
textsToWaitFor: ['Unsubscribe Successful']
}),
@@ -180,6 +189,7 @@ module.exports = list => ({
}),
webUnsubscribe: web({
+ baseUrl: config.basePublicUrl,
elementsToWaitFor: ['submitButton'],
textsToWaitFor: ['Unsubscribe'],
elements: {
@@ -188,6 +198,7 @@ module.exports = list => ({
}),
webConfirmUnsubscriptionNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/confirm-unsubscription-notice`,
textsToWaitFor: ['We need to confirm your email address']
}),
@@ -201,6 +212,7 @@ module.exports = list => ({
}),
webManualUnsubscribeNotice: web({
+ baseUrl: config.basePublicUrl,
url: `/subscription/${list.cid}/manual-unsubscribe-notice`,
elementsToWaitFor: ['contactLink'],
textsToWaitFor: ['Online Unsubscription Is Not Possible', config.settings['admin-email']],
diff --git a/server/test/e2e/page-objects/user.js b/server/test/e2e/page-objects/user.js
index e432a22b..f9def3be 100644
--- a/server/test/e2e/page-objects/user.js
+++ b/server/test/e2e/page-objects/user.js
@@ -1,9 +1,11 @@
'use strict';
+const config = require('../lib/config');
const web = require('../lib/web');
module.exports = {
login: web({
+ baseUrl: config.baseTrustedUrl,
url: '/users/login',
elementsToWaitFor: ['submitButton'],
elements: {
@@ -14,11 +16,13 @@ module.exports = {
}),
logout: web({
+ baseUrl: config.baseTrustedUrl,
requestUrl: '/users/logout',
url: '/'
}),
account: web({
+ baseUrl: config.baseTrustedUrl,
url: '/users/account',
elementsToWaitFor: ['form'],
elements: {
diff --git a/server/test/e2e/tests/login.js b/server/test/e2e/tests/login.js
index 56692592..de2c625f 100644
--- a/server/test/e2e/tests/login.js
+++ b/server/test/e2e/tests/login.js
@@ -17,7 +17,7 @@ suite('Login use-cases', () => {
});
test('Anonymous user cannot access restricted content', async () => {
- await driver.navigate().to(config.baseUrl + '/settings');
+ await driver.navigate().to(config.baseTrustedUrl + '/settings');
await page.login.waitUntilVisible();
await page.login.waitForFlash();
expect(await page.login.getFlash()).to.contain('Need to be logged in to access restricted content');
diff --git a/server/test/e2e/tests/subscription.js b/server/test/e2e/tests/subscription.js
index 0b6737a7..62509d07 100644
--- a/server/test/e2e/tests/subscription.js
+++ b/server/test/e2e/tests/subscription.js
@@ -136,7 +136,7 @@ suite('Subscription use-cases', () => {
});
await step('User submits an invalid email.', async () => {
- await page.webSubscribe.setValue('emailInput', 'foo@bar.nope');
+ await page.webSubscribe.setValue('emailInput', 'foo-bar');
await page.webSubscribe.submit();
});
@@ -545,7 +545,7 @@ suite('Subscription use-cases', () => {
async function apiSubscribe(listConf, subscription) {
await step('Add subscription via API call.', async () => {
const response = await request({
- uri: `${config.baseUrl}/api/subscribe/${listConf.cid}?access_token=${config.users.admin.accessToken}`,
+ uri: `${config.baseTrustedUrl}/api/subscribe/${listConf.cid}?access_token=${config.users.admin.accessToken}`,
method: 'POST',
json: subscription
});
@@ -630,7 +630,7 @@ suite('API Subscription use-cases', () => {
await step('Unsubsribe via API call.', async () => {
const response = await request({
- uri: `${config.baseUrl}/api/unsubscribe/${config.lists.l1.cid}?access_token=${config.users.admin.accessToken}`,
+ uri: `${config.baseTrustedUrl}/api/unsubscribe/${config.lists.l1.cid}?access_token=${config.users.admin.accessToken}`,
method: 'POST',
json: {
EMAIL: subscription.EMAIL
diff --git a/server/views/subscription/layout.mjml.hbs b/server/views/subscription/layout.mjml.hbs
index c5bb83b4..04f20e13 100644
--- a/server/views/subscription/layout.mjml.hbs
+++ b/server/views/subscription/layout.mjml.hbs
@@ -13,50 +13,47 @@
-
-
+
+
+ {{#if isWeb}}
+
+ {{/if}}
+
-
- {{#if isWeb}}
-
- {{/if}}
-
+
+
+
+ {{title}}
+
+
+
+
-
-
-
- {{title}}
-
-
-
-
+
+ {{#if isWeb}}
+ {{> subscription_flash_messages}}
+ {{/if}}
+
-
- {{#if isWeb}}
- {{> subscription_flash_messages}}
- {{/if}}
-
+ {{{body}}}
- {{{body}}}
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+ {{#if isWeb}}
+
+ {{> subscription_footer_scripts btnBgColor='#2D3E4F' btnBgColorHover='#1A242F'}}
+ {{/if}}
+
-
- {{#if isWeb}}
-
- {{> subscription_footer_scripts btnBgColor='#2D3E4F' btnBgColorHover='#1A242F'}}
- {{/if}}
-
-
-
diff --git a/server/views/subscription/partials/subscription-custom-fields.hbs b/server/views/subscription/partials/subscription-custom-fields.hbs
index ee4a843d..2a8b7083 100644
--- a/server/views/subscription/partials/subscription-custom-fields.hbs
+++ b/server/views/subscription/partials/subscription-custom-fields.hbs
@@ -11,6 +11,7 @@
{{else}}
{{/if}}
+ {{help}}
{{/if}}
@@ -18,6 +19,7 @@
+ {{help}}
{{/if}}
@@ -25,6 +27,7 @@
+ {{help}}
{{/if}}
@@ -32,6 +35,7 @@
+ {{help}}
{{/if}}
@@ -39,6 +43,7 @@
+ {{help}}
{{/if}}
@@ -46,6 +51,7 @@
+ {{help}}
{{/if}}
@@ -56,6 +62,7 @@
{{field.settings.checkedLabel}}
+ {{help}}
{{/if}}
@@ -66,9 +73,8 @@
{{/if}}
-
- {{#translate}}insertYourGpgPublicKeyHereToEncrypt{{/translate}}
-
+ {{#translate}}insertYourGpgPublicKeyHereToEncrypt{{/translate}}
+ {{help}}
{{/if}}
@@ -76,6 +82,7 @@
+ {{help}}
{{/if}}
@@ -83,6 +90,7 @@
+ {{help}}
{{/if}}
@@ -90,6 +98,7 @@
+ {{help}}
{{/if}}
@@ -97,6 +106,7 @@
+ {{help}}
{{/if}}
@@ -111,6 +121,7 @@
{{/each}}
+ {{help}}
{{/if}}
@@ -122,6 +133,7 @@
{{name}}
{{/each}}
+ {{help}}
{{/if}}
@@ -133,6 +145,7 @@
+ {{help}}
{{/each}}
{{/if}}
@@ -148,6 +161,7 @@
{{/each}}
+ {{help}}
{{/if}}
@@ -159,6 +173,7 @@
{{name}}
{{/each}}
+ {{help}}
{{/if}}
diff --git a/setup/delete-modules.sh b/setup/delete-modules.sh
new file mode 100644
index 00000000..b1519e15
--- /dev/null
+++ b/setup/delete-modules.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+set -e
+
+SCRIPT_PATH=$(dirname $(realpath -s $0))
+. $SCRIPT_PATH/functions
+cd $SCRIPT_PATH/..
+
+deleteAllModules
\ No newline at end of file
diff --git a/setup/functions b/setup/functions
index b84488cb..3752bd1a 100644
--- a/setup/functions
+++ b/setup/functions
@@ -234,7 +234,6 @@ function reinstallAllModules {
doForAllModules reinstallModules
}
-
function installHttpd {
local portTrusted="$1"
local portSandbox="$2"
@@ -443,3 +442,59 @@ EOT
systemctl daemon-reload
}
+
+
+
+function deleteModules {
+ local idx=$1
+ echo Deleting modules in $idx
+ cd $idx && rm -rf node_modules
+}
+
+function deleteAllModules {
+ doForAllModules deleteModules
+}
+
+
+function setupTest {
+ mysqlPassword=`pwgen 12 -1`
+
+ # Setup MySQL user for Mailtrain
+ mysql -u root -e "CREATE USER 'mailtrain_test'@'localhost' IDENTIFIED BY '$mysqlPassword';"
+ mysql -u root -e "GRANT ALL PRIVILEGES ON mailtrain_test.* TO 'mailtrain_test'@'localhost';"
+ mysql -u mailtrain_test --password="$mysqlPassword" -e "CREATE database mailtrain_test;"
+
+ # Setup installation configuration
+ cat > server/config/test.yaml <> server/services/workers/reports/config/test.yaml <\n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' TITLE\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- '\n' +
- '\n' +
- '\n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Unsubscribe | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- '\n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Title | \n' +
- ' \n' +
- ' Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' Section Title | \n' +
- ' \n' +
- ' Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Section Title | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' Far far away, behind the word mountains, far from the countries Vokalia and Consonantia, there live the blind texts. \n' +
- ' Separated they live in Bookmarksgrove right at the coast of the Semantics, a large language ocean. A small river named Duden flows by their place and supplies it with the necessary regelialia. | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '  | \n' +
- '  | \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- '\n' +
- ' \n' +
- '\n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' | \n' +
- ' \n' +
- ' \n' +
- ' \n' +
- ' |