feat: expand internationalization and add identity management

This release significantly expands internationalization support and adds comprehensive identity management features.

Internationalization (i18n):
- Add 5 new languages: Spanish, Italian, German, Dutch, Portuguese
- Expand from 3 to 8 total supported languages
- Redesign language switcher for better scalability (dropdown UI)
- Complete translations for all features across all languages

Identity Management:
- Multiple sender identities with per-identity signatures
- Sub-addressing support (user+tag@domain.com)
- Context-aware tag suggestions for sub-addresses
- Identity badges in email viewer and list
- Full CRUD operations for managing identities

Newsletter Management:
- RFC 2369 List-Unsubscribe support (one-click unsubscribe)
- HTTP and mailto unsubscribe methods
- Security validation prevents XSS attacks
- Two-step confirmation with persistent dismissal

Security & Accessibility:
- Dark mode email readability (intelligent color transformation)
- WCAG 2.0 Level AA color contrast compliance
- Comprehensive XSS prevention with validation utilities
- Unit test coverage for security-critical code (57 validation tests)

Testing:
- Add unit tests for validation utilities
- Add unit tests for email sanitization
- Add unit tests for color transformation
- Full test coverage for XSS attack vectors
This commit is contained in:
Matthieu MALVACHE
2026-01-08 22:15:32 +01:00
committed by Matthieu MALVACHE
parent 0d68851b63
commit 5d273e2109
49 changed files with 11018 additions and 432 deletions
+11 -4
View File
@@ -1,4 +1,5 @@
import { AuthenticationResults } from './jmap/types';
import { parseUnsubscribeUrls } from './validation';
/**
* Parse Authentication-Results header to extract SPF, DKIM, DMARC results
@@ -191,7 +192,11 @@ export function parseSpamLLM(header: string): { verdict: string; explanation: st
*/
interface ListHeaders {
listId?: string;
listUnsubscribe?: string;
listUnsubscribe?: {
http?: string;
mailto?: string;
preferred?: 'http' | 'mailto';
};
listHelp?: string;
listPost?: string;
}
@@ -209,9 +214,11 @@ export function extractListHeaders(headers: Record<string, string | string[]>):
const unsub = Array.isArray(headers['List-Unsubscribe'])
? headers['List-Unsubscribe'][0]
: headers['List-Unsubscribe'];
// Extract URL from <url> format
const match = unsub.match(/<([^>]+)>/);
result.listUnsubscribe = match ? match[1] : unsub;
const parsed = parseUnsubscribeUrls(unsub);
if (parsed.preferred) {
result.listUnsubscribe = parsed;
}
}
if (headers['List-Help']) {