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:
@@ -25,6 +25,8 @@ import {
|
||||
Send,
|
||||
File,
|
||||
Folder,
|
||||
ShieldAlert,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -41,6 +43,7 @@ interface EmailContextMenuProps {
|
||||
menuRef: React.RefObject<HTMLDivElement | null>;
|
||||
mailboxes: Mailbox[];
|
||||
selectedMailbox: string;
|
||||
currentMailboxRole?: string;
|
||||
isMultiSelect?: boolean;
|
||||
selectedCount?: number;
|
||||
// Single email actions
|
||||
@@ -53,23 +56,16 @@ interface EmailContextMenuProps {
|
||||
onArchive?: () => void;
|
||||
onSetColorTag?: (color: string | null) => void;
|
||||
onMoveToMailbox?: (mailboxId: string) => void;
|
||||
onMarkAsSpam?: () => void;
|
||||
onUndoSpam?: () => void;
|
||||
// Batch actions
|
||||
onBatchMarkAsRead?: (read: boolean) => void;
|
||||
onBatchDelete?: () => void;
|
||||
onBatchMoveToMailbox?: (mailboxId: string) => void;
|
||||
onBatchMarkAsSpam?: () => void;
|
||||
onBatchUndoSpam?: () => void;
|
||||
}
|
||||
|
||||
// Color options for email tags
|
||||
const colorOptions = [
|
||||
{ name: "Red", value: "red", color: "bg-red-500" },
|
||||
{ name: "Orange", value: "orange", color: "bg-orange-500" },
|
||||
{ name: "Yellow", value: "yellow", color: "bg-yellow-500" },
|
||||
{ name: "Green", value: "green", color: "bg-green-500" },
|
||||
{ name: "Blue", value: "blue", color: "bg-blue-500" },
|
||||
{ name: "Purple", value: "purple", color: "bg-purple-500" },
|
||||
{ name: "Pink", value: "pink", color: "bg-pink-500" },
|
||||
];
|
||||
|
||||
// Get mailbox icon based on role
|
||||
const getMailboxIcon = (role?: string) => {
|
||||
switch (role) {
|
||||
@@ -107,6 +103,7 @@ export function EmailContextMenu({
|
||||
menuRef,
|
||||
mailboxes,
|
||||
selectedMailbox,
|
||||
currentMailboxRole,
|
||||
isMultiSelect = false,
|
||||
selectedCount = 1,
|
||||
onReply,
|
||||
@@ -118,15 +115,32 @@ export function EmailContextMenu({
|
||||
onArchive,
|
||||
onSetColorTag,
|
||||
onMoveToMailbox,
|
||||
onMarkAsSpam,
|
||||
onUndoSpam,
|
||||
onBatchMarkAsRead,
|
||||
onBatchDelete,
|
||||
onBatchMoveToMailbox,
|
||||
onBatchMarkAsSpam,
|
||||
onBatchUndoSpam,
|
||||
}: EmailContextMenuProps) {
|
||||
const t = useTranslations("context_menu");
|
||||
const tColor = useTranslations("email_viewer.color_tag");
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const isStarred = email.keywords?.$flagged;
|
||||
const currentColor = getCurrentColor(email.keywords);
|
||||
const showBatchActions = isMultiSelect && selectedCount > 1;
|
||||
const isInJunkFolder = currentMailboxRole === 'junk';
|
||||
|
||||
// Color options for email tags (using translations)
|
||||
const colorOptions = [
|
||||
{ name: tColor("red"), value: "red", color: "bg-red-500" },
|
||||
{ name: tColor("orange"), value: "orange", color: "bg-orange-500" },
|
||||
{ name: tColor("yellow"), value: "yellow", color: "bg-yellow-500" },
|
||||
{ name: tColor("green"), value: "green", color: "bg-green-500" },
|
||||
{ name: tColor("blue"), value: "blue", color: "bg-blue-500" },
|
||||
{ name: tColor("purple"), value: "purple", color: "bg-purple-500" },
|
||||
{ name: tColor("pink"), value: "pink", color: "bg-pink-500" },
|
||||
];
|
||||
|
||||
// Filter mailboxes for move-to submenu (exclude current, drafts, virtual nodes)
|
||||
const moveTargets = mailboxes.filter(
|
||||
@@ -239,6 +253,23 @@ export function EmailContextMenu({
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{/* Spam - contextual based on folder */}
|
||||
<ContextMenuItem
|
||||
icon={isInJunkFolder ? ShieldCheck : ShieldAlert}
|
||||
label={isInJunkFolder ? t("not_spam") : t("mark_as_spam")}
|
||||
onClick={() =>
|
||||
handleAction(
|
||||
showBatchActions
|
||||
? (isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)
|
||||
: (isInJunkFolder ? onUndoSpam! : onMarkAsSpam!)
|
||||
)
|
||||
}
|
||||
disabled={showBatchActions ? (isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) : (isInJunkFolder ? !onUndoSpam : !onMarkAsSpam)}
|
||||
destructive={!isInJunkFolder}
|
||||
/>
|
||||
|
||||
<ContextMenuSeparator />
|
||||
|
||||
{/* Set color submenu - only for single email */}
|
||||
{!showBatchActions && (
|
||||
<ContextMenuSubMenu icon={Palette} label={t("color_tag")}>
|
||||
|
||||
Reference in New Issue
Block a user