diff options
author | David Timber <dxdt@dev.snart.me> | 2024-08-19 02:17:29 +0200 |
---|---|---|
committer | David Timber <dxdt@dev.snart.me> | 2024-08-19 02:23:35 +0200 |
commit | f350500627967412318c788dc516cc6f99c37b58 (patch) | |
tree | 911f4965f611a268efeaacd2f767d3e0ef013728 /mx-aaaa | |
parent | ef5b99386d8c021d9cba38a7a615cdbfcbe80477 (diff) |
Add mx-aaaa
Diffstat (limited to 'mx-aaaa')
-rw-r--r-- | mx-aaaa/README.md | 9 | ||||
-rw-r--r-- | mx-aaaa/domains.txt | 46 | ||||
-rw-r--r-- | mx-aaaa/image.png | bin | 0 -> 338102 bytes | |||
l--------- | mx-aaaa/index.html | 1 | ||||
-rw-r--r-- | mx-aaaa/modules/mx-aaaa.js | 190 | ||||
-rw-r--r-- | mx-aaaa/resolve-mx-aaaa.css | 53 | ||||
-rw-r--r-- | mx-aaaa/resolve-mx-aaaa.html | 39 | ||||
-rw-r--r-- | mx-aaaa/resolve-mx-aaaa.js | 140 |
8 files changed, 478 insertions, 0 deletions
diff --git a/mx-aaaa/README.md b/mx-aaaa/README.md new file mode 100644 index 0000000..243875b --- /dev/null +++ b/mx-aaaa/README.md @@ -0,0 +1,9 @@ +# Resolve AAAA MX of popular email service providers +See how many email providers support IPv6 MX servers. DNS query is done entirely +on the browser. No server involved(except for the public DNS APIs). + +https://dxdxdt.github.io/mx-aaaa + + + +Made to track progress of major service providers' transition to IPv6. diff --git a/mx-aaaa/domains.txt b/mx-aaaa/domains.txt new file mode 100644 index 0000000..eccd8ed --- /dev/null +++ b/mx-aaaa/domains.txt @@ -0,0 +1,46 @@ +# US
+## Amazon WorkMail
+gemings.awsapps.com
+## att.net
+att.net
+## Exchange (Office365)
+glockapps.tech
+## G Suite
+buyemailsoftware.com
+## gmx.com
+gmx.com
+## mailo.com
+mailo.com
+## Zoho
+zohomail.eu
+
+# AOL
+#aol.com
+## Gmail
+gmail.com
+## Hotmail
+hotmail.com
+## Outlook
+outlook.com
+## Yahoo
+yahoo.com
+
+# Europe
+proton.me
+seznam.cz
+laposte.net
+freenet.de
+gmx.de
+web.de
+interia.pl
+o2.pl
+onet.pl
+ukr.net
+
+# Russia
+mail.ru
+rambler.ru
+yandex.ru
+
+# Asia
+naver.com
diff --git a/mx-aaaa/image.png b/mx-aaaa/image.png Binary files differnew file mode 100644 index 0000000..ba9835a --- /dev/null +++ b/mx-aaaa/image.png diff --git a/mx-aaaa/index.html b/mx-aaaa/index.html new file mode 120000 index 0000000..37cf92b --- /dev/null +++ b/mx-aaaa/index.html @@ -0,0 +1 @@ +resolve-mx-aaaa.html
\ No newline at end of file diff --git a/mx-aaaa/modules/mx-aaaa.js b/mx-aaaa/modules/mx-aaaa.js new file mode 100644 index 0000000..807f691 --- /dev/null +++ b/mx-aaaa/modules/mx-aaaa.js @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2019-2022 David Timber <dxdt@dev.snart.me> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +export class Resolver { + async resolve (type, rname) {} // abstract +}; + +function getRTypeNum (typestrd) { + switch (typestrd) { + case 'A': return 1; + case 'AAAA': return 28; + case 'MX': return 15; + } + throw new Error(); +} + +export class GooglePublicDNSResolver { + _mk_query_url (type, rname) { + return `https://dns.google/resolve?name=${rname}&type=${type}`; + } + + async _resolve_inner (url) { + let rsp; + + for (let i = 0; i < 3; i += 1) { + rsp = await fetch(url); + + if (rsp.status / 100 === 5) { + await new Promise(r => setTimeout(r, 500)); + continue; + } + break; + } + + return rsp; + } + + async resolve (type, rname) { + if (!rname.endsWith('.')) { + rname += '.'; + } + const url = this._mk_query_url(type, rname); + const rsp = await this._resolve_inner(url); + const doc = await rsp.json(); + const nrtype = getRTypeNum(type); + let ret = []; + + if ('Answer' in doc) { + for (const rr of doc['Answer']) { + // ignore other extra answers like CNAME + if ('data' in rr && + 'name' in rr && + 'type' in rr && + rr['name'] === rname && + rr['type'] === nrtype) + { + ret.push(rr['data']); + } + } + } + + return ret; + } +}; + +export function parseList (txt) { + let ret = []; + const lines = txt.split(/[\r\n]+/gm); + + for (let l of lines) { + l = l.trim(); + + const d = l.search('#'); + if (d >= 0) { + l = l.substring(0, d).trim(); + } + + if (l) { + ret.push(l); + } + } + + return ret; +}; + +const defaultResolver = GooglePublicDNSResolver; + +export class ResolveMXAAAA { + constructor (onresolve) { + this.onresolve = onresolve; + } + + async doResolve (list, resolver = null) { + let ret = { + mxmap: {}, + counts: { + list: list.length, + mx: 0, + has_a: 0, + has_aaaa: 0 + } + }; + + if (!resolver) { + resolver = new defaultResolver(); + } + + // init the map + for (const domain of list) { + ret.mxmap[domain] = { + mx: [], + amap: {}, + acnt: 0, + aaaamap: {}, + aaaacnt: 0 + }; + } + + // Get all MX + for (const domain of list) { + const q_mx = await resolver.resolve('MX', domain); + let has_a = 0, has_aaaa = 0; + + for (let mx of q_mx) { + // don't need the number part + const d = mx.search(/\s+/); + if (d > 0) { + mx = mx.substring(d).trim(); + } + + ret.mxmap[domain].mx.push(mx); + ret.counts.mx += 1; + + this.onresolve('mx', { + domain: domain, + mx: mx + }, ret); + + // Resolve all A of MX + const q_a = await resolver.resolve('A', mx); + if (q_a.length) { + has_a = 1; + ret.mxmap[domain].acnt += 1; + } + ret.mxmap[domain].amap[mx] = q_a; + this.onresolve('a', { + domain: domain, + mx: mx, + a: q_a + }, ret); + + // Resolve all AAAA of MX + const q_aaaa = await resolver.resolve('AAAA', mx); + if (q_aaaa.length) { + has_aaaa = 1; + ret.mxmap[domain].aaaacnt += 1; + } + ret.mxmap[domain].aaaamap[mx] = q_aaaa; + this.onresolve('aaaa', { + domain: domain, + mx: mx, + aaaa: q_aaaa + }, ret); + } + + ret.counts.has_a += has_a; + ret.counts.has_aaaa += has_aaaa; + } + + return ret; + } +}; diff --git a/mx-aaaa/resolve-mx-aaaa.css b/mx-aaaa/resolve-mx-aaaa.css new file mode 100644 index 0000000..43d7db2 --- /dev/null +++ b/mx-aaaa/resolve-mx-aaaa.css @@ -0,0 +1,53 @@ +div.domainCard { + display: inline-block; + padding: 0.8em; + margin: 0.2em; + color: midnightblue; + border-radius: 1ch; +} + +div.redDomainCard { + background-color: red; +} + +div.greenDomainCard { + background-color: green; +} + +div.yellowDomainCard { + background-color: yellow; +} + +div.magentaDomainCard { + background-color: magenta; +} + +p.domainSubtitle { + font-weight: bold; +} + +span.domainStatus {} + +table.header { + width: 100%; +} + +td { + vertical-align: top; +} + +td.listContainer { + width: 80ch; +} + +td.resultContainer { + width: max-content; +} + +pre.result { + font-weight: bold; +} + +div.header { + text-align: right; +} diff --git a/mx-aaaa/resolve-mx-aaaa.html b/mx-aaaa/resolve-mx-aaaa.html new file mode 100644 index 0000000..d9d3082 --- /dev/null +++ b/mx-aaaa/resolve-mx-aaaa.html @@ -0,0 +1,39 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <title>Resolve AAAA MX of popular email providers</title> + <script type="module" src="resolve-mx-aaaa.js"></script> + <link rel="stylesheet" href="resolve-mx-aaaa.css"> +</head> +<body> + +<h1>Resolve AAAA MX of popular email providers</h1> +<div class="header"> + <small>by David Timber <dxdt@dev.snart.me> (c) 2024</small> +</div> +<table class="header"> +<tbody> +<tr> + <td class="listContainer"> + <h2 id="hlist">List</h2> + <pre id="txtlist" contenteditable></pre> + <div> + <button type="submit" id="btn-listapply">Apply</button> + <button type="reset" id="btn-listreset">Reset</button> + </div> + </td> + <td class="resultContainer"> + <h2>Result</h2> + <pre id="status" class="result">PRESTART</pre> + <div id="card-holder"></div> + </td> +</tr> +</tbody> +</table> +<div> + <h2>Output</h2> + <pre id="output"></pre> +</div> + +</body> +</html> diff --git a/mx-aaaa/resolve-mx-aaaa.js b/mx-aaaa/resolve-mx-aaaa.js new file mode 100644 index 0000000..870626a --- /dev/null +++ b/mx-aaaa/resolve-mx-aaaa.js @@ -0,0 +1,140 @@ +import {parseList, ResolveMXAAAA} from './modules/mx-aaaa.js'; + +const rt_status = document.getElementById('status'); +const rt_cardHolder = document.getElementById('card-holder'); +const rt_output = document.getElementById('output'); +const i_txtlist = document.getElementById('txtlist'); +const rt_hlist = document.getElementById('hlist'); + +function onresolvereducer (what, obj, map) { + try { + const cnt_mx = map.mxmap[obj.domain].mx.length; + const cnt_a = map.mxmap[obj.domain].acnt; + const cnt_aaaa = map.mxmap[obj.domain].aaaacnt; + + function determineCard () { + if (cnt_mx === 0) { + return; + } + else if ((cnt_a > 0 && cnt_mx !== cnt_a) || + (cnt_aaaa > 0 && cnt_mx !== cnt_aaaa)) + { + return ['redDomainCard', 'some mx missing']; + } + else if (cnt_aaaa > 0) { + if (cnt_a > 0) { + return ['greenDomainCard', 'dual stack']; + } + else { + return ['magentaDomainCard', 'ipv6 only']; + } + } + else if (cnt_a > 0) { + return ['yellowDomainCard', 'ipv4 only']; + } + else { + return ['redDomainCard', 'no a or aaaa']; + } + } + + const cardId = `resolve-mx-card-${obj.domain}`; + const card = document.getElementById(cardId); + const statusLine = card.getElementsByClassName('domainStatus')[0]; + + statusLine.innerText = `mx: ${cnt_mx}, a: ${cnt_a}, aaaa: ${cnt_aaaa}`; + switch (what) { + case 'a': + case 'aaaa': + const d = determineCard(); + if (d) { + card.classList = `domainCard ${d[0]}`; + statusLine.innerText += ` (${d[1]})`; + } + break; + } + + } + catch (e) { + console.error(e); + } + finally { + rt_output.innerText += what + ': ' + JSON.stringify(obj) + '\n'; + } +} + +function initCards (list) { + for (const domain of list) { + const subtitle = document.createElement('p'); + subtitle.className = 'domainSubtitle'; + subtitle.innerText = domain; + + const statusLine = document.createElement('span'); + statusLine.className = 'domainStatus'; + statusLine.innerText = 'mx: 0, a: 0, aaaa: 0 (pending)'; + + const card = document.createElement('div'); + card.classList = 'domainCard redDomainCard'; + card.id = `resolve-mx-card-${domain}`; + + rt_cardHolder.appendChild(card); + card.appendChild(subtitle); + card.appendChild(statusLine); + } +} + +new Promise(async function () { + let progressCounter = 0; + function doProgressRender () { + const dots = '.'.repeat(progressCounter); + + rt_status.innerText = 'in progress ' + dots; + progressCounter = (progressCounter + 1) % 4; + } + + doProgressRender(); + const progressRenderTimer = setInterval(doProgressRender, 500); + + try { + let txt = localStorage.getItem("domains"); + + if (txt) { + i_txtlist.innerText = txt; + rt_hlist.innerText = 'List *(from localStorage)'; + } + else { + const rsp = await fetch('./domains.txt'); + i_txtlist.innerText = txt = await rsp.text(); + rt_hlist.innerText = 'List'; + } + + const list = parseList(txt); + const m = new ResolveMXAAAA(onresolvereducer); + + initCards(list); + + const result = await m.doResolve(list); + const percent = (result.counts.has_aaaa / result.counts.mx * 100).toFixed(2); + + rt_status.innerText = `SUCCESS: ${result.counts.has_aaaa} of ${result.counts.mx} service providers have AAAA (${percent}%)`; + rt_output.innerText += '\n'; + rt_output.innerText += JSON.stringify(result, null, '\t'); + } + catch (e) { + rt_status.innerText = "ERROR: " + e; + throw e; + } + finally { + clearInterval(progressRenderTimer); + } +}); + + +document.getElementById('btn-listapply').onclick = function () { + localStorage.setItem("domains", i_txtlist.innerText); + location.reload(); +}; + +document.getElementById('btn-listreset').onclick = function () { + localStorage.removeItem("domains"); + location.reload(); +}; |