openvibers/openvibe.mail Commits
openvibe.email's home says what Mail does now, and follows the sending switch (#33)
Changes
nnew file mode 100644@@@ -0,0 +1,28 @@++'use strict';++/**++ * What openvibe.email says about itself, in one place, so the home page, its description and the agent files say the++ * same thing. Sending to other providers follows MAIL_OUTBOUND (config.outbound.enabled): the lines about it change++ * with the switch instead of going stale the day it is turned on. (Until 11-09 the page still said "Receive only" and++ * "your own domain comes next" a day after addresses, aliases, custom domains and forwarding went live.)++ */++function mailCopy(config) {++ const sending = !!(config && config.outbound && config.outbound.enabled);++ const what = 'Your own address and domain, masks for every sign-up, ten-minute addresses that show you the code, and inboxes your agents can read.';++ return {++ sending,++ summary: what,++ // Under 160 characters: the page shell cuts a longer description.++ description: sending++ ? 'Your own address and domain, masks, ten-minute addresses and inboxes for your agents, with plain-text sending.'++ : 'Your own address and domain, masks, ten-minute addresses and inboxes for your agents. Mail arrives today; sending opens next.',++ // The "Your address" card's last line and the rules' first line.++ sendLine: sending++ ? 'Write and reply from it in plain text (up to 10 recipients a message, 20 a day to start, rising with your history), and forward a copy to an address you verified.'++ : 'Mail between OpenVibe addresses works now; sending and forwarding to other providers open next.',++ sendRule: sending++ ? 'Sending is plain text with a daily allowance and no bulk lists, and every message is checked for spam before it leaves.'++ : 'Sending to other providers is closed for now. When it opens, every message is checked for spam before it leaves.',++ };++}++++module.exports = { mailCopy };@@@ -63,8 +63,13 @@ const CSP = [].join('; ');const BODY = fs.readFileSync(path.join(__dirname, '..', 'views', 'home.html'), 'utf8');++const { mailCopy } = require('./mail-copy');++const escHtml = (v) => String(v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));function homePage(config) {++ const words = mailCopy(config);++ const body = BODY.replace('{{LEAD}}', escHtml(words.summary)).replace('{{SEND_LINE}}', escHtml(words.sendLine))++ .replace('{{SEND_RULE}}', escHtml(words.sendRule));const base = String(config.baseUrl || 'https://openvibe.email').replace(/\/+$/, '');const nav = {service: SERVICE,@@@ -82,7 +87,7 @@ function homePage(config) {lang: 'en',title: `${SITE_NAME}: an address for every sign-up`,siteName: SITE_NAME,-- description: 'Ten-minute addresses that show you the code, and inboxes your agents can read. Receive only: nothing here sends mail.',++ description: words.description,canonical: `${base}/`,robots: 'index, follow',jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebApplication', name: SITE_NAME, url: `${base}/`, applicationCategory: 'CommunicationApplication', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } }],@@@ -96,7 +101,7 @@ function homePage(config) {<script src="${asset('js/mail.js')}" defer></script>`,body: `<a class="skip" href="#main">Skip to content</a><div id="navbar-mount"></div>--${BODY}++${body}<script>window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer }).replace(/</g, '\\u003c')};document.addEventListener('DOMContentLoaded', function () {@@@ -2,7 +2,7 @@<header class="ml-head"><p class="ml-kicker">OpenVibe.Email</p><h1>An address for every sign-up</h1>--<p class="ml-lead">Throwaway addresses, private mailboxes and Inbox AI that leaves every reply for you to review and send.</p>++<p class="ml-lead">{{LEAD}}</p></header><section class="ml-burner" data-mail-burner aria-labelledby="ml-burner-h">@@@ -33,17 +33,18 @@<section aria-labelledby="ml-more-h"><h2 id="ml-more-h" class="ml-sr">What else it does</h2><div class="ml-grid">--<article class="ml-card"><h3>Inboxes for your agents</h3><p>Give an agent its own address for sign-ups and codes, so it never needs yours. It reads only what arrives there.</p></article>++<article class="ml-card"><h3>Your address</h3><p>A name at openvibe.email, aliases, and your own domain, all into one inbox with trackers off until you say. Bring another mailbox in over IMAP, read only. {{SEND_LINE}}</p><p><a class="ml-card-link" href="/inbox">Open your inbox</a></p></article><article class="ml-card"><h3>Masks</h3><p>An address per site that delivers to your inbox, so a leak or a spammer never learns your real one. One switch turns it off.</p><p><a class="ml-card-link" href="/inbox">Make one in your inbox</a></p></article>--<article class="ml-card"><h3>Your inbox</h3><p>Read what came to your masks and your agents' inboxes, with trackers off until you say. Sending and your own domain come next.</p><p><a class="ml-card-link" href="/inbox">Open your inbox</a></p></article>++<article class="ml-card"><h3>Inboxes for your agents</h3><p>Give an agent its own address for sign-ups and codes, so it never needs yours. It reads only what arrives there.</p></article>++<article class="ml-card"><h3>Inbox AI, if you want it</h3><p>Off until you turn it on: summaries of long threads and reply drafts. A draft is never sent until you edit it and press Send.</p></article></div></section><section aria-labelledby="ml-rules-h"><h2 id="ml-rules-h" class="ml-h2">How it treats mail</h2><ul class="ml-rules">--<li>Receive only: there is no way to send from these addresses.</li>--<li>A message body is read only to find a code, and it is never stored outside the mailbox or shown here.</li>++<li>{{SEND_RULE}}</li>++<li>A message body stays in its mailbox: never in a log, an event or this page. For a ten-minute address, only the code, the sender and the time are shown.</li><li>Expired addresses bounce, and their mail is deleted.</li></ul></section>@@@ -34,10 +34,15 @@ test('/ is the page: the ten-minute address, the honest cards, the navbar, a strfor (const marker of ['data-mail-burner', 'Get a 10-minute address', 'data-mail-mine', 'Inboxes for your agents', 'navbar-mount', '/shared/']) {assert.ok(r.text.includes(marker), marker);}-- // Masks and the inbox work since M1.5: no card says Later, and both lead to /inbox; sending is said to come next.++ // Masks, your address and the inbox work: no card says Later, and two lead to /inbox. With MAIL_OUTBOUND off (the++ // default) the page says sending to other providers opens next and never "Receive only" or "your own domain comes++ // next" (both stale since 11-08); the description says the same.assert.strictEqual((r.text.match(/class="ml-soon">Later</g) || []).length, 0);assert.strictEqual((r.text.match(/href="\/inbox"/g) || []).length, 2);-- assert.match(r.text, /Sending and your own domain come next/);++ assert.match(r.text, /Your address<\/h3><p>A name at openvibe\.email, aliases, and your own domain/);++ assert.match(r.text, /sending and forwarding to other providers open next/);++ assert.match(r.text, /<meta name="description" content="[^"…]*Mail arrives today; sending opens next\.">/);++ assert.doesNotMatch(r.text, /Receive only|your own domain come next|\{\{[A-Z_]+\}\}/);assert.doesNotMatch(r.text, /<form[^>]*action="\/api/, 'nothing posts to the API without the script');const csp = r.headers.get('content-security-policy') || '';assert.match(csp, /frame-ancestors 'none'/);@@@ -83,3 +88,15 @@ test('/release.json is the release manifest open tabs poll, never cached', asyncassert.ok(typeof m.release === 'string' && m.release.length > 0, 'the deployed commit');assert.ok(m.packages && m.packages['openvibe-shared'], 'the platform packages it runs');});++++test('with sending on, the page says how sending works instead of "opens next"', () => {++ const { mailCopy } = require('../server/http/mail-copy');++ const on = mailCopy({ outbound: { enabled: true } });++ const off = mailCopy({ outbound: { enabled: false } });++ assert.match(on.sendLine, /up to 10 recipients a message, 20 a day to start/);++ assert.match(on.sendRule, /checked for spam before it leaves/);++ assert.doesNotMatch(`${on.description} ${on.sendLine} ${on.sendRule}`, /opens? next|closed/);++ assert.match(off.sendRule, /closed for now/);++ assert.strictEqual(on.summary, off.summary, 'what the site is does not change with the switch');++ for (const d of [on.description, off.description]) assert.ok(d.length <= 158, `the description fits the shell: ${d.length}`);++});
new file mode 100644
new file mode 100644
@@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
+'use strict';
+/**
+ * What openvibe.email says about itself, in one place, so the home page, its description and the agent files say the
+ * same thing. Sending to other providers follows MAIL_OUTBOUND (config.outbound.enabled): the lines about it change
+ * with the switch instead of going stale the day it is turned on. (Until 11-09 the page still said "Receive only" and
+ * "your own domain comes next" a day after addresses, aliases, custom domains and forwarding went live.)
+ */
+function mailCopy(config) {
+ const sending = !!(config && config.outbound && config.outbound.enabled);
+ const what = 'Your own address and domain, masks for every sign-up, ten-minute addresses that show you the code, and inboxes your agents can read.';
+ return {
+ sending,
+ summary: what,
+ // Under 160 characters: the page shell cuts a longer description.
+ description: sending
+ ? 'Your own address and domain, masks, ten-minute addresses and inboxes for your agents, with plain-text sending.'
+ : 'Your own address and domain, masks, ten-minute addresses and inboxes for your agents. Mail arrives today; sending opens next.',
+ // The "Your address" card's last line and the rules' first line.
+ sendLine: sending
+ ? 'Write and reply from it in plain text (up to 10 recipients a message, 20 a day to start, rising with your history), and forward a copy to an address you verified.'
+ : 'Mail between OpenVibe addresses works now; sending and forwarding to other providers open next.',
+ sendRule: sending
+ ? 'Sending is plain text with a daily allowance and no bulk lists, and every message is checked for spam before it leaves.'
+ : 'Sending to other providers is closed for now. When it opens, every message is checked for spam before it leaves.',
+ };
+}
+
+module.exports = { mailCopy };
@@ -63,8 +63,13 @@ const CSP = [
@@ -63,8 +63,13 @@ const CSP = [
].join('; ');
].join('; ');
const BODY = fs.readFileSync(path.join(__dirname, '..', 'views', 'home.html'), 'utf8');
const BODY = fs.readFileSync(path.join(__dirname, '..', 'views', 'home.html'), 'utf8');
+const { mailCopy } = require('./mail-copy');
+const escHtml = (v) => String(v).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
function homePage(config) {
function homePage(config) {
+ const words = mailCopy(config);
+ const body = BODY.replace('{{LEAD}}', escHtml(words.summary)).replace('{{SEND_LINE}}', escHtml(words.sendLine))
+ .replace('{{SEND_RULE}}', escHtml(words.sendRule));
const base = String(config.baseUrl || 'https://openvibe.email').replace(/\/+$/, '');
const base = String(config.baseUrl || 'https://openvibe.email').replace(/\/+$/, '');
const nav = {
const nav = {
service: SERVICE,
service: SERVICE,
@@ -82,7 +87,7 @@ function homePage(config) {
@@ -82,7 +87,7 @@ function homePage(config) {
lang: 'en',
lang: 'en',
title: `${SITE_NAME}: an address for every sign-up`,
title: `${SITE_NAME}: an address for every sign-up`,
siteName: SITE_NAME,
siteName: SITE_NAME,
- description: 'Ten-minute addresses that show you the code, and inboxes your agents can read. Receive only: nothing here sends mail.',
+ description: words.description,
canonical: `${base}/`,
canonical: `${base}/`,
robots: 'index, follow',
robots: 'index, follow',
jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebApplication', name: SITE_NAME, url: `${base}/`, applicationCategory: 'CommunicationApplication', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } }],
jsonLd: [{ '@context': 'https://schema.org', '@type': 'WebApplication', name: SITE_NAME, url: `${base}/`, applicationCategory: 'CommunicationApplication', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } }],
@@ -96,7 +101,7 @@ function homePage(config) {
@@ -96,7 +101,7 @@ function homePage(config) {
<script src="${asset('js/mail.js')}" defer></script>`,
<script src="${asset('js/mail.js')}" defer></script>`,
body: `<a class="skip" href="#main">Skip to content</a>
body: `<a class="skip" href="#main">Skip to content</a>
<div id="navbar-mount"></div>
<div id="navbar-mount"></div>
-${BODY}
+${body}
<script>
<script>
window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer }).replace(/</g, '\\u003c')};
window.__OV_PAGE = ${JSON.stringify({ navbar: nav, footer }).replace(/</g, '\\u003c')};
document.addEventListener('DOMContentLoaded', function () {
document.addEventListener('DOMContentLoaded', function () {
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
<header class="ml-head">
<header class="ml-head">
<p class="ml-kicker">OpenVibe.Email</p>
<p class="ml-kicker">OpenVibe.Email</p>
<h1>An address for every sign-up</h1>
<h1>An address for every sign-up</h1>
-<p class="ml-lead">Throwaway addresses, private mailboxes and Inbox AI that leaves every reply for you to review and send.</p>
+<p class="ml-lead">{{LEAD}}</p>
</header>
</header>
<section class="ml-burner" data-mail-burner aria-labelledby="ml-burner-h">
<section class="ml-burner" data-mail-burner aria-labelledby="ml-burner-h">
@@ -33,17 +33,18 @@
@@ -33,17 +33,18 @@
<section aria-labelledby="ml-more-h">
<section aria-labelledby="ml-more-h">
<h2 id="ml-more-h" class="ml-sr">What else it does</h2>
<h2 id="ml-more-h" class="ml-sr">What else it does</h2>
<div class="ml-grid">
<div class="ml-grid">
-<article class="ml-card"><h3>Inboxes for your agents</h3><p>Give an agent its own address for sign-ups and codes, so it never needs yours. It reads only what arrives there.</p></article>
+<article class="ml-card"><h3>Your address</h3><p>A name at openvibe.email, aliases, and your own domain, all into one inbox with trackers off until you say. Bring another mailbox in over IMAP, read only. {{SEND_LINE}}</p><p><a class="ml-card-link" href="/inbox">Open your inbox</a></p></article>
<article class="ml-card"><h3>Masks</h3><p>An address per site that delivers to your inbox, so a leak or a spammer never learns your real one. One switch turns it off.</p><p><a class="ml-card-link" href="/inbox">Make one in your inbox</a></p></article>
<article class="ml-card"><h3>Masks</h3><p>An address per site that delivers to your inbox, so a leak or a spammer never learns your real one. One switch turns it off.</p><p><a class="ml-card-link" href="/inbox">Make one in your inbox</a></p></article>
-<article class="ml-card"><h3>Your inbox</h3><p>Read what came to your masks and your agents' inboxes, with trackers off until you say. Sending and your own domain come next.</p><p><a class="ml-card-link" href="/inbox">Open your inbox</a></p></article>
+<article class="ml-card"><h3>Inboxes for your agents</h3><p>Give an agent its own address for sign-ups and codes, so it never needs yours. It reads only what arrives there.</p></article>
+<article class="ml-card"><h3>Inbox AI, if you want it</h3><p>Off until you turn it on: summaries of long threads and reply drafts. A draft is never sent until you edit it and press Send.</p></article>
</div>
</div>
</section>
</section>
<section aria-labelledby="ml-rules-h">
<section aria-labelledby="ml-rules-h">
<h2 id="ml-rules-h" class="ml-h2">How it treats mail</h2>
<h2 id="ml-rules-h" class="ml-h2">How it treats mail</h2>
<ul class="ml-rules">
<ul class="ml-rules">
-<li>Receive only: there is no way to send from these addresses.</li>
-<li>A message body is read only to find a code, and it is never stored outside the mailbox or shown here.</li>
+<li>{{SEND_RULE}}</li>
+<li>A message body stays in its mailbox: never in a log, an event or this page. For a ten-minute address, only the code, the sender and the time are shown.</li>
<li>Expired addresses bounce, and their mail is deleted.</li>
<li>Expired addresses bounce, and their mail is deleted.</li>
</ul>
</ul>
</section>
</section>
@@ -34,10 +34,15 @@ test('/ is the page: the ten-minute address, the honest cards, the navbar, a str
@@ -34,10 +34,15 @@ test('/ is the page: the ten-minute address, the honest cards, the navbar, a str
for (const marker of ['data-mail-burner', 'Get a 10-minute address', 'data-mail-mine', 'Inboxes for your agents', 'navbar-mount', '/shared/']) {
for (const marker of ['data-mail-burner', 'Get a 10-minute address', 'data-mail-mine', 'Inboxes for your agents', 'navbar-mount', '/shared/']) {
assert.ok(r.text.includes(marker), marker);
assert.ok(r.text.includes(marker), marker);
}
}
- // Masks and the inbox work since M1.5: no card says Later, and both lead to /inbox; sending is said to come next.
+ // Masks, your address and the inbox work: no card says Later, and two lead to /inbox. With MAIL_OUTBOUND off (the
+ // default) the page says sending to other providers opens next and never "Receive only" or "your own domain comes
+ // next" (both stale since 11-08); the description says the same.
assert.strictEqual((r.text.match(/class="ml-soon">Later</g) || []).length, 0);
assert.strictEqual((r.text.match(/class="ml-soon">Later</g) || []).length, 0);
assert.strictEqual((r.text.match(/href="\/inbox"/g) || []).length, 2);
assert.strictEqual((r.text.match(/href="\/inbox"/g) || []).length, 2);
- assert.match(r.text, /Sending and your own domain come next/);
+ assert.match(r.text, /Your address<\/h3><p>A name at openvibe\.email, aliases, and your own domain/);
+ assert.match(r.text, /sending and forwarding to other providers open next/);
+ assert.match(r.text, /<meta name="description" content="[^"…]*Mail arrives today; sending opens next\.">/);
+ assert.doesNotMatch(r.text, /Receive only|your own domain come next|\{\{[A-Z_]+\}\}/);
assert.doesNotMatch(r.text, /<form[^>]*action="\/api/, 'nothing posts to the API without the script');
assert.doesNotMatch(r.text, /<form[^>]*action="\/api/, 'nothing posts to the API without the script');
const csp = r.headers.get('content-security-policy') || '';
const csp = r.headers.get('content-security-policy') || '';
assert.match(csp, /frame-ancestors 'none'/);
assert.match(csp, /frame-ancestors 'none'/);
@@ -83,3 +88,15 @@ test('/release.json is the release manifest open tabs poll, never cached', async
@@ -83,3 +88,15 @@ test('/release.json is the release manifest open tabs poll, never cached', async
assert.ok(typeof m.release === 'string' && m.release.length > 0, 'the deployed commit');
assert.ok(typeof m.release === 'string' && m.release.length > 0, 'the deployed commit');
assert.ok(m.packages && m.packages['openvibe-shared'], 'the platform packages it runs');
assert.ok(m.packages && m.packages['openvibe-shared'], 'the platform packages it runs');
});
});
+
+test('with sending on, the page says how sending works instead of "opens next"', () => {
+ const { mailCopy } = require('../server/http/mail-copy');
+ const on = mailCopy({ outbound: { enabled: true } });
+ const off = mailCopy({ outbound: { enabled: false } });
+ assert.match(on.sendLine, /up to 10 recipients a message, 20 a day to start/);
+ assert.match(on.sendRule, /checked for spam before it leaves/);
+ assert.doesNotMatch(`${on.description} ${on.sendLine} ${on.sendRule}`, /opens? next|closed/);
+ assert.match(off.sendRule, /closed for now/);
+ assert.strictEqual(on.summary, off.summary, 'what the site is does not change with the switch');
+ for (const d of [on.description, off.description]) assert.ok(d.length <= 158, `the description fits the shell: ${d.length}`);
+});