Configuration split to lists, send configurations and server config.
This is before testing.
This commit is contained in:
parent
4fce4b6f81
commit
c12efeb97f
40 changed files with 819 additions and 311 deletions
|
@ -50,8 +50,9 @@ const mosaicoTemplatesRest = require('./routes/rest/mosaico-templates');
|
|||
const blacklistRest = require('./routes/rest/blacklist');
|
||||
const editorsRest = require('./routes/rest/editors');
|
||||
const filesRest = require('./routes/rest/files');
|
||||
const settingsRest = require('./routes/rest/settings');
|
||||
|
||||
const root = require('./routes/root');
|
||||
const index = require('./routes/index');
|
||||
|
||||
const interoperableErrors = require('./shared/interoperable-errors');
|
||||
|
||||
|
@ -259,7 +260,7 @@ function createApp(trusted) {
|
|||
// Regular endpoints
|
||||
useWith404Fallback('/subscription', subscription);
|
||||
useWith404Fallback('/files', files);
|
||||
useWith404Fallback('/mosaico', mosaico);
|
||||
useWith404Fallback('/mosaico', mosaico.getRouter(trusted));
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
useWith404Fallback('/reports', reports);
|
||||
|
@ -286,6 +287,7 @@ function createApp(trusted) {
|
|||
app.use('/rest', blacklistRest);
|
||||
app.use('/rest', editorsRest);
|
||||
app.use('/rest', filesRest);
|
||||
app.use('/rest', settingsRest);
|
||||
|
||||
if (config.reports && config.reports.enabled === true) {
|
||||
app.use('/rest', reportTemplatesRest);
|
||||
|
@ -293,7 +295,7 @@ function createApp(trusted) {
|
|||
}
|
||||
install404Fallback('/rest');
|
||||
|
||||
app.use('/', root);
|
||||
app.use('/', index.getRouter(trusted));
|
||||
|
||||
// Error handlers
|
||||
if (app.get('env') === 'development') {
|
||||
|
|
|
@ -7,6 +7,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
|||
import URL from 'url-parse';
|
||||
import axios from '../lib/axios';
|
||||
import { Button } from '../lib/bootstrap-components';
|
||||
import { getUrl } from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withPageHelpers
|
||||
|
@ -43,8 +44,6 @@ export default class API extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
const thisUrl = new URL();
|
||||
const serviceUrl = thisUrl.origin + '/'; // FIXME - use urls.js and getTrustedUrl
|
||||
const accessToken = this.state.accessToken || 'ACCESS_TOKEN';
|
||||
|
||||
let accessTokenMsg;
|
||||
|
@ -123,7 +122,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/subscribe/B16uVTdW?access_token={accessToken} \
|
||||
<pre>curl -XPOST '{getUrl(`api/subscribe/B16uVTdW?access_token=${accessToken}`)}' \
|
||||
--data 'EMAIL=test@example.com&MERGE_CHECKBOX=yes&REQUIRE_CONFIRMATION=yes'</pre>
|
||||
|
||||
<h3>POST /api/unsubscribe/:listId – {t('Remove subscription')}</h3>
|
||||
|
@ -150,7 +149,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/unsubscribe/B16uVTdW?access_token={accessToken} \
|
||||
<pre>curl -XPOST '{getUrl(`api/unsubscribe/B16uVTdW?access_token=${accessToken}`)}' \
|
||||
--data 'EMAIL=test@example.com'</pre>
|
||||
|
||||
<h3>POST /api/delete/:listId – {t('Delete subscription')}</h3>
|
||||
|
@ -177,7 +176,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/delete/B16uVTdW?access_token={accessToken} \
|
||||
<pre>curl -XPOST '{getUrl(`api/delete/B16uVTdW?access_token=${accessToken}`)}' \
|
||||
--data 'EMAIL=test@example.com'</pre>
|
||||
|
||||
<h3>POST /api/field/:listId – {t('Add new custom field')}</h3>
|
||||
|
@ -225,7 +224,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {serviceUrl}api/field/B16uVTdW?access_token={accessToken} \
|
||||
<pre>curl -XPOST '{getUrl(`api/field/B16uVTdW?access_token=${accessToken}`)}' \
|
||||
--data 'NAME=Birthday&TYPE=birthday-us&VISIBLE=yes'</pre>
|
||||
|
||||
<h3>GET /api/blacklist/get – {t('Get list of blacklisted emails')}</h3>
|
||||
|
@ -251,7 +250,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XGET '{serviceUrl}api/blacklist/get?access_token={accessToken}&limit=10&start=10&search=gmail' </pre>
|
||||
<pre>curl -XGET '{getUrl(`api/blacklist/get?access_token=${accessToken}&limit=10&start=10&search=gmail`)}' </pre>
|
||||
|
||||
<h3>POST /api/blacklist/add – {t('Add email to blacklist')}</h3>
|
||||
|
||||
|
@ -277,7 +276,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST '{serviceUrl}api/blacklist/add?access_token={accessToken}' \
|
||||
<pre>curl -XPOST '{getUrl(`api/blacklist/add?access_token={accessToken}`)}' \
|
||||
--data 'EMAIL=test@example.com&'</pre>
|
||||
|
||||
<h3>POST /api/blacklist/delete – {t('Delete email from blacklist')}</h3>
|
||||
|
@ -304,7 +303,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST '{serviceUrl}api/blacklist/delete?access_token={accessToken}' \
|
||||
<pre>curl -XPOST '{getUrl(`api/blacklist/delete?access_token=${accessToken}`)}' \
|
||||
--data 'EMAIL=test@example.com&'</pre>
|
||||
|
||||
<h3>GET /api/lists/:email – {t('Get the lists a user has subscribed to')}</h3>
|
||||
|
@ -324,7 +323,7 @@ export default class API extends Component {
|
|||
<strong>{t('Example')}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XGET '{{serviceUrl}}api/lists/test@example.com?access_token={{accessToken}} </pre>
|
||||
<pre>curl -XGET '{getUrl(`api/lists/test@example.com?access_token=${accessToken}`)}'</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { withErrorHandling } from '../lib/error-handling';
|
|||
import qs from 'querystringify';
|
||||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getUrl} from "../lib/urls";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -61,7 +62,7 @@ export default class Login extends Component {
|
|||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.POST, '/rest/login');
|
||||
|
||||
if (submitSuccessful) {
|
||||
const nextUrl = qs.parse(this.props.location.search).next || mailtrainConfig.urlBase;
|
||||
const nextUrl = qs.parse(this.props.location.search).next || getUrl();
|
||||
|
||||
/* This ensures we get config for the authenticated user */
|
||||
window.location = nextUrl;
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import i18n from '../lib/i18n';
|
||||
|
||||
import { Section } from '../lib/page';
|
||||
import Account from './Account';
|
||||
import Login from './Login';
|
||||
import Reset from './Forgot';
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import {I18nextProvider} from "react-i18next";
|
||||
import i18n from "../lib/i18n";
|
||||
|
||||
import {Section} from "../lib/page";
|
||||
import List from "./List";
|
||||
|
||||
function getMenus(t) {
|
||||
|
|
|
@ -11,6 +11,7 @@ import {Button, DismissibleAlert} from "./bootstrap-components";
|
|||
import mailtrainConfig from "mailtrainConfig";
|
||||
import styles from "./styles.scss";
|
||||
import {getRoutes, needsResolve, resolve, withPageHelpers} from "./page-common";
|
||||
import {getBaseDir} from "./urls";
|
||||
|
||||
class Breadcrumb extends Component {
|
||||
static propTypes = {
|
||||
|
@ -382,7 +383,7 @@ class Section extends Component {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Router basename={mailtrainConfig.urlBase}>
|
||||
<Router basename={getBaseDir()}>
|
||||
<SectionContent root={this.props.root} structure={this.structure} />
|
||||
</Router>
|
||||
);
|
||||
|
|
|
@ -63,7 +63,7 @@ export class UntrustedContentHost extends Component {
|
|||
|
||||
sendMessage(type, data) {
|
||||
if (this.contentNodeIsLoaded) { // This is to avoid errors "common.js:45744 Failed to execute 'postMessage' on 'DOMWindow': The target origin provided ('http://localhost:8081') does not match the recipient window's origin ('http://localhost:3000')"
|
||||
this.contentNode.contentWindow.postMessage({type, data}, getSandboxUrl(''));
|
||||
this.contentNode.contentWindow.postMessage({type, data}, getSandboxUrl());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -197,7 +197,7 @@ export class UntrustedContentRoot extends Component {
|
|||
}
|
||||
|
||||
sendMessage(type, data) {
|
||||
window.parent.postMessage({type, data}, getTrustedUrl(''));
|
||||
window.parent.postMessage({type, data}, getTrustedUrl());
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
|
|
@ -2,36 +2,33 @@
|
|||
|
||||
import mailtrainConfig from "mailtrainConfig";
|
||||
|
||||
let urlBase;
|
||||
let sandboxUrlBase;
|
||||
|
||||
if (mailtrainConfig.urlBase.startsWith('/')) {
|
||||
urlBase = window.location.protocol + '//' + window.location.hostname + ':' + mailtrainConfig.port + mailtrainConfig.urlBase;
|
||||
} else {
|
||||
urlBase = mailtrainConfig.urlBase
|
||||
}
|
||||
|
||||
if (mailtrainConfig.sandboxUrlBase) {
|
||||
if (mailtrainConfig.urlBase.startsWith('/')) {
|
||||
sandboxUrlBase = window.location.protocol + '//' + window.location.hostname + ':' + mailtrainConfig.sandboxPort + mailtrainConfig.sandboxUrlBase;
|
||||
} else {
|
||||
sandboxUrlBase = mailtrainConfig.sandboxUrlBase
|
||||
}
|
||||
} else {
|
||||
const loc = document.createElement("a");
|
||||
loc.href = urlBase;
|
||||
sandboxUrlBase = loc.protocol + '//' + loc.hostname + ':' + mailtrainConfig.sandboxPort + loc.pathname;
|
||||
}
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return urlBase + path;
|
||||
return mailtrainConfig.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return sandboxUrlBase + path;
|
||||
return mailtrainConfig.sandboxUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getUrl(path) {
|
||||
if (mailtrainConfig.trusted) {
|
||||
return getTrustedUrl(path);
|
||||
} else {
|
||||
return getSandboxUrl(path);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseDir() {
|
||||
if (mailtrainConfig.trusted) {
|
||||
return mailtrainConfig.trustedUrlBaseDir;
|
||||
} else {
|
||||
return mailtrainConfig.sandboxUrlBaseDir;
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl
|
||||
getSandboxUrl,
|
||||
getUrl,
|
||||
getBaseDir
|
||||
}
|
|
@ -46,6 +46,8 @@ export default class CUD extends Component {
|
|||
form: 'default',
|
||||
default_form: null,
|
||||
public_subscribe: true,
|
||||
contact_email: '',
|
||||
homepage: '',
|
||||
unsubscription_mode: UnsubscriptionMode.ONE_STEP,
|
||||
namespace: mailtrainConfig.user.namespace
|
||||
});
|
||||
|
@ -170,7 +172,10 @@ export default class CUD extends Component {
|
|||
</StaticField>
|
||||
}
|
||||
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
<InputField id="contact_email" label={t('Contact email')} help={t('Contact email used in subscription forms and emails that are sent out. If not filled in, the admin email from the global settings will be used.')}/>
|
||||
<InputField id="homepage" label={t('Homepage')} help={t('Homepage URL used in subscription forms and emails that are sent out. If not filled in, the default homepage from global settings will be used.')}/>
|
||||
|
||||
<NamespaceSelect/>
|
||||
|
||||
|
|
|
@ -386,7 +386,7 @@ export default class CUD extends Component {
|
|||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
<NamespaceSelect/>
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling'
|
|||
import interoperableErrors from '../../../shared/interoperable-errors';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
import {getGlobalNamespaceId} from "../../../shared/namespaces";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -32,7 +33,7 @@ export default class CUD extends Component {
|
|||
}
|
||||
|
||||
isEditGlobal() {
|
||||
return this.props.entity && this.props.entity.id === 1; /* Global namespace id */
|
||||
return this.props.entity && this.props.entity.id === getGlobalNamespaceId();
|
||||
}
|
||||
|
||||
removeNsIdSubtree(data) {
|
||||
|
|
|
@ -229,7 +229,7 @@ export default class CUD extends Component {
|
|||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
<TableSelect id="report_template" label={t('Report Template')} withHeader dropdown dataUrl="/rest/report-templates-table" columns={reportTemplateColumns} selectionLabelIndex={1}/>
|
||||
|
||||
|
|
|
@ -293,7 +293,7 @@ export default class CUD extends Component {
|
|||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
<Dropdown id="mime_type" label={t('Type')} options={[{key: 'text/html', label: t('HTML')}, {key: 'text/csv', label: t('CSV')}]}/>
|
||||
<NamespaceSelect/>
|
||||
<ACEEditor id="user_fields" height="250px" mode="json" label={t('User selectable fields')} help={t('JSON specification of user selectable fields.')}/>
|
||||
|
|
|
@ -16,6 +16,7 @@ import reports from './reports/root';
|
|||
import templates from './templates/root';
|
||||
import users from './users/root';
|
||||
import sendConfigurations from './send-configurations/root';
|
||||
import settings from './settings/root';
|
||||
|
||||
import {
|
||||
MenuLink,
|
||||
|
@ -31,6 +32,7 @@ import {
|
|||
} from "./lib/bootstrap-components";
|
||||
import {Link} from "react-router-dom";
|
||||
import axios from './lib/axios';
|
||||
import {getUrl} from "./lib/urls";
|
||||
|
||||
|
||||
@translate()
|
||||
|
@ -79,7 +81,7 @@ class Root extends Component {
|
|||
<DropdownMenuItem label={t('Administration')}>
|
||||
<MenuLink to="/users"><Icon icon='cog'/> {t('Users')}</MenuLink>
|
||||
<MenuLink to="/namespaces"><Icon icon='cog'/> {t('Namespaces')}</MenuLink>
|
||||
<MenuLink to="/settings"><Icon icon='cog'/> {t('Settings')}</MenuLink>
|
||||
<MenuLink to="/settings"><Icon icon='cog'/> {t('Global Settings')}</MenuLink>
|
||||
<MenuLink to="/send-configurations"><Icon icon='cog'/> {t('Send Configurations')}</MenuLink>
|
||||
<MenuLink to="/blacklist"><Icon icon='ban-circle'/> {t('Blacklist')}</MenuLink>
|
||||
<MenuLink to="/account/api"><Icon icon='retweet'/> {t('API')}</MenuLink>
|
||||
|
@ -118,6 +120,7 @@ class Root extends Component {
|
|||
...users.getMenus(t),
|
||||
...blacklist.getMenus(t),
|
||||
...account.getMenus(t),
|
||||
...settings.getMenus(t),
|
||||
...sendConfigurations.getMenus(t)
|
||||
}
|
||||
}
|
||||
|
@ -126,7 +129,7 @@ class Root extends Component {
|
|||
|
||||
async logout() {
|
||||
await axios.post('/rest/logout');
|
||||
window.location = mailtrainConfig.urlBase;
|
||||
window.location = getUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
@ -1,28 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import React, {Component} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page'
|
||||
import {
|
||||
withForm,
|
||||
Trans,
|
||||
translate
|
||||
} from 'react-i18next';
|
||||
import {
|
||||
NavButton,
|
||||
requiresAuthenticatedUser,
|
||||
Title,
|
||||
withPageHelpers
|
||||
} from '../lib/page'
|
||||
import {
|
||||
Button,
|
||||
ButtonRow,
|
||||
CheckBox,
|
||||
Fieldset,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
TextArea,
|
||||
Dropdown,
|
||||
ButtonRow,
|
||||
Button,
|
||||
CheckBox,
|
||||
Fieldset
|
||||
withForm
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling, withAsyncErrorHandler } from '../lib/error-handling';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import {withErrorHandling} from '../lib/error-handling';
|
||||
import {
|
||||
NamespaceSelect,
|
||||
validateNamespace
|
||||
} from '../lib/namespace';
|
||||
import {DeleteModalDialog} from "../lib/modals";
|
||||
|
||||
import { getMailerTypes, mailerTypesOrder } from "./helpers";
|
||||
import {getMailerTypes} from "./helpers";
|
||||
|
||||
import {
|
||||
getSystemSendConfigurationId,
|
||||
MailerType
|
||||
} from "../../../shared/send-configurations";
|
||||
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
import {MailerType} from "../../../shared/send-configurations";
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
|
@ -35,14 +51,6 @@ export default class CUD extends Component {
|
|||
|
||||
this.mailerTypes = getMailerTypes(props.t);
|
||||
|
||||
this.typeOptions = [];
|
||||
for (const type of mailerTypesOrder) {
|
||||
this.typeOptions.push({
|
||||
key: type,
|
||||
label: this.mailerTypes[type].typeName
|
||||
});
|
||||
}
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm({
|
||||
|
@ -68,7 +76,8 @@ export default class CUD extends Component {
|
|||
componentDidMount() {
|
||||
if (this.props.entity) {
|
||||
this.getFormValuesFromEntity(this.props.entity, data => {
|
||||
this.mailerTypes[data.type].afterLoad(data);
|
||||
this.mailerTypes[data.mailer_type].afterLoad(data);
|
||||
data.verpEnabled = !!data.verp_hostname;
|
||||
});
|
||||
|
||||
} else {
|
||||
|
@ -79,6 +88,8 @@ export default class CUD extends Component {
|
|||
from_email_overridable: false,
|
||||
from_name_overridable: false,
|
||||
subject_overridable: false,
|
||||
verpEnabled: false,
|
||||
verp_hostname: '',
|
||||
mailer_type: MailerType.ZONE_MTA,
|
||||
...this.mailerTypes[MailerType.ZONE_MTA].initData()
|
||||
});
|
||||
|
@ -100,6 +111,13 @@ export default class CUD extends Component {
|
|||
state.setIn(['mailer_type', 'error'], null);
|
||||
}
|
||||
|
||||
if (state.getIn(['verpEnabled', 'value']) && !state.getIn(['verp_hostname', 'value'])) {
|
||||
state.setIn(['verp_hostname', 'error'], t('VERP hostname must not be empty'));
|
||||
} else {
|
||||
state.setIn(['verp_hostname', 'error'], null);
|
||||
}
|
||||
|
||||
|
||||
validateNamespace(t, state);
|
||||
}
|
||||
|
||||
|
@ -119,7 +137,10 @@ export default class CUD extends Component {
|
|||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(sendMethod, url, data => {
|
||||
this.mailerTypes[data.type].beforeSave(data);
|
||||
this.mailerTypes[data.mailer_type].beforeSave(data);
|
||||
if (!data.verpEnabled) {
|
||||
data.verp_hostname = null;
|
||||
}
|
||||
});
|
||||
|
||||
if (submitSuccessful) {
|
||||
|
@ -133,14 +154,17 @@ export default class CUD extends Component {
|
|||
render() {
|
||||
const t = this.props.t;
|
||||
const isEdit = !!this.props.entity;
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete');
|
||||
const canDelete = isEdit && this.props.entity.permissions.includes('delete') && this.props.entity.id !== getSystemSendConfigurationId();
|
||||
|
||||
const typeKey = this.getFormValue('mailer_type');
|
||||
console.log(typeKey);
|
||||
let mailerForm = null;
|
||||
if (typeKey) {
|
||||
mailerForm = this.mailerTypes[typeKey].getForm(this);
|
||||
}
|
||||
|
||||
const verpEnabled = this.getFormValue('verpEnabled');
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canDelete &&
|
||||
|
@ -159,7 +183,7 @@ export default class CUD extends Component {
|
|||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
<NamespaceSelect/>
|
||||
|
||||
<Fieldset label={t('Email Header')}>
|
||||
|
@ -171,9 +195,27 @@ export default class CUD extends Component {
|
|||
<CheckBox id="subject_overridable" text={t('Overridable')}/>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset label={t('Mailer Settings')}>
|
||||
<Dropdown id="type" label={t('Type')} options={this.typeOptions}/>
|
||||
{mailerForm}
|
||||
{mailerForm}
|
||||
{/* TODO - add "Check mail config" button */}
|
||||
|
||||
<Fieldset label={t('VERP Bounce Handling')}>
|
||||
<Trans><p>Mailtrain is able to use VERP based routing to detect bounces. In this case the message is sent to the recipient using a custom VERP address as the return path of the message. If the message is not accepted a bounce email is sent to this special VERP address and thus a bounce is detected.</p></Trans>
|
||||
<Trans><p>To get VERP working you need to set up a DNS MX record that points to your Mailtrain hostname. You must also ensure that Mailtrain VERP interface is available from port 25 of your server (port 25 usually requires root user privileges). This way if anyone tries to send email to someuser@verp-hostname then the email should end up to this server.</p></Trans>
|
||||
<Trans><p className="text-warning">VERP usually only works if you are using your own SMTP server. Regular relay services (SES, SparkPost, Gmail etc.) tend to remove the VERP address from the message.</p></Trans>
|
||||
{mailtrainConfig.verpEnabled ?
|
||||
<div>
|
||||
<CheckBox id="verpEnabled" text={t('verpEnabled')}/>
|
||||
{verpEnabled && <InputField id="verp_hostname" label={t('Server hostname')} placeholder={t('The VERP server hostname, eg. bounces.example.com')} help={t('VERP bounce handling server hostname. This hostname is used in the SMTP envelope FROM address and the MX DNS records should point to this server')}/>}
|
||||
</div>
|
||||
:
|
||||
<Trans><p>VERP bounce handling server is not enabled. Modify your server configuration file and restart server to enable it.</p></Trans>
|
||||
}
|
||||
<InputField id="from_email" label={t('Default "from" email')}/>
|
||||
<CheckBox id="from_email_overridable" text={t('Overridable')}/>
|
||||
<InputField id="from_name" label={t('Default "from" name')}/>
|
||||
<CheckBox id="from_name_overridable" text={t('Overridable')}/>
|
||||
<InputField id="subject" label={t('Subject')}/>
|
||||
<CheckBox id="subject_overridable" text={t('Overridable')}/>
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
|
@ -186,16 +228,4 @@ export default class CUD extends Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
<Fieldset label={t('GPG Signing')}>
|
||||
<Trans><p>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</p></Trans>
|
||||
<Trans><p className="text-warning">Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</p></Trans>
|
||||
<InputField id="gpg_signing_key_passphrase" label={t('Private key passphrase')} placeholder={t('Passphrase for the key if set')} help={t('Only fill this if your private key is encrypted with a passphrase')}/>
|
||||
<TextArea id="gpg_signing_key" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/>
|
||||
|
||||
</Fieldset>
|
||||
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -3,6 +3,14 @@
|
|||
import React from "react";
|
||||
|
||||
import {MailerType} from "../../../shared/send-configurations";
|
||||
import {
|
||||
CheckBox,
|
||||
Dropdown,
|
||||
Fieldset,
|
||||
InputField,
|
||||
TextArea
|
||||
} from "../lib/form";
|
||||
import {Trans} from "react-i18next";
|
||||
|
||||
export const mailerTypesOrder = [
|
||||
MailerType.ZONE_MTA,
|
||||
|
@ -10,52 +18,239 @@ export const mailerTypesOrder = [
|
|||
MailerType.AWS_SES
|
||||
];
|
||||
|
||||
function getInitCommon() {
|
||||
return {
|
||||
maxConnections: '5',
|
||||
throttling: '',
|
||||
logTransactions: false
|
||||
};
|
||||
}
|
||||
|
||||
function getInitGenericSMTP() {
|
||||
return {
|
||||
...getInitCommon(),
|
||||
smtpHostname: '',
|
||||
smtpPort: '',
|
||||
smtpEncryption: 'NONE',
|
||||
smtpUseAuth: false,
|
||||
smtpUser: '',
|
||||
smtpPassword: '',
|
||||
smtpAllowSelfSigned: false,
|
||||
smtpMaxMessages: '100'
|
||||
};
|
||||
}
|
||||
|
||||
function afterLoadCommon(data) {
|
||||
data.maxConnections = data.mailer_settings.maxConnections;
|
||||
data.throttling = data.mailer_settings.throttling || '';
|
||||
data.logTransaction = data.mailer_settings.logTransactions;
|
||||
}
|
||||
|
||||
function afterLoadGenericSMTP(data) {
|
||||
afterLoadCommon(data);
|
||||
data.smtpHostname = data.mailer_settings.hostname;
|
||||
data.smtpPort = data.mailer_settings.port || '';
|
||||
data.smtpEncryption = data.mailer_settings.encryption;
|
||||
data.smtpUseAuth = data.mailer_settings.useAuth;
|
||||
data.smtpUser = data.mailer_settings.user;
|
||||
data.smtpPassword = data.mailer_settings.password;
|
||||
data.smtpAllowSelfSigned = data.mailer_settings.allowSelfSigned;
|
||||
data.smtpMaxMessages = data.mailer_settings.maxMessages;
|
||||
}
|
||||
|
||||
function beforeSaveCommon(data) {
|
||||
data.mailer_settings = {};
|
||||
data.mailer_settings.maxConnections = Number(data.maxConnections);
|
||||
data.mailer_settings.throttling = Number(data.throttling);
|
||||
data.mailer_settings.logTransactions = data.logTransaction;
|
||||
}
|
||||
|
||||
function beforeSaveGenericSMTP(data) {
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.hostname = data.smtpHostname;
|
||||
data.mailer_settings.port = Number(data.smtpPort);
|
||||
data.mailer_settings.encryption = data.smtpEncryption;
|
||||
data.mailer_settings.useAuth = data.smtpUseAuth;
|
||||
data.mailer_settings.user = data.smtpUser;
|
||||
data.mailer_settings.password = data.smtpPassword;
|
||||
data.mailer_settings.allowSelfSigned = data.smtpAllowSelfSigned;
|
||||
data.mailer_settings.maxMessages = Number(data.smtpMaxMessages);
|
||||
}
|
||||
|
||||
export function getMailerTypes(t) {
|
||||
const mailerTypes = {};
|
||||
|
||||
function clearBeforeSend(data) {
|
||||
function initFieldsIfMissing(mutState, mailerType) {
|
||||
const initVals = mailerTypes[mailerType].initData();
|
||||
|
||||
for (const key in initVals) {
|
||||
if (!mutState.hasIn([key])) {
|
||||
mutState.setIn([key, 'value'], initVals[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearBeforeSave(data) {
|
||||
for (const mailerKey in mailerTypes) {
|
||||
const initVals = mailerTypes[mailerKey].initData();
|
||||
for (const fieldKey in initVals) {
|
||||
delete data[fieldKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions = [
|
||||
{ key: MailerType.GENERIC_SMTP, label: t('Generic SMTP')},
|
||||
{ key: MailerType.ZONE_MTA, label: t('Zone MTA')},
|
||||
{ key: MailerType.AWS_SES, label: t('Amazon SES')}
|
||||
];
|
||||
|
||||
const smtpEncryptionOptions = [
|
||||
{ key: 'NONE', label: t('Do not use encryption')},
|
||||
{ key: 'TLS', label: t('Use TLS – usually selected for port 465')},
|
||||
{ key: 'STARTTLS', label: t('Use STARTTLS – usually selected for port 587 and 25')}
|
||||
];
|
||||
|
||||
const sesRegionOptions = [
|
||||
{ key: 'us-east-1', label: t('US-EAST-1')},
|
||||
{ key: 'us-west-2', label: t('US-WEST-2')},
|
||||
{ key: 'eu-west-1', label: t('EU-WEST-1')}
|
||||
];
|
||||
|
||||
mailerTypes[MailerType.GENERIC_SMTP] = {
|
||||
typeName: t('Generic SMTP'),
|
||||
getForm: owner => null,
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('Mailer Settings')}>
|
||||
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('Hostname')} placeholder={t('Hostname, eg. smtp.example.com')}/>
|
||||
<InputField id="smtpPort" label={t('Port')} placeholder={t('Port, eg. 465. Autodetected if left blank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('Encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('Enable SMTP authentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('Username')} placeholder={t('Username, eg. myaccount@example.com')}/>
|
||||
<InputField id="smtpPassword" label={t('Password')} placeholder={t('Username, eg. myaccount@example.com')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('Advanced Mailer Settings')}>
|
||||
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('Allow self-signed certificates')}/>
|
||||
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('Max messages')} placeholder={t('The count of max messages, eg. 100')} help={t('The number of messages to send through a single connection before the connection is closed and reopened (defaults to 100)')}/>
|
||||
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP()
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
beforeSaveGenericSMTP(data);
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
// mutState.setIn(['type', 'value'], '');
|
||||
initFieldsIfMissing(mutState, MailerType.GENERIC_SMTP);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.ZONE_MTA] = {
|
||||
typeName: t('Zone MTA'),
|
||||
getForm: owner => null,
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('Mailer Settings')}>
|
||||
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
|
||||
<InputField id="smtpHostname" label={t('Hostname')} placeholder={t('Hostname, eg. smtp.example.com')}/>
|
||||
<InputField id="smtpPort" label={t('Port')} placeholder={t('Port, eg. 465. Autodetected if left blank')}/>
|
||||
<Dropdown id="smtpEncryption" label={t('Encryption')} options={smtpEncryptionOptions}/>
|
||||
<CheckBox id="smtpUseAuth" text={t('Enable SMTP authentication')}/>
|
||||
{ owner.getFormValue('smtpUseAuth') &&
|
||||
<div>
|
||||
<InputField id="smtpUser" label={t('Username')} placeholder={t('Username, eg. myaccount@example.com')}/>
|
||||
<InputField id="smtpPassword" label={t('Password')} placeholder={t('Username, eg. myaccount@example.com')}/>
|
||||
</div>
|
||||
}
|
||||
</Fieldset>
|
||||
<Fieldset label={t('DKIM Signing')}>
|
||||
<Trans><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages. Other services usually provide their own means to DKIM sign your messages.</p></Trans>
|
||||
<Trans><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
|
||||
<InputField id="dkimApiKey" label={t('ZoneMTA DKIM API key')} help={t('Secret value known to ZoneMTA for requesting DKIM key information. If this value was generated by the Mailtrain installation script then you can keep it as it is.')}/>
|
||||
<InputField id="dkimDomain" label={t('DKIM domain')} help={t('Leave blank to use the sender email address domain.')}/>
|
||||
<InputField id="dkimSelector" label={t('DKIM key selector')} help={t('Signing is disabled without a valid selector value.')}/>
|
||||
<TextArea id="dkimPrivateKey" label={t('DKIM private key')} placeholder={t('Begins with "-----BEGIN RSA PRIVATE KEY-----"')} help={t('Signing is disabled without a valid private key.')}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('Advanced Mailer Settings')}>
|
||||
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
|
||||
<CheckBox id="smtpAllowSelfSigned" text={t('Allow self-signed certificates')}/>
|
||||
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
|
||||
<InputField id="smtpMaxMessages" label={t('Max messages')} placeholder={t('The count of max messages, eg. 100')} help={t('The number of messages to send through a single connection before the connection is closed and reopened (defaults to 100)')}/>
|
||||
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitGenericSMTP(),
|
||||
dkimApiKey: '',
|
||||
dkimDomain: '',
|
||||
dkimSelector: '',
|
||||
dkimPrivateKey: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadGenericSMTP(data);
|
||||
data.dkimApiKey = data.mailer_settings.dkimApiKey;
|
||||
data.dkimDomain = data.mailer_settings.dkimDomain;
|
||||
data.dkimSelector = data.mailer_settings.dkimSelector;
|
||||
data.dkimPrivateKey = data.mailer_settings.dkimPrivateKey;
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
beforeSaveGenericSMTP(data);
|
||||
data.mailer_settings.dkimApiKey = data.dkimApiKey;
|
||||
data.mailer_settings.dkimDomain = data.dkimDomain;
|
||||
data.mailer_settings.dkimSelector = data.dkimSelector;
|
||||
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.ZONE_MTA);
|
||||
}
|
||||
};
|
||||
|
||||
mailerTypes[MailerType.AWS_SES] = {
|
||||
typeName: t('Amazon SES'),
|
||||
getForm: owner => null,
|
||||
getForm: owner =>
|
||||
<div>
|
||||
<Fieldset label={t('Mailer Settings')}>
|
||||
<Dropdown id="mailer_type" label={t('Mailer type')} options={typeOptions}/>
|
||||
<InputField id="sesKey" label={t('Access key')} placeholder={t('AWS access key ID')}/>
|
||||
<InputField id="sesSecret" label={t('Port')} placeholder={t('AWS secret access key')}/>
|
||||
<Dropdown id="sesRegion" label={t('Region')} options={sesRegionOptions}/>
|
||||
</Fieldset>
|
||||
<Fieldset label={t('Advanced Mailer Settings')}>
|
||||
<CheckBox id="logTransactions" text={t('Log SMTP transactions')}/>
|
||||
<InputField id="maxConnections" label={t('Max connections')} placeholder={t('The count of max connections, eg. 10')} help={t('The count of maximum simultaneous connections to make against the SMTP server (defaults to 5). This limit is per sending process.')}/>
|
||||
<InputField id="throttling" label={t('Throttling')} placeholder={t('Messages per hour eg. 1000')} help={t('Maximum number of messages to send in an hour. Leave empty or zero for no throttling. If your provider uses a different speed limit (messages/minute or messages/second) then convert this limit into messages/hour (1m/s => 3600m/h). This limit is per sending process.')}/>
|
||||
</Fieldset>
|
||||
</div>,
|
||||
initData: () => ({
|
||||
...getInitCommon(),
|
||||
sesKey: '',
|
||||
sesSecret: '',
|
||||
sesRegion: ''
|
||||
}),
|
||||
afterLoad: data => {
|
||||
afterLoadCommon(data);
|
||||
data.sesKey = data.mailer_settings.key;
|
||||
data.sesSecret = data.mailer_settings.secret;
|
||||
data.sesRegion = data.mailer_settings.region;
|
||||
},
|
||||
beforeSave: data => {
|
||||
clearBeforeSend(data);
|
||||
beforeSaveCommon(data);
|
||||
data.mailer_settings.key = data.sesKey;
|
||||
data.mailer_settings.secret = data.sesSecret;
|
||||
data.mailer_settings.region = data.sesRegion;
|
||||
clearBeforeSave(data);
|
||||
},
|
||||
afterTypeChange: mutState => {
|
||||
initFieldsIfMissing(mutState, MailerType.AWS_SES);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
99
client/src/settings/Update.js
Normal file
99
client/src/settings/Update.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
'use strict';
|
||||
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { translate, Trans } from 'react-i18next';
|
||||
import {requiresAuthenticatedUser, withPageHelpers, Title, NavButton} from '../lib/page';
|
||||
import {
|
||||
withForm,
|
||||
Form,
|
||||
FormSendMethod,
|
||||
InputField,
|
||||
TextArea,
|
||||
TableSelect,
|
||||
ButtonRow,
|
||||
Button,
|
||||
Dropdown,
|
||||
StaticField,
|
||||
CheckBox,
|
||||
Fieldset
|
||||
} from '../lib/form';
|
||||
import { withErrorHandling } from '../lib/error-handling';
|
||||
import { DeleteModalDialog } from '../lib/modals';
|
||||
import { validateNamespace, NamespaceSelect } from '../lib/namespace';
|
||||
import { UnsubscriptionMode } from '../../../shared/lists';
|
||||
import styles from "../lib/styles.scss";
|
||||
import mailtrainConfig from 'mailtrainConfig';
|
||||
|
||||
@translate()
|
||||
@withForm
|
||||
@withPageHelpers
|
||||
@withErrorHandling
|
||||
@requiresAuthenticatedUser
|
||||
export default class Update extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {};
|
||||
|
||||
this.initForm();
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
entity: PropTypes.object
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getFormValuesFromEntity(this.props.entity);
|
||||
}
|
||||
|
||||
localValidateFormValues(state) {
|
||||
const t = this.props.t;
|
||||
}
|
||||
|
||||
async submitHandler() {
|
||||
const t = this.props.t;
|
||||
|
||||
this.disableForm();
|
||||
this.setFormStatusMessage('info', t('Saving ...'));
|
||||
|
||||
const submitSuccessful = await this.validateAndSendFormValuesToURL(FormSendMethod.PUT, '/rest/settings');
|
||||
|
||||
if (submitSuccessful) {
|
||||
this.navigateToWithFlashMessage('/settings', 'success', t('Global settings saved'));
|
||||
} else {
|
||||
this.enableForm();
|
||||
this.setFormStatusMessage('warning', t('There are errors in the form. Please fix them and submit again.'));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const t = this.props.t;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title>{t('Global Settings')}</Title>
|
||||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="adminEmail" label={t('Admin email')} help={t('This email is used as the main contact and as a default email address if no email address is specified in list settings.')}/>
|
||||
<InputField id="defaultHomepage" label={t('Default homepage URL')} help={t('This URL will be used in list subscription forms if no homepage is specified in list settings.')}/>
|
||||
|
||||
<InputField id="uaCode" label={t('Tracking ID')} placeholder={t('UA-XXXXX-XX')} help={t('Enter Google Analytics tracking code')}/>
|
||||
|
||||
<TextArea id="shoutout" label={t('Frontpage shout out')} help={t('HTML code shown in the front page header section')}/>
|
||||
|
||||
<Fieldset label={t('GPG Signing')}>
|
||||
<Trans><p>Only messages that are encrypted can be signed. Subsribers who have not set up a GPG public key in their profile receive normal email messages. Users with GPG key set receive encrypted messages and if you have signing key also set, the messages are signed with this key.</p></Trans>
|
||||
<Trans><p className="text-warning">Do not use sensitive keys here. The private key and passphrase are not encrypted in the database.</p></Trans>
|
||||
<InputField id="pgpPassphrase" label={t('Private key passphrase')} placeholder={t('Passphrase for the key if set')} help={t('Only fill this if your private key is encrypted with a passphrase')}/>
|
||||
<TextArea id="pgpPrivateKey" label={t('GPG private key')} placeholder={t('Begins with \'-----BEGIN PGP PRIVATE KEY BLOCK-----\'')} help={t('This value is optional. If you do not provide a private key GPG encrypted messages are sent without signing.')}/>
|
||||
</Fieldset>
|
||||
|
||||
<ButtonRow>
|
||||
<Button type="submit" className="btn-primary" icon="ok" label={t('Save')}/>
|
||||
</ButtonRow>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
21
client/src/settings/root.js
Normal file
21
client/src/settings/root.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
import React from "react";
|
||||
import Update from "./Update";
|
||||
|
||||
function getMenus(t) {
|
||||
return {
|
||||
'settings': {
|
||||
title: t('Global Settings'),
|
||||
link: '/settings',
|
||||
resolve: {
|
||||
configItems: params => `/rest/settings`
|
||||
},
|
||||
panelRender: props => <Update entity={props.resolved.configItems} />
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
getMenus
|
||||
}
|
|
@ -281,7 +281,7 @@ export default class CUD extends Component {
|
|||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitHandler}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
|
||||
{isEdit
|
||||
?
|
||||
|
|
|
@ -166,7 +166,7 @@ export default class CUD extends Component {
|
|||
|
||||
<Form stateOwner={this} onSubmitAsync={::this.submitAndLeave}>
|
||||
<InputField id="name" label={t('Name')}/>
|
||||
<TextArea id="description" label={t('Description')} help={t('HTML is allowed')}/>
|
||||
<TextArea id="description" label={t('Description')}/>
|
||||
<Dropdown id="type" label={t('Type')} options={this.typeOptions}/>
|
||||
<NamespaceSelect/>
|
||||
|
||||
|
|
|
@ -52,13 +52,11 @@ port=3000
|
|||
sandboxPort=8081
|
||||
# HTTP interface to listen on
|
||||
host="0.0.0.0"
|
||||
# URL Base (must end with slash). It can be either relative (starting with slash) or absolute (starting with http:// or https://).
|
||||
# If it is relative, an absolute URL will be constructed automatically based on the domain where it is served and the "port" config parameter
|
||||
urlBase="/"
|
||||
# URL Base for sandbox urls (must end with slash). It can be either relative (starting with slash) or absolute (starting with http:// or https://)
|
||||
# If it is relative, an absolute URL will be constructed automatically based on the domain where it is served and the "port" config parameter
|
||||
# If not given at all, it is automatically constructed based on urlBase and sandboxPort.
|
||||
# sandboxUrlBase="/"
|
||||
# URL base for trusted urls. It must be absolute (starting with http:// or https://). If Mailtrain is served on
|
||||
# a non-standard port (e.g. 3000), the URL must also specify the port.
|
||||
trustedUrlBase="http://localhost:3000"
|
||||
# URL base for sandbox urls. It must be absolute (starting with http:// or https://) and contain the sandbox port.
|
||||
sandboxUrlBase="http://localhost:8081"
|
||||
|
||||
# Secret for signing the session ID cookie
|
||||
secret="a cat"
|
||||
|
|
|
@ -5,18 +5,20 @@ const config = require('config');
|
|||
const permissions = require('./permissions');
|
||||
const forms = require('../models/forms');
|
||||
const shares = require('../models/shares');
|
||||
const urls = require('./urls');
|
||||
|
||||
async function getAnonymousConfig(context) {
|
||||
async function getAnonymousConfig(context, trusted) {
|
||||
return {
|
||||
authMethod: passport.authMethod,
|
||||
isAuthMethodLocal: passport.isAuthMethodLocal,
|
||||
externalPasswordResetLink: config.ldap.passwordresetlink,
|
||||
language: config.language || 'en',
|
||||
isAuthenticated: !!context.user,
|
||||
urlBase: config.www.urlBase,
|
||||
sandboxUrlBase: config.www.sandboxUrlBase,
|
||||
port: config.www.port,
|
||||
sandboxPort: config.www.sandboxPort
|
||||
trustedUrlBase: urls.getTrustedUrl(),
|
||||
trustedUrlBaseDir: urls.getTrustedUrlBaseDir(),
|
||||
sandboxUrlBase: urls.getSandboxUrl(),
|
||||
sandboxUrlBaseDir: urls.getSandboxUrlBaseDir(),
|
||||
trusted
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,7 +31,8 @@ async function getAuthenticatedConfig(context) {
|
|||
namespace: context.user.namespace
|
||||
},
|
||||
globalPermissions: shares.getGlobalPermissions(context),
|
||||
editors: config.editors
|
||||
editors: config.editors,
|
||||
verpEnabled: config.verp.enabled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -171,6 +171,15 @@ function getTemplate(template, callback) {
|
|||
}
|
||||
|
||||
function createMailer(callback) {
|
||||
// FIXME
|
||||
module.exports.transport = {
|
||||
on: () => {},
|
||||
isIdle: () => true,
|
||||
checkThrottling: next => next()
|
||||
};
|
||||
return callback(null, module.exports.transport);
|
||||
|
||||
|
||||
getSettings(['smtpHostname', 'smtpPort', 'smtpEncryption', 'smtpUser', 'smtpPass', 'smtpLog', 'smtpDisableAuth', 'smtpMaxConnections', 'smtpMaxMessages', 'smtpSelfSigned', 'pgpPrivateKey', 'pgpPassphrase', 'smtpThrottling', 'mailTransport', 'sesKey', 'sesSecret', 'sesRegion'], (err, configItems) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
|
|
@ -29,55 +29,43 @@ async function sendSubscriptionConfirmed(list, email, subscription) {
|
|||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
|
||||
await _sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, {}, subscription);
|
||||
await _sendMail(list, email, 'subscription_confirmed', _('%s: Subscription Confirmed'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendAlreadySubscribed(list, email, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
preferencesUrl: '/subscription/' + list.cid + '/manage/' + subscription.cid,
|
||||
unsubscribeUrl: '/subscription/' + list.cid + '/unsubscribe/' + subscription.cid
|
||||
};
|
||||
await _sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, mailOpts, subscription);
|
||||
await _sendMail(list, email, 'already_subscribed', _('%s: Email Address Already Registered'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmAddressChange(list, email, cid, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/change-address/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, mailOpts, subscription);
|
||||
await _sendMail(list, email, 'confirm_address_change', _('%s: Please Confirm Email Change in Subscription'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmSubscription(list, email, cid, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/subscribe/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, mailOpts, subscription);
|
||||
await _sendMail(list, email, 'confirm_subscription', _('%s: Please Confirm Subscription'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendConfirmUnsubscription(list, email, cid, subscription) {
|
||||
const mailOpts = {
|
||||
ignoreDisableConfirmations: true
|
||||
};
|
||||
const relativeUrls = {
|
||||
confirmUrl: '/subscription/confirm/unsubscribe/' + cid
|
||||
};
|
||||
await _sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, mailOpts, subscription);
|
||||
await _sendMail(list, email, 'confirm_unsubscription', _('%s: Please Confirm Unsubscription'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
async function sendUnsubscriptionConfirmed(list, email, subscription) {
|
||||
const relativeUrls = {
|
||||
subscribeUrl: '/subscription/' + list.cid + '?cid=' + subscription.cid
|
||||
};
|
||||
await _sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, {}, subscription);
|
||||
await _sendMail(list, email, 'unsubscription_confirmed', _('%s: Unsubscription Confirmed'), relativeUrls, subscription);
|
||||
}
|
||||
|
||||
function getDisplayName(flds, subscription) {
|
||||
|
@ -110,7 +98,7 @@ function getDisplayName(flds, subscription) {
|
|||
}
|
||||
}
|
||||
|
||||
async function _sendMail(list, email, template, subject, relativeUrls, mailOpts, subscription) {
|
||||
async function _sendMail(list, email, template, subject, relativeUrls, subscription) {
|
||||
console.log(subscription);
|
||||
|
||||
const flds = await fields.list(contextHelpers.getAdminContext(), list.id);
|
||||
|
@ -122,16 +110,12 @@ async function _sendMail(list, email, template, subject, relativeUrls, mailOpts,
|
|||
}
|
||||
}
|
||||
|
||||
const configItems = await settings.get(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'defaultPostaddress', 'serviceUrl', 'disableConfirmations']);
|
||||
const configItems = await settings.get(['defaultHomepage', 'defaultFrom', 'defaultAddress', 'serviceUrl']);
|
||||
|
||||
if (!mailOpts.ignoreDisableConfirmations && configItems.disableConfirmations) {
|
||||
return;
|
||||
}
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress
|
||||
};
|
||||
|
||||
for (let relativeUrlKey in relativeUrls) {
|
||||
|
|
29
lib/urls.js
Normal file
29
lib/urls.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const url = require('url');
|
||||
|
||||
function getTrustedUrl(path) {
|
||||
return config.www.trustedUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getSandboxUrl(path) {
|
||||
return config.www.sandboxUrlBase + (path || '');
|
||||
}
|
||||
|
||||
function getTrustedUrlBaseDir() {
|
||||
const mailtrainUrl = url.parse(getTrustedUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
function getSandboxUrlBaseDir() {
|
||||
const mailtrainUrl = url.parse(getSandboxUrl());
|
||||
return mailtrainUrl.pathname;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTrustedUrl,
|
||||
getSandboxUrl,
|
||||
getTrustedUrlBaseDir,
|
||||
getSandboxUrlBaseDir
|
||||
};
|
|
@ -13,7 +13,7 @@ const segments = require('./segments');
|
|||
|
||||
const UnsubscriptionMode = require('../shared/lists').UnsubscriptionMode;
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'namespace']);
|
||||
const allowedKeys = new Set(['name', 'description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'contact_email', 'homepage', 'namespace']);
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
|
|
|
@ -7,9 +7,9 @@ const { enforce, filterObject } = require('../lib/helpers');
|
|||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const shares = require('./shares');
|
||||
const namespaceHelpers = require('../lib/namespace-helpers');
|
||||
const {MailerType} = require('../shared/send-configurations');
|
||||
const {MailerType, getSystemSendConfigurationId} = require('../shared/send-configurations');
|
||||
|
||||
const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'subject', 'subject_overridable', 'mailer_type', 'mailer_settings', 'namespace']);
|
||||
const allowedKeys = new Set(['name', 'description', 'from_email', 'from_email_overridable', 'from_name', 'from_name_overridable', 'subject', 'subject_overridable', 'verp_hostname', 'mailer_type', 'mailer_settings', 'namespace']);
|
||||
|
||||
|
||||
function hash(entity) {
|
||||
|
@ -95,6 +95,10 @@ async function updateWithConsistencyCheck(context, entity) {
|
|||
}
|
||||
|
||||
async function remove(context, id) {
|
||||
if (id === getSystemSendConfigurationId()) {
|
||||
shares.throwPermissionDenied();
|
||||
}
|
||||
|
||||
await knex.transaction(async tx => {
|
||||
await shares.enforceEntityPermissionTx(tx, context, 'sendConfiguration', id, 'delete');
|
||||
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const knex = require('../lib/knex');
|
||||
const { filterObject } = require('../lib/helpers');
|
||||
const hasher = require('node-object-hash')();
|
||||
const shares = require('./shares');
|
||||
|
||||
const allowedKeys = new Set(['adminEmail', 'uaCode', 'pgpPassphrase', 'pgpPrivateKey', 'defaultHomepage']);
|
||||
const allowedKeys = new Set(['adminEmail', 'uaCode', 'shoutout', 'pgpPassphrase', 'pgpPrivateKey', 'defaultHomepage']);
|
||||
// defaultHomepage is used as a default to list.homepage - if the list.homepage is not filled in
|
||||
|
||||
function hash(entity) {
|
||||
return hasher.hash(filterObject(entity, allowedKeys));
|
||||
}
|
||||
|
||||
async function get(context, keyOrKeys) {
|
||||
shares.enforceGlobalPermission(context, 'manageSettings');
|
||||
|
||||
let keys;
|
||||
if (!keyOrKeys) {
|
||||
keys = allowedKeys.values();
|
||||
keys = [...allowedKeys.values()];
|
||||
} else if (!Array.isArray(keyOrKeys)) {
|
||||
keys = [ keys ];
|
||||
} else {
|
||||
|
@ -25,7 +31,7 @@ async function get(context, keyOrKeys) {
|
|||
settings[row.key] = row.value;
|
||||
}
|
||||
|
||||
if (!Array.isArray(keyOrKeys)) {
|
||||
if (!Array.isArray(keyOrKeys) && keyOrKeys) {
|
||||
return settings[keyOrKeys];
|
||||
} else {
|
||||
return settings;
|
||||
|
@ -48,6 +54,7 @@ async function set(context, data) {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
hash,
|
||||
get,
|
||||
set
|
||||
};
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
let _ = require('../lib/translate')._;
|
||||
const knex = require('../lib/knex');
|
||||
const config = require('config');
|
||||
const { enforce } = require('../lib/helpers');
|
||||
|
@ -8,6 +7,7 @@ const dtHelpers = require('../lib/dt-helpers');
|
|||
const permissions = require('../lib/permissions');
|
||||
const interoperableErrors = require('../shared/interoperable-errors');
|
||||
const log = require('npmlog');
|
||||
const {getGlobalNamespaceId} = require('../shared/namespaces');
|
||||
|
||||
// TODO: This would really benefit from some permission cache connected to rebuildPermissions
|
||||
// A bit of the problem is that the cache would have to expunged as the result of other processes modifying entites/permissions
|
||||
|
@ -190,7 +190,7 @@ async function rebuildPermissionsTx(tx, restriction) {
|
|||
const usersWithRoleInRootNamespaceQuery = tx('users')
|
||||
.leftJoin(namespaceEntityType.sharesTable, {
|
||||
'users.id': `${namespaceEntityType.sharesTable}.user`,
|
||||
[`${namespaceEntityType.sharesTable}.entity`]: 1 /* Global namespace id */
|
||||
[`${namespaceEntityType.sharesTable}.entity`]: getGlobalNamespaceId()
|
||||
})
|
||||
.select(['users.id', 'users.role as userRole', `${namespaceEntityType.sharesTable}.role`]);
|
||||
if (restriction.userId) {
|
||||
|
@ -204,8 +204,8 @@ async function rebuildPermissionsTx(tx, restriction) {
|
|||
if (roleConf) {
|
||||
const desiredRole = roleConf.rootNamespaceRole;
|
||||
if (desiredRole && user.role !== desiredRole) {
|
||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: 1 /* Global namespace id */ }).del();
|
||||
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: 1 /* Global namespace id */, role: desiredRole, auto: 1 });
|
||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: getGlobalNamespaceId() }).del();
|
||||
await tx(namespaceEntityType.sharesTable).insert({ user: user.id, entity: getGlobalNamespaceId(), role: desiredRole, auto: 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -393,7 +393,7 @@ async function regenerateRoleNamesTable() {
|
|||
|
||||
|
||||
function throwPermissionDenied() {
|
||||
throw new interoperableErrors.PermissionDeniedError(_('Permission denied'));
|
||||
throw new interoperableErrors.PermissionDeniedError('Permission denied');
|
||||
}
|
||||
|
||||
async function removeDefaultShares(tx, user) {
|
||||
|
@ -409,7 +409,7 @@ async function removeDefaultShares(tx, user) {
|
|||
}
|
||||
|
||||
if (roleConf.rootNamespaceRole) {
|
||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: 1 /* Global namespace id */ }).del();
|
||||
await tx(namespaceEntityType.sharesTable).where({ user: user.id, entity: getGlobalNamespaceId() }).del();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
routes/index.js
Normal file
32
routes/index.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const routerFactory = require('../lib/router-async');
|
||||
|
||||
function getRouter(trusted) {
|
||||
const router = routerFactory.create();
|
||||
|
||||
if (trusted) {
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, trusted);
|
||||
if (req.user) {
|
||||
Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
|
||||
}
|
||||
|
||||
res.render('root', {
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
module.exports = {
|
||||
getRouter
|
||||
};
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const router = require('../lib/router-async').create();
|
||||
const routerFactory = require('../lib/router-async');
|
||||
const passport = require('../lib/passport');
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
const gm = require('gm').subClass({
|
||||
|
@ -110,73 +110,84 @@ function sanitizeSize(val, min, max, defaultVal, allowNull) {
|
|||
return val;
|
||||
}
|
||||
|
||||
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const method = req.query.method;
|
||||
const params = req.query.params;
|
||||
let [width, height] = params.split(',');
|
||||
let image;
|
||||
|
||||
if (method === 'placeholder') {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||
image = await placeholderImage(width, height);
|
||||
} else {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||
// TODO - validate that one has the rights to read this ???
|
||||
image = await resizedImage(req.query.src, method, width, height);
|
||||
|
||||
function getRouter(trusted) {
|
||||
const router = routerFactory.create();
|
||||
|
||||
if (!trusted) {
|
||||
router.getAsync('/img/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const method = req.query.method;
|
||||
const params = req.query.params;
|
||||
let [width, height] = params.split(',');
|
||||
let image;
|
||||
|
||||
if (method === 'placeholder') {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, false);
|
||||
image = await placeholderImage(width, height);
|
||||
} else {
|
||||
width = sanitizeSize(width, 1, 2048, 600, false);
|
||||
height = sanitizeSize(height, 1, 2048, 300, true);
|
||||
// TODO - validate that one has the rights to read this ???
|
||||
image = await resizedImage(req.query.src, method, width, height);
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
|
||||
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
|
||||
|
||||
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const entries = await files.list(req.context, req.params.type, req.params.fileId);
|
||||
|
||||
const filesOut = [];
|
||||
for (const entry of entries) {
|
||||
filesOut.push({
|
||||
name: entry.originalname,
|
||||
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
|
||||
size: entry.size,
|
||||
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
files: filesOut
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const resourceType = req.query.type;
|
||||
const resourceId = req.query.id;
|
||||
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context, trusted);
|
||||
|
||||
let languageStrings = null;
|
||||
if (config.language && config.language !== 'en') {
|
||||
const lang = config.language.split('_')[0];
|
||||
try {
|
||||
const file = path.join(__dirname, '..', 'client', 'public', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
|
||||
languageStrings = await fsReadFile(file, 'utf8');
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
|
||||
res.render('mosaico/root', {
|
||||
layout: 'mosaico/layout',
|
||||
editorConfig: config.mosaico,
|
||||
languageStrings: languageStrings,
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
res.set('Content-Type', 'image/' + image.format);
|
||||
image.stream.pipe(res);
|
||||
});
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
fileHelpers.installUploadHandler(router, '/upload/:type/:entityId', true);
|
||||
|
||||
router.getAsync('/upload/:type/:fileId', passport.loggedIn, async (req, res) => {
|
||||
const entries = await files.list(req.context, req.params.type, req.params.fileId);
|
||||
|
||||
const filesOut = [];
|
||||
for (const entry of entries) {
|
||||
filesOut.push({
|
||||
name: entry.originalname,
|
||||
url: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}`,
|
||||
size: entry.size,
|
||||
thumbnailUrl: `/files/${req.params.type}/${req.params.fileId}/${entry.filename}` // TODO - use smaller thumbnails
|
||||
})
|
||||
}
|
||||
|
||||
res.json({
|
||||
files: filesOut
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
router.getAsync('/editor', passport.csrfProtection, async (req, res) => {
|
||||
const resourceType = req.query.type;
|
||||
const resourceId = req.query.id;
|
||||
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
|
||||
|
||||
let languageStrings = null;
|
||||
if (config.language && config.language !== 'en') {
|
||||
const lang = config.language.split('_')[0];
|
||||
try {
|
||||
const file = path.join(__dirname, '..', 'client', 'public', 'mosaico', 'lang', 'mosaico-' + lang + '.json');
|
||||
languageStrings = await fsReadFile(file, 'utf8');
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
|
||||
res.render('mosaico/root', {
|
||||
layout: 'mosaico/layout',
|
||||
editorConfig: config.mosaico,
|
||||
languageStrings: languageStrings,
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
||||
module.exports = {
|
||||
getRouter
|
||||
};
|
||||
|
|
22
routes/rest/settings.js
Normal file
22
routes/rest/settings.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../../lib/passport');
|
||||
const settings = require('../../models/settings');
|
||||
|
||||
const router = require('../../lib/router-async').create();
|
||||
|
||||
|
||||
router.getAsync('/settings', passport.loggedIn, async (req, res) => {
|
||||
const configItems = await settings.get(req.context);
|
||||
configItems.hash = settings.hash(configItems);
|
||||
return res.json(configItems);
|
||||
});
|
||||
|
||||
router.putAsync('/settings', passport.loggedIn, passport.csrfProtection, async (req, res) => {
|
||||
const configItems = req.body;
|
||||
await settings.set(req.context, configItems);
|
||||
return res.json();
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -1,22 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const passport = require('../lib/passport');
|
||||
const _ = require('../lib/translate')._;
|
||||
const clientHelpers = require('../lib/client-helpers');
|
||||
|
||||
const router = require('../lib/router-async').create();
|
||||
|
||||
router.getAsync('/*', passport.csrfProtection, async (req, res) => {
|
||||
const mailtrainConfig = await clientHelpers.getAnonymousConfig(req.context);
|
||||
if (req.user) {
|
||||
Object.assign(mailtrainConfig, await clientHelpers.getAuthenticatedConfig(req.context));
|
||||
}
|
||||
|
||||
res.render('root', {
|
||||
reactCsrfToken: req.csrfToken(),
|
||||
mailtrainConfig: JSON.stringify(mailtrainConfig)
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
module.exports = router;
|
|
@ -205,10 +205,9 @@ async function _renderSubscribe(req, res, list, subscription) {
|
|||
data.customFields = await fields.forHbs(contextHelpers.getAdminContext(), list.id, subscription);
|
||||
data.useEditor = true;
|
||||
|
||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress']);
|
||||
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||
data.defaultAddress = configItems.defaultAddress;
|
||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
data.template = {
|
||||
template: 'subscription/web-subscribe.mjml.hbs',
|
||||
|
@ -407,10 +406,9 @@ router.getAsync('/:lcid/manage/:ucid', passport.csrfProtection, async (req, res)
|
|||
|
||||
data.useEditor = true;
|
||||
|
||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress', 'defaultPostaddress']);
|
||||
const configItems = await settings.get(['pgpPrivateKey', 'defaultAddress']);
|
||||
data.hasPubkey = !!configItems.pgpPrivateKey;
|
||||
data.defaultAddress = configItems.defaultAddress;
|
||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
data.template = {
|
||||
template: 'subscription/web-manage.mjml.hbs',
|
||||
|
@ -454,7 +452,7 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
|
|||
throw new interoperableErrors.NotFoundError('Subscription not found in this list');
|
||||
}
|
||||
|
||||
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
||||
const configItems = await settings.get(['defaultAddress']);
|
||||
|
||||
const data = {};
|
||||
data.email = subscription.email;
|
||||
|
@ -463,7 +461,6 @@ router.getAsync('/:lcid/manage-address/:ucid', passport.csrfProtection, async (r
|
|||
data.title = list.name;
|
||||
data.csrfToken = req.csrfToken();
|
||||
data.defaultAddress = configItems.defaultAddress;
|
||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
data.template = {
|
||||
template: 'subscription/web-manage-address.mjml.hbs',
|
||||
|
@ -538,7 +535,7 @@ router.postAsync('/:lcid/manage-address', passport.parseForm, passport.csrfProte
|
|||
router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req, res) => {
|
||||
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.lcid);
|
||||
|
||||
const configItems = await settings.get(['defaultAddress', 'defaultPostaddress']);
|
||||
const configItems = await settings.get(['defaultAddress']);
|
||||
|
||||
const autoUnsubscribe = req.query.auto === 'yes';
|
||||
|
||||
|
@ -563,7 +560,6 @@ router.getAsync('/:lcid/unsubscribe/:ucid', passport.csrfProtection, async (req,
|
|||
data.csrfToken = req.csrfToken();
|
||||
data.campaign = req.query.c;
|
||||
data.defaultAddress = configItems.defaultAddress;
|
||||
data.defaultPostaddress = configItems.defaultPostaddress;
|
||||
|
||||
data.template = {
|
||||
template: 'subscription/web-unsubscribe.mjml.hbs',
|
||||
|
@ -702,14 +698,13 @@ router.postAsync('/publickey', passport.parseForm, async (req, res) => {
|
|||
async function webNotice(type, req, res) {
|
||||
const list = await lists.getByCid(contextHelpers.getAdminContext(), req.params.cid);
|
||||
|
||||
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'defaultPostaddress', 'adminEmail']);
|
||||
const configItems = await settings.get(['defaultHomepage', 'serviceUrl', 'defaultAddress', 'adminEmail']);
|
||||
|
||||
|
||||
const data = {
|
||||
title: list.name,
|
||||
homepage: configItems.defaultHomepage || configItems.serviceUrl,
|
||||
defaultAddress: configItems.defaultAddress,
|
||||
defaultPostaddress: configItems.defaultPostaddress,
|
||||
contactAddress: configItems.defaultAddress,
|
||||
template: {
|
||||
template: 'subscription/web-' + type + '-notice.mjml.hbs',
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
const {getGlobalNamespaceId} = require('../../../shared/namespaces');
|
||||
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
const entityTypesAddNamespace = ['list', 'custom_form', 'template', 'campaign', 'report', 'report_template', 'user'];
|
||||
await knex.schema.createTable('namespaces', table => {
|
||||
|
@ -8,7 +10,7 @@ exports.up = (knex, Promise) => (async() => {
|
|||
});
|
||||
|
||||
await knex('namespaces').insert({
|
||||
id: 1, /* Global namespace id */
|
||||
id: getGlobalNamespaceId(),
|
||||
name: 'Root',
|
||||
description: 'Root namespace'
|
||||
});
|
||||
|
@ -19,7 +21,7 @@ exports.up = (knex, Promise) => (async() => {
|
|||
});
|
||||
|
||||
await knex(`${entityType}s`).update({
|
||||
namespace: 1 /* Global namespace id */
|
||||
namespace: getGlobalNamespaceId()
|
||||
});
|
||||
|
||||
await knex.schema.table(`${entityType}s`, table => {
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.createTable('send_configurations', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.string('from_email');
|
||||
table.boolean('from_email_overridable').defaultTo(false);
|
||||
table.string('from_name');
|
||||
table.boolean('from_name_overridable').defaultTo(false);
|
||||
table.string('subject');
|
||||
table.boolean('subject_overridable').defaultTo(false);
|
||||
table.string('mailer_type');
|
||||
table.text('mailer_settings', 'longtext');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.integer('namespace').unsigned().references('namespaces.id');
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`shares_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('role', 128).notNullable();
|
||||
table.boolean('auto').defaultTo(false);
|
||||
table.primary(['entity', 'user']);
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`permissions_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('operation', 128).notNullable();
|
||||
table.primary(['entity', 'user', 'operation']);
|
||||
});
|
||||
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
await knex.schema
|
||||
.dropTable('shares_send_configuration')
|
||||
.dropTable('permissions_send_configuration')
|
||||
.dropTable('send_configurations')
|
||||
;
|
||||
})();
|
122
setup/knex/migrations/20180414120444_transform_settings.js
Normal file
122
setup/knex/migrations/20180414120444_transform_settings.js
Normal file
|
@ -0,0 +1,122 @@
|
|||
const { MailerType, getSystemSendConfigurationId } = require('../../../shared/send-configurations');
|
||||
const {getGlobalNamespaceId} = require('../../../shared/namespaces');
|
||||
|
||||
exports.up = (knex, Promise) => (async() => {
|
||||
await knex.schema.createTable('send_configurations', table => {
|
||||
table.increments('id').primary();
|
||||
table.string('name');
|
||||
table.text('description');
|
||||
table.string('from_email');
|
||||
table.boolean('from_email_overridable').defaultTo(false);
|
||||
table.string('from_name');
|
||||
table.boolean('from_name_overridable').defaultTo(false);
|
||||
table.string('subject');
|
||||
table.boolean('subject_overridable').defaultTo(false);
|
||||
table.string('verp_hostname'); // VERP is not used if verp_hostname is null
|
||||
table.string('mailer_type');
|
||||
table.text('mailer_settings', 'longtext');
|
||||
table.timestamp('created').defaultTo(knex.fn.now());
|
||||
table.integer('namespace').unsigned().references('namespaces.id');
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`shares_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('role', 128).notNullable();
|
||||
table.boolean('auto').defaultTo(false);
|
||||
table.primary(['entity', 'user']);
|
||||
});
|
||||
|
||||
await knex.schema.createTable(`permissions_send_configuration`, table => {
|
||||
table.integer('entity').unsigned().notNullable().references(`send_configurations.id`).onDelete('CASCADE');
|
||||
table.integer('user').unsigned().notNullable().references('users.id').onDelete('CASCADE');
|
||||
table.string('operation', 128).notNullable();
|
||||
table.primary(['entity', 'user', 'operation']);
|
||||
});
|
||||
|
||||
await knex.schema.table('lists', table => {
|
||||
table.string('contact_email');
|
||||
table.string('homepage');
|
||||
});
|
||||
|
||||
const settingsRows = await knex('settings').select(['key', 'value']);
|
||||
const settings = {};
|
||||
for (const row of settingsRows) {
|
||||
settings[row.key] = row.value;
|
||||
}
|
||||
|
||||
await knex('lists').update({contact_email: settings.defaultAddress});
|
||||
await knex('lists').update({homepage: settings.defaultHomepage});
|
||||
|
||||
let mailer_settings;
|
||||
let mailer_type;
|
||||
if (settings.mailTransport === 'ses') {
|
||||
mailer_type = MailerType.AWS_SES;
|
||||
mailer_settings = {
|
||||
key: settings.sesKey,
|
||||
secret: settings.sesSecret,
|
||||
region: settings.sesSecret,
|
||||
maxConnections: Number(settings.smtpMaxConnections),
|
||||
throttling: Number(settings.smtpThrottling),
|
||||
logTransactions: !!settings.smtpLog
|
||||
};
|
||||
} else {
|
||||
mailer_type = MailerType.GENERIC_SMTP;
|
||||
mailer_settings = {
|
||||
hostname: settings.smtpHostname,
|
||||
port: Number(settings.smtpPort),
|
||||
encryption: settings.smtpEncryption,
|
||||
useAuth: !settings.smtpDisableAuth,
|
||||
user: settings.smtpUser,
|
||||
password: settings.smtpPass,
|
||||
allowSelfSigned: settings.smtpSelfSigned,
|
||||
maxConnections: Number(settings.smtpMaxConnections),
|
||||
maxMessages: Number(settings.smtpMaxMessages),
|
||||
throttling: Number(settings.smtpThrottling),
|
||||
logTransactions: !!settings.smtpLog
|
||||
};
|
||||
|
||||
if (settings.dkimApiKey) {
|
||||
mailer_type = MailerType.ZONE_MTA;
|
||||
mailer_settings.dkimApiKey = settings.dkimApiKey;
|
||||
mailer_settings.dkimDomain = settings.dkimDomain;
|
||||
mailer_settings.dkimSelector = settings.dkimSelector;
|
||||
mailer_settings.dkimPrivateKey = settings.dkimPrivateKey;
|
||||
}
|
||||
}
|
||||
|
||||
await knex('send_configurations').insert({
|
||||
id: getSystemSendConfigurationId(),
|
||||
name: 'System',
|
||||
description: 'Send configuration used to deliver system emails',
|
||||
from_email: settings.defaultAddress,
|
||||
from_email_overridable: true,
|
||||
from_name: settings.defaultFrom,
|
||||
from_name_overridable: true,
|
||||
subject: settings.defaultSubject,
|
||||
subject_overridable: true,
|
||||
verp_hostname: settings.verpUse ? settings.verpHostname : null,
|
||||
mailer_type,
|
||||
mailer_settings: JSON.stringify(mailer_settings),
|
||||
namespace: getGlobalNamespaceId()
|
||||
});
|
||||
|
||||
await knex('settings').del();
|
||||
await knex('settings').insert([
|
||||
{ key: 'uaCode', value: settings.uaCode },
|
||||
{ key: 'shoutout', value: settings.shoutout },
|
||||
{ key: 'adminEmail', value: settings.adminEmail },
|
||||
{ key: 'defaultHomepage', value: settings.defaultHomepage },
|
||||
{ key: 'pgpPassphrase', value: settings.pgpPassphrase },
|
||||
{ key: 'pgpPrivateKey', value: settings.pgpPrivateKey }
|
||||
]);
|
||||
})();
|
||||
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
await knex.schema
|
||||
.dropTable('shares_send_configuration')
|
||||
.dropTable('permissions_send_configuration')
|
||||
.dropTable('send_configurations')
|
||||
;
|
||||
})();
|
|
@ -1,5 +0,0 @@
|
|||
exports.up = (knex, Promise) => (async() => {
|
||||
})();
|
||||
|
||||
exports.down = (knex, Promise) => (async() => {
|
||||
})();
|
9
shared/namespaces.js
Normal file
9
shared/namespaces.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
'use strict';
|
||||
|
||||
function getGlobalNamespaceId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getGlobalNamespaceId
|
||||
};
|
|
@ -1,12 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const MailerType = {
|
||||
GENERIC_SMTP: 0,
|
||||
ZONE_MTA: 1,
|
||||
AWS_SES: 2,
|
||||
GENERIC_SMTP: 'generic_smtp',
|
||||
ZONE_MTA: 'zone_mta',
|
||||
AWS_SES: 'aws_ses',
|
||||
MAX: 3
|
||||
};
|
||||
|
||||
function getSystemSendConfigurationId() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
MailerType
|
||||
MailerType,
|
||||
getSystemSendConfigurationId
|
||||
};
|
Loading…
Reference in a new issue