Brand the webmail INBUXA: name, mark and wordmark

INBUXA is a product suite and ihasmail an independent product, so INBUXA's
webmail says INBUXA: the sign-in page, header, page title, installed-app name
and About page. The wordmark is drawn in the current text color. ihasmail's
version and AGPL source line stay as its credit. Two new strings, in all nine
catalogues; three Stalwart-only ones are no longer used.
This commit is contained in:
2026-09-18 15:58:56 -07:00
parent cdd8fabff9
commit 2e668edb51
20 changed files with 92 additions and 30 deletions
+11
View File
@@ -20,11 +20,22 @@ keep up. Nothing here is pushed there.
no address: only whether this is the person's own device, then the server's 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, page takes it from there. With several servers, the address comes first,
since its domain picks the server. since its domain picks the server.
- **Branded INBUXA.** INBUXA is a product suite, and its webmail carries the
INBUXA name, mark and wordmark, so it can't be taken for public ihasmail,
which stays an independent product. The default `APP_NAME` is `INBUXA`; the
sign-in page, header, page title, installed-app name and About page say
INBUXA. ihasmail's version and AGPL source line stay as its credit. Set
`APP_NAME` to something else and that name shows as text, as in public
ihasmail.
- **Tenants are offered on every server**, whatever edition it reports. - **Tenants are offered on every server**, whatever edition it reports.
`SHOW_ENTERPRISE_NOTICES` still adds the notice for an upstream Stalwart. `SHOW_ENTERPRISE_NOTICES` still adds the notice for an upstream Stalwart.
## Configuration ## Configuration
**Before any deployment, set `SOURCE_URL`** to where this fork's source is
published. The AGPL's offer has to point at the source of the code that's
running, and the default still points at public ihasmail.
Server sign-in is on when `OAUTH_CLIENT_SECRET` is set. Without it, Server sign-in is on when `OAUTH_CLIENT_SECRET` is set. Without it,
ihasmail-inbuxa keeps public ihasmail's password form. ihasmail-inbuxa keeps public ihasmail's password form.
+1 -1
View File
@@ -278,7 +278,7 @@ if (oauthClientSecret && !publicUrl) {
export const config = { export const config = {
isProd, isProd,
appName: env("APP_NAME", "ihasmail"), appName: env("APP_NAME", "INBUXA"),
settingsPolicy: readSettingsPolicy(), settingsPolicy: readSettingsPolicy(),
/** /**
* What this build calls itself: `2.16.57`. Set by the image build from * What this build calls itself: `2.16.57`. Set by the image build from
+1 -1
View File
@@ -23,7 +23,7 @@
<link rel="icon" type="image/png" sizes="64x64" href="/img/favicon-64.png" /> <link rel="icon" type="image/png" sizes="64x64" href="/img/favicon-64.png" />
<link rel="apple-touch-icon" href="/img/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/img/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.webmanifest" /> <link rel="manifest" href="/manifest.webmanifest" />
<title>ihasmail</title> <title>INBUXA</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+3 -3
View File
@@ -1,7 +1,7 @@
{ {
"name": "ihasmail", "name": "INBUXA",
"short_name": "ihasmail", "short_name": "INBUXA",
"description": "Fast, friendly JMAP webmail for Stalwart", "description": "INBUXA webmail",
"_comment": "JSON has no comments, so: every URL below is relative on purpose. Manifest members resolve against the manifest's own address, so these follow BASE_PATH with nothing substituted into them at build time. Root-absolute values pinned the installed app, its scope and its shortcuts to the domain root whatever the mount was.", "_comment": "JSON has no comments, so: every URL below is relative on purpose. Manifest members resolve against the manifest's own address, so these follow BASE_PATH with nothing substituted into them at build time. Root-absolute values pinned the installed app, its scope and its shortcuts to the domain root whatever the mount was.",
"_comment_id": "There is deliberately no `id`. It is the one member NOT resolved against this file's address -- the spec resolves it against the origin of start_url, so `./`, `mail` and `/mail` all mean the same thing at the domain root and none of them can name a subpath mount. Adding one would therefore break the same thing the note above describes. Worse, the default id IS start_url, which is already mount-correct: writing an id now would give every installed copy a new identity and orphan it as a second app rather than updating it. If one is ever wanted it has to be substituted at build time from BASE_PATH, and the changeover costs everybody their install.", "_comment_id": "There is deliberately no `id`. It is the one member NOT resolved against this file's address -- the spec resolves it against the origin of start_url, so `./`, `mail` and `/mail` all mean the same thing at the domain root and none of them can name a subpath mount. Adding one would therefore break the same thing the note above describes. Worse, the default id IS start_url, which is already mount-correct: writing an id now would give every installed copy a new identity and orphan it as a second app rather than updating it. If one is ever wanted it has to be substituted at build time from BASE_PATH, and the changeover costs everybody their install.",
"start_url": "mail", "start_url": "mail",
+3 -1
View File
@@ -10,4 +10,6 @@
* One constant rather than the string written out at each of them, because * One constant rather than the string written out at each of them, because
* three copies of a default is how two of them end up stale. * three copies of a default is how two of them end up stale.
*/ */
export const DEFAULT_APP_NAME = "ihasmail"; // ihasmail-inbuxa: INBUXA's webmail goes by INBUXA, so it can't be taken for
// public ihasmail. APP_NAME still names a deployment whatever it likes.
export const DEFAULT_APP_NAME = "INBUXA";
+1 -1
View File
@@ -1,6 +1,6 @@
import { withBase } from "../basePath"; import { withBase } from "../basePath";
let baseTitle = "ihasmail"; let baseTitle = "INBUXA";
let faviconCanvas: HTMLCanvasElement | null = null; let faviconCanvas: HTMLCanvasElement | null = null;
let baseFavicon: HTMLImageElement | null = null; let baseFavicon: HTMLImageElement | null = null;
+2
View File
@@ -1242,6 +1242,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Filter konnten nicht gespeichert werden: {error}", "Could not save filters: {error}": "Filter konnten nicht gespeichert werden: {error}",
"Could not send the receipt: {error}": "Die Lesebestätigung konnte nicht gesendet werden: {error}", "Could not send the receipt: {error}": "Die Lesebestätigung konnte nicht gesendet werden: {error}",
"Could not sign in.": "Anmeldung fehlgeschlagen.", "Could not sign in.": "Anmeldung fehlgeschlagen.",
"About INBUXA webmail": "Über INBUXA Webmail",
"Mail server": "Mailserver",
"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 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.", "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.", "This sign-in didn't start in this browser. Try again.": "Diese Anmeldung wurde nicht in diesem Browser begonnen. Bitte versuchen Sie es erneut.",
+2
View File
@@ -1215,6 +1215,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "No se pudieron guardar los filtros: {error}", "Could not save filters: {error}": "No se pudieron guardar los filtros: {error}",
"Could not send the receipt: {error}": "No se pudo enviar la confirmación de lectura: {error}", "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.", "Could not sign in.": "No se pudo iniciar sesión.",
"About INBUXA webmail": "Acerca de INBUXA webmail",
"Mail server": "Servidor de correo",
"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 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.", "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.", "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.",
+2
View File
@@ -1220,6 +1220,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Impossible denregistrer les filtres : {error}", "Could not save filters: {error}": "Impossible denregistrer les filtres : {error}",
"Could not send the receipt: {error}": "Impossible denvoyer laccusé de lecture : {error}", "Could not send the receipt: {error}": "Impossible denvoyer laccusé de lecture : {error}",
"Could not sign in.": "Connexion impossible.", "Could not sign in.": "Connexion impossible.",
"About INBUXA webmail": "À propos dINBUXA webmail",
"Mail server": "Serveur de messagerie",
"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 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.", "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 na pas commencé dans ce navigateur. Réessayez.", "This sign-in didn't start in this browser. Try again.": "Cette connexion na pas commencé dans ce navigateur. Réessayez.",
+2
View File
@@ -1223,6 +1223,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "フィルターを保存できませんでした: {error}", "Could not save filters: {error}": "フィルターを保存できませんでした: {error}",
"Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}", "Could not send the receipt: {error}": "開封確認を送信できませんでした: {error}",
"Could not sign in.": "サインインできませんでした。", "Could not sign in.": "サインインできませんでした。",
"About INBUXA webmail": "INBUXA ウェブメールについて",
"Mail server": "メールサーバー",
"You'll enter your password on your mail server's sign-in page.": "パスワードはメールサーバーのサインインページで入力します。", "You'll enter your password on your mail server's sign-in page.": "パスワードはメールサーバーのサインインページで入力します。",
"You'll sign in on your mail server's own page.": "メールサーバー自身のページでサインインします。", "You'll sign in on your mail server's own page.": "メールサーバー自身のページでサインインします。",
"This sign-in didn't start in this browser. Try again.": "このサインインはこのブラウザーで開始されたものではありません。もう一度お試しください。", "This sign-in didn't start in this browser. Try again.": "このサインインはこのブラウザーで開始されたものではありません。もう一度お試しください。",
+2
View File
@@ -1213,6 +1213,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Filters opslaan mislukt: {error}", "Could not save filters: {error}": "Filters opslaan mislukt: {error}",
"Could not send the receipt: {error}": "De leesbevestiging kon niet worden verzonden: {error}", "Could not send the receipt: {error}": "De leesbevestiging kon niet worden verzonden: {error}",
"Could not sign in.": "Aanmelden mislukt.", "Could not sign in.": "Aanmelden mislukt.",
"About INBUXA webmail": "Over INBUXA webmail",
"Mail server": "Mailserver",
"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 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.", "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.", "This sign-in didn't start in this browser. Try again.": "Deze aanmelding is niet in deze browser begonnen. Probeer het opnieuw.",
+2
View File
@@ -1218,6 +1218,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Não foi possível salvar os filtros: {error}", "Could not save filters: {error}": "Não foi possível salvar os filtros: {error}",
"Could not send the receipt: {error}": "Não foi possível enviar a confirmação de leitura: {error}", "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.", "Could not sign in.": "Não foi possível entrar.",
"About INBUXA webmail": "Sobre o INBUXA webmail",
"Mail server": "Servidor de e-mail",
"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 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.", "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.", "This sign-in didn't start in this browser. Try again.": "Esta entrada não começou neste navegador. Tente de novo.",
+2
View File
@@ -1217,6 +1217,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Не удалось сохранить фильтры: {error}", "Could not save filters: {error}": "Не удалось сохранить фильтры: {error}",
"Could not send the receipt: {error}": "Не удалось отправить уведомление о прочтении: {error}", "Could not send the receipt: {error}": "Не удалось отправить уведомление о прочтении: {error}",
"Could not sign in.": "Не удалось войти.", "Could not sign in.": "Не удалось войти.",
"About INBUXA webmail": "О веб-почте INBUXA",
"Mail server": "Почтовый сервер",
"You'll enter your password on your mail server's sign-in page.": "Пароль вводится на странице входа вашего почтового сервера.", "You'll enter your password on your mail server's sign-in page.": "Пароль вводится на странице входа вашего почтового сервера.",
"You'll sign in on your mail server's own page.": "Вход выполняется на странице вашего почтового сервера.", "You'll sign in on your mail server's own page.": "Вход выполняется на странице вашего почтового сервера.",
"This sign-in didn't start in this browser. Try again.": "Этот вход начат не в этом браузере. Попробуйте ещё раз.", "This sign-in didn't start in this browser. Try again.": "Этот вход начат не в этом браузере. Попробуйте ещё раз.",
+2
View File
@@ -1211,6 +1211,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "Не вдалося зберегти фільтри: {error}", "Could not save filters: {error}": "Не вдалося зберегти фільтри: {error}",
"Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}", "Could not send the receipt: {error}": "Не вдалося надіслати сповіщення про прочитання: {error}",
"Could not sign in.": "Не вдалося увійти.", "Could not sign in.": "Не вдалося увійти.",
"About INBUXA webmail": "Про вебпошту INBUXA",
"Mail server": "Поштовий сервер",
"You'll enter your password on your mail server's sign-in page.": "Пароль вводиться на сторінці входу вашого поштового сервера.", "You'll enter your password on your mail server's sign-in page.": "Пароль вводиться на сторінці входу вашого поштового сервера.",
"You'll sign in on your mail server's own page.": "Вхід виконується на сторінці вашого поштового сервера.", "You'll sign in on your mail server's own page.": "Вхід виконується на сторінці вашого поштового сервера.",
"This sign-in didn't start in this browser. Try again.": "Цей вхід розпочато не в цьому браузері. Спробуйте ще раз.", "This sign-in didn't start in this browser. Try again.": "Цей вхід розпочато не в цьому браузері. Спробуйте ще раз.",
+2
View File
@@ -1222,6 +1222,8 @@ export const catalog: Catalog = {
"Could not save filters: {error}": "无法保存过滤器:{error}", "Could not save filters: {error}": "无法保存过滤器:{error}",
"Could not send the receipt: {error}": "无法发送已读回执:{error}", "Could not send the receipt: {error}": "无法发送已读回执:{error}",
"Could not sign in.": "无法登录。", "Could not sign in.": "无法登录。",
"About INBUXA webmail": "关于 INBUXA 网页邮箱",
"Mail server": "邮件服务器",
"You'll enter your password on your mail server's sign-in page.": "您将在邮件服务器的登录页面上输入密码。", "You'll enter your password on your mail server's sign-in page.": "您将在邮件服务器的登录页面上输入密码。",
"You'll sign in on your mail server's own page.": "您将在邮件服务器自己的页面上登录。", "You'll sign in on your mail server's own page.": "您将在邮件服务器自己的页面上登录。",
"This sign-in didn't start in this browser. Try again.": "此次登录不是在这个浏览器中发起的。请重试。", "This sign-in didn't start in this browser. Try again.": "此次登录不是在这个浏览器中发起的。请重试。",
+30
View File
@@ -0,0 +1,30 @@
/**
* The INBUXA wordmark, "inbuxa" in Space Grotesk Bold, as vector paths in the
* current text color, so it reads on every palette, light or dark. The mark
* beside it is `/img/inbuxa-mark.png`: ihasmail's own cat and envelope, which
* INBUXA's logo reuses unchanged. The paths are the same ones INBUXA Admin
* draws, from the INBUXA logo bundle.
*
* ihasmail-inbuxa only. Public ihasmail keeps its own name.
*/
export function InbuxaWordmark({ className, height }: { className?: string; height: number }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="296 44 488 112"
height={height}
width={(height * 488) / 112}
role="img"
aria-label="INBUXA"
className={`notranslate ${className ?? ""}`}
fill="currentColor"
>
<path d="M70 0V496H196V0ZM133 554Q99 554 75.5 576.0Q52 598 52 634Q52 670 75.5 692.0Q99 714 133 714Q168 714 191.0 692.0Q214 670 214 634Q214 598 191.0 576.0Q168 554 133 554Z" transform="translate(303.25,151.90) scale(0.150000,-0.150000)" />
<path d="M70 0V496H194V431H212Q224 457 257.0 480.5Q290 504 357 504Q415 504 458.5 477.5Q502 451 526.0 404.5Q550 358 550 296V0H424V286Q424 342 396.5 370.0Q369 398 318 398Q260 398 228.0 359.5Q196 321 196 252V0Z" transform="translate(340.15,151.90) scale(0.150000,-0.150000)" />
<path d="M368 -14Q301 -14 265.0 9.0Q229 32 212 60H194V0H70V700H196V439H214Q225 457 243.5 473.0Q262 489 292.5 499.5Q323 510 368 510Q428 510 479.0 480.5Q530 451 561.0 394.0Q592 337 592 256V240Q592 159 561.0 102.0Q530 45 479.0 15.5Q428 -14 368 -14ZM330 96Q388 96 427.0 133.5Q466 171 466 243V253Q466 325 427.5 362.5Q389 400 330 400Q272 400 233.0 362.5Q194 325 194 253V243Q194 171 233.0 133.5Q272 96 330 96Z" transform="translate(429.55,151.90) scale(0.150000,-0.150000)" />
<path d="M259 -8Q201 -8 157.5 18.5Q114 45 90.0 92.0Q66 139 66 200V496H192V210Q192 154 219.5 126.0Q247 98 298 98Q356 98 388.0 136.5Q420 175 420 244V496H546V0H422V65H404Q392 40 359.0 16.0Q326 -8 259 -8Z" transform="translate(522.25,151.90) scale(0.150000,-0.150000)" />
<path d="M26 0 206 250 28 496H174L287 331H305L418 496H564L386 250L566 0H418L305 167H287L174 0Z" transform="translate(611.65,151.90) scale(0.150000,-0.150000)" />
<path d="M224 -14Q171 -14 129.0 4.5Q87 23 62.5 58.5Q38 94 38 145Q38 196 62.5 230.5Q87 265 130.5 282.5Q174 300 230 300H366V328Q366 363 344.0 385.5Q322 408 274 408Q227 408 204.0 386.5Q181 365 174 331L58 370Q70 408 96.5 439.5Q123 471 167.5 490.5Q212 510 276 510Q374 510 431.0 461.0Q488 412 488 319V134Q488 104 516 104H556V0H472Q435 0 411.0 18.0Q387 36 387 66V67H368Q364 55 350.0 35.5Q336 16 306.0 1.0Q276 -14 224 -14ZM246 88Q299 88 332.5 117.5Q366 147 366 196V206H239Q204 206 184.0 191.0Q164 176 164 149Q164 122 185.0 105.0Q206 88 246 88Z" transform="translate(697.45,151.90) scale(0.150000,-0.150000)" />
</svg>
);
}
+6 -1
View File
@@ -4,6 +4,7 @@ import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogO
import { useSession } from "@/store/session"; import { useSession } from "@/store/session";
import { withBase } from "@/lib/basePath"; import { withBase } from "@/lib/basePath";
import { DEFAULT_APP_NAME } from "@/lib/brand"; import { DEFAULT_APP_NAME } from "@/lib/brand";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
import { useEffectiveTheme, useSettings } from "@/store/settings"; import { useEffectiveTheme, useSettings } from "@/store/settings";
import { toggleTarget } from "@/lib/palette"; import { toggleTarget } from "@/lib/palette";
import { useMail } from "@/store/mail"; import { useMail } from "@/store/mail";
@@ -147,14 +148,18 @@ export function AppShell({ children }: { children: ReactNode }) {
<MenuIcon size={22} /> <MenuIcon size={22} />
</button> </button>
<Link href="/mail" className="brand"> <Link href="/mail" className="brand">
<img src={withBase("/img/logo.png")} alt="" /> <img src={withBase(appName === DEFAULT_APP_NAME ? "/img/inbuxa-mark.png" : "/img/logo.png")} alt="" />
{/* A product name, not a word: translated it is a different product. {/* A product name, not a word: translated it is a different product.
Read from the session rather than written here, so a deployment Read from the session rather than written here, so a deployment
that set APP_NAME is called what it calls itself -- the document that set APP_NAME is called what it calls itself -- the document
title has taken it from there all along. */} title has taken it from there all along. */}
{appName === DEFAULT_APP_NAME ? (
<InbuxaWordmark className="brand-name" height={22} />
) : (
<span className="brand-name notranslate" translate="no"> <span className="brand-name notranslate" translate="no">
{appName} {appName}
</span> </span>
)}
</Link> </Link>
<SearchBar /> <SearchBar />
<div className="topbar-actions"> <div className="topbar-actions">
+6 -3
View File
@@ -7,6 +7,7 @@ import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version"; import { APP_VERSION } from "@/lib/version";
import { DEFAULT_APP_NAME } from "@/lib/brand"; import { DEFAULT_APP_NAME } from "@/lib/brand";
import { t } from "@/lib/i18n"; import { t } from "@/lib/i18n";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
export function LoginPage() { export function LoginPage() {
const login = useSession((s) => s.login); const login = useSession((s) => s.login);
@@ -92,10 +93,12 @@ export function LoginPage() {
<div className="login-page"> <div className="login-page">
<form className="login-card" onSubmit={submit}> <form className="login-card" onSubmit={submit}>
<div className="logo"> <div className="logo">
<img src={withBase("/img/logo.png")} alt="" width={120} height={143} /> <img src={withBase(appName === DEFAULT_APP_NAME ? "/img/inbuxa-mark.png" : "/img/logo.png")} alt="" width={120} height={143} />
{/* A product name, not a word: not translated, and not guessed at {/* A product name, not a word: not translated, and not guessed at
from the page it is on. */} from the page it is on. INBUXA's is its wordmark. */}
<h1 className="notranslate" translate="no">{appName}</h1> <h1 className="notranslate" translate="no">
{appName === DEFAULT_APP_NAME ? <InbuxaWordmark height={34} /> : appName}
</h1>
<p className="tagline">{t("Fast, friendly webmail. Your mailbox, your way.")}</p> <p className="tagline">{t("Fast, friendly webmail. Your mailbox, your way.")}</p>
</div> </div>
{error && ( {error && (
+9 -16
View File
@@ -4,6 +4,7 @@ import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version"; import { APP_VERSION } from "@/lib/version";
import { withBase } from "@/lib/basePath"; import { withBase } from "@/lib/basePath";
import { t, tNode } from "@/lib/i18n"; import { t, tNode } from "@/lib/i18n";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
export function AboutSettings() { export function AboutSettings() {
const session = useSession((s) => s.session); const session = useSession((s) => s.session);
@@ -12,13 +13,16 @@ export function AboutSettings() {
const sourceUrl = session?.ihasmail?.sourceUrl ?? DEFAULT_SOURCE_URL; const sourceUrl = session?.ihasmail?.sourceUrl ?? DEFAULT_SOURCE_URL;
return ( return (
<div> <div>
<h1>{t("About ihasmail")}</h1> {/* ihasmail-inbuxa: INBUXA's webmail, built on ihasmail, whose version
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <a href="https://stalw.art" target="_blank" rel="noreferrer">{t("Stalwart Mail Server")}</a> })}</p> and source stay here as its credit and the AGPL's offer. */}
<h1>{t("About INBUXA webmail")}</h1>
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <span className="notranslate" translate="no">INBUXA</span> })}</p>
<div className="row" style={{ gap: 16, alignItems: "center", marginBottom: 16 }}> <div className="row" style={{ gap: 16, alignItems: "center", marginBottom: 16 }}>
<img src={withBase("/img/logo.png")} alt={t("ihasmail")} width={96} /> <img src={withBase("/img/inbuxa-mark.png")} alt="" width={80} />
<div> <div>
<InbuxaWordmark height={26} />
{/* A product name and a version string: neither is a word to translate. */} {/* A product name and a version string: neither is a word to translate. */}
<div style={{ fontWeight: 700, fontSize: "1.2em" }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div> <div style={{ fontWeight: 700 }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div>
<div className="hint">{tNode("AGPL-3.0-or-later · {source}", { source: <a href={sourceUrl} target="_blank" rel="noreferrer">{sourceUrl.replace(/^https?:\/\//, "")}</a> })}</div> <div className="hint">{tNode("AGPL-3.0-or-later · {source}", { source: <a href={sourceUrl} target="_blank" rel="noreferrer">{sourceUrl.replace(/^https?:\/\//, "")}</a> })}</div>
</div> </div>
</div> </div>
@@ -26,7 +30,7 @@ export function AboutSettings() {
<table className="sessions-table"> <table className="sessions-table">
<tbody> <tbody>
<tr><td>{t("Signed in as")}</td><td>{session?.username}</td></tr> <tr><td>{t("Signed in as")}</td><td>{session?.username}</td></tr>
<tr><td>{t("Stalwart")}</td><td>{describeServer(session?.ihasmail?.server)}</td></tr> <tr><td>{t("Mail server")}</td><td className="notranslate" translate="no">INBUXA</td></tr>
<tr><td>{t("Accounts")}</td><td>{Object.values(session?.accounts ?? {}).map((a) => a.name).join(", ")}</td></tr> <tr><td>{t("Accounts")}</td><td>{Object.values(session?.accounts ?? {}).map((a) => a.name).join(", ")}</td></tr>
<tr><td>{t("Max upload")}</td><td>{t("{size} MB", { size: Math.round(client.maxSizeUpload / 1048576) })}</td></tr> <tr><td>{t("Max upload")}</td><td>{t("{size} MB", { size: Math.round(client.maxSizeUpload / 1048576) })}</td></tr>
<tr><td>{t("Image privacy proxy")}</td><td>{session?.ihasmail?.imageProxy ? t("enabled") : t("disabled")}</td></tr> <tr><td>{t("Image privacy proxy")}</td><td>{session?.ihasmail?.imageProxy ? t("enabled") : t("disabled")}</td></tr>
@@ -41,14 +45,3 @@ export function AboutSettings() {
</div> </div>
); );
} }
/**
* Stalwart deliberately withholds its version from clients (it reports a fixed
* "1.0.0" wherever it publishes one at all), so the edition is all there is to
* show. The generation used to be reported here too, back when ihasmail spoke
* to both 0.15 and 0.16; it requires 0.16 now, so signing in at all is the
* answer to that question.
*/
function describeServer(server: { edition?: string | null } | undefined): string {
return server?.edition ? `0.16 or newer (${server.edition})` : "0.16 or newer";
}