Skip to content

Commit

Permalink
feat(submit): Allow to set proxy url and local address when submittin…
Browse files Browse the repository at this point in the history
…g emails for delivery
  • Loading branch information
andris9 committed Feb 23, 2024
1 parent a7c6abc commit af1d253
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 82 deletions.
2 changes: 1 addition & 1 deletion data/google-crawlers.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"creationTime": "2024-02-13T23:00:54.000000",
"creationTime": "2024-02-20T23:01:07.000000",
"prefixes": [
{
"ipv6Prefix": "2001:4860:4801:2008::/64"
Expand Down
29 changes: 26 additions & 3 deletions lib/connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,16 @@ class Connection {
if (!imapConnectionConfig.tls) {
imapConnectionConfig.tls = {};
}
imapConnectionConfig.tls.localAddress = (await getLocalAddress(redis, 'imap', this.account)).localAddress;
const localAddress = await getLocalAddress(redis, 'imap', this.account);
imapConnectionConfig.tls.localAddress = localAddress.localAddress;
this.logger.info({
msg: 'Selected local address',
account: this.account,
proto: 'IMAP',
address: localAddress.localAddress,
name: localAddress.name,
selector: localAddress.addressSelector
});

for (let key of Object.keys(TLS_DEFAULTS)) {
if (!(key in imapConnectionConfig.tls)) {
Expand Down Expand Up @@ -2036,7 +2045,15 @@ class Connection {
}
}

let { localAddress: address, name } = await getLocalAddress(redis, 'smtp', this.account);
let { localAddress: address, name, addressSelector: selector } = await getLocalAddress(redis, 'smtp', this.account, data.localAddress);
this.logger.info({
msg: 'Selected local address',
account: this.account,
proto: 'SMTP',
address,
name,
selector
});

let smtpLogger = {};
let smtpSettings = Object.assign(
Expand Down Expand Up @@ -2087,7 +2104,9 @@ class Connection {
}

// set up proxy if needed
if (accountData.proxy) {
if (data.proxy) {
smtpSettings.proxy = data.proxy;
} else if (accountData.proxy) {
smtpSettings.proxy = accountData.proxy;
} else {
let proxyUrl = await settings.get('proxyUrl');
Expand Down Expand Up @@ -2996,6 +3015,8 @@ class Connection {
sentMailPath: data.sentMailPath,
feedbackKey: data.feedbackKey || null,
dsn: data.dsn || null,
proxy: data.proxy || null,
localAddress: data.localAddress || null,
raw
});

Expand All @@ -3015,6 +3036,8 @@ class Connection {
messageId,
envelope,
subject,
proxy: data.proxy,
localAddress: data.localAddress,
created: now.getTime()
});

Expand Down
37 changes: 2 additions & 35 deletions lib/routes-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const {
readEnvValue,
getServiceHostname,
getByteSize,
parseSignedFormData
parseSignedFormData,
updatePublicInterfaces
} = require('./tools');
const packageData = require('../package.json');
const he = require('he');
Expand All @@ -31,7 +32,6 @@ const { oauth2Apps, LEGACY_KEYS, OAUTH_PROVIDERS, oauth2ProviderData } = require
const { autodetectImapSettings } = require('./autodetect-imap-settings');
const getSecret = require('./get-secret');
const humanize = require('humanize');
const { resolvePublicInterfaces } = require('pubface');
const os = require('os');
const { ADDRESS_STRATEGIES, settingsSchema, templateSchemas, oauthCreateSchema, accountIdSchema } = require('./schemas');
const fs = require('fs');
Expand Down Expand Up @@ -559,39 +559,6 @@ function formatServerState(state, payload) {
}
}

async function updatePublicInterfaces() {
let interfaces = await resolvePublicInterfaces();

for (let iface of interfaces) {
if (!iface.localAddress) {
continue;
}

if (iface.defaultInterface) {
await redis.hset(`${REDIS_PREFIX}interfaces`, `default:${iface.family}`, iface.localAddress);
}

let existingEntry = await redis.hget(`${REDIS_PREFIX}interfaces`, iface.localAddress);
if (existingEntry) {
try {
existingEntry = JSON.parse(existingEntry);

iface.name = iface.name || existingEntry.name;

if (!iface.localAddress || !iface.ip || !iface.name) {
// not much point in updating
continue;
}
} catch (err) {
// ignore?
}
}

delete iface.defaultInterface;
await redis.hset(`${REDIS_PREFIX}interfaces`, iface.localAddress, JSON.stringify(iface));
}
}

async function getOpenAiError() {
let openAiError = await redis.get(`${REDIS_PREFIX}:openai:error`);
if (openAiError) {
Expand Down
67 changes: 62 additions & 5 deletions lib/tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const bullmqPackage = require('bullmq/package.json');
const v8 = require('node:v8');
const { reverse: dnsReverse } = require('dns').promises;
const googleCrawlerRanges = require('../data/google-crawlers.json');
const { resolvePublicInterfaces } = require('pubface');

const { fetch: fetchCmd, Agent } = require('undici');
const fetchAgent = new Agent({ connect: { timeout: FETCH_TIMEOUT } });
Expand Down Expand Up @@ -1462,11 +1463,57 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg
return hostname;
},

async getLocalAddress(redis, protocol, account) {
async updatePublicInterfaces(redis) {
let interfaces = await resolvePublicInterfaces();

for (let iface of interfaces) {
if (!iface.localAddress) {
continue;
}

if (iface.defaultInterface) {
await redis.hset(`${REDIS_PREFIX}interfaces`, `default:${iface.family}`, iface.localAddress);
}

let existingEntry = await redis.hget(`${REDIS_PREFIX}interfaces`, iface.localAddress);
if (existingEntry) {
try {
existingEntry = JSON.parse(existingEntry);

iface.name = iface.name || existingEntry.name;

if (!iface.localAddress || !iface.ip || !iface.name) {
// not much point in updating
continue;
}
} catch (err) {
// ignore?
}
}

delete iface.defaultInterface;
await redis.hset(`${REDIS_PREFIX}interfaces`, iface.localAddress, JSON.stringify(iface));
}
},

async getLocalAddress(redis, protocol, account, hint) {
let existingAddresses = Object.values(os.networkInterfaces())
.flatMap(entry => entry)
.map(entry => entry.address);

if (hint) {
let parsedHint = ipaddr.parse(hint);
let normalizedHint = parsedHint.toNormalizedString();
let iface = await redis.hget(`${REDIS_PREFIX}interfaces`, normalizedHint);
try {
iface = iface ? JSON.parse(iface) : null;
} catch (err) {}

Check failure on line 1510 in lib/tools.js

View workflow job for this annotation

GitHub Actions / test (16.x, ubuntu-20.04)

Empty block statement

Check failure on line 1510 in lib/tools.js

View workflow job for this annotation

GitHub Actions / test (18.x, ubuntu-20.04)

Empty block statement

Check failure on line 1510 in lib/tools.js

View workflow job for this annotation

GitHub Actions / test (20.x, ubuntu-20.04)

Empty block statement
if (iface && existingAddresses.includes(iface.localAddress)) {
iface.addressSelector = 'hint';
return iface;
}
}

let addressStartegy = await settings.get(`${protocol}Strategy`);
let localAddresses = []
.concat((await settings.get(`localAddresses`)) || [])
Expand All @@ -1476,35 +1523,45 @@ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEV3QUiYsp13nD9suD1/ZkEXnuMoSg

let hostname = await module.exports.getServiceHostname(await settings.get('smtpEhloName'));

let addressSelector;

if (!localAddresses.length) {
return { address: false, name: hostname };
addressSelector = 'default';
return { address: false, name: hostname, addressSelector };
} else if (localAddresses.length === 1) {
addressSelector = 'single';
localAddress = localAddresses[0];
} else {
switch (addressStartegy) {
case 'random': {
addressSelector = 'random';
localAddress = localAddresses[Math.floor(Math.random() * localAddresses.length)];
break;
}
case 'dedicated':
addressSelector = 'dedicated';
localAddress = module.exports.selectRendezvousAddress(account, localAddresses);
break;
default:
return { address: false, name: hostname };
addressSelector = 'unknown';
return { address: false, name: hostname, addressSelector };
}
}

if (!localAddress) {
return { address: false, name: hostname };
addressSelector = 'unset';
return { address: false, name: hostname, addressSelector };
}

try {
let addressData = JSON.parse(await redis.hget(`${REDIS_PREFIX}interfaces`, localAddress));
addressData.name = addressData.name || hostname;
addressData.addressSelector = addressSelector;
return addressData;
} catch (err) {
logger.error({ msg: 'Failed to load address data', localAddress, err });
return { address: false, name: hostname };
addressSelector = 'error';
return { address: false, name: hostname, addressSelector };
}
},

Expand Down
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"ace-builds": "1.32.6",
"base32.js": "0.1.0",
"bull-arena": "4.2.0",
"bullmq": "5.2.1",
"bullmq": "5.3.1",
"compare-versions": "6.1.0",
"dotenv": "16.4.5",
"encoding-japanese": "2.0.0",
Expand All @@ -79,7 +79,7 @@
"ioredfour": "1.3.0-ioredis-07",
"ioredis": "5.3.2",
"ipaddr.js": "2.1.0",
"joi": "17.12.1",
"joi": "17.12.2",
"jquery": "3.7.1",
"libbase64": "1.2.1",
"libmime": "5.2.1",
Expand All @@ -93,15 +93,15 @@
"murmurhash": "2.0.1",
"nanoid": "3.3.4",
"node-gettext": "3.0.0",
"nodemailer": "6.9.9",
"nodemailer": "6.9.10",
"pino": "8.19.0",
"prom-client": "15.1.0",
"psl": "1.9.0",
"pubface": "1.0.7",
"pubface": "1.0.8",
"punycode": "2.3.1",
"qrcode": "1.5.3",
"smtp-server": "3.13.2",
"socks": "2.7.3",
"socks": "2.8.1",
"speakeasy": "2.0.0",
"startbootstrap-sb-admin-2": "3.3.7",
"timezones-list": "3.0.3",
Expand Down
Loading

0 comments on commit af1d253

Please sign in to comment.