fix(outbound-mta-relay): ZMS-171 Add support for an outbound MTA relay (#787)

* add support for an outbound MTA relay

* add mtaRelay logic to all necessary parts
This commit is contained in:
NickOvt 2025-03-14 10:46:53 +02:00 committed by GitHub
parent 1f4fc20950
commit 77625e89d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 108 additions and 16 deletions

View file

@ -2634,6 +2634,7 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
let message = result.value.message;
let messageData;
let userData;
try {
messageData = await db.database.collection('messages').findOne(
{
@ -2653,6 +2654,18 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
}
}
);
userData = await db.database.collection('users').findOne(
{
_id: user
},
{
projection: {
_id: true,
mtaRelay: true
}
}
);
} catch (err) {
res.status(500);
return res.json({
@ -2705,7 +2718,8 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
sender: messageData.meta.from,
recipient: messageData.meta.to,
targets: forwardTargets,
stream: response.value
stream: response.value,
userData
};
let queueId;
@ -3983,7 +3997,8 @@ module.exports = (db, server, messageHandler, userHandler, storageHandler, setti
to: envelope.to,
sendTime,
origin: options.origin || options.ip,
runPlugins: true
runPlugins: true,
mtaRelay: userData.mtaRelay || false
},
(err, ...args) => {
if (err || !args[0]) {

View file

@ -58,7 +58,8 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
pubKey: true,
disabled: true,
suspended: true,
fromWhitelist: true
fromWhitelist: true,
mtaRelay: true
}
},
(err, userData) => {
@ -473,7 +474,8 @@ module.exports = (db, server, messageHandler, userHandler, settingsHandler) => {
to: compiledEnvelope.to,
sendTime,
origin: options.ip,
runPlugins: true
runPlugins: true,
mtaRelay: userData.mtaRelay || false
},
(err, ...args) => {
if (err || !args[0]) {

View file

@ -352,6 +352,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
'An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to'
),
mtaRelay: Joi.string()
.uri({
scheme: [/smtps?/],
allowRelative: false,
relativeOnly: false
})
.description('An address of an SMTP MTA relay. The value should be a relay url. If specified uses the this relay as the outbound MTA.'),
spamLevel: Joi.number()
.min(0)
.max(100)
@ -500,6 +508,7 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let values = permission.filter(result.value);
let targets = values.targets;
let mtaRelay = values.mtaRelay;
if (targets) {
for (let i = 0, len = targets.length; i < len; i++) {
@ -535,6 +544,15 @@ module.exports = (db, server, userHandler, settingsHandler) => {
values.targets = targets;
}
if (mtaRelay && /^smtps?:/i.test(mtaRelay)) {
mtaRelay = {
id: new ObjectId(),
type: 'relay',
value: mtaRelay // current mtaRelay string value
};
values.mtaRelay = mtaRelay;
}
if ('pubKey' in req.params && !values.pubKey) {
values.pubKey = '';
}
@ -796,6 +814,7 @@ module.exports = (db, server, userHandler, settingsHandler) => {
.required()
.description('Custom internal metadata object set for this user. Not available for user-role tokens'),
targets: Joi.array().items(Joi.string()).required().description('List of forwarding targets'),
mtaRelay: Joi.string().required().description('MTA Relay url'),
spamLevel: Joi.number()
.required()
.description('Relative scale for detecting spam. 0 means that everything is spam, 100 means that nothing is spam'),
@ -1073,6 +1092,8 @@ module.exports = (db, server, userHandler, settingsHandler) => {
.map(target => target.value)
.filter(target => target),
mtaRelay: userData.mtaRelay?.value || false,
limits: {
quota: {
allowed: Number(userData.quota) || settings['const:max:storage'],
@ -1194,6 +1215,14 @@ module.exports = (db, server, userHandler, settingsHandler) => {
'An array of forwarding targets. The value could either be an email address or a relay url to next MX server ("smtp://mx2.zone.eu:25") or an URL where mail contents are POSTed to'
),
mtaRelay: Joi.string()
.uri({
scheme: [/smtps?/],
allowRelative: false,
relativeOnly: false
})
.description('An address of an SMTP MTA relay. The value should be a relay url. If specified uses the this relay as the outbound MTA.'),
spamLevel: Joi.number()
.min(0)
.max(100)
@ -1326,6 +1355,7 @@ module.exports = (db, server, userHandler, settingsHandler) => {
let targets = values.targets;
let existingTargets;
let mtaRelay = values.mtaRelay;
if (targets) {
for (let i = 0, len = targets.length; i < len; i++) {
@ -1382,6 +1412,15 @@ module.exports = (db, server, userHandler, settingsHandler) => {
}
}
if (mtaRelay && /^smtps?:/i.test(mtaRelay)) {
mtaRelay = {
id: new ObjectId(),
type: 'relay',
value: mtaRelay // current mtaRelay string value
};
values.mtaRelay = mtaRelay;
}
if (!values.name && 'name' in req.params) {
values.name = '';
}

View file

@ -101,7 +101,8 @@ async function autoreply(options, autoreplyData) {
reason: 'autoreply',
from: '',
to: options.sender,
interface: 'autoreplies'
interface: 'autoreplies',
mtaRelay: options.userData?.mtaRelay || false
},
(err, ...args) => {
if (err || !args[0]) {

View file

@ -74,7 +74,8 @@ class FilterHandler {
encryptForwarded: true,
pubKey: true,
spamLevel: true,
tagsview: true
tagsview: true,
mtaRelay: true
};
if (collection === 'users') {

View file

@ -13,7 +13,9 @@ module.exports = (options, callback) => {
targets: options.targets,
interface: 'forwarder'
interface: 'forwarder',
mtaRelay: options.userData?.mtaRelay || false
};
let message = options.maildrop.push(mail, (err, ...args) => {

View file

@ -144,16 +144,42 @@ class Maildropper {
let deliveries = [];
let mxData = false;
if (options.mtaRelay) {
// user has MTA Relay set, use it to send outbound email
let relayData = options.mtaRelay.value;
if (typeof relayData === 'string') {
relayData = tools.getRelayData(relayData);
}
mxData = {
mx: relayData.mx,
mxPort: relayData.mxPort,
mxAuth: relayData.mxAuth,
mxSecure: relayData.mxSecure,
skipSRS: true,
skipSTS: true
};
}
if (options.targets) {
options.targets.forEach(target => {
switch (target.type) {
case 'mail':
deliveries.push({
to: target.value,
forwardedFor: target.recipient
});
break;
{
let delivery = {
to: target.value,
forwardedFor: target.recipient
};
if (mxData) {
delivery = { ...delivery, ...mxData };
}
deliveries.push(delivery);
}
break;
case 'relay':
{
let recipients = new Set([].concat(options.to || []).concat(target.recipient || []));
@ -196,9 +222,15 @@ class Maildropper {
}
if (!deliveries.length) {
deliveries = envelope.to.map(to => ({
to
}));
deliveries = envelope.to.map(to => {
let delivery = { to };
if (mxData) {
delivery = { ...delivery, ...mxData };
}
return delivery;
});
}
if (!deliveries.length) {

View file

@ -61,7 +61,7 @@ secret=\"$ZONEMTA_SECRET\"
algo=\"md5\"" > /etc/zone-mta/plugins/loop-breaker.toml
echo "[wildduck]
enabled=[\"receiver\", \"sender\"]
enabled=[\"receiver\", \"sender\", \"main\"]
# which interfaces this plugin applies to
interfaces=[\"feeder\"]