From 5157e42bc443580fc0c334c26662845595906a23 Mon Sep 17 00:00:00 2001 From: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:03:34 -0400 Subject: [PATCH] Mail opens the provider shortcuts by default; the webmail is one button away Co-Authored-By: Claude Sonnet 5.5 --- README.md | 5 +++-- public/app.js | 9 +++++---- server.js | 2 +- tools/webmail-test.js | 2 +- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 094ba32..f890c42 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke `{u, id, exp, n}` and `sig` is the base64url HMAC-SHA256 of `body`. Without either, the plain site opens and the saved username can be copied. - **Podcast** and **Podcast Admin**: launchers that open `PODCAST_URL` (a home-network address) and `VERSE_URL` in their own tabs. - **Office**: embeds onlyoffice.necloud.us in a frame, with an "open in new tab" fallback if it blocks framing. -- **Mail**: Cypht comes with the project (a second container in `docker-compose.yml`), so the Mail tab is a full webmail inside the app, built for **several +- **Mail**: Cypht comes with the project (a second container in `docker-compose.yml`), so the Mail tab can open a full webmail inside the app (the shortcut cards are the default page; the **Open webmail** button there opens it, and the envelope button in + the webmail goes back), built for **several accounts at once** (iCloud, Gmail, Outlook.com, Office365, Yahoo, Fastmail and others are presets, plus any IMAP server) with combined views of them all. The hub passes `/webmail/` through to it, so it is not exposed to the internet, sits behind your passkey sign-in and shares the hub's address (which is what lets it be framed). Set `CYPHT_PASSWORD` (any password you choose; required) in `.env` or your stack; the hub uses it to sign in to Cypht for you, so you @@ -63,7 +64,7 @@ e.g. hub.necloud.us. Settings: `SEAFILE_URL`, `ONLYOFFICE_URL` env vars in docke address and an **app password**, e.g. appleid.apple.com -> App-Specific Passwords; Gmail needs 2-step verification, then Google account -> App passwords). Everything is kept in the `nehub-cypht` volume. Everyone who has the Mail tab shares this one Cypht account; for separate mailboxes give only the people who should share it the tab, or run `docker exec php /usr/local/share/cypht/scripts/create_account.php ` and sign in as that person in Cypht - (the automatic sign-in is then skipped by logging out of Cypht once). The envelope button in the Mail toolbar still opens the Gmail / Outlook.com / Microsoft 365 / + (the automatic sign-in is then skipped by logging out of Cypht once). The default Mail page is the Gmail / Outlook.com / Microsoft 365 / iCloud web shortcuts, and Settings -> Mail apps chooses which of those cards show. **Sign in with Google or Microsoft instead of app passwords** (optional): create an OAuth app and put its ID and secret in the environment of the `cypht` service (`GMAIL_CLIENT_ID` / `GMAIL_CLIENT_SECRET` from console.cloud.google.com, with `/webmail/?page=home` as the authorized redirect URI; diff --git a/public/app.js b/public/app.js index 9871203..434f3ee 100644 --- a/public/app.js +++ b/public/app.js @@ -245,7 +245,7 @@ function viewHome() { ${allowed('audio') ? tile('#/audio', '#8a5cf0', 'audio', 'Audiobooks', 'Listen') : ''} ${allowed('podcast') ? tile('#/podcast', '#c2417a', 'mic', 'Podcast', 'Podcasts') : ''} ${allowed('verse') && cfg.verse ? tile(cfg.verse, '#0f9d9a', 'book', 'Podcast Admin', 'Opens in a new tab', true) : ''} - ${allowed('mail') ? tile('#/mail', '#4f7cff', 'mail', 'Mail', '4 accounts') : ''} + ${allowed('mail') ? tile('#/mail', '#4f7cff', 'mail', 'Mail', mailOn().length + (mailOn().length === 1 ? ' app' : ' apps')) : ''} ${allowed('mail') ? `

Mail

${MAIL.map(m => tile(mailUrl(m, 'inbox'), m.color, 'mail', m.name, 'Open inbox', true)).join('')} @@ -718,13 +718,13 @@ async function renderTasksCard(el) { // Mail: with WEBMAIL_URL set, your own webmail (which you can allow to be framed) opens inside the app and keeps running between tabs. // The provider shortcuts (Gmail, Outlook, ...) stay one tap away, because those sites refuse to be framed. function viewMail(args = []) { - if (cfg.webmail && args[0] !== 'accounts') return viewWebmail(); + if (cfg.webmail && args[0] === 'webmail') return viewWebmail(); // the shortcuts are the default; the webmail is one tap away return viewMailCards(); } function viewWebmail() { const old = $('#audiowrap iframe[data-k="mail"]'); $('#view').innerHTML = `
Mail${old ? '' : 'Opening…'} - ${icon('mail')} + ${icon('mail')} ${icon('ext')}
`; @@ -763,8 +763,9 @@ async function webmailHooks(f) { } function viewMailCards() { const hints = mailHints(), on = mailOn(), shown = MAIL.filter(m => on.includes(m.id)), anyHint = shown.some(m => hints[m.id]); - $('#view').innerHTML = `
${cfg.webmail ? `` : ''}

Mail

+ $('#view').innerHTML = `

Mail

Each account opens in its own webmail tab. Nothing is stored here.

+ ${cfg.webmail ? `` : ''} ${shown.length ? '' : '

No mail apps chosen. Pick the ones you use in Settings.

'}
${shown.map(m => `
${icon('mail')}${esc(m.name)}${esc(hints[m.id] || (m.id === 'icloud' ? 'Opens iCloud Mail' : 'No address saved'))}
${icon('mail')}Inbox diff --git a/server.js b/server.js index b0a0e85..dd65c11 100644 --- a/server.js +++ b/server.js @@ -368,7 +368,7 @@ function proxyWebmail(req, res) { r.resume(); return res.end(`

Opening the sign-in page… Continue

`); } if (loc && dest === 'document' && /[?&]state=nux_authorization/.test(req.url)) { // back from Google / Microsoft in the whole window: carry on inside the Mail tab - res.writeHead(302, { 'set-cookie': r.headers['set-cookie'] || [], location: '/#/mail', 'cache-control': 'no-store' }); r.resume(); return res.end(); + res.writeHead(302, { 'set-cookie': r.headers['set-cookie'] || [], location: '/#/mail/webmail', 'cache-control': 'no-store' }); r.resume(); return res.end(); } if (loc) { // keep redirects on the hub, under /webmail/ (the webmail thinks it lives at the root) const u = new URL(loc, 'http://' + (req.headers.host || 'hub') + '/'); diff --git a/tools/webmail-test.js b/tools/webmail-test.js index 1ec3b15..3d27162 100644 --- a/tools/webmail-test.js +++ b/tools/webmail-test.js @@ -47,7 +47,7 @@ const SECRET = 'webmail-test-secret-'.repeat(2); r = await call('/webmail/?_task=login&_action=oauth', { headers: { 'sec-fetch-dest': 'document' } }); ok('opened on its own (new tab), the redirect to the sign-in page is passed on as it is', r.status === 302 && r.headers.get('location').startsWith('https://login.example.test/'), [r.status, r.headers.get('location')]); r = await call('/webmail/?page=home&state=nux_authorization&code=1', { headers: { 'sec-fetch-dest': 'document' } }); - ok('coming back from the sign-in page keeps the cookies and returns to the Mail tab', r.status === 302 && r.headers.get('location') === '/#/mail' && (r.headers.getSetCookie() || []).some(c => c.startsWith('hm_session=oauth')), [r.status, r.headers.get('location'), r.headers.getSetCookie()]); + ok('coming back from the sign-in page keeps the cookies and returns to the Mail tab', r.status === 302 && r.headers.get('location') === '/#/mail/webmail' && (r.headers.getSetCookie() || []).some(c => c.startsWith('hm_session=oauth')), [r.status, r.headers.get('location'), r.headers.getSetCookie()]); r = await call('/api/webmail/login'); const lg = await r.json(); ok('the Mail tab can sign in to the webmail for you', lg.user === 'hub' && lg.pass === 'secret-pass', lg); r = await fetch(base + '/api/webmail/login', { redirect: 'manual' });