diff --git a/assets/elements.css b/assets/elements.css
index 3296192..89b9f12 100644
--- a/assets/elements.css
+++ b/assets/elements.css
@@ -25,6 +25,12 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
color: var(--color-bg);
}
+a.noblock {
+ display: inline;
+ margin-inline: 0;
+ padding-block: 0.25rem;
+}
+
p {
margin-inline: 2ch;
margin-block: 1.5rem;
@@ -44,6 +50,18 @@ ul li {
margin-block: 1.5rem;
}
+details > summary {
+ list-style-type: '► ';
+}
+
+details[open] > summary {
+ list-style-type: '▼ ';
+}
+
+summary > h1, summary > h2, summary > h3, summary > h4, summary > h5, summary > h6 {
+ display: inline-block;
+}
+
section {
margin-block: 1.5em;
margin-inline-start: 2ch;
@@ -61,11 +79,16 @@ a {
code {
display: inline-block;
+ max-width: 100%;
+ box-sizing: border-box;
font: inherit;
background-color: var(--color-code);
padding-inline: 1ch;
color: var(--color-code-text);
user-select: all;
+ word-break: break-word;
+ overflow: auto;
+ vertical-align: top;
}
button, input, textarea, select {
@@ -112,4 +135,25 @@ marquee {
content: "";
display: table;
clear: both;
+}
+
+progress {
+ border: none;
+ border-radius: 0;
+ border-inline: 1ch solid var(--color-text);
+ background-color: var(--color-bg-scrim);
+ height: 1.5rem;
+ vertical-align: top;
+}
+progress::-webkit-progress-bar {
+ border: none;
+ border-radius: 0;
+ border-inline: 1ch solid var(--color-text);
+ background-color: var(--color-bg-scrim);
+}
+progress::-webkit-progress-value {
+ background-color: var(--color-accent);
+}
+progress::-moz-progress-bar {
+ background-color: var(--color-accent);
}
\ No newline at end of file
diff --git a/index.js b/index.js
index 82ed663..3ae2291 100644
--- a/index.js
+++ b/index.js
@@ -6,6 +6,12 @@ import { Kysely, SqliteDialect } from 'kysely';
import { nanoid } from "nanoid";
import { JSDOM } from "jsdom";
import Parser from "rss-parser";
+import nacl from "tweetnacl";
+import expressBasicAuth from "express-basic-auth";
+import { getRelativeTime } from "./modules/relativeTime.js";
+import { fromHex } from "./modules/fromHex.js";
+
+import sitemap from "./sitemap.json" assert { type: "json" };
dotenvConfig();
@@ -22,6 +28,7 @@ const db = new Kysely({
const rssParser = new Parser();
rawDB.exec(`CREATE TABLE IF NOT EXISTS blurbs( 'id' TEXT, 'site' TEXT, 'blurb' TEXT, 'verified' INTEGER, 'time' INTEGER );`);
+rawDB.exec(`CREATE TABLE IF NOT EXISTS mininq( 'id' TEXT, 'name' TEXT, 'ip' TEXT, 'msg' TEXT, 'url' TEXT, 'public' INTEGER, 'reply' TEXT, 'time' INTEGER );`);
rawDB.exec(`CREATE TABLE IF NOT EXISTS blacklist( 'domain' TEXT );`);
const app = express();
@@ -29,33 +36,11 @@ const app = express();
app.set('view engine', 'ejs');
app.use('/assets', express.static('assets'));
app.set('views', path.join(__dirname, "views", "pages"));
+app.use(express.urlencoded({
+ extended: true
+}))
-app.locals.siteMap = [
- {
- link: "blurbs",
- description: "what are other sites saying about abtmtr.link?"
- },
- {
- link: "characters",
- description: "about abtmtr.link's mascots and other such characters"
- },
- {
- link: "updates",
- description: "what's going on with abtmtr.link?"
- },
- {
- link: "servers",
- description: "about the services on abtmtr.link and the machines that serve them."
- },
- {
- link: "sites",
- description: "a collection of other sites in the form of 88x31s."
- },
- {
- link: "about",
- description: "who runs abtmtr.link?"
- }
-];
+app.locals.siteMap = sitemap;
app.locals.curCommit = "0000000000";
async function getCurCommit() {
@@ -99,26 +84,6 @@ app.get('/servers', async (req, res) => {
});
});
-var units = {
- year: 24 * 60 * 60 * 1000 * 365,
- month: 24 * 60 * 60 * 1000 * 365 / 12,
- day: 24 * 60 * 60 * 1000,
- hour: 60 * 60 * 1000,
- minute: 60 * 1000,
- second: 1000
-}
-
-var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
-
-var getRelativeTime = (d1, d2 = new Date()) => {
- var elapsed = d1 - d2
-
- // "Math.abs" accounts for both "past" & "future" scenarios
- for (var u in units)
- if (Math.abs(elapsed) > units[u] || u == 'second')
- return rtf.format(Math.round(elapsed / units[u]), u)
-}
-
app.get('/about', async (req, res) => {
const linksJson = await fetch("https://cdn.abtmtr.link/site_content/v13/links.json")
.catch(() => res.status(500).send())
@@ -142,6 +107,292 @@ app.get('/characters', async (req, res) => {
res.render('characters', { characters: charactersJson });
});
+app.get('/mininq', async (req, res) => {
+ const mailbox = await db
+ .selectFrom('mininq')
+ .select('id')
+ .where('public', '=', 0)
+ .where('url', '=', '')
+ .orderBy('time', "desc")
+ .limit(process.env.MININQ_MAILBOX_MAX)
+ .execute();
+
+ const verifiedMailbox = await db
+ .selectFrom('mininq')
+ .select('id')
+ .where('public', '=', 0)
+ .where('url', '!=', '')
+ .orderBy('time', "desc")
+ .limit(process.env.MININQ_VERIFIED_MAILBOX_MAX)
+ .execute();
+
+ const isIpAlreadyMininq = await db
+ .selectFrom('mininq')
+ .select('ip')
+ .where('ip', '=', req.ip)
+ .where('public', '=', 0)
+ .executeTakeFirst();
+
+ const { publicKey, secretKey } = nacl.box.keyPair();
+
+ const data = await db
+ .selectFrom('mininq')
+ .selectAll()
+ .where('public', '=', 1)
+ .orderBy('time', "desc")
+ .execute();
+
+ res.render('mininq', {
+ keypair: {
+ publicKey: Buffer.from(publicKey).toString('hex'),
+ secretKey: Buffer.from(secretKey).toString('hex'),
+ },
+ data,
+ mailboxCount: mailbox.length,
+ isIpAlreadyMininq,
+ mailboxVerifiedCount: verifiedMailbox.length,
+ mailboxMaximum: process.env.MININQ_MAILBOX_MAX,
+ mailboxVerifiedMaximum: process.env.MININQ_VERIFIED_MAILBOX_MAX
+ });
+});
+
+async function checkIsMtr(user, pass, cb) {
+ try {
+ let publicKeyWorks = await checkPublicKey({
+ skey: pass,
+ pkeyurl: `http://localhost:${process.env.PORT}/`
+ });
+
+ return cb(null, publicKeyWorks);
+ } catch (err) {
+ return cb(null, false);
+ }
+}
+
+app.get('/mininq/mbox', expressBasicAuth({
+ authorizer: checkIsMtr,
+ authorizeAsync: true,
+ challenge: true
+}), async (req, res) => {
+ const mailbox = await db
+ .selectFrom('mininq')
+ .selectAll()
+ .where('public', '=', 0)
+ .where('url', '=', '')
+ .orderBy('time', "desc")
+ .execute();
+
+ const verifiedMailbox = await db
+ .selectFrom('mininq')
+ .selectAll()
+ .where('public', '=', 0)
+ .where('url', '!=', '')
+ .orderBy('time', "desc")
+ .execute();
+
+ res.render('mininqbox', {
+ mail: verifiedMailbox.concat(mailbox),
+ mailboxCount: mailbox.length,
+ mailboxVerifiedCount: verifiedMailbox.length,
+ mailboxMaximum: process.env.MININQ_MAILBOX_MAX,
+ mailboxVerifiedMaximum: process.env.MININQ_VERIFIED_MAILBOX_MAX
+ });
+});
+
+app.post('/mininq/mbox/r', async (req, res) => {
+ const body = req.body;
+
+ if (typeof body.skey != "string") return res.status(400).send("skey is not a string");
+ if (body.skey.length != 64) return res.status(400).send("skey must be a valid secret key");
+
+ try {
+ let publicKeyWorks = await checkPublicKey({
+ skey: body.skey,
+ pkeyurl: `http://localhost:${process.env.PORT}/`
+ });
+
+ if (!publicKeyWorks) return res.status(401).send("No?");
+ } catch (err) {
+ return res.status(400).send(err.message);
+ }
+
+ if (typeof body.id != "string") return res.status(400).send("id is not a string");
+ if (body.id.length != 32) return res.status(400).send("id must be a mininq ID");
+
+ if (typeof body.action != "string") return res.status(400).send("action is not a string");
+ if (body.action.length > 10) return res.status(400).send("action must be valid");
+
+ switch (body.action) {
+ case "public":
+ await db
+ .updateTable('mininq')
+ .set({
+ public: 1
+ })
+ .where('id', '=', body.id)
+ .where('public', '=', 0)
+ .executeTakeFirst();
+ break;
+ case "delete":
+ await db
+ .deleteFrom('mininq')
+ .where('id', '=', body.id)
+ .executeTakeFirst();
+ break;
+ default:
+ if (typeof body.msg != "string") return res.status(400).send("msg is not a string");
+ if (body.msg.length < 1) return res.status(400).send("msg is required");
+ if (body.msg.length > 4000) return res.status(400).send("msg must not be longer than 4000 characters");
+
+ await db
+ .updateTable('mininq')
+ .set({
+ public: 1,
+ reply: body.msg
+ })
+ .where('id', '=', body.id)
+ .where('public', '=', 0)
+ .executeTakeFirst();
+ break;
+ }
+
+ res.redirect('/mininq/mbox');
+});
+
+async function checkPublicKey(body) {
+ if (typeof body.skey != "string") throw ("skey is not a string");
+ if (body.skey.length != 64) throw ("skey must be a valid secret key");
+
+ if (body.pkeyurl.length < 1) throw ("pkeyurl is required");
+ if (body.pkeyurl.length > 512) throw ("pkeyurl must not be longer than 512 characters");
+ const site = await fetch(body.pkeyurl).then(async x => ({ s: x.status, t: await x.text() })).catch(_ => _);
+ if (site.s != 200) throw ("pkeyurl's site is not online");
+ try {
+ const domain = new URL(body.pkeyurl).hostname;
+ const isBadSite = await db
+ .selectFrom('blacklist')
+ .selectAll()
+ .where('domain', '=', domain)
+ .executeTakeFirst();
+
+ if (isBadSite != null)
+ throw ({
+ error: "Forbidden",
+ message: "\u{1f595} (or don't authenticate, i guess)"
+ });
+ } catch (err) {
+ throw ({
+ error: "Internal Server Error",
+ message: "URL CONSTRUCTOR FAILED???? i think this might be your fault...."
+ });
+ }
+ let relationMeta;
+ try {
+ let webDom = new JSDOM(site.t);
+ relationMeta = webDom.window.document.querySelector(
+ `meta[name="abtmtr-mininq-key"]`
+ );
+ } catch (err) {
+ throw ("Something went wrong parsing your site's DOM");
+ }
+ let publicKey;
+ if (relationMeta != null) publicKey = relationMeta.content;
+ else throw ("Can't find public key");
+
+ let pkMatches;
+ try {
+ const generatedPublicKey = nacl.box.keyPair.fromSecretKey(fromHex(body.skey));
+ pkMatches = Buffer.from(generatedPublicKey.publicKey).toString('hex') == publicKey;
+ } catch (err) {
+ throw ("Something went wrong verifying your keypair");
+ }
+
+ return pkMatches;
+}
+
+app.post('/mininq/send', async (req, res) => {
+
+ const isIpAlreadyMininq = await db
+ .selectFrom('mininq')
+ .select('ip')
+ .where('ip', '=', req.ip)
+ .where('public', '=', 0)
+ .executeTakeFirst();
+
+ if (isIpAlreadyMininq != null) return res.status(403).send("IP is already in mailbox");
+
+ const body = req.body;
+
+ let newObject = {
+ url: '',
+ id: nanoid(32),
+ public: 0,
+ reply: '',
+ ip: req.ip
+ };
+
+ if (typeof body.name != "string") return res.status(400).send("name is not a string");
+ if (body.name.length < 1) return res.status(400).send("name is required");
+ if (body.name.length > 40) return res.status(400).send("name must not be longer than 40 characters");
+
+ newObject.name = body.name;
+
+ if (typeof body.msg != "string") return res.status(400).send("msg is not a string");
+ if (body.msg.length < 1) return res.status(400).send("msg is required");
+ if (body.msg.length > 2000) return res.status(400).send("msg must not be longer than 2000 characters");
+
+ newObject.msg = body.msg;
+
+ if (typeof body.pkeyurl == "string") {
+ try {
+ let publicKeyWorks = await checkPublicKey(body);
+
+ if (publicKeyWorks) newObject.url = body.pkeyurl;
+ } catch (err) {
+ // do nothing lol
+ }
+ }
+
+ const mailbox = await db
+ .selectFrom('mininq')
+ .select('id')
+ .where('public', '=', 0)
+ .where('url', '=', '')
+ .orderBy('time', "desc")
+ .limit(process.env.MININQ_MAILBOX_MAX)
+ .execute();
+
+ const verifiedMailbox = await db
+ .selectFrom('mininq')
+ .select('id')
+ .where('public', '=', 0)
+ .where('url', '!=', '')
+ .orderBy('time', "desc")
+ .limit(process.env.MININQ_VERIFIED_MAILBOX_MAX)
+ .execute();
+
+ if (newObject.url) {
+ if (verifiedMailbox.length >= process.env.MININQ_VERIFIED_MAILBOX_MAX)
+ return res.status(403).send("Mailbox is full");
+ } else {
+ if (mailbox.length >= process.env.MININQ_MAILBOX_MAX)
+ return res.status(403).send("Mailbox is full");
+ }
+
+ newObject.time = Date.now();
+
+ try {
+ await db.insertInto('mininq')
+ .values(newObject)
+ .executeTakeFirstOrThrow();
+ } catch (err) {
+ console.log(err)
+ return res.status(500).send('Internal Server Error');
+ }
+
+ res.redirect('/mininq/');
+});
+
app.get('/updates', async (req, res) => {
const rssXML = await fetch("https://cdn.abtmtr.link/site_content/rss.xml")
.catch(() => res.status(500).send())
diff --git a/modules/fromHex.js b/modules/fromHex.js
new file mode 100644
index 0000000..97d686f
--- /dev/null
+++ b/modules/fromHex.js
@@ -0,0 +1,20 @@
+const MAP_HEX = {
+ 0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6,
+ 7: 7, 8: 8, 9: 9, a: 10, b: 11, c: 12, d: 13,
+ e: 14, f: 15, A: 10, B: 11, C: 12, D: 13,
+ E: 14, F: 15
+};
+
+export function fromHex(hexString) {
+ const bytes = new Uint8Array(Math.floor((hexString || "").length / 2));
+ let i;
+ for (i = 0; i < bytes.length; i++) {
+ const a = MAP_HEX[hexString[i * 2]];
+ const b = MAP_HEX[hexString[i * 2 + 1]];
+ if (a === undefined || b === undefined) {
+ break;
+ }
+ bytes[i] = (a << 4) | b;
+ }
+ return i === bytes.length ? bytes : bytes.slice(0, i);
+}
\ No newline at end of file
diff --git a/modules/relativeTime.js b/modules/relativeTime.js
new file mode 100644
index 0000000..0cd7253
--- /dev/null
+++ b/modules/relativeTime.js
@@ -0,0 +1,19 @@
+var units = {
+ year: 24 * 60 * 60 * 1000 * 365,
+ month: 24 * 60 * 60 * 1000 * 365 / 12,
+ day: 24 * 60 * 60 * 1000,
+ hour: 60 * 60 * 1000,
+ minute: 60 * 1000,
+ second: 1000
+}
+
+var rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
+
+export function getRelativeTime(d1, d2 = new Date()) {
+ var elapsed = d1 - d2
+
+ // "Math.abs" accounts for both "past" & "future" scenarios
+ for (var u in units)
+ if (Math.abs(elapsed) > units[u] || u == 'second')
+ return rtf.format(Math.round(elapsed / units[u]), u)
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 8fb8f9e..191533f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,11 +13,13 @@
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
+ "express-basic-auth": "^1.2.1",
"jsdom": "^25.0.0",
"kysely": "^0.27.4",
"nanoid": "^5.0.7",
"pg": "^8.12.0",
- "rss-parser": "^3.13.0"
+ "rss-parser": "^3.13.0",
+ "tweetnacl": "^1.0.3"
},
"devDependencies": {
"nodemon": "^3.1.4"
@@ -144,6 +146,24 @@
],
"license": "MIT"
},
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/basic-auth/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
"node_modules/better-sqlite3": {
"version": "11.2.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz",
@@ -680,6 +700,15 @@
"node": ">= 0.10.0"
}
},
+ "node_modules/express-basic-auth": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz",
+ "integrity": "sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==",
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.1"
+ }
+ },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
@@ -2178,6 +2207,12 @@
"node": "*"
}
},
+ "node_modules/tweetnacl": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+ "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
+ "license": "Unlicense"
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
diff --git a/package.json b/package.json
index 93456a8..ed858c5 100644
--- a/package.json
+++ b/package.json
@@ -22,11 +22,13 @@
"dotenv": "^16.4.5",
"ejs": "^3.1.10",
"express": "^4.19.2",
+ "express-basic-auth": "^1.2.1",
"jsdom": "^25.0.0",
"kysely": "^0.27.4",
"nanoid": "^5.0.7",
"pg": "^8.12.0",
- "rss-parser": "^3.13.0"
+ "rss-parser": "^3.13.0",
+ "tweetnacl": "^1.0.3"
},
"devDependencies": {
"nodemon": "^3.1.4"
diff --git a/sitemap.json b/sitemap.json
new file mode 100644
index 0000000..517af61
--- /dev/null
+++ b/sitemap.json
@@ -0,0 +1,30 @@
+[
+ {
+ "link": "blurbs",
+ "description": "what are other sites saying about abtmtr.link?"
+ },
+ {
+ "link": "characters",
+ "description": "about abtmtr.link's mascots and other such characters"
+ },
+ {
+ "link": "updates",
+ "description": "what's going on with abtmtr.link?"
+ },
+ {
+ "link": "mininq",
+ "description": "slowchat, for sysadmins who aren't quite equine."
+ },
+ {
+ "link": "servers",
+ "description": "about the services on abtmtr.link and the machines that serve them."
+ },
+ {
+ "link": "sites",
+ "description": "a collection of other sites in the form of 88x31s."
+ },
+ {
+ "link": "about",
+ "description": "who runs abtmtr.link?"
+ }
+]
diff --git a/views/components/mininq.ejs b/views/components/mininq.ejs
new file mode 100644
index 0000000..641d928
--- /dev/null
+++ b/views/components/mininq.ejs
@@ -0,0 +1,14 @@
+
+ <%= mininq.name %>
+ <% if (mininq.url != '') { %>
+ @ <%= new URL(mininq.url).host %>
+ <% } %>
+ sent:
+
+<%= mininq.msg %>
+<% if (mininq.reply != '') { %>
+
+ sysadmin reply:
+ <%= mininq.reply %>
+
+<% } %>
\ No newline at end of file
diff --git a/views/components/page-head.ejs b/views/components/page-head.ejs
index c5edbe4..238480f 100644
--- a/views/components/page-head.ejs
+++ b/views/components/page-head.ejs
@@ -3,6 +3,7 @@
abtmtr.link
+
diff --git a/views/pages/mininq.ejs b/views/pages/mininq.ejs
new file mode 100644
index 0000000..18b4999
--- /dev/null
+++ b/views/pages/mininq.ejs
@@ -0,0 +1,86 @@
+
+
+ <%- include("../components/page-head.ejs") %>
+
+
+ mininq
+ slowchat, for sysadmins who aren't quite equine.
+ submit a mininq
+
+ MAILBOX:
+
+ <%= mailboxCount %>/<%= mailboxMaximum %>
+
+
+ MAILBOX (PRIORITY):
+
+ <%= mailboxVerifiedCount %>/<%= mailboxVerifiedMaximum %>
+
+ mininq ([min]i [inq]uiry
) is a ping-ponging non-realtime chat interface made in the early 2000s to satisfy the needs of the single abtmtr.link sysadmin writing it.
+ either way, mininq was conceptualized, developed, and deployed without any second consideration. now, here you are.
+ <% if (!isIpAlreadyMininq) { %>
+ <% if (mailboxCount < mailboxMaximum || mailboxVerifiedCount < mailboxVerifiedMaximum) { %>
+
+ <% } else { %>
+ uh-oh! looks like both mailboxes are full. you'll have to wait until the mailbox has emptied - then you can submit your mininq.
+ <% } %>
+ <% } else { %>
+ you've submitted a mininq!
+ <% } %>
+ current mininqs
+
+ <% data.forEach((mininq) => { %>
+
+ <%- include("../components/mininq.ejs", {mininq}) %>
+
+ <% }) %>
+
+
+ <%- include("../components/footer.ejs") %>
+ <%- include("../components/post-main.ejs") %>
+
+
\ No newline at end of file
diff --git a/views/pages/mininqbox.ejs b/views/pages/mininqbox.ejs
new file mode 100644
index 0000000..c8b6979
--- /dev/null
+++ b/views/pages/mininqbox.ejs
@@ -0,0 +1,58 @@
+
+
+ <%- include("../components/page-head.ejs") %>
+
+
+ mininq mailbox
+ slowchat, for sysadmins who aren't quite equine.
+ mailboxes
+
+ MAILBOX:
+
+ <%= mailboxCount %>/<%= mailboxMaximum %>
+
+
+ MAILBOX (PRIORITY):
+
+ <%= mailboxVerifiedCount %>/<%= mailboxVerifiedMaximum %>
+
+ current mininqs
+
+
+ <%- include("../components/footer.ejs") %>
+ <%- include("../components/post-main.ejs") %>
+
+
\ No newline at end of file