Skip to content

Commit c5544cc

Browse files
committed
fix: improved logic for automated SMTP approval (parallel requests, expanded admin email with more detail, added more parking IP's, added more ns providers, added 0.0.0.0 to REGEX_LOCALHOST check, fixed, added more accurate user-agent to HTTP requests, updated snapshots)
1 parent d7353eb commit c5544cc

10 files changed

Lines changed: 192 additions & 83 deletions

File tree

app/controllers/web/my-account/verify-smtp.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,9 @@ async function verifySMTP(ctx) {
151151
);
152152

153153
if (
154-
(ctx.state.user.has_passed_kyc && !hasSomeSuspendedDomains) ||
155-
hasLegitimateHosting ||
156-
hasExistingApprovedDomains
154+
ctx.state.user.has_passed_kyc ||
155+
(hasLegitimateHosting && !hasSomeSuspendedDomains) ||
156+
(hasExistingApprovedDomains && !hasSomeSuspendedDomains)
157157
) {
158158
domain.has_smtp = true;
159159
if (!ctx.api)
@@ -198,7 +198,24 @@ async function verifySMTP(ctx) {
198198
subject
199199
},
200200
locals: {
201-
message: `<a href="${config.urls.web}/admin/domains?name=${domain.name}" class="btn btn-dark btn-md">Review Domain</a>`,
201+
message: `
202+
<ul>
203+
<li><strong>Legitimate Hosting</strong> ${hasLegitimateHosting.toString()}</li>
204+
<li><strong>Suspended Domains</strong> ${hasSomeSuspendedDomains.toString()}</li>
205+
<li><strong>Approved Domains</strong> ${hasExistingApprovedDomains.toString()}</li>
206+
<li>
207+
<strong>NS Provider(s):</strong>
208+
${
209+
ns && ns.length > 0
210+
? `<ul><li>${ns.join('</li><li>')}</li></ul>`
211+
: ''
212+
}
213+
</li>
214+
</ul>
215+
<a href="${config.urls.web}/admin/domains?name=${
216+
domain.name
217+
}" class="btn btn-dark btn-md">Review Domain</a>
218+
`.trim(),
202219
locale
203220
}
204221
})

app/models/domains.js

Lines changed: 27 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,9 @@ async function verifySMTP(domain, resolver, purgeCache = true) {
13631363
let returnPath = false;
13641364
let dmarc = false;
13651365
let hasLegitimateHosting = false;
1366+
let reputableDNS = false;
1367+
let legitimateA = false;
1368+
let httpResponds = false;
13661369

13671370
//
13681371
// attempt to purge Cloudflare cache programmatically
@@ -1434,54 +1437,36 @@ async function verifySMTP(domain, resolver, purgeCache = true) {
14341437
const results = await getNSRecords(domain, resolver);
14351438
ns = results.ns;
14361439
errors.push(...results.errors);
1440+
//
1441+
// check NS records for reputable DNS providers
1442+
//
1443+
if (Array.isArray(ns) && ns.length > 0)
1444+
reputableDNS = hasReputableDNS(ns, domain.name);
14371445
})(),
14381446

14391447
//
1440-
// check domain reputation for auto-approval
1448+
// check A records for legitimate hosting (not parking pages)
14411449
//
14421450
(async function () {
14431451
try {
1444-
//
1445-
// check NS records for reputable DNS providers
1446-
//
1447-
let reputableDNS = false;
1448-
if (Array.isArray(ns) && ns.length > 0) {
1449-
reputableDNS = hasReputableDNS(ns, domain.name);
1450-
}
1451-
1452-
//
1453-
// check A records for legitimate hosting (not parking pages)
1454-
//
1455-
let legitimateA = false;
1456-
try {
1457-
const aRecords = await resolver.resolve4(domain.name, {
1458-
purgeCache: true
1459-
});
1460-
if (Array.isArray(aRecords) && aRecords.length > 0) {
1461-
legitimateA = hasLegitimateHosting(aRecords);
1462-
}
1463-
} catch (err) {
1464-
logger.debug(err);
1465-
}
1466-
1467-
//
1468-
// check HTTP response (basic availability check)
1469-
//
1470-
let httpResponds = false;
1471-
if (legitimateA) {
1472-
try {
1473-
httpResponds = await respondsToHTTP(domain.name);
1474-
} catch (err) {
1475-
logger.debug(err);
1476-
}
1477-
}
1452+
const aRecords = await resolver.resolve4(domain.name, {
1453+
purgeCache: true
1454+
});
1455+
if (Array.isArray(aRecords) && aRecords.length > 0)
1456+
legitimateA = hasLegitimateHosting(aRecords);
1457+
} catch (err) {
1458+
logger.debug(err);
1459+
}
1460+
})(),
14781461

1479-
// domain has legitimate hosting if it has reputable DNS AND legitimate A records AND HTTP response
1480-
hasLegitimateHosting = reputableDNS && legitimateA && httpResponds;
1462+
//
1463+
// check HTTP response (basic availability check)
1464+
//
1465+
(async function () {
1466+
try {
1467+
httpResponds = await respondsToHTTP(domain.name);
14811468
} catch (err) {
14821469
logger.debug(err);
1483-
// reputation check is not required for SMTP, so we don't add to errors
1484-
// this is just for auto-approval logic
14851470
}
14861471
})(),
14871472

@@ -1605,6 +1590,9 @@ async function verifySMTP(domain, resolver, purgeCache = true) {
16051590
})()
16061591
]);
16071592

1593+
// domain has legitimate hosting if it has reputable DNS AND legitimate A records AND HTTP response
1594+
hasLegitimateHosting = reputableDNS && legitimateA && httpResponds;
1595+
16081596
if (!dkim) {
16091597
errors.push(
16101598
Boom.badRequest(

config/smtp-reputation.js

Lines changed: 81 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -37,60 +37,111 @@ const REPUTABLE_DNS_PROVIDER_SLUGS = new Set([
3737

3838
// Known parking/default IPs that indicate non-legitimate hosting
3939
const PARKING_IPS = new Set([
40-
// GoDaddy parking
41-
'184.168.131.241',
42-
'50.63.202.1',
43-
'50.63.202.2',
44-
45-
// Namecheap parking
40+
// GoDaddy Parking & CashParking
41+
// Note: GoDaddy frequently uses Google Cloud IPs for parking. [1]
42+
'3.33.130.190', // New GoDaddy Parking [3]
43+
'15.197.148.33', // New GoDaddy Parking [3]
44+
'34.98.99.30', // GoDaddy Free Parking [1]
45+
'34.102.136.180', // GoDaddy Free Parking [1]
46+
'50.63.202.1', // Old GoDaddy
47+
'184.168.131.241', // Old GoDaddy
48+
49+
// Namecheap Parking
4650
'198.54.117.197',
4751
'198.54.117.198',
4852
'198.54.116.250',
53+
'104.219.251.159', // Added Namecheap
4954

50-
// Google Domains parking
51-
'216.239.32.21',
52-
'216.239.34.21',
53-
'216.239.36.21',
54-
'216.239.38.21',
55+
// Squarespace (formerly Google Domains)
56+
// Google Domains sold to Squarespace. These are common Squarespace IPs.
57+
'198.185.159.144',
58+
'198.185.159.145',
59+
'198.49.23.144',
60+
'198.49.23.145',
5561

56-
// Bluehost default/parking
62+
// Bluehost/Unified Layer Default/Parking
5763
'162.241.216.10',
5864
'50.87.144.227',
65+
'74.220.216.100', // Added Bluehost/Unified Layer
5966

60-
// Network Solutions
67+
// Network Solutions / Register.com (Web.com Group)
6168
'69.46.86.18',
6269
'69.46.86.19',
70+
'209.67.50.203', // Register.com Futuresite [2]
71+
'65.254.248.100',
6372

6473
// Domain.com
6574
'69.46.80.151',
6675

67-
// Register.com
68-
'65.254.248.100',
69-
70-
// Porkbun parking
71-
'104.21.2.106',
72-
'172.67.148.83',
76+
// Porkbun Parking
77+
'64.190.63.111',
7378

74-
// Hover.com parking
75-
'216.40.47.26',
79+
// Hover.com Parking/Forwarding
80+
'216.40.34.41', // Used for forwarding, often indicates a parked state [40]
81+
'64.99.80.28', // General Hover IP [30]
7682

77-
// Name.com parking
83+
// Name.com Parking
7884
'69.46.88.64',
7985

80-
// 123-reg parking
86+
// 123-reg Parking (Part of GoDaddy)
8187
'212.78.114.207',
8288
'212.78.114.211',
89+
'94.136.40.82', // Added 123-reg
8390

84-
// OVH parking
85-
'213.186.33.5',
91+
// OVHcloud Parking
92+
'213.186.33.5', // Often shows "site en construction" [7, 20]
8693
'87.98.231.6',
8794

88-
// CloudFlare parking
89-
'104.21.2.106',
90-
'172.67.148.83',
95+
// Sedo Parking (Major Parking Service)
96+
'64.191.115.111',
97+
'64.191.115.110',
98+
99+
// Afternic (GoDaddy's Resale/Parking Platform)
100+
'52.5.19.209',
101+
'52.72.48.1',
102+
103+
// Tucows (Hover, Enom)
104+
'64.99.64.37',
105+
106+
// IONOS (formerly 1&1)
107+
'74.208.23.19',
108+
'82.165.229.138',
109+
110+
// HostGator (Endurance International Group)
111+
'192.185.16.149',
112+
113+
// NameSilo Parking (namesilo)
114+
'198.105.244.228',
115+
'198.105.251.228',
116+
117+
// Tucows/Hover (hover) - More specific IPs
118+
'64.99.80.28', // Hover default IP
119+
'216.40.34.41', // Hover forwarding service IP
120+
121+
// Wix (wix)
122+
'23.236.62.147', // General Wix IP, often for parked/unassigned domains
123+
124+
// Squarespace (squarespace) - Already added, but confirming from your list
125+
'198.185.159.144',
126+
'198.185.159.145',
127+
'198.49.23.144',
128+
'198.49.23.145',
129+
130+
// IONOS by 1&1 (ionos) - Already added, confirming from your list
131+
'74.208.23.19',
132+
'82.165.229.138',
133+
134+
// Digital Ocean (digital-ocean) - Placeholder IP
135+
'50.116.59.212', // Often used as a placeholder on unconfigured droplets
136+
137+
// Vultr (vultr) - Placeholder IP
138+
'108.61.193.159', // Common default IP for unconfigured instances
139+
140+
// Linode (linode-akamai)
141+
'50.116.59.212', // Also used by Linode for default pages
91142

92-
// Common sinkhole/blackhole IPs
93-
'0.0.0.0'
143+
// SiteGround (siteground)
144+
'198.54.116.250' // Shared with Namecheap, but also used by SiteGround for parked domains
94145
]);
95146

96147
// Function to validate resolved A records are not local/private IPs

config/utilities.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,54 @@ const NS_PROVIDERS = {
465465
'https://portal.ultradns.com/',
466466
false,
467467
''
468+
],
469+
sedo: [
470+
'sedo',
471+
'Sedo',
472+
// Sedo is a major domain parking service
473+
'https://sedo.com/us/login/',
474+
false,
475+
''
476+
],
477+
afternic: [
478+
'afternic',
479+
'Afternic (GoDaddy)',
480+
// Afternic is GoDaddy's domain resale and parking platform
481+
'https://www.afternic.com/login',
482+
false,
483+
''
484+
],
485+
bodis: [
486+
'bodis',
487+
'Bodis',
488+
// Bodis is a domain parking and monetization service
489+
'https://www.bodis.com/login',
490+
false,
491+
''
492+
],
493+
hostgator: [
494+
'hostgator',
495+
'HostGator',
496+
// Part of Endurance International Group (EIG )
497+
'https://portal.hostgator.com/login',
498+
false,
499+
''
500+
],
501+
'unifiedlayer.com': [
502+
'unified-layer',
503+
'Unified Layer (Bluehost/HostGator)',
504+
// The infrastructure behind many EIG brands
505+
'https://www.unifiedlayer.com/', // No public login
506+
false,
507+
''
508+
],
509+
'web.com': [
510+
'web-com-group',
511+
'Web.com Group',
512+
// Parent company of Network Solutions, Register.com
513+
'https://www.web.com/my-account/login',
514+
false,
515+
''
468516
]
469517
};
470518

helpers/check-domain-reputation.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
const { isIP } = require('node:net');
77
const ms = require('ms');
88

9+
const pkg = require('../package.json');
10+
11+
const REGEX_LOCALHOST = require('./regex-localhost');
912
const logger = require('./logger');
1013
const retryRequest = require('./retry-request');
11-
const REGEX_LOCALHOST = require('#helpers/regex-localhost');
12-
const { nsProviderLookup } = require('#config/utilities');
14+
1315
const {
1416
REPUTABLE_DNS_PROVIDER_SLUGS,
1517
isValidPublicIP
1618
} = require('#config/smtp-reputation');
19+
const config = require('#config');
20+
const { nsProviderLookup } = require('#config/utilities');
1721

1822
function hasReputableDNS(nsRecords, domain = null) {
1923
if (!Array.isArray(nsRecords) || nsRecords.length === 0) return false;
@@ -48,8 +52,7 @@ async function respondsToHTTP(domain, timeout = ms('5s')) {
4852
timeout,
4953
retries: 0,
5054
headers: {
51-
'User-Agent':
52-
'Mozilla/5.0 (compatible; ForwardEmail/1.0; +https://forwardemail.net)'
55+
'User-Agent': `Mozilla/5.0 (compatible; ${pkg.name}/${pkg.version}; +${config.urls.web})`
5356
}
5457
});
5558

helpers/regex-localhost.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
// <https://github.com/tinovyatkin/is-localhost-ip>
88

99
const IP_RANGES = [
10+
// 0.0.0.0
11+
/^(::f{4}: )?0\.0\.0\.0$/,
1012
// 10.0.0.0 - 10.255.255.255
1113
/^(:{2}f{4}:)?10(?:\.\d{1,3}){3}$/,
1214
// 127.0.0.0 - 127.255.255.255

test/web/snapshots/index.js.md

Lines changed: 4 additions & 4 deletions
Large diffs are not rendered by default.

test/web/snapshots/index.js.snap

486 Bytes
Binary file not shown.

test/web/snapshots/otp.js.md

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

test/web/snapshots/otp.js.snap

240 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)