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
+55
View File
@@ -0,0 +1,55 @@
'use client';
import { useState } from 'react';
import { useTranslations } from 'next-intl';
import { Button } from '@/components/ui/button';
import { SettingsSection, SettingItem } from './settings-section';
import { IdentityManagerModal } from '@/components/identity/identity-manager-modal';
import { useIdentityStore } from '@/stores/identity-store';
export function IdentitySettings() {
const t = useTranslations('settings.identities');
const { identities } = useIdentityStore();
const [showModal, setShowModal] = useState(false);
return (
<>
<SettingsSection title={t('title')} description={t('description')}>
{/* Identity Count */}
<SettingItem
label={t('identities_count.label')}
description={t('identities_count.description')}
>
<div className="flex items-center gap-2">
<span className="text-sm text-foreground">
{identities.length === 0
? t('identities_count.count_zero')
: identities.length === 1
? t('identities_count.count_one')
: t('identities_count.count_other', { count: identities.length })}
</span>
<Button onClick={() => setShowModal(true)} size="sm">
{t('manage')}
</Button>
</div>
</SettingItem>
{/* Sub-Addressing Info */}
<SettingItem
label={t('sub_addressing.label')}
description={t('sub_addressing.description')}
>
<Button variant="outline" size="sm" onClick={() => setShowModal(true)}>
{t('sub_addressing.learn_more')}
</Button>
</SettingItem>
</SettingsSection>
{/* Identity Manager Modal */}
<IdentityManagerModal
isOpen={showModal}
onClose={() => setShowModal(false)}
/>
</>
);
}
+2 -1
View File
@@ -111,7 +111,8 @@ export function Select({ value, onChange, options }: SelectProps) {
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className="px-3 py-1.5 text-sm rounded bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary"
dir="auto"
className="px-3 py-1.5 text-sm rounded bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-primary cursor-pointer"
>
{options.map((option) => (
<option key={option.value} value={option.value}>