From cdd8fabff90a182b2b2ccd87228cc2c0b2353cb0 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Fri, 18 Sep 2026 15:51:54 -0700 Subject: [PATCH] No address step before signing in when there's only one mail server The server's own page asks for the username, so with a single server the sign-in page keeps only the own-device choice. With several servers the address still comes first, since its domain picks the server. One new string, in all nine catalogues. --- INBUXA.md | 6 +++++- server/src/app.ts | 5 ++++- server/src/oauth.test.ts | 12 ++++++++++++ server/src/oauth.ts | 10 ++++++++++ web/src/locales/de.ts | 1 + web/src/locales/es.ts | 1 + web/src/locales/fr.ts | 1 + web/src/locales/ja.ts | 1 + web/src/locales/nl.ts | 1 + web/src/locales/pt-BR.ts | 1 + web/src/locales/ru.ts | 1 + web/src/locales/uk.ts | 1 + web/src/locales/zh-Hans.ts | 1 + web/src/views/Login.tsx | 17 +++++++++++++---- 14 files changed, 53 insertions(+), 6 deletions(-) diff --git a/INBUXA.md b/INBUXA.md index a26925f..0f143cb 100644 --- a/INBUXA.md +++ b/INBUXA.md @@ -15,7 +15,11 @@ keep up. Nothing here is pushed there. handles a password to sign someone in. Two-factor codes are asked for on that page. Sessions hold sealed tokens and renew them before they expire. A password change revokes the tokens, so it signs the person out everywhere, - this session included. + this session included. With one mail server (no `STALWART_SERVERS_FILE`, + or one whose domains all map to `STALWART_URL`), the sign-in page asks for + no address: only whether this is the person's own device, then the server's + page takes it from there. With several servers, the address comes first, + since its domain picks the server. - **Tenants are offered on every server**, whatever edition it reports. `SHOW_ENTERPRISE_NOTICES` still adds the notice for an upstream Stalwart. diff --git a/server/src/app.ts b/server/src/app.ts index 97d0b94..ff67dfd 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -41,7 +41,7 @@ import { revokeAppPassword, } from "./account.js"; import { imageProxyHandler } from "./imageproxy.js"; -import { SignInError, finish as finishSignIn, needsRefresh, oauthEnabled, refreshTokens, start as startSignIn, type TokenSet } from "./oauth.js"; +import { SignInError, finish as finishSignIn, needsRefresh, oauthEnabled, refreshTokens, singleServer, start as startSignIn, type TokenSet } from "./oauth.js"; import { icsProxyHandler } from "./icsproxy.js"; import { staticHandler } from "./static.js"; @@ -384,6 +384,9 @@ export function createApp(basePath = config.basePath): Hono { settingsPolicy: config.settingsPolicy, /* "oauth": sign in on the mail server's own page (see oauth.ts). */ signIn: oauthEnabled() ? "oauth" : "password", + /* With "oauth": true when the server's page can take it from here, so + the sign-in form doesn't ask for an address first. */ + signInDirect: oauthEnabled() && singleServer(), }), ); diff --git a/server/src/oauth.test.ts b/server/src/oauth.test.ts index fd06a30..7700cb3 100644 --- a/server/src/oauth.test.ts +++ b/server/src/oauth.test.ts @@ -84,6 +84,18 @@ after(() => { test("the configuration tells the web app to use the server's page", async () => { const body = await jsonOf(await call("/api/config")); assert.equal(body.signIn, "oauth"); + assert.equal(body.signInDirect, true, "one mail server: its page asks for the username, not ihasmail"); +}); + +test("with one mail server, sign-in starts without an address", async () => { + const res = await call("/api/auth/oauth/start"); + assert.equal(res.status, 302); + const to = new URL(res.headers.get("location")!); + assert.equal(to.searchParams.has("login_hint"), false); + const approved = await fetch(to, { redirect: "manual" }); + const back = new URL(approved.headers.get("location")!); + assert.equal((await call(`/api/auth/callback${back.search}`)).headers.get("location"), "/"); + assert.equal((await call("/api/auth/session")).status, 200); }); test("the password form is refused: ihasmail never sees a password", async () => { diff --git a/server/src/oauth.ts b/server/src/oauth.ts index 78ef922..5a587fb 100644 --- a/server/src/oauth.ts +++ b/server/src/oauth.ts @@ -52,6 +52,16 @@ export function oauthEnabled(): boolean { return Boolean(config.oauthClientSecret); } +/** + * Whether every account is on the same server. Then sign-in needs no address + * first: the server's page asks for the username itself. With several servers + * (STALWART_SERVERS_FILE), the domain picks the server, so the address comes + * first. + */ +export function singleServer(): boolean { + return Object.values(config.stalwartServers).every((url) => url === config.stalwartUrl); +} + /** The one redirect URI registered for this client on the server. */ export function redirectUri(): string { return `${config.publicUrl}${config.basePath}/api/auth/callback`; diff --git a/web/src/locales/de.ts b/web/src/locales/de.ts index f97ee63..0856551 100644 --- a/web/src/locales/de.ts +++ b/web/src/locales/de.ts @@ -1243,6 +1243,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "Die Lesebestätigung konnte nicht gesendet werden: {error}", "Could not sign in.": "Anmeldung fehlgeschlagen.", "You'll enter your password on your mail server's sign-in page.": "Ihr Passwort geben Sie auf der Anmeldeseite Ihres Mailservers ein.", + "You'll sign in on your mail server's own page.": "Sie melden sich auf der eigenen Seite Ihres Mailservers an.", "This sign-in didn't start in this browser. Try again.": "Diese Anmeldung wurde nicht in diesem Browser begonnen. Bitte versuchen Sie es erneut.", "The sign-in took too long. Try again.": "Die Anmeldung hat zu lange gedauert. Bitte versuchen Sie es erneut.", "Sign-in was cancelled.": "Die Anmeldung wurde abgebrochen.", diff --git a/web/src/locales/es.ts b/web/src/locales/es.ts index ba9d7ee..a68a676 100644 --- a/web/src/locales/es.ts +++ b/web/src/locales/es.ts @@ -1216,6 +1216,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "No se pudo enviar la confirmación de lectura: {error}", "Could not sign in.": "No se pudo iniciar sesión.", "You'll enter your password on your mail server's sign-in page.": "Introducirá su contraseña en la página de inicio de sesión de su servidor de correo.", + "You'll sign in on your mail server's own page.": "Iniciará sesión en la propia página de su servidor de correo.", "This sign-in didn't start in this browser. Try again.": "Este inicio de sesión no empezó en este navegador. Inténtelo de nuevo.", "The sign-in took too long. Try again.": "El inicio de sesión tardó demasiado. Inténtelo de nuevo.", "Sign-in was cancelled.": "Se canceló el inicio de sesión.", diff --git a/web/src/locales/fr.ts b/web/src/locales/fr.ts index 0d5124c..0114003 100644 --- a/web/src/locales/fr.ts +++ b/web/src/locales/fr.ts @@ -1221,6 +1221,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "Impossible d’envoyer l’accusé de lecture : {error}", "Could not sign in.": "Connexion impossible.", "You'll enter your password on your mail server's sign-in page.": "Vous saisirez votre mot de passe sur la page de connexion de votre serveur de messagerie.", + "You'll sign in on your mail server's own page.": "Vous vous connecterez sur la page de votre serveur de messagerie.", "This sign-in didn't start in this browser. Try again.": "Cette connexion n’a pas commencé dans ce navigateur. Réessayez.", "The sign-in took too long. Try again.": "La connexion a pris trop de temps. Réessayez.", "Sign-in was cancelled.": "La connexion a été annulée.", diff --git a/web/src/locales/ja.ts b/web/src/locales/ja.ts index 8d144aa..75d7bac 100644 --- a/web/src/locales/ja.ts +++ b/web/src/locales/ja.ts @@ -1224,6 +1224,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}", "Could not sign in.": "サインインできませんでした。", "You'll enter your password on your mail server's sign-in page.": "パスワードはメールサーバーのサインインページで入力します。", + "You'll sign in on your mail server's own page.": "メールサーバー自身のページでサインインします。", "This sign-in didn't start in this browser. Try again.": "このサインインはこのブラウザーで開始されたものではありません。もう一度お試しください。", "The sign-in took too long. Try again.": "サインインに時間がかかりすぎました。もう一度お試しください。", "Sign-in was cancelled.": "サインインはキャンセルされました。", diff --git a/web/src/locales/nl.ts b/web/src/locales/nl.ts index 914f81f..de54627 100644 --- a/web/src/locales/nl.ts +++ b/web/src/locales/nl.ts @@ -1214,6 +1214,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "De leesbevestiging kon niet worden verzonden: {error}", "Could not sign in.": "Aanmelden mislukt.", "You'll enter your password on your mail server's sign-in page.": "U voert uw wachtwoord in op de aanmeldpagina van uw mailserver.", + "You'll sign in on your mail server's own page.": "U meldt zich aan op de eigen pagina van uw mailserver.", "This sign-in didn't start in this browser. Try again.": "Deze aanmelding is niet in deze browser begonnen. Probeer het opnieuw.", "The sign-in took too long. Try again.": "Het aanmelden duurde te lang. Probeer het opnieuw.", "Sign-in was cancelled.": "Het aanmelden is geannuleerd.", diff --git a/web/src/locales/pt-BR.ts b/web/src/locales/pt-BR.ts index 460144c..911b703 100644 --- a/web/src/locales/pt-BR.ts +++ b/web/src/locales/pt-BR.ts @@ -1219,6 +1219,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "Não foi possível enviar a confirmação de leitura: {error}", "Could not sign in.": "Não foi possível entrar.", "You'll enter your password on your mail server's sign-in page.": "Você vai digitar sua senha na página de entrada do seu servidor de e-mail.", + "You'll sign in on your mail server's own page.": "Você vai entrar na própria página do seu servidor de e-mail.", "This sign-in didn't start in this browser. Try again.": "Esta entrada não começou neste navegador. Tente de novo.", "The sign-in took too long. Try again.": "A entrada demorou demais. Tente de novo.", "Sign-in was cancelled.": "A entrada foi cancelada.", diff --git a/web/src/locales/ru.ts b/web/src/locales/ru.ts index fec6981..6bafd9a 100644 --- a/web/src/locales/ru.ts +++ b/web/src/locales/ru.ts @@ -1218,6 +1218,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "Не удалось отправить уведомление о прочтении: {error}", "Could not sign in.": "Не удалось войти.", "You'll enter your password on your mail server's sign-in page.": "Пароль вводится на странице входа вашего почтового сервера.", + "You'll sign in on your mail server's own page.": "Вход выполняется на странице вашего почтового сервера.", "This sign-in didn't start in this browser. Try again.": "Этот вход начат не в этом браузере. Попробуйте ещё раз.", "The sign-in took too long. Try again.": "Вход занял слишком много времени. Попробуйте ещё раз.", "Sign-in was cancelled.": "Вход отменён.", diff --git a/web/src/locales/uk.ts b/web/src/locales/uk.ts index ab20e56..9c87944 100644 --- a/web/src/locales/uk.ts +++ b/web/src/locales/uk.ts @@ -1212,6 +1212,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}", "Could not sign in.": "Не вдалося увійти.", "You'll enter your password on your mail server's sign-in page.": "Пароль вводиться на сторінці входу вашого поштового сервера.", + "You'll sign in on your mail server's own page.": "Вхід виконується на сторінці вашого поштового сервера.", "This sign-in didn't start in this browser. Try again.": "Цей вхід розпочато не в цьому браузері. Спробуйте ще раз.", "The sign-in took too long. Try again.": "Вхід тривав занадто довго. Спробуйте ще раз.", "Sign-in was cancelled.": "Вхід скасовано.", diff --git a/web/src/locales/zh-Hans.ts b/web/src/locales/zh-Hans.ts index 0ca7345..b4958b1 100644 --- a/web/src/locales/zh-Hans.ts +++ b/web/src/locales/zh-Hans.ts @@ -1223,6 +1223,7 @@ export const catalog: Catalog = { "Could not send the receipt: {error}": "无法发送已读回执:{error}", "Could not sign in.": "无法登录。", "You'll enter your password on your mail server's sign-in page.": "您将在邮件服务器的登录页面上输入密码。", + "You'll sign in on your mail server's own page.": "您将在邮件服务器自己的页面上登录。", "This sign-in didn't start in this browser. Try again.": "此次登录不是在这个浏览器中发起的。请重试。", "The sign-in took too long. Try again.": "登录耗时过长。请重试。", "Sign-in was cancelled.": "登录已取消。", diff --git a/web/src/views/Login.tsx b/web/src/views/Login.tsx index cb130f8..6a141f1 100644 --- a/web/src/views/Login.tsx +++ b/web/src/views/Login.tsx @@ -33,6 +33,8 @@ export function LoginPage() { * and the password form if it never does. */ const [signIn, setSignIn] = useState<"oauth" | "password" | null>(null); + /* With "oauth" and one mail server, its page asks for the username: no address here. */ + const [direct, setDirect] = useState(false); useEffect(() => { let live = true; fetch(withBase("/api/config")) @@ -42,6 +44,7 @@ export function LoginPage() { if (c.sourceUrl) setSourceUrl(c.sourceUrl as string); if (typeof c.appName === "string" && c.appName.trim()) setAppName(c.appName.trim()); setSignIn(c.signIn === "oauth" ? "oauth" : "password"); + setDirect(c.signIn === "oauth" && c.signInDirect === true); }) .catch(() => { /* the default stands */ }) .finally(() => { if (live) setSignIn((m) => m ?? "password"); }); @@ -58,10 +61,10 @@ export function LoginPage() { e.preventDefault(); if (signIn === "oauth") { // Off to the mail server's page, which asks for the password there. - if (!username.trim()) return; + if (!direct && !username.trim()) return; setBusy(true); - if (trustDevice) localStorage.setItem("ihasmail:lastUser", username.trim()); - const params = new URLSearchParams({ username: username.trim(), ...(trustDevice ? { remember: "1" } : {}) }); + if (trustDevice && !direct) localStorage.setItem("ihasmail:lastUser", username.trim()); + const params = new URLSearchParams({ ...(direct ? {} : { username: username.trim() }), ...(trustDevice ? { remember: "1" } : {}) }); window.location.assign(withBase(`/api/auth/oauth/start?${params}`)); return; } @@ -100,12 +103,18 @@ export function LoginPage() { {error} )} + {!direct && (
setUsername(e.target.value)} autoFocus={!username} required />
+ )} {signIn === "oauth" ? ( -

{t("You'll enter your password on your mail server's sign-in page.")}

+

+ {direct + ? t("You'll sign in on your mail server's own page.") + : t("You'll enter your password on your mail server's sign-in page.")} +

) : (