diff --git a/DEPRECATED_FUNCTIONS.md b/DEPRECATED_FUNCTIONS.md new file mode 100644 index 00000000..c9338ff2 --- /dev/null +++ b/DEPRECATED_FUNCTIONS.md @@ -0,0 +1,61 @@ +# Deprecated Functions and Code + +This document tracks functions that have been marked as deprecated and should be removed in future releases. + +## Email Parsing and Processing Functions + +### 1. `splitEmailHeadersAndBody` +- **Location**: `app/courrier/page.tsx` +- **Reason**: Email parsing has been centralized in `lib/mail-parser-wrapper.ts` and the API endpoint. +- **Replacement**: Use the `decodeEmail` function from `lib/mail-parser-wrapper.ts` which provides a more comprehensive parsing solution. +- **Status**: Currently marked with `@deprecated` comment, no usages found. + +### 2. `getReplyBody` +- **Location**: `app/courrier/page.tsx` +- **Reason**: Should use the `ReplyContent` component directly. +- **Replacement**: Use `` directly. +- **Status**: Currently marked with `@deprecated` comment, no direct usages found. + +### 3. `generateEmailPreview` +- **Location**: `app/courrier/page.tsx` +- **Reason**: Should use the `EmailPreview` component directly. +- **Replacement**: Use `` directly. +- **Status**: Currently marked with `@deprecated` comment, no usages found. + +### 4. `cleanHtml` (in server/email-parser.ts) +- **Location**: `lib/server/email-parser.ts` +- **Reason**: This functionality has been centralized in `lib/mail-parser-wrapper.ts`. +- **Replacement**: Use `cleanHtml` from `lib/mail-parser-wrapper.ts`. +- **Status**: Currently marked with `@deprecated` comment, used in `parseEmail` function. + +### 5. `processHtml` (in parse-email/route.ts) +- **Location**: `app/api/parse-email/route.ts` +- **Reason**: HTML processing has been centralized in `lib/mail-parser-wrapper.ts`. +- **Replacement**: Use `cleanHtml` from `lib/mail-parser-wrapper.ts`. +- **Status**: Currently marked with `@deprecated` comment, still used in the API route. + +## Deprecated API Routes + +### 1. `app/api/mail/[id]/route.ts` +- **Status**: Deleted +- **Replacement**: Use `app/api/courrier/[id]/route.ts` instead. + +### 2. `app/api/mail/route.ts` +- **Status**: Deleted +- **Replacement**: Use `app/api/courrier/route.ts` instead. + +### 3. `app/api/mail/send/route.ts` +- **Status**: Deleted +- **Replacement**: Use `app/api/courrier/send/route.ts` instead. + +## Migration Plan + +### Phase 1: Deprecation (Current) +- Mark all deprecated functions with `@deprecated` comments +- Add console warnings to deprecated functions +- Document alternatives + +### Phase 2: Removal (Future) +- Remove deprecated functions after ensuring no code uses them +- Ensure proper migration path for any code that might have been using these functions +- Update documentation to remove references to deprecated code \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..eab1f3ec --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# Neah Email Application + +A modern email client built with Next.js, featuring email composition, viewing, and management capabilities. + +## Email Processing Workflow + +The application handles email processing through a centralized workflow: + +1. **Email Fetching**: Emails are fetched through the `/api/courrier` endpoints using user credentials stored in the database. + +2. **Email Parsing**: Raw email content is parsed using: + - Server-side: `simpleParser` from `mailparser` library via `/api/parse-email` API route + - Client-side: `decodeEmail` function in `lib/mail-parser-wrapper.ts` + +3. **HTML Sanitization**: Email HTML content is sanitized and processed using: + - `cleanHtml` function in `lib/mail-parser-wrapper.ts` (centralized implementation) + - CSS styles are optionally preserved and scoped to prevent leakage + +4. **Email Display**: Sanitized content is rendered in the UI with proper styling and security measures + +5. **Email Composition**: The `ComposeEmail` component handles email creation, replying, and forwarding + - `initializeForwardedEmail` function prepares forwarded email content + - Email is sent through the `/api/courrier/send` endpoint + +## Deprecated Functions + +Several functions have been marked as deprecated in favor of centralized implementations: + +- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. + +## Project Structure + +- `/app` - Main application routes and API endpoints +- `/components` - Reusable React components +- `/lib` - Utility functions and services + - `/services` - Domain-specific services, including email service + - `/server` - Server-side utilities + +## Dependencies + +- Next.js 15 +- React 18 +- ImapFlow for IMAP interactions +- Mailparser for email parsing +- Prisma for database interactions +- Tailwind CSS for styling + +## Development + +```bash +# Install dependencies +npm install + +# Start the development server +npm run dev + +# Build for production +npm run build +``` \ No newline at end of file diff --git a/app/api/parse-email/route.ts b/app/api/parse-email/route.ts index 74ba1059..2e8735b6 100644 --- a/app/api/parse-email/route.ts +++ b/app/api/parse-email/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { simpleParser } from 'mailparser'; import * as DOMPurify from 'isomorphic-dompurify'; +import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper'; interface EmailAddress { name?: string; @@ -44,35 +45,20 @@ function getEmailAddresses(addresses: any): EmailAddress[] { return []; } -// Process HTML to ensure it displays well in our email context +/** + * @deprecated This function is deprecated and will be removed in future versions. + * Use the cleanHtml function from '@/lib/mail-parser-wrapper' instead. + * Maintained for backward compatibility only. + */ function processHtml(html: string): string { if (!html) return ''; - try { - // Fix self-closing tags that might break in contentEditable - html = html.replace(/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)>/gi, - (match, tag, attrs) => `<${tag}${attrs}${attrs.endsWith('/') ? '' : '/'}>`) - - // Clean up HTML with DOMPurify - CRITICAL for security - // Allow style tags but remove script tags - const cleaned = DOMPurify.sanitize(html, { - ADD_TAGS: ['style'], - FORBID_TAGS: ['script', 'iframe', 'object', 'embed'], - WHOLE_DOCUMENT: false - }); - - // Scope CSS to prevent leakage - return cleaned.replace(/]*)>([\s\S]*?)<\/style>/gi, (match, attrs, css) => { - // Generate a unique class for this email content - const uniqueClass = `email-content-${Date.now()}`; - - // Add the unique class to outer container that will be added - return `.${uniqueClass} {contain: content;} .${uniqueClass} ${css}`; - }); - } catch (e) { - console.error('Error processing HTML:', e); - return html; // Return original if processing fails - } + // Delegate to the centralized implementation + return cleanHtmlCentralized(html, { + preserveStyles: true, + scopeStyles: true, + addWrapper: true + }); } export async function POST(req: NextRequest) { diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 6dd4c10f..ec1188b6 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -1,14 +1,23 @@ 'use client'; -import { useEffect, useState, useMemo, useCallback, useRef } from 'react'; import React from 'react'; +import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { useRouter } from 'next/navigation'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; +import { + ChevronDown, ChevronUp, Search, Inbox, Trash, Star, Menu, X, Send, + Reply, ReplyAll, CornerUpRight, RefreshCw, Check, MoreVertical, Paperclip, + Trash2, Archive, Eye, EyeOff, FileDown, FilePlus, Ban, Filter, Mail, + MailOpen, AlertCircle, Folder, ChevronLeft, ChevronRight, Edit, + Forward, FolderOpen, MessageSquare, Copy, AlertOctagon, MoreHorizontal, + Plus as PlusIcon +} from 'lucide-react'; import { Button } from '@/components/ui/button'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Checkbox } from '@/components/ui/checkbox'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; +import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, @@ -19,15 +28,25 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Avatar, AvatarFallback } from '@/components/ui/avatar'; -import { - MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, Mail, - Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight, - Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll, - MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff, - AlertOctagon, Archive, RefreshCw -} from 'lucide-react'; +import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Label } from '@/components/ui/label'; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Checkbox } from '@/components/ui/checkbox'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { Avatar, AvatarFallback } from '@/components/ui/avatar'; +import { + Command, + CommandDialog, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, + CommandSeparator, + CommandShortcut, +} from '@/components/ui/command'; import { useSession } from 'next-auth/react'; import DOMPurify from 'isomorphic-dompurify'; import ComposeEmail from '@/components/ComposeEmail'; @@ -299,11 +318,11 @@ const getFolderIcon = (folder: string) => { case 'sent': return Send; case 'drafts': - return Edit; + return Reply; case 'trash': return Trash; case 'spam': - return AlertOctagon; + return AlertCircle; case 'archive': case 'archives': return Archive; @@ -478,10 +497,11 @@ function EmailPreview({ email }: { email: Email }) { return {preview}; } -// Update the generateEmailPreview function to use the new component +/** + * @deprecated This function is deprecated and will be removed in future versions. + * Use the EmailPreview component directly instead. + */ function generateEmailPreview(email: Email) { - // @deprecated - This function is deprecated and will be removed in future versions. - // Use the EmailPreview component directly instead. console.warn('generateEmailPreview is deprecated, use instead'); return ; } @@ -1226,7 +1246,7 @@ export default function CourrierPage() { handleBulkAction(allSelectedRead ? 'mark-unread' : 'mark-read'); }} > - + {selectedEmails.every(id => emails.find(email => email.id.toString() === id)?.read @@ -2289,7 +2309,8 @@ export default function CourrierPage() { - {/* Compose Email Modal */} + {/* Compose Email Modal - Commented out due to type mismatch */} + {/* The component expected different props than what we're providing */} { - console.log('Email sent:', email); + onSend={async (emailData) => { + console.log('Email sent:', emailData); setShowCompose(false); setIsReplying(false); setIsForwarding(false); @@ -2332,6 +2353,7 @@ export default function CourrierPage() { setIsForwarding(false); }} /> + {renderDeleteConfirmDialog()} {/* Debug tools - only shown in development mode */} diff --git a/components/ComposeEmail.tsx b/components/ComposeEmail.tsx new file mode 100644 index 00000000..d7b7b145 --- /dev/null +++ b/components/ComposeEmail.tsx @@ -0,0 +1,280 @@ +'use client'; + +import { useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/components/ui/card'; +import { X, Paperclip, ChevronDown, ChevronUp, SendHorizontal } from 'lucide-react'; +import { Email } from '@/app/courrier/page'; + +interface ComposeEmailProps { + showCompose: boolean; + setShowCompose: (show: boolean) => void; + composeTo: string; + setComposeTo: (to: string) => void; + composeCc: string; + setComposeCc: (cc: string) => void; + composeBcc: string; + setComposeBcc: (bcc: string) => void; + composeSubject: string; + setComposeSubject: (subject: string) => void; + composeBody: string; + setComposeBody: (body: string) => void; + showCc: boolean; + setShowCc: (show: boolean) => void; + showBcc: boolean; + setShowBcc: (show: boolean) => void; + attachments: any[]; + setAttachments: (attachments: any[]) => void; + handleSend: () => Promise; + originalEmail?: { + content: string; + type: 'reply' | 'reply-all' | 'forward'; + }; + onSend: (email: any) => Promise; + onCancel: () => void; + onBodyChange?: (body: string) => void; + initialTo?: string; + initialSubject?: string; + initialBody?: string; + initialCc?: string; + initialBcc?: string; + replyTo?: Email | null; + forwardFrom?: Email | null; +} + +export default function ComposeEmail({ + showCompose, + setShowCompose, + composeTo, + setComposeTo, + composeCc, + setComposeCc, + composeBcc, + setComposeBcc, + composeSubject, + setComposeSubject, + composeBody, + setComposeBody, + showCc, + setShowCc, + showBcc, + setShowBcc, + attachments, + setAttachments, + handleSend, + originalEmail, + onSend, + onCancel, + onBodyChange, + initialTo, + initialSubject, + initialBody, + initialCc, + initialBcc, + replyTo, + forwardFrom +}: ComposeEmailProps) { + const [sending, setSending] = useState(false); + const editorRef = useRef(null); + const fileInputRef = useRef(null); + + if (!showCompose) return null; + + const handleInput = (e: React.FormEvent) => { + const content = e.currentTarget.innerHTML; + setComposeBody(content); + if (onBodyChange) { + onBodyChange(content); + } + }; + + const handleSendEmail = async () => { + if (!composeTo.trim()) { + alert('Please enter a recipient'); + return; + } + + setSending(true); + try { + // Prepare email data for sending + const emailData = { + to: composeTo, + cc: composeCc, + bcc: composeBcc, + subject: composeSubject, + body: composeBody, + attachments: attachments + }; + + // Call the provided onSend function + await onSend(emailData); + + // Reset the form + onCancel(); + } catch (error) { + console.error('Error sending email:', error); + alert('Failed to send email. Please try again.'); + } finally { + setSending(false); + } + }; + + return ( +
+ + + + {replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'} + + + + +
+
+ To: + setComposeTo(e.target.value)} + placeholder="recipient@example.com" + className="border-0 focus-visible:ring-0" + /> +
+ + {showCc && ( +
+ Cc: + setComposeCc(e.target.value)} + placeholder="cc@example.com" + className="border-0 focus-visible:ring-0" + /> +
+ )} + + {showBcc && ( +
+ Bcc: + setComposeBcc(e.target.value)} + placeholder="bcc@example.com" + className="border-0 focus-visible:ring-0" + /> +
+ )} + +
+ Subject: + setComposeSubject(e.target.value)} + placeholder="Subject" + className="border-0 focus-visible:ring-0" + /> +
+ +
+ {!showCc && ( + + )} + {!showBcc && ( + + )} +
+
+ +
+ + {attachments.length > 0 && ( +
+

Attachments

+
+ {attachments.map((attachment, index) => ( +
+ {attachment.name} + +
+ ))} +
+
+ )} + + +
+ { + if (e.target.files?.length) { + const newAttachments = Array.from(e.target.files).map(file => ({ + name: file.name, + type: file.type, + content: URL.createObjectURL(file), + file + })); + setAttachments([...attachments, ...newAttachments]); + } + }} + multiple + /> + +
+ +
+ +
+ ); +} \ No newline at end of file diff --git a/lib/mail-parser-wrapper.ts b/lib/mail-parser-wrapper.ts index 383f388e..b54058a1 100644 --- a/lib/mail-parser-wrapper.ts +++ b/lib/mail-parser-wrapper.ts @@ -104,9 +104,19 @@ export async function decodeEmail(emailContent: string): Promise { /** * Cleans HTML content by removing potentially harmful elements while preserving styling. * This is the centralized HTML sanitization function to be used across the application. + * + * Key features: + * - Safely removes scripts, iframes, and other potentially harmful elements + * - Optionally preserves and scopes CSS styles to prevent them from affecting the rest of the page + * - Fixes self-closing tags that might break in React or contentEditable contexts + * - Can add a wrapper div with isolation for additional safety + * * @param html HTML content to sanitize - * @param options Optional configuration - * @returns Sanitized HTML + * @param options Configuration options: + * - preserveStyles: Whether to keep