Security hardening from the assessment; testing fixes
Security (ultimo-security-report.md):
- Security headers on every response, the app shell included: they lived at
server level and nginx dropped them in any location with its own header.
CSP, framing, nosniff, referrer, permissions (docker/nginx-security.conf).
- Plain HTTP forwarded by the reverse proxy is redirected to HTTPS; HSTS on
HTTPS responses. server_tokens off.
- Cross-site state-changing /api requests refused (login CSRF), WebSocket too.
- Validation errors in the usual error shape, without the request model.
- New-password policy: max 128, common/own-address/repeated refused,
breached-password check (HIBP k-anonymity, fails open).
- Message HTML: links keep only http(s)/mailto (sanitizer v6); the HTML
endpoint carries its own no-script CSP.
- Outbound fetches pinned to the checked IP (DNS rebinding); external
IMAP/SMTP servers re-checked before each connection; CGNAT and mapped
addresses counted as non-public.
Fixes from testing:
- IDLE loop catches up before idling: mail that came while the worker was
away no longer waits up to 10 minutes.
- Restore from Spam/Trash/Archive moves only the removed messages, not your
reply in Sent.
- Narrow desktop list stacks sender/subject/snippet (subject was squeezed
to nothing at the default width), with hover actions.
- Phone: wide emails scale to fit the screen.
- Singular forms ("1 message"); no zip button for a single attachment;
app passwords no longer claim "never used" for mail apps.
- ruff format across the backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,7 @@ export const en = {
|
||||
"mail.selectAll": "Select all",
|
||||
"mail.conversationSelected": "1 conversation selected",
|
||||
"mail.conversationsSelected": "{count} conversations selected",
|
||||
"mail.conversationsSelected_one": "1 conversation selected",
|
||||
"mail.alsoIn": "Also delivered to",
|
||||
"mail.emptyTrash": "Empty trash",
|
||||
"mail.emptySpam": "Empty spam",
|
||||
@@ -201,6 +202,7 @@ export const en = {
|
||||
"mail.hideQuoted": "Hide quoted text",
|
||||
"mail.markReadUnread": "Mark read / unread",
|
||||
"mail.messageCount": "{count} messages",
|
||||
"mail.messageCount_one": "1 message",
|
||||
"mail.downloadAll": "Download all {count} attachments",
|
||||
"mail.downloadZip": "Download zip",
|
||||
"mail.download": "Download",
|
||||
@@ -218,6 +220,7 @@ export const en = {
|
||||
"compose.closePrompt": "Save this message as a draft?",
|
||||
"compose.sent": "Message sent.",
|
||||
"compose.undoIn": "{count} seconds to",
|
||||
"compose.undoIn_one": "1 second to",
|
||||
"compose.undo": "Undo",
|
||||
"compose.sendFailed": "Send failed.",
|
||||
"compose.savingDraft": "Saving draft…",
|
||||
@@ -419,6 +422,7 @@ export const en = {
|
||||
"appPasswords.created": "Created {date}",
|
||||
"appPasswords.lastUsed": "last used {date}",
|
||||
"appPasswords.neverUsed": "never used",
|
||||
"appPasswords.lastUsedDav": "calendars or contacts last synced {date}",
|
||||
"appPasswords.confirmRevoke": "Revoke the app password “{name}”? The app using it will stop signing in.",
|
||||
"settings.thisDevice": "this device",
|
||||
"settings.revoke": "Revoke",
|
||||
@@ -474,6 +478,7 @@ export const en = {
|
||||
"settings.icloudHint":
|
||||
"Use an app-specific password from account.apple.com → Sign-In and Security.",
|
||||
"settings.calendarCount": "{count} calendars",
|
||||
"settings.calendarCount_one": "1 calendar",
|
||||
"settings.removeSubscription": "Remove subscription",
|
||||
"settings.removeSubscriptionConfirm": "Stop syncing “{name}” and remove its events?",
|
||||
"settings.deleteCalendar": "Delete calendar",
|
||||
@@ -597,6 +602,7 @@ export const en = {
|
||||
"offline.folderNotSaved": "This folder isn't saved on this device.",
|
||||
"offline.noticeScheduled": "Saved to Scheduled — it will be sent when you're back online.",
|
||||
"offline.noticeSendingScheduled": "Sending {count} scheduled messages…",
|
||||
"offline.noticeSendingScheduled_one": "Sending 1 scheduled message…",
|
||||
"offline.draftsNotSaved": "Only drafts written on this device are available offline.",
|
||||
"compose.sendLater": "Schedule",
|
||||
"compose.savedOnDevice": "Saved on this device",
|
||||
@@ -615,6 +621,8 @@ export const en = {
|
||||
"scheduled.delete": "Delete",
|
||||
"scheduled.statusRetrying":
|
||||
"The mail server couldn't take this message yet ({count} tries). Next try {when}; it keeps trying until {until}.",
|
||||
"scheduled.statusRetrying_one":
|
||||
"The mail server couldn't take this message yet (1 try). Next try {when}; it keeps trying until {until}.",
|
||||
"scheduled.statusFailed": "Not sent: {error}",
|
||||
"scheduled.lastError": "Last error: {error}",
|
||||
"scheduled.to": "To",
|
||||
@@ -631,7 +639,9 @@ export const en = {
|
||||
"offline.settingHelp":
|
||||
"Keep your newest 100 Inbox conversations, and the ones you open, on this device, so you can read them without a connection. They are stored unencrypted in this browser and removed when you sign out — leave this off on shared computers.",
|
||||
"offline.settingStatus": "{count} conversations saved · {size} · updated {when}",
|
||||
"offline.settingStatus_one": "1 conversation saved · {size} · updated {when}",
|
||||
"offline.settingStatusNoTime": "{count} conversations saved · {size}",
|
||||
"offline.settingStatusNoTime_one": "1 conversation saved · {size}",
|
||||
"offline.settingSaving": "Saving recent mail…",
|
||||
"offline.settingUnsupported":
|
||||
"Not available here: browsers allow offline access only when the app is opened over HTTPS.",
|
||||
|
||||
Reference in New Issue
Block a user