Admin user management, password reset by email, and the Ultimo Email name

Branding: the page titles and app name are now "Ultimo Email".

Admin panel (now two columns with a divider):
- Users: search by login or by a mail account's address or name (results
  only, nothing listed until something is typed). A user opens a window to
  change their quota, block/unblock them, or delete them; block and delete ask
  for confirmation, and an admin cannot do either to their own account.
- Blocking (users.blocked_at, migration 0019) refuses sign-in with a clear
  message and revokes every session; a new user.signed_out event sends the
  user's open tabs to the sign-in screen at once. Any "not signed in" answer
  now does the same, not only /auth/me. Deleting removes mail accounts (sync
  stops), plugin rows (cleared inside the user's RLS context, since plugin
  tables have no FK to users), blobs, the search index, sessions and the user.
- Outgoing system mail (migration 0020): SMTP server, port, security, login,
  password (vault-encrypted, never sent back), From address and the public
  address reset links point to, plus a "Send test" that reports the server's
  error.

Password reset:
- "Forgot password?" on the sign-in page asks for the user name and always
  answers "If this account exists you will receive an email to your recovery
  email address"; the mail goes out after the response, so neither the reply
  nor its timing reveals an account, and it is capped at 3 per hour.
- The emailed link (token in the URL fragment, single use, 30 minutes) opens a
  page to set a new password. With 2-step verification on, the code is checked
  before anything changes; five wrong codes burn the link.
- Settings > Profile: the recovery email can be added, changed or removed,
  confirmed with the current password, which must be typed — autofill and
  paste are refused (TypedPasswordInput).

Also adds frontend/icons/ultimo-logo-bimi.svg, an SVG Tiny PS version of the
logo for a BIMI record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:16:27 +02:00
co-authored by Claude Opus 5.5
parent 8dc60dbf5c
commit 0ddc0cb001
32 changed files with 2267 additions and 87 deletions
+72 -2
View File
@@ -3,7 +3,7 @@
// Interpolate with {name} placeholders.
export const en = {
"common.appName": "Webmail",
"common.appName": "Ultimo Email",
"common.loading": "Loading…",
"common.cancel": "Cancel",
"common.save": "Save",
@@ -34,6 +34,26 @@ export const en = {
"auth.create": "Create account",
"auth.creating": "Creating…",
"auth.noAccount": "No account?",
"auth.forgotPassword": "Forgot password?",
"auth.forgotTitle": "Reset your password",
"auth.forgotHint":
"Enter your user name. We'll email a link for choosing a new password to the recovery email address of the account.",
"auth.changePassword": "Change password",
"auth.sending": "Sending…",
"auth.forgotSent": "If this account exists you will receive an email to your recovery email address.",
"auth.backToSignIn": "Back to sign in",
"auth.resetTitle": "Choose a new password",
"auth.newPassword": "New password",
"auth.repeatPassword": "Repeat the new password",
"auth.passwordsDiffer": "The two passwords are different.",
"auth.passwordTooShort": "Use at least 8 characters.",
"auth.resetTotpPrompt":
"This account uses 2-step verification. Enter the 6-digit code from your authenticator app to finish.",
"auth.confirm": "Confirm",
"auth.resetDone": "Your password has been changed. You have been signed out everywhere — sign in with the new password.",
"auth.resetInvalid":
"This link is invalid, was already used, or has expired. Request a new one to change your password.",
"auth.requestNewLink": "Request a new link",
"auth.haveAccount": "Already have an account?",
"auth.register": "Register",
"auth.checking": "Checking…",
@@ -327,6 +347,15 @@ export const en = {
"settings.appearance": "Appearance",
"settings.handle": "Handle",
"settings.recoveryEmail": "Recovery email",
"settings.recoveryEmailHint":
"If you forget your password, a link to choose a new one is sent here.",
"settings.recoveryEmailNew": "Recovery email address",
"settings.recoveryEmailRemoveHint": "Leave empty to remove it.",
"settings.currentPassword": "Current password",
"settings.typePassword": "Type your password — for this confirmation it can't be filled in or pasted.",
"settings.change": "Change",
"settings.add": "Add",
"settings.saving": "Saving…",
"settings.twoFactor": "Two-factor authentication",
"settings.twoFactorOn": "2FA is enabled.",
"settings.twoFactorOff": "2FA is not enabled.",
@@ -412,7 +441,7 @@ export const en = {
"settings.alsoFile": "Also mark the message as read and file it",
"settings.integrations": "Integrations",
"admin.title": "Webmail administration",
"admin.title": "Ultimo Email administration",
"admin.signInTitle": "Administrator sign-in",
"admin.signInHint": "Sign in with an administrator account to manage this server.",
"admin.signOut": "Sign out",
@@ -432,6 +461,47 @@ export const en = {
"admin.saving": "Saving…",
"admin.saved": "Saved",
"admin.quotaInvalid": "Enter a size greater than 0.",
"admin.mail": "Outgoing system mail",
"admin.mailHelp":
"The mail account the server sends password-reset links from. Without it, “Forgot password?” sends nothing.",
"admin.mailOn": "Set up — password-reset links can be sent.",
"admin.mailOff": "Not set up — password-reset links cannot be sent yet.",
"admin.smtpHost": "SMTP server",
"admin.smtpPort": "Port",
"admin.smtpSecurity": "Security",
"admin.smtpStarttls": "STARTTLS",
"admin.smtpSsl": "SSL/TLS",
"admin.smtpNone": "None",
"admin.smtpUsername": "Login",
"admin.smtpPassword": "Password",
"admin.smtpPasswordKept": "(stored — leave empty to keep)",
"admin.smtpPasswordRemove": "Remove the stored password",
"admin.smtpFrom": "From address",
"admin.publicUrl": "Public address of this server",
"admin.publicUrlHint": "Where users open Ultimo Email, e.g. https://mail.example.com — reset links point here.",
"admin.testTo": "Send a test email to",
"admin.sendTest": "Send test",
"admin.testSent": "Test email sent.",
"admin.users": "Users",
"admin.userSearch": "Search by login, email address or name…",
"admin.noUsers": "No users match.",
"admin.you": "you",
"admin.adminBadge": "admin",
"admin.blockedBadge": "blocked",
"admin.usage": "{used} of {quota}",
"admin.noMailAccounts": "No mail accounts connected",
"admin.userQuota": "Quota",
"admin.blockedSince": "Blocked since {date}. This user cannot sign in.",
"admin.block": "Block account",
"admin.unblock": "Unblock account",
"admin.blockConfirm":
"Block {handle}? They are signed out everywhere and cannot sign in until you unblock them. Their mail and data are kept.",
"admin.deleteUser": "Delete account",
"admin.deleteConfirm":
"Permanently delete {handle} and all of their mail, contacts, calendars and settings? This can't be undone.",
"admin.selfNote": "This is your own account: it cannot be blocked or deleted from here.",
"admin.cancel": "Cancel",
"admin.close": "Close",
"settings.jitsi": "Jitsi Meet",
"settings.jitsiHint":
"Server used to generate video-call links for calendar events. Leave empty to use the public https://meet.jit.si/ server.",