aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Timber <dxdt@dev.snart.me>2024-08-19 02:17:29 +0200
committerDavid Timber <dxdt@dev.snart.me>2024-08-19 02:23:35 +0200
commitf350500627967412318c788dc516cc6f99c37b58 (patch)
tree911f4965f611a268efeaacd2f767d3e0ef013728
parentef5b99386d8c021d9cba38a7a615cdbfcbe80477 (diff)
Add mx-aaaa
-rw-r--r--README.md2
-rw-r--r--mx-aaaa/README.md9
-rw-r--r--mx-aaaa/domains.txt46
-rw-r--r--mx-aaaa/image.pngbin0 -> 338102 bytes
l---------mx-aaaa/index.html1
-rw-r--r--mx-aaaa/modules/mx-aaaa.js190
-rw-r--r--mx-aaaa/resolve-mx-aaaa.css53
-rw-r--r--mx-aaaa/resolve-mx-aaaa.html39
-rw-r--r--mx-aaaa/resolve-mx-aaaa.js140
9 files changed, 480 insertions, 0 deletions
diff --git a/README.md b/README.md
index 22abc5c..d96856e 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,8 @@ I hope you enjoy your stay!
ranges in CSV format. No middle man involved - everything done on your browser
- [toss-aws-eip](toss-aws-eip/README.md): get an Elastic IP address until you get one in
the range you want
+- [mx-aaaa](mx-aaaa/README.md): query AAAA MX of popular email service providers
+ (part of tracking transition to IPv6 progress)
- [flock_mmap](flock_mmap/README.md): test code demonstrating a bug in Linux's
implementation of `flock()`.
- writeups: all the write ups written up
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
+
+![Screenshot of result cards](image.png)
+
+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
new file mode 100644
index 0000000..ba9835a
--- /dev/null
+++ b/mx-aaaa/image.png
Binary files differ
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 &lt;dxdt@dev.snart.me&gt; (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();
+};