Files
LambdaMail/services/webmail/messages/en.json
T
Lucas RayanandClaude Opus 5 57198e0263 #28: stop enrolment from silently replacing a secret the authenticator already has
Enrolling a second factor could reach a state where the authenticator showed
perfectly valid codes and every one of them was rejected, with the only
feedback being "TOTP verification failed".

The browser holds the enrolment secret in component state alone. Reloading the
settings page - or leaving and coming back - brought the "enable" button back,
and pressing it called the enrol endpoint again, which deleted the pending row
and stored a brand new secret. The entry already saved in the authenticator
then belonged to a secret the server no longer had. Nothing about that is
visible from either side: the app generates codes correctly, the server
compares them correctly, and they are for different keys.

Enrolment now resumes. The endpoint returns the pending secret if there is
one, so the QR shown after a reload is the same one already scanned and
rescanning is harmless. Starting over is still possible - ?reset=1, exposed in
the settings screen - because someone who deleted the entry from their app
needs a secret they can actually generate codes for. A pending row that can no
longer be decrypted is replaced rather than resumed, since nobody could ever
confirm it.

Also: the submitted code now has every non-digit stripped rather than just
surrounding whitespace. Authenticators display the code grouped as "123 456",
and a value typed or pasted with that space failed the six-character length
check before it was ever compared - which, to someone reading the right number
off their phone, looks like the server being wrong. The inputs sanitise as you
type for the same reason.

Ruled out first, rather than guessed at: the algorithm passes all six RFC 6238
reference vectors, base32 round-trips, and the deployment's clock is exactly in
sync with real time, so neither the implementation nor drift explains it.

Verified: the resume test fails without the change and passes with it; 47 auth
tests green against a real Postgres.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 09:47:59 -03:00

290 lines
11 KiB
JSON

{
"admin": {
"addAlias": "Add alias",
"aliasesTitle": "Aliases",
"auditLog": "Audit log",
"backToWebmail": "Back to webmail",
"cancelJob": "Cancel",
"certificateState": "Certificate",
"checksPassed": "{passed} of {total} passed",
"createAccount": "Create account",
"createAlias": "Create a routing alias",
"csvPreview": "Preview",
"dashboardSubtitle": "Live figures read from the database.",
"dashboardTitle": "Overview",
"dkimAlgorithm": "Key type",
"dkimOverlap": "Both keys sign outbound mail for {days} days so nothing fails while DNS propagates.",
"dkimPublishRecord": "Publish this record in DNS:",
"dkimRotate": "Generate a new key",
"dkimSelector": "Selector",
"dkimTitle": "DKIM key rotation",
"dmarcSubtitle": "Breakdown of the aggregate XML reports that have been ingested.",
"dmarcTitle": "DMARC reports",
"dnsRecordsFor": "DNS records for {domain}",
"dnsStatusDrift": "Drift",
"dnsStatusPartial": "Partial",
"dnsStatusVerified": "Verified",
"domainName": "Domain name",
"domainsTitle": "Domains",
"expiresInDays": "Expires in {days} days",
"freezeDomain": "Freeze domain",
"greylistScore": "Greylist score",
"headerScore": "Header-tagging score",
"jobCancelled": "Job {id} was cancelled.",
"jobRetried": "Job {id} was queued for another attempt.",
"lastChecked": "Last checked: {when}",
"lastReload": "Last reload",
"mailHost": "Mail host",
"mailboxCount": "{count} mailboxes",
"mailboxesSubtitle": "Accounts, aliases, CSV import and the second-factor policy.",
"mailboxesTitle": "Mailboxes",
"mfaAdmins": "Required for admins",
"mfaEveryone": "Required for everyone",
"mfaOptional": "Optional",
"mfaPolicyIntro": "Choose how strictly this domain requires a second factor.",
"mfaPolicyTitle": "Second-factor requirement",
"noCertificate": "No certificate is loaded for this host.",
"noChecks": "No checks reported.",
"noDomains": "No domains have been onboarded yet.",
"onboardIntro": "Creates the domain and its standard RFC 2142 aliases, pointed at your address.",
"onboardTitle": "Add a domain",
"openAdmin": "Admin console",
"preflightTitle": "Readiness checks",
"queueEmpty": "The outbound queue is empty.",
"queueSubtitle": "Inspect the outbound queue, retry a job, or cancel one.",
"queueTitle": "Outbound queue",
"readinessIntro": "Checks the server reports about itself before it carries production mail.",
"reconcileDns": "Reconcile DNS",
"reconcileNotImplemented": "Per-record DNS verification is not implemented yet: this records the request and re-reads the stored status, it does not query a resolver.",
"rejectScore": "Reject score",
"retryJob": "Retry",
"rspamdIntro": "Scores that trigger greylisting, header tagging and rejection.",
"rspamdSaved": "Thresholds saved.",
"saveThresholds": "Save thresholds",
"securitySubtitle": "Readiness checks, TLS state, spam thresholds and the audit trail.",
"securityTitle": "Security",
"tabAudit": "Audit trail",
"tabReadiness": "Readiness",
"tabRspamd": "Spam thresholds",
"tabTls": "TLS",
"tlsMode": "TLS mode",
"tlsTitle": "Certificate and transport",
"traceNoResults": "No trace found for that queue ID.",
"tracePlaceholder": "Queue ID",
"traceTitle": "Trace a message",
"unknownSni": "Unknown SNI requests",
"watcherHealthy": "Certificate watcher"
},
"auth": {
"adminLoginTitle": "Admin console",
"adminStepUpNote": "The admin console asks for your second factor again, even though you are already signed in to webmail.",
"authFailed": "Authentication failed",
"cantScanQr": "Cannot scan? Enter this key manually:",
"emailLabel": "Email address",
"enrolmentFailed": "Could not start the second-factor setup",
"enrolmentIntro": "The admin console requires a second factor. Set one up now to continue.",
"genericError": "Something went wrong",
"invalidCredentials": "Invalid email or password",
"mfaRequired": "Enter the six-digit code from your authenticator app.",
"passwordLabel": "Password",
"qrCodeAlt": "QR code for two-factor setup",
"scanQr": "Scan this code with Google Authenticator, Aegis, Authy or 1Password.",
"signInButton": "Sign in",
"signOutOfAdmin": "Leave admin",
"totpCodeLabel": "Authentication code",
"userLoginTitle": "Sign in to your mailbox",
"verificationFailed": "Verification failed",
"verifyCodeButton": "Verify code"
},
"common": {
"actions": "Actions",
"add": "Add",
"adminPortal": "Admin",
"appName": "LambdaMail",
"back": "Back",
"cancel": "Cancel",
"confirm": "Confirm",
"continue": "Continue",
"copied": "Copied",
"copy": "Copy",
"delete": "Delete",
"loading": "Loading...",
"logout": "Sign out",
"never": "Never",
"none": "None",
"refresh": "Refresh",
"retry": "Retry",
"save": "Save",
"search": "Search",
"status": "Status",
"unavailable": "Unavailable",
"userPortal": "Webmail"
},
"errors": {
"forbidden": "You do not have access to this",
"loadFailed": "Could not load the data",
"serverError": "Something went wrong on the server",
"unauthorized": "Please sign in again"
},
"mail": {
"addFiles": "Add files...",
"allMessages": "All",
"archive": "Archive",
"attachments": "Attachments ({count})",
"attachmentsFilter": "With attachments",
"attachmentsUnsupported": "Attachments are listed by name only; file upload is not wired up yet.",
"backToList": "Back to the message list",
"bold": "Bold",
"bulletList": "Bulleted list",
"compose": "Compose",
"discard": "Discard",
"draftSaveFailed": "Draft could not be saved",
"draftSaved": "Draft saved automatically",
"draftSaving": "Saving draft...",
"drafts": "Drafts",
"forward": "Forward",
"hideCcBcc": "- Hide Cc/Bcc",
"inbox": "Inbox",
"insertLink": "Insert link",
"insertUrl": "Enter the URL:",
"italic": "Italic",
"junk": "Spam",
"loadImages": "Load images",
"noMessages": "No messages",
"numberedList": "Numbered list",
"reply": "Reply",
"sendMessage": "Send message",
"sending": "Sending...",
"sent": "Sent",
"showCcBcc": "+ Add Cc/Bcc",
"strikethrough": "Strikethrough",
"trash": "Trash",
"underline": "Underline",
"undoSend": "Sending in {seconds}s",
"undoSendAction": "Undo",
"unreadCount": "{count} unread",
"unreadFilter": "Unread"
},
"settings": {
"actionDiscard": "Discard",
"actionFlag": "Flag message",
"actionMove": "Move to Archive",
"addRule": "Add rule",
"appPasswordPlaceholder": "Thunderbird / Outlook Mobile",
"appPasswords": "App passwords",
"autoReplySaveFailed": "Could not save the auto-reply",
"enableMfa": "Enable two-factor",
"fieldFrom": "From",
"fieldHeader": "Header",
"fieldSubject": "Subject",
"generateAppPassword": "Generate",
"langEn": "English",
"langEs": "Español",
"langPtBR": "Português (Brasil)",
"language": "Language",
"matchContains": "contains",
"matchEquals": "is exactly",
"matchRegex": "matches regex",
"mfaDisabled": "Disabled",
"mfaEnabled": "Enabled",
"mfaStatus": "Two-factor status",
"recoveryCodesTitle": "Recovery codes",
"ruleIf": "IF",
"ruleThen": "THEN",
"saveAutoReply": "Save auto-reply",
"saveRecoveryCodes": "Save these somewhere safe. Each one works once, and they are shown only now.",
"security": "Security",
"sessions": "Sessions",
"settingsSaved": "Settings saved.",
"sieveIntro": "Rules that sort incoming mail automatically.",
"sieveTitle": "Filters (Sieve)",
"signOut": "Sign out",
"signatureIntro": "Appended automatically to the messages you compose.",
"signaturePlaceholder": "Kind regards,\nYour name\nYour role",
"signatureSaveFailed": "Could not save the signature",
"signatureSaved": "Signature saved.",
"signatureTitle": "Email signature",
"sixDigitCode": "Six-digit code",
"startOverMfa": "Lost the entry? Start over with a new key",
"subtitle": "Two-factor authentication, filters, auto-reply and your signature.",
"title": "Settings",
"twoFactorIntro": "Protect your account with Google Authenticator, Aegis, Authy or 1Password.",
"vacationBodyLabel": "Reply message",
"vacationDefaultBody": "Thank you for your message. I am currently away and will reply when I return.",
"vacationDefaultSubject": "Out of office",
"vacationIntro": "Reply automatically to incoming mail while you are away.",
"vacationSubjectLabel": "Reply subject",
"vacationTitle": "Out-of-office auto-reply",
"valuePlaceholder": "Value to match..."
},
"ui": {
"accountStatus": "Account status",
"actions": "Actions",
"active": "Active",
"activeSessions": "Active sessions",
"actor": "Actor",
"actualRecord": "Actual value",
"allChecksPassed": "All checks passed",
"appPasswordGenerated": "App password generated",
"appPasswordsSection": "App passwords",
"attempts": "Attempts",
"bounceRate": "Bounce rate",
"changePassword": "Change password",
"clientIp": "Client IP",
"copyPasswordNow": "Copy it now. It is not shown again.",
"coreInfra": "Core services",
"currentPassword": "Current password",
"currentSession": "This device",
"daneVerified": "DANE verified",
"destination": "Destination",
"diskUsed": "Storage used",
"dkimAlignment": "DKIM alignment",
"dkimResult": "DKIM",
"dmarcEnforcement": "DMARC policy",
"domainVerification": "Domain verification",
"domainsVerified": "Domains verified",
"emailAddress": "Email address",
"eventAction": "Action",
"expectedValue": "Expected value",
"healthy": "Healthy",
"imapEngine": "IMAP service",
"inboundVolume": "Received (24h)",
"jobId": "Job",
"lastError": "Last error",
"liveMetrics": "Live metrics",
"localDisk": "Local disk",
"mailboxesActive": "Active mailboxes",
"messageBody": "Message",
"messageVolume": "Messages",
"newPassword": "New password",
"noAppPasswords": "No app passwords yet.",
"noData": "No data yet",
"online": "Online",
"outboundDelivered": "Sent (24h)",
"passwordChanged": "Password changed.",
"postgresRunner": "Postgres-backed queue",
"preflightTitle": "Readiness",
"provisionMailbox": "Create mailbox",
"queueDepth": "Queue depth",
"recipient": "Recipient",
"recordName": "Record",
"reportingOrg": "Reporting organisation",
"scanners": "Spam and virus scanning",
"sender": "Sender",
"signOutOthers": "Sign out other devices",
"smtpListener": "SMTP service",
"sourceIp": "Source IP",
"spfRatio": "SPF pass rate",
"spfResult": "SPF",
"storageQuota": "Storage quota",
"subject": "Subject",
"targetObject": "Target",
"timestamp": "Time",
"toRecipients": "To",
"toggleMenu": "Toggle menu",
"totalEvaluated": "Messages evaluated",
"twoFactorSection": "Two-factor authentication",
"watching": "Watching"
}
}