Webmail is now Cypht (several accounts in one place) instead of Roundcube
build-container / build (push) Successful in 6m58s

- cypht/: official Cypht 2.13.0 on a SQLite file, served under /webmail/; workflow builds git.necloud.us/nate/hub-cypht
- the hub passes /webmail/ through behind the passkey sign-in and signs in to Cypht for you (CYPHT_PASSWORD)
- provider sign-in pages (Google, Microsoft) open in the whole window and return to the Mail tab; other links open in a new tab
- Gmail / Outlook.com / Office365 OAuth settings (Microsoft permissions patched to the current ones)
- Roundcube removed; README, compose and webmail tests updated

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 09:28:50 -04:00
1 parent a7a5b155e3
commit 7823bbcb03
12 files changed
+211 -135

No files matched your search

+22 -1
View File
@@ -733,13 +733,34 @@ function viewWebmail() {
setSt('Opening…'); let f = $('#audiowrap iframe[data-k="mail"]'); if (f) f.remove();
showFrame('mail', cfg.webmail + (fresh ? (cfg.webmail.includes('?') ? '&' : '?') + '_=' + Date.now() : ''), 'Mail', 'fullscreen; clipboard-write; clipboard-read');
f = $('#audiowrap iframe[data-k="mail"]'); let done = false;
f.addEventListener('load', () => { done = true; setSt(''); });
f.addEventListener('load', () => { done = true; setSt(''); webmailHooks(f); });
setTimeout(() => { if (!done) setSt('Still loading… if it says it refused to connect, the webmail must allow framing (see the README)'); }, 10000);
};
if (!old) open(false); else { showFrame('mail', '', 'Mail', ''); setSt(''); }
$('#ar').onclick = () => open(true);
$('#af').onclick = () => { const f = $('#audiowrap iframe[data-k="mail"]'); if (f && f.requestFullscreen) f.requestFullscreen().catch(() => toast('Full screen is not available here')); else toast('Full screen is not available here'); };
}
// The webmail page shares our address, so we can help it from outside: sign in for you, and send links that cannot be framed to the right place.
const SIGNIN_HOSTS = /^(accounts\.google\.com|login\.microsoftonline\.com|login\.live.com|appleid\.apple\.com|github\.com)$/;
async function webmailHooks(f) {
let d; try { d = f.contentDocument; if (!d || !d.body) return; } catch { return; }
if (!d.__hubHooks) {
d.__hubHooks = true;
d.addEventListener('click', e => {
const a = e.target.closest && e.target.closest('a[href]'); if (!a || e.defaultPrevented) return;
let u; try { u = new URL(a.href, d.baseURI); } catch { return; }
if (!/^https?:$/.test(u.protocol) || u.origin === location.origin) return;
e.preventDefault(); e.stopPropagation();
if (SIGNIN_HOSTS.test(u.hostname)) location.href = u.href; // a provider's sign-in page refuses to be framed: use the whole window
else window.open(u.href, '_blank', 'noopener');
}, true);
}
const u = d.querySelector('input[name=username]'), p = d.querySelector('input[name=password]'), form = u && u.form;
if (form && p && !d.querySelector('.error, .danger, .alert-danger') && !f.dataset.tried) { // the webmail's own login box: fill it in (once, so a wrong password is not retried)
f.dataset.tried = 1;
try { const c = await jfetch('/api/webmail/login'); if (c.user && c.pass) { u.value = c.user; p.value = c.pass; form.submit(); } } catch {}
}
}
function viewMailCards() {
const hints = mailHints(), on = mailOn(), shown = MAIL.filter(m => on.includes(m.id)), anyHint = shown.some(m => hints[m.id]);
$('#view').innerHTML = `<div class="wrap">${cfg.webmail ? `<div class="bar" style="margin-top:0"><a class="btn ghost" href="#/mail">${icon('back')}Webmail</a></div>` : ''}<h1>Mail</h1>