diff --git a/CHANGES b/CHANGES index e4f5ae2d9..fc3ecdec2 100644 --- a/CHANGES +++ b/CHANGES @@ -2952,4 +2952,5 @@ * eventlog: Fix incorrect eventlog that the update crashed * database: change charset to utf8mb4. this allows emojis everywhere! * mail: add brevo as relay provider +* mail: add rbl6 check diff --git a/dashboard/src/components/MailDomainStatus.vue b/dashboard/src/components/MailDomainStatus.vue index ab1f8fcf6..f31bb6b13 100644 --- a/dashboard/src/components/MailDomainStatus.vue +++ b/dashboard/src/components/MailDomainStatus.vue @@ -17,6 +17,8 @@ const dnsRecordLabels = { 'ptr6': 'PTR6', }; +const rblTypes = [ 'rbl4', 'rbl6' ]; + const busy = ref(false); const mailConfig = ref({}); const domainStatus = ref({}); @@ -112,25 +114,28 @@ onMounted(async () => { -
-
- - -   - {{ $t('email.smtpStatus.blacklistCheck') }} -
-
-
{{ domainStatus.rbl4.message }}
-
-
-
+
+
+
+ + +   + {{ $t('email.smtpStatus.blacklistCheck') }} +
+
+
{{ domainStatus[type].message }}
+
+
+
+
{{ domainStatus[type].message }}
-
diff --git a/src/mail.js b/src/mail.js index 0fa9818c5..1e8838f7e 100644 --- a/src/mail.js +++ b/src/mail.js @@ -373,6 +373,18 @@ async function checkDmarc(mailDomain) { return result; } +function reverseIPv6(ipv6) { + const parts = ipv6.split('::'); + const left = parts[0].split(':'); + const right = parts[1] ? parts[1].split(':') : []; + const fill = new Array(8 - left.length - right.length).fill('0'); + const full = [...left, ...fill, ...right]; + const expanded = full.map(part => part.padStart(4, '0')).join(''); + const reversed = expanded.split('').reverse().join(''); + const reversedWithDots = reversed.split('').join('.'); + return reversedWithDots; +} + async function checkPtr6(mailDomain, mailFqdn) { assert.strictEqual(typeof mailDomain, 'object'); assert.strictEqual(typeof mailFqdn, 'string'); @@ -394,20 +406,8 @@ async function checkPtr6(mailDomain, mailFqdn) { if (error) return Object.assign(result, { status: 'failed', message: error.message }); if (ip === null) return Object.assign(result, { status: 'skipped', message: 'PTR6 check was skipped, server has no IPv6' }); - function expandIPv6(ipv6) { - const parts = ipv6.split('::'); - const left = parts[0].split(':'); - const right = parts[1] ? parts[1].split(':') : []; - const fill = new Array(8 - left.length - right.length).fill('0'); - const full = [...left, ...fill, ...right]; - return full.map(part => part.padStart(4, '0')).join(''); - } - - const expanded = expandIPv6(ip); - const reversed = expanded.split('').reverse().join(''); - const reversedWithDots = reversed.split('').join('.'); - - result.domain = `${reversedWithDots}.ip6.arpa`; + const reversed = reverseIPv6(ip); + result.domain = `${reversed}.ip6.arpa`; result.name = ip; const [error2, ptrRecords] = await safe(dig.resolve(result.domain, 'PTR', DNS_OPTIONS)); @@ -457,60 +457,52 @@ async function checkPtr4(mailDomain, mailFqdn) { // https://raw.githubusercontent.com/jawsome/node-dnsbl/master/list.json https://multirbl.valli.org/list/ const RBL_LIST = [ { - 'name': 'Abuse.ch', - 'dns': 'spam.abuse.ch', - 'site': 'http://abuse.ch/' - }, - - { - 'name': 'Barracuda', - 'dns': 'b.barracudacentral.org', - 'site': 'http://www.barracudacentral.org/rbl/removal-request' + name: 'Barracuda', + dns: 'b.barracudacentral.org', + site: 'https://barracudacentral.org/', + removal: 'http://www.barracudacentral.org/rbl/removal-request', }, { - 'name': 'Multi SURBL', - 'dns': 'multi.surbl.org', - 'site': 'http://www.surbl.org' + name: 'Multi SURBL', + dns: 'multi.surbl.org', + site: 'http://www.surbl.org', + removal: 'https://surbl.org/surbl-analysis', }, { - 'name': 'Passive Spam Block List', - 'dns': 'psbl.surriel.com', - 'site': 'https://psbl.org' + name: 'Passive Spam Block List', + dns: 'psbl.surriel.com', + site: 'https://psbl.org', + removal: 'https://psbl.org', }, { - 'name': 'Sorbs Aggregate Zone', - 'dns': 'dnsbl.sorbs.net', - 'site': 'http://dnsbl.sorbs.net/' + name: 'SpamCop', + dns: 'bl.spamcop.net', + site: 'http://spamcop.net', + removal: 'https://www.spamcop.net/bl.shtml', }, { - 'name': 'Sorbs spam.dnsbl Zone', - 'dns': 'spam.dnsbl.sorbs.net', - 'site': 'http://sorbs.net' + name: 'SpamHaus Zen', + dns: 'zen.spamhaus.org', + site: 'https://www.spamhaus.org/blocklists/zen-blocklist/', + removal: 'https://check.spamhaus.org/', + ipv6: true }, { - 'name': 'SpamCop', - 'dns': 'bl.spamcop.net', - 'site': 'http://spamcop.net' + name: 'The Unsubscribe Blacklist(UBL)', + dns: 'ubl.unsubscore.com ', + site: 'https://blacklist.lashback.com/', + removal: 'https://blacklist.lashback.com/', }, { - 'name': 'SpamHaus Zen', - 'dns': 'zen.spamhaus.org', - 'site': 'http://spamhaus.org' - }, - { - 'name': 'The Unsubscribe Blacklist(UBL)', - 'dns': 'ubl.unsubscore.com ', - 'site': 'http://www.lashback.com/blacklist/' - }, - { - 'name': 'UCEPROTECT Network', - 'dns': 'dnsbl-1.uceprotect.net', - 'site': 'http://www.uceprotect.net/en' + name: 'UCEPROTECT Network', + dns: 'dnsbl-1.uceprotect.net', // it has 3 "zones" + site: 'http://www.uceprotect.net/en', + removal: 'https://www.uceprotect.net/en/index.php?m=7&s=0', } ]; -// this function currently only looks for black lists based on IP. TODO: also look up by domain -async function checkRbl4(mailDomain) { +async function checkRbl(type, mailDomain) { + assert.strictEqual(typeof type, 'string'); assert.strictEqual(typeof mailDomain, 'object'); if (mailDomain.relay.provider === 'noop') return { status: 'skipped', message: 'Outbound disabled' }; @@ -518,33 +510,38 @@ async function checkRbl4(mailDomain) { const { domain } = mailDomain; - const [error, ip] = await safe(network.getIPv4()); - if (error) return { status: 'failed', ip: null, servers: [], message: `Unable to determine server IPv4: ${error.message}` }; - if (ip === null) return { status: 'skipped', ip: null, servers: [], message: 'RBL check was skipped, server has no IPv4' }; + const [error, ip] = await safe(type === 'ipv4' ? network.getIPv4() : network.getIPv6()); + if (error) return { status: 'failed', ip: null, servers: [], message: `Unable to determine server ${type}: ${error.message}` }; + if (ip === null) return { status: 'skipped', ip: null, servers: [], message: `RBL check was skipped, server has no ${type}` }; - const flippedIp = ip.split('.').reverse().join('.'); + const flippedIp = type === 'ipv4' ? ip.split('.').reverse().join('.') : reverseIPv6(ip); // https://tools.ietf.org/html/rfc5782 const blockedServers = []; for (const rblServer of RBL_LIST) { - const [error, records] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'A', DNS_OPTIONS)); - if (error || !records) continue; // not listed + if (type === 'ipv6' && rblServer[type] !== true) continue; // all support ipv4 - debug(`checkRblStatus: ${domain} (flippedIp: ${flippedIp}) is in the blocklist of ${JSON.stringify(rblServer)}`); + const [error, records] = await safe(dig.resolve(`${flippedIp}.${rblServer.dns}`, 'A', DNS_OPTIONS)); + if (error || !records) continue; // not listed + + debug(`checkRbl: ${domain} flippedIp: ${flippedIp} is in the blocklist of ${rblServer.dns}`); const result = Object.assign({}, rblServer); - const [error2, txtRecords] = await safe(dig.resolve(flippedIp + '.' + rblServer.dns, 'TXT', DNS_OPTIONS)); - result.txtRecords = error2 || !txtRecords ? 'No txt record' : txtRecords.map(x => x.join('')); + const [error2, txtRecords] = await safe(dig.resolve(`${flippedIp}.${rblServer.dns}`, 'TXT', DNS_OPTIONS)); + result.txtRecords = error2 || !txtRecords ? 'No TXT record' : txtRecords.map(x => x.join('')); - debug(`checkRblStatus: ${domain} (error: ${error2?.message || null}) (txtRecords: ${JSON.stringify(txtRecords)})`); + debug(`checkRbl: ${domain} error: ${error2?.message || null} txtRecords: ${JSON.stringify(txtRecords)}`); blockedServers.push(result); } - debug(`checkRblStatus: ${domain} (ip: ${ip}) blockedServers: ${JSON.stringify(blockedServers)})`); - - return { status: blockedServers.length === 0 ? 'passed' : 'failed', ip, servers: blockedServers }; + return { + status: blockedServers.length === 0 ? 'passed' : 'failed', + ip, + servers: blockedServers, + message: `Check using "host ${flippedIp} 127.0.0.150"` + }; } async function getStatus(domain) { @@ -567,7 +564,8 @@ async function getStatus(domain) { { what: 'dkim', promise: checkDkim(mailDomain) }, { what: 'ptr4', promise: checkPtr4(mailDomain, fqdn) }, { what: 'ptr6', promise: checkPtr6(mailDomain, fqdn) }, - { what: 'rbl4', promise: checkRbl4(mailDomain) }, + { what: 'rbl4', promise: checkRbl('ipv4', mailDomain) }, + { what: 'rbl6', promise: checkRbl('ipv6', mailDomain) }, { what: 'relay', promise: checkSmtpRelay(mailDomain.relay) } ]; @@ -579,7 +577,7 @@ async function getStatus(domain) { continue; } - if (response.value.message) debug(`${check.what} : ${response.value.message}`); + if (response.value.message) debug(`${check.what} (${domain}): ${response.value.message}`); safe.set(results, checks[i].what, response.value || {}); }