Built-in Zone MTA

Plugin for ZoneMTA for per-message DKIM keys.
This commit is contained in:
Tomas Bures 2018-12-16 22:35:21 +01:00
parent d103a2cc79
commit 77c64f487d
18 changed files with 231 additions and 110 deletions

View file

@ -142,14 +142,16 @@ export default class CUD extends Component {
@withAsyncErrorHandler
async fetchSendConfiguration(sendConfigurationId) {
this.fetchSendConfigurationId = sendConfigurationId;
if (sendConfigurationId) {
this.fetchSendConfigurationId = sendConfigurationId;
const result = await axios.get(getUrl(`rest/send-configurations-public/${sendConfigurationId}`));
const result = await axios.get(getUrl(`rest/send-configurations-public/${sendConfigurationId}`));
if (sendConfigurationId === this.fetchSendConfigurationId) {
this.setState({
sendConfiguration: result.data
});
if (sendConfigurationId === this.fetchSendConfigurationId) {
this.setState({
sendConfiguration: result.data
});
}
}
}

View file

@ -442,7 +442,8 @@ class TextArea extends Component {
label: PropTypes.string.isRequired,
placeholder: PropTypes.string,
help: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
format: PropTypes.string
format: PropTypes.string,
className: PropTypes.string
}
static contextTypes = {
@ -452,11 +453,12 @@ class TextArea extends Component {
render() {
const props = this.props;
const owner = this.context.formStateOwner;
const id = this.props.id;
const id = props.id;
const htmlId = 'form_' + id;
const className = props.className || ''
return wrapInput(id, htmlId, owner, props.format, '', props.label, props.help,
<textarea id={htmlId} placeholder={props.placeholder} value={owner.getFormValue(id) || ''} className="form-control" aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
<textarea id={htmlId} placeholder={props.placeholder} value={owner.getFormValue(id) || ''} className={`form-control ${className}`} aria-describedby={htmlId + '_help'} onChange={evt => owner.updateFormValue(id, evt.target.value)}></textarea>
);
}
}

View file

@ -82,6 +82,7 @@ export default class CUD extends Component {
this.getFormValuesFromEntity(this.props.entity, data => {
this.mailerTypes[data.mailer_type].afterLoad(data);
data.verpEnabled = !!data.verp_hostname;
data.verp_hostname = data.verp_hostname || '';
});
} else {
@ -89,9 +90,13 @@ export default class CUD extends Component {
name: '',
description: '',
namespace: mailtrainConfig.user.namespace,
from_email: '',
from_email_overridable: false,
from_name: '',
from_name_overridable: false,
reply_to: '',
reply_to_overridable: false,
subject: '',
subject_overridable: false,
verpEnabled: false,
verp_hostname: '',

View file

@ -2,7 +2,7 @@
import React from "react";
import {MailerType} from "../../../shared/send-configurations";
import {MailerType, ZoneMTAType} from "../../../shared/send-configurations";
import {
CheckBox,
Dropdown,
@ -11,6 +11,7 @@ import {
TextArea
} from "../lib/form";
import {Trans} from "react-i18next";
import styles from "./styles.scss";
export const mailerTypesOrder = [
MailerType.ZONE_MTA,
@ -139,6 +140,12 @@ export function getMailerTypes(t) {
{ key: 'eu-west-1', label: t('euwest1')}
];
const zoneMtaTypeOptions = [
{ key: ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF, label: t('Dynamic configuration of DKIM keys via ZoneMTA\'s Mailtrain plugin (use this option for builtin ZoneMTA)')},
{ key: ZoneMTAType.WITH_HTTP_CONF, label: t('Dynamic configuration of DKIM keys via ZoneMTA\'s HTTP config plugin')},
{ key: ZoneMTAType.REGULAR, label: t('No dynamic configuration of DKIM keys')}
]
mailerTypes[MailerType.GENERIC_SMTP] = {
getForm: owner =>
<div>
@ -182,39 +189,49 @@ export function getMailerTypes(t) {
};
mailerTypes[MailerType.ZONE_MTA] = {
getForm: owner =>
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
getForm: owner => {
const zoneMtaType = Number.parseInt(owner.getFormValue('zoneMtaType'));
return (
<div>
<Fieldset label={t('mailerSettings')}>
<Dropdown id="mailer_type" label={t('mailerType')} options={typeOptions}/>
<Dropdown id="zoneMtaType" label={t('Dynamic configuration')} options={zoneMtaTypeOptions}/>
<InputField id="smtpHostname" label={t('hostname')} placeholder={t('hostnameEgSmtpexamplecom')}/>
<InputField id="smtpPort" label={t('port')} placeholder={t('portEg465AutodetectedIfLeftBlank')}/>
<Dropdown id="smtpEncryption" label={t('encryption')} options={smtpEncryptionOptions}/>
<CheckBox id="smtpUseAuth" text={t('enableSmtpAuthentication')}/>
{ owner.getFormValue('smtpUseAuth') &&
<div>
<InputField id="smtpUser" label={t('username')} placeholder={t('usernameEgMyaccount@examplecom')}/>
<InputField id="smtpPassword" label={t('password')} placeholder={t('usernameEgMyaccount@examplecom')}/>
</div>
}
</Fieldset>
{(zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF || zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) &&
<Fieldset label={t('dkimSigning')}>
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><p>If you are using ZoneMTA then Mailtrain can provide a DKIM key for signing all outgoing messages.</p></Trans>
<Trans i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><p className="text-warning">Do not use sensitive keys here. The private key is not encrypted in the database.</p></Trans>
{zoneMtaType === ZoneMTAType.WITH_HTTP_CONF &&
<InputField id="dkimApiKey" label={t('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
}
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
<TextArea id="dkimPrivateKey" className={styles.dkimPrivateKey} label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
</Fieldset>
}
</Fieldset>
<Fieldset label={t('dkimSigning')}>
<Trans i18nKey="ifYouAreUsingZoneMtaThenMailtrainCan"><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 i18nKey="doNotUseSensitiveKeysHereThePrivateKeyIs"><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('zoneMtaDkimApiKey')} help={t('secretValueKnownToZoneMtaForRequesting')}/>
<InputField id="dkimDomain" label={t('dkimDomain')} help={t('leaveBlankToUseTheSenderEmailAddress')}/>
<InputField id="dkimSelector" label={t('dkimKeySelector')} help={t('signingIsDisabledWithoutAValidSelector')}/>
<TextArea id="dkimPrivateKey" label={t('dkimPrivateKey')} placeholder={t('beginsWithBeginRsaPrivateKey')} help={t('signingIsDisabledWithoutAValidPrivateKey')}/>
</Fieldset>
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>,
<Fieldset label={t('advancedMailerSettings')}>
<CheckBox id="logTransactions" text={t('logSmtpTransactions')}/>
<CheckBox id="smtpAllowSelfSigned" text={t('allowSelfsignedCertificates')}/>
<InputField id="maxConnections" label={t('maxConnections')} placeholder={t('theCountOfMaxConnectionsEg10')} help={t('theCountOfMaximumSimultaneousConnections')}/>
<InputField id="smtpMaxMessages" label={t('maxMessages')} placeholder={t('theCountOfMaxMessagesEg100')} help={t('theNumberOfMessagesToSendThroughASingle')}/>
<InputField id="throttling" label={t('throttling')} placeholder={t('messagesPerHourEg1000')} help={t('maximumNumberOfMessagesToSendInAnHour')}/>
</Fieldset>
</div>
);
},
initData: () => ({
...getInitGenericSMTP(),
zoneMtaType: ZoneMTAType.REGULAR,
dkimApiKey: '',
dkimDomain: '',
dkimSelector: '',
@ -222,6 +239,7 @@ export function getMailerTypes(t) {
}),
afterLoad: data => {
afterLoadGenericSMTP(data);
data.zoneMtaType = data.mailer_settings.zoneMtaType;
data.dkimApiKey = data.mailer_settings.dkimApiKey;
data.dkimDomain = data.mailer_settings.dkimDomain;
data.dkimSelector = data.mailer_settings.dkimSelector;
@ -229,10 +247,17 @@ export function getMailerTypes(t) {
},
beforeSave: 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;
const zoneMtaType = Number.parseInt(data.zoneMtaType);
data.mailer_settings.zoneMtaType = zoneMtaType;
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF || zoneMtaType === ZoneMTAType.WITH_MAILTRAIN_HEADER_CONF) {
data.mailer_settings.dkimDomain = data.dkimDomain;
data.mailer_settings.dkimSelector = data.dkimSelector;
data.mailer_settings.dkimPrivateKey = data.dkimPrivateKey;
}
if (zoneMtaType === ZoneMTAType.WITH_HTTP_CONF) {
data.mailer_settings.dkimApiKey = data.dkimApiKey;
}
clearBeforeSave(data);
},
afterTypeChange: mutState => {

View file

@ -0,0 +1,3 @@
textarea.dkimPrivateKey {
height: 200px;
}

View file

@ -215,6 +215,9 @@ testServer:
password: testpass
logger: false
builtinZoneMTA:
enabled: true
seleniumWebDriver:
browser: phantomjs

View file

@ -23,4 +23,6 @@ passwordresetlink="xxx"
[reports]
enabled=true
[redis]
enabled=true
enabled=true
[log]
level="verbose"

View file

@ -21,6 +21,7 @@ const privilegeHelpers = require('./lib/privilege-helpers');
const knex = require('./lib/knex');
const shares = require('./models/shares');
const { AppType } = require('../shared/app');
const builtinZoneMta = require('./lib/builtin-zone-mta');
const trustedPort = config.www.trustedPort;
const sandboxPort = config.www.sandboxPort;
@ -94,29 +95,31 @@ dbcheck(err => { // Check if database needs upgrading before starting the server
.then(() =>
executor.spawn(() =>
testServer(() =>
verpServer(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges();
verpServer(() =>
builtinZoneMta.spawn(() =>
startHTTPServer(AppType.TRUSTED, 'trusted', trustedPort, () =>
startHTTPServer(AppType.SANDBOXED, 'sandbox', sandboxPort, () =>
startHTTPServer(AppType.PUBLIC, 'public', publicPort, () => {
privilegeHelpers.dropRootPrivileges();
tzupdate.start();
tzupdate.start();
importer.spawn(() =>
feedcheck.spawn(() =>
senders.spawn(() => {
triggers.start();
gdprCleanup.start();
importer.spawn(() =>
feedcheck.spawn(() =>
senders.spawn(() => {
triggers.start();
gdprCleanup.start();
postfixBounceServer(async () => {
await reportProcessor.init();
log.info('Service', 'All services started');
appBuilder.setReady();
});
})
)
);
})
postfixBounceServer(async () => {
await reportProcessor.init();
log.info('Service', 'All services started');
appBuilder.setReady();
});
})
)
);
})
)
)
)
)

View file

@ -0,0 +1,45 @@
'use strict';
const config = require('config');
const fork = require('child_process').fork;
const log = require('./log');
const path = require('path');
let zoneMtaProcess;
module.exports = {
spawn
};
function spawn(callback) {
if (config.builtinZoneMTA.enabled) {
log.info('ZoneMTA', 'Starting built-in Zone MTA process');
zoneMtaProcess = fork(
path.join(__dirname, '..', '..', 'zone-mta', 'index.js'),
['--config=' + path.join(__dirname, '..', '..', 'zone-mta', 'config', 'zonemta.js')],
{
cwd: path.join(__dirname, '..', '..', 'zone-mta'),
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);
});
} else {
callback();
}
}

View file

@ -328,9 +328,9 @@ class CampaignSender {
const getOverridable = key => {
if (sendConfiguration[key + '_overridable'] && this.campaign[key + '_override'] !== null) {
return campaign[key + '_override'];
return campaign[key + '_override'] || '';
} else {
return sendConfiguration[key];
return sendConfiguration[key] || '';
}
}

View file

@ -22,7 +22,7 @@ const transports = new Map();
async function getOrCreateMailer(sendConfigurationId) {
let sendConfiguration;
if (!sendConfiguration) {
if (!sendConfigurationId) {
sendConfiguration = await sendConfigurations.getSystemSendConfiguration();
} else {
sendConfiguration = await sendConfigurations.getById(contextHelpers.getAdminContext(), sendConfigurationId, false, true);
@ -38,8 +38,35 @@ function invalidateMailer(sendConfigurationId) {
function _addDkimKeys(transport, mail) {
const sendConfiguration = transport.mailer.sendConfiguration;
if (sendConfiguration.mailer_type === sendConfigurations.MailerType.ZONE_MTA) {
if (!mail.headers) {
mail.headers = {};
}
const dkimDomain = sendConfiguration.mailer_settings.dkimDomain;
const dkimSelector = (sendConfiguration.mailer_settings.dkimSelector || '').trim();
const dkimPrivateKey = (sendConfiguration.mailer_settings.dkimPrivateKey || '').trim();
if (dkimSelector && dkimPrivateKey) {
const from = (mail.from.address || '').trim();
const domain = from.split('@').pop().toLowerCase().trim();
mail.headers['x-mailtrain-dkim'] = JSON.stringify({
domainName: dkimDomain || domain,
keySelector: dkimSelector,
privateKey: dkimPrivateKey
});
}
}
}
async function _sendMail(transport, mail, template) {
_addDkimKeys(transport, mail);
let tryCount = 0;
const trySend = (callback) => {
tryCount++;
@ -209,6 +236,7 @@ async function _createTransport(sendConfiguration) {
}
transport.mailer = {
sendConfiguration,
throttleWait: bluebird.promisify(throttleWait),
sendTransactionalMail: async (mail, template) => await _sendTransactionalMail(transport, mail, template),
sendMassMail: async (mail, template) => await _sendMail(transport, mail)

View file

@ -666,46 +666,33 @@ async function getMessageByCid(messageCid) {
const [campaignCid, listCid, subscriptionCid] = messageCidElems;
await knex.transaction(async tx => {
return await knex.transaction(async tx => {
const list = await tx('lists').where('cid', listCid).select('id');
const subscrTblName = subscriptions.getSubscriptionTableName(list.id);
const message = await tx('campaign_messages')
.innerJoin('campaigns', 'campaign_messages.campaign', 'campaigns.id')
.innerJoin(subscrTblName, subscrTblName + '.id', 'campaign_messages.subscription')
.leftJoin('segments', 'segment.id', 'campaign_messages.segment') // This is just to make sure that the respective segment still exists or return null if it doesn't
.leftJoin('send_configurations', 'send_configurations.id', 'campaign_messages.send_configuration') // This is just to make sure that the respective send_configuration still exists or return null if it doesn't
.where(subscrTblName + '.cid', subscriptionCid)
.where('campaigns.cid', campaignCid)
.select([
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'segments.id AS segment', 'campaign_messages.subscription',
'send_configurations.id AS send_configuration', 'campaign_messages.status', 'campaign_messages.response', 'campaign_messages.response_id',
'campaign_messages.updated', 'campaign_messages.created', 'send_configurations.verp_hostname AS verp_hostname'
]);
if (message) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', message.campaign, 'manageMessages');
}
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
])
.first();
return message;
});
}
async function getMessageByResponseId(responseId) {
await knex.transaction(async tx => {
return await knex.transaction(async tx => {
console.log(responseId);
const message = await tx('campaign_messages')
.leftJoin('segments', 'segment.id', 'campaign_messages.segment') // This is just to make sure that the respective segment still exists or return null if it doesn't
.leftJoin('send_configurations', 'send_configurations.id', 'campaign_messages.send_configuration') // This is just to make sure that the respective send_configuration still exists or return null if it doesn't
.where('campaign_messages.response_id', responseId)
.select([
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'segments.id AS segment', 'campaign_messages.subscription',
'send_configurations.id AS send_configuration', 'campaign_messages.status', 'campaign_messages.response', 'campaign_messages.response_id',
'campaign_messages.updated', 'campaign_messages.created', 'send_configurations.verp_hostname AS verp_hostname'
]);
if (message) {
await shares.enforceEntityPermissionTx(tx, context, 'campaign', message.campaign, 'manageMessages');
}
'campaign_messages.id', 'campaign_messages.campaign', 'campaign_messages.list', 'campaign_messages.subscription', 'campaign_messages.status'
])
.first();
return message;
});
@ -763,6 +750,7 @@ async function changeStatusByMessage(context, message, subscriptionStatus, updat
}
await _changeStatusByMessageTx(tx, context, message, subscriptionStatus);
});
}

View file

@ -180,22 +180,28 @@ router.postAsync('/mailgun', uploads.any(), async (req, res) => {
router.postAsync('/zone-mta', async (req, res) => {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
if (req.body.id) {
const message = await campaigns.getMessageByCid(req.body.id);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
try {
if (typeof req.body === 'string') {
req.body = JSON.parse(req.body);
}
}
res.json({
success: true
});
if (req.body.id) {
const message = await campaigns.getMessageByResponseId(req.body.id);
console.log(message);
if (message) {
await campaigns.changeStatusByMessage(contextHelpers.getAdminContext(), message, SubscriptionStatus.BOUNCED, true);
log.verbose('ZoneMTA', 'Marked message %s as bounced', req.body.id);
}
}
res.json({
success: true
});
} catch (err) {
console.log(err);
throw err;
}
});

View file

@ -164,7 +164,7 @@ async function processCampaign(campaignId) {
}
} catch (err) {
log.error('Senders', `Sending campaign ${campaignId} failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
}
@ -204,7 +204,7 @@ async function scheduleCampaigns() {
}
} catch (err) {
log.error('Senders', `Scheduling campaigns failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
@ -249,7 +249,7 @@ async function processQueued() {
}
} catch (err) {
log.error('Senders', `Processing queued messages failed with error: ${err.message}`)
log.verbose(err);
log.verbose(err.stack);
}
queuedSchedulerRunning = false;
@ -326,7 +326,7 @@ async function init() {
scheduleCampaigns();
} else if (type === 'reload-config') {
for (const worker of workerProcesses.keys()) {
for (const workerId of workerProcesses.keys()) {
sendToWorker(workerId, 'reload-config', msg.data);
}
}

View file

@ -35,7 +35,7 @@ async function processMessages(campaignId, subscribers) {
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);
log.verbose(err.stack);
}
}

View file

@ -4,7 +4,7 @@ const contextHelpers = require('../../../lib/context-helpers');
const mosaicoTemplates = require('../../../../shared/mosaico-templates');
const {getGlobalNamespaceId} = require('../../../../shared/namespaces');
const {getAdminId} = require('../../../../shared/users');
const { MailerType, getSystemSendConfigurationId, getSystemSendConfigurationCid } = require('../../../../shared/send-configurations');
const { MailerType, ZoneMTAType, getSystemSendConfigurationId, getSystemSendConfigurationCid } = require('../../../../shared/send-configurations');
const { enforce } = require('../../../lib/helpers');
const { EntityVals: TriggerEntityVals, EventVals: TriggerEventVals } = require('../../../../shared/triggers');
const { SubscriptionSource } = require('../../../../shared/lists');
@ -753,6 +753,7 @@ async function migrateSettings(knex) {
if (settings.dkimApiKey) {
mailer_type = MailerType.ZONE_MTA;
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;

View file

@ -6,6 +6,12 @@ const MailerType = {
AWS_SES: 'aws_ses'
};
const ZoneMTAType = {
REGULAR: 0,
WITH_HTTP_CONF: 1,
WITH_MAILTRAIN_HEADER_CONF: 2
}
function getSystemSendConfigurationId() {
return 1;
}
@ -16,6 +22,7 @@ function getSystemSendConfigurationCid() {
module.exports = {
MailerType,
ZoneMTAType,
getSystemSendConfigurationId,
getSystemSendConfigurationCid
};

1
zone-mta Submodule

@ -0,0 +1 @@
Subproject commit 7990c49dda71c8cb65ddd83da3b785711d26894b