diff --git a/DEPRECATED_FUNCTIONS.md b/DEPRECATED_FUNCTIONS.md index 3de68443..588b01cb 100644 --- a/DEPRECATED_FUNCTIONS.md +++ b/DEPRECATED_FUNCTIONS.md @@ -6,29 +6,29 @@ This document lists functions and files that have been deprecated and should not ### 1. `lib/email-formatter.ts` (REMOVED) - **Status**: Removed -- **Replacement**: Use `lib/utils/email-formatter.ts` instead +- **Replacement**: Use `lib/utils/email-utils.ts` instead - **Reason**: Consolidated email formatting to a single source of truth ### 2. `lib/mail-parser-wrapper.ts` (REMOVED) - **Status**: Removed -- **Replacement**: Use functions from `lib/utils/email-formatter.ts` instead +- **Replacement**: Use functions from `lib/utils/email-utils.ts` instead - **Reason**: Consolidated email formatting and sanitization to a single source of truth ### 3. `lib/email-parser.ts` (REMOVED) - **Status**: Removed -- **Replacement**: Use `lib/server/email-parser.ts` for parsing and `lib/utils/email-formatter.ts` for sanitization +- **Replacement**: Use `lib/server/email-parser.ts` for parsing and `lib/utils/email-utils.ts` for sanitization - **Reason**: Consolidated email parsing and formatting to dedicated files ### 4. `lib/compose-mime-decoder.ts` (REMOVED) - **Status**: Removed -- **Replacement**: Use `decodeComposeContent` and `encodeComposeContent` functions from `lib/utils/email-formatter.ts` +- **Replacement**: Use `decodeComposeContent` and `encodeComposeContent` functions from `lib/utils/email-utils.ts` - **Reason**: Consolidated MIME handling into the centralized formatter ## Deprecated Functions ### 1. `formatEmailForReplyOrForward` in `lib/services/email-service.ts` (REMOVED) - **Status**: Removed -- **Replacement**: Use `formatEmailForReplyOrForward` from `lib/utils/email-formatter.ts` +- **Replacement**: Use `formatEmailForReplyOrForward` from `lib/utils/email-utils.ts` - **Reason**: Consolidated email formatting to a single source of truth ### 2. `formatSubject` in `lib/services/email-service.ts` (REMOVED) @@ -43,7 +43,7 @@ This document lists functions and files that have been deprecated and should not ## Centralized Email Formatting -All email formatting is now handled by the centralized formatter in `lib/utils/email-formatter.ts`. This file contains: +All email formatting is now handled by the centralized formatter in `lib/utils/email-utils.ts`. This file contains: 1. `formatForwardedEmail`: Format emails for forwarding 2. `formatReplyEmail`: Format emails for replying or replying to all @@ -73,36 +73,36 @@ Use these functions for all email formatting needs. ### 4. `cleanHtml` (REMOVED) - **Location**: Removed from `lib/server/email-parser.ts` -- **Reason**: HTML sanitization has been consolidated in `lib/utils/email-formatter.ts`. -- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-formatter.ts`. +- **Reason**: HTML sanitization has been consolidated in `lib/utils/email-utils.ts`. +- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`. ### 5. `processHtml` (REMOVED) - **Location**: Removed from `app/api/parse-email/route.ts` -- **Reason**: HTML processing has been consolidated in `lib/utils/email-formatter.ts`. -- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-formatter.ts`. +- **Reason**: HTML processing has been consolidated in `lib/utils/email-utils.ts`. +- **Replacement**: Use `sanitizeHtml` from `lib/utils/email-utils.ts`. ## Deprecated API Routes -### 1. `app/api/mail/[id]/route.ts` -- **Status**: Deleted +### 1. `app/api/mail/[id]/route.ts` (REMOVED) +- **Status**: Removed - **Replacement**: Use `app/api/courrier/[id]/route.ts` instead. -### 2. `app/api/mail/route.ts` -- **Status**: Deleted +### 2. `app/api/mail/route.ts` (REMOVED) +- **Status**: Removed - **Replacement**: Use `app/api/courrier/route.ts` instead. -### 3. `app/api/mail/send/route.ts` -- **Status**: Deleted +### 3. `app/api/mail/send/route.ts` (REMOVED) +- **Status**: Removed - **Replacement**: Use `app/api/courrier/send/route.ts` instead. ## Deprecated Components -### ComposeEmail (components/ComposeEmail.tsx) +### ComposeEmail (components/ComposeEmail.tsx) (REMOVED) -**Status:** Deprecated since November 2023 +**Status:** Removed **Replacement:** Use `components/email/ComposeEmail.tsx` instead -This component has been deprecated in favor of the more modular and better structured version in the email directory. The newer version has the following improvements: +This component has been removed in favor of the more modular and better structured version in the email directory. The newer version has the following improvements: - Better separation between user message and quoted content in replies/forwards - Improved styling and visual hierarchy @@ -112,11 +112,6 @@ This component has been deprecated in favor of the more modular and better struc A compatibility layer has been added to the new component to ensure backward compatibility with existing code that uses the old component. This allows for a smooth transition without breaking changes. -**Migration Plan:** -1. Update imports in files using the old component to import from `@/components/email/ComposeEmail` -2. Test to ensure functionality works with the new component -3. Delete the old component file once all usages have been migrated - ## Migration Plan ### Phase 1: Deprecation (Completed) @@ -126,7 +121,7 @@ A compatibility layer has been added to the new component to ensure backward com ### Phase 2: Removal (Completed) - Remove deprecated files: `lib/email-parser.ts` and `lib/mail-parser-wrapper.ts` -- Consolidate all email formatting in `lib/utils/email-formatter.ts` +- Consolidate all email formatting in `lib/utils/email-utils.ts` - All email parsing now in `lib/server/email-parser.ts` - Update documentation to point to the centralized utilities diff --git a/README.md b/README.md index ff3141ae..8a549111 100644 --- a/README.md +++ b/README.md @@ -13,70 +13,70 @@ The application handles email processing through a centralized workflow: - API route: `/api/parse-email` provides a REST interface to the parser 3. **HTML Sanitization**: Email HTML content is sanitized and processed using: - - `sanitizeHtml` function in `lib/utils/email-formatter.ts` (centralized implementation) - - Text direction (RTL/LTR) is preserved automatically during sanitization + - `sanitizeHtml` function in `lib/utils/email-utils.ts` (centralized implementation) + - DOMPurify with specific configuration to handle email content safely 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 - - Uses the centralized formatter functions to prepare content - Email is sent through the `/api/courrier/send` endpoint -## Deprecated Functions +## Key Features -Several functions have been deprecated and removed in favor of centralized implementations: - -- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. +- **Email Fetching and Management**: Connect to IMAP servers and manage email fetching and caching logic +- **Email Composition**: Rich text editor with reply and forwarding capabilities +- **Email Display**: Secure rendering of HTML emails +- **Attachment Handling**: View and download attachments ## Project Structure -- `/app` - Main application routes and API endpoints -- `/components` - Reusable React components -- `/lib` - Utility functions and services +The project follows a modular structure: + +- `/app` - Next.js App Router structure with routes and API endpoints +- `/components` - React components organized by domain +- `/lib` - Core library code: + - `/server` - Server-only code like email parsing - `/services` - Domain-specific services, including email service - - `/server` - Server-side utilities + - `/reducers` - State management logic - `/utils` - Utility functions including the centralized email formatter -## Dependencies +## Technologies -- Next.js 15 -- React 18 -- ImapFlow for IMAP interactions +- Next.js 14+ with App Router +- React Server Components +- TailwindCSS for styling - Mailparser for email parsing -- Prisma for database interactions -- Tailwind CSS for styling +- ImapFlow for email fetching +- DOMPurify for HTML sanitization +- Redis for caching -## Development +## State Management -```bash -# Install dependencies -npm install - -# Start the development server -npm run dev - -# Build for production -npm run build -``` +Email state is managed through React context and reducers, with server data fetched through React Server Components or client-side API calls as needed. # Email Formatting ## Centralized Email Formatter -All email formatting is now handled by a centralized formatter in `lib/utils/email-formatter.ts`. This ensures consistent handling of: +All email formatting is now handled by a centralized formatter in `lib/utils/email-utils.ts`. This ensures consistent handling of: -- Text direction (RTL/LTR) +- Reply and forward formatting - HTML sanitization -- Content formatting for forwards and replies +- RTL/LTR text direction - MIME encoding and decoding for email composition -### Key Functions - +Key functions include: - `formatForwardedEmail`: Format emails for forwarding - `formatReplyEmail`: Format emails for replying -- `sanitizeHtml`: Sanitize HTML while preserving direction attributes +- `sanitizeHtml`: Safely sanitize HTML email content - `formatEmailForReplyOrForward`: Compatibility function for both - `decodeComposeContent`: Parse MIME content for email composition - `encodeComposeContent`: Create MIME-formatted content for sending emails -This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages. \ No newline at end of file +This centralized approach prevents formatting inconsistencies and direction problems when dealing with emails in different languages. + +## Deprecated Functions + +Several functions have been deprecated and removed in favor of centralized implementations: + +- Check the `DEPRECATED_FUNCTIONS.md` file for a complete list of deprecated functions and their replacements. \ No newline at end of file diff --git a/app/api/mail/[id]/route.ts b/app/api/mail/[id]/route.ts deleted file mode 100644 index 08a57256..00000000 --- a/app/api/mail/[id]/route.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { NextResponse } from 'next/server'; -import { ImapFlow } from 'imapflow'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { prisma } from '@/lib/prisma'; - -export async function GET(request: Request, { params }: { params: { id: string } }) { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // Get credentials from database - const credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - } - }); - - if (!credentials) { - return NextResponse.json( - { error: 'No mail credentials found. Please configure your email account.' }, - { status: 401 } - ); - } - - // Get the current folder from the request URL - const url = new URL(request.url); - const folder = url.searchParams.get('folder') || 'INBOX'; - - // Connect to IMAP server - const client = new ImapFlow({ - host: credentials.host, - port: credentials.port, - secure: true, - auth: { - user: credentials.email, - pass: credentials.password, - }, - logger: false, - emitLogs: false, - tls: { - rejectUnauthorized: false - } - }); - - try { - await client.connect(); - await client.mailboxOpen(folder); - - // Fetch the full email content - const message = await client.fetchOne(params.id, { - source: true, - envelope: true, - flags: true - }); - - if (!message) { - return NextResponse.json( - { error: 'Email not found' }, - { status: 404 } - ); - } - - // Extract email content - const result = { - id: message.uid.toString(), - from: message.envelope.from[0].address, - subject: message.envelope.subject || '(No subject)', - date: message.envelope.date.toISOString(), - read: message.flags.has('\\Seen'), - starred: message.flags.has('\\Flagged'), - folder: folder, - body: message.source.toString(), - to: message.envelope.to?.map(addr => addr.address).join(', ') || '', - cc: message.envelope.cc?.map(addr => addr.address).join(', ') || '', - bcc: message.envelope.bcc?.map(addr => addr.address).join(', ') || '', - }; - - return NextResponse.json(result); - } finally { - try { - await client.logout(); - } catch (e) { - console.error('Error during logout:', e); - } - } - } catch (error) { - console.error('Error fetching email:', error); - return NextResponse.json( - { error: 'An unexpected error occurred' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/mail/bulk-actions/route.ts b/app/api/mail/bulk-actions/route.ts deleted file mode 100644 index d2656823..00000000 --- a/app/api/mail/bulk-actions/route.ts +++ /dev/null @@ -1,172 +0,0 @@ -import { NextResponse } from 'next/server'; -import { cookies } from 'next/headers'; -import Imap from 'imap'; - -interface StoredCredentials { - email: string; - password: string; - host: string; - port: number; -} - -function getStoredCredentials(): StoredCredentials | null { - const cookieStore = cookies(); - const credentialsCookie = cookieStore.get('imap_credentials'); - - if (!credentialsCookie?.value) { - return null; - } - - try { - const credentials = JSON.parse(credentialsCookie.value); - if (!credentials.email || !credentials.password || !credentials.host || !credentials.port) { - return null; - } - return credentials; - } catch (error) { - return null; - } -} - -export async function POST(request: Request) { - try { - const { emailIds, action } = await request.json(); - - if (!emailIds || !Array.isArray(emailIds) || !action) { - return NextResponse.json( - { error: 'Invalid request parameters' }, - { status: 400 } - ); - } - - // Get the current folder from the request URL - const url = new URL(request.url); - const folder = url.searchParams.get('folder') || 'INBOX'; - - // Get stored credentials - const credentials = getStoredCredentials(); - if (!credentials) { - return NextResponse.json( - { error: 'No stored credentials found' }, - { status: 401 } - ); - } - - return new Promise((resolve) => { - const imap = new Imap({ - user: credentials.email, - password: credentials.password, - host: credentials.host, - port: credentials.port, - tls: true, - tlsOptions: { rejectUnauthorized: false }, - authTimeout: 30000, - connTimeout: 30000 - }); - - const timeout = setTimeout(() => { - console.error('IMAP connection timeout'); - imap.end(); - resolve(NextResponse.json({ error: 'Connection timeout' })); - }, 30000); - - imap.once('error', (err: Error) => { - console.error('IMAP error:', err); - clearTimeout(timeout); - resolve(NextResponse.json({ error: 'IMAP connection error' })); - }); - - imap.once('ready', () => { - imap.openBox(folder, false, (err, box) => { - if (err) { - console.error(`Error opening box ${folder}:`, err); - clearTimeout(timeout); - imap.end(); - resolve(NextResponse.json({ error: `Failed to open folder ${folder}` })); - return; - } - - // Convert string IDs to numbers - const numericIds = emailIds.map(id => parseInt(id, 10)); - - // Process each email - let processedCount = 0; - const totalEmails = numericIds.length; - - const processNextEmail = (index: number) => { - if (index >= totalEmails) { - clearTimeout(timeout); - imap.end(); - resolve(NextResponse.json({ success: true })); - return; - } - - const id = numericIds[index]; - const fetch = imap.fetch(id.toString(), { - bodies: '', - struct: true - }); - - fetch.on('message', (msg) => { - msg.once('attributes', (attrs) => { - const uid = attrs.uid; - if (!uid) { - processedCount++; - processNextEmail(index + 1); - return; - } - - switch (action) { - case 'delete': - imap.move(uid, 'Trash', (err) => { - if (err) console.error('Error moving to trash:', err); - processedCount++; - processNextEmail(index + 1); - }); - break; - case 'mark-read': - imap.addFlags(uid, ['\\Seen'], (err) => { - if (err) console.error('Error marking as read:', err); - processedCount++; - processNextEmail(index + 1); - }); - break; - case 'mark-unread': - imap.removeFlags(uid, ['\\Seen'], (err) => { - if (err) console.error('Error marking as unread:', err); - processedCount++; - processNextEmail(index + 1); - }); - break; - case 'archive': - imap.move(uid, 'Archive', (err) => { - if (err) console.error('Error moving to archive:', err); - processedCount++; - processNextEmail(index + 1); - }); - break; - } - }); - }); - - fetch.on('error', (err) => { - console.error('Error fetching email:', err); - processedCount++; - processNextEmail(index + 1); - }); - }; - - processNextEmail(0); - }); - }); - - imap.connect(); - }); - } catch (error) { - console.error('Error in bulk actions:', error); - return NextResponse.json( - { error: 'Failed to perform bulk action' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/mail/login/route.ts b/app/api/mail/login/route.ts deleted file mode 100644 index 685cdd78..00000000 --- a/app/api/mail/login/route.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { NextResponse } from 'next/server'; -import { ImapFlow } from 'imapflow'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { prisma } from '@/lib/prisma'; -import { saveUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service'; -import { prefetchUserEmailData } from '@/lib/services/prefetch-service'; -import { invalidateUserEmailCache, getCachedEmailCredentials } from '@/lib/redis'; - -export async function POST(request: Request) { - try { - const session = await getServerSession(authOptions); - console.log('Session in mail login:', session); - - if (!session?.user?.id) { - console.error('No user ID in session'); - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // Verify user exists - console.log('Checking for user with ID:', session.user.id); - const user = await prisma.user.findUnique({ - where: { id: session.user.id } - }); - - console.log('User found in database:', user); - - if (!user) { - console.error('User not found in database'); - return NextResponse.json( - { error: 'User not found' }, - { status: 404 } - ); - } - - const { email, password, host, port } = await request.json(); - - if (!email || !password || !host || !port) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ); - } - - // Test connection before saving - const connectionSuccess = await testEmailConnection({ - email, - password, - host, - port: parseInt(port) - }); - - if (!connectionSuccess) { - return NextResponse.json( - { error: 'Failed to connect to email server. Please check your credentials.' }, - { status: 401 } - ); - } - - // Invalidate all cached data for this user - await invalidateUserEmailCache(session.user.id); - - // Save credentials using the service that handles both database and Redis - await saveUserEmailCredentials(session.user.id, { - email, - password, - host, - port: parseInt(port) - }); - - // Start prefetching email data in the background - prefetchUserEmailData(session.user.id).catch(err => { - console.error('Background prefetch error:', err); - }); - - return NextResponse.json({ success: true }); - - } catch (error) { - console.error('Error in login handler:', error); - return NextResponse.json( - { error: 'An unexpected error occurred' }, - { status: 500 } - ); - } -} - -export async function GET() { - try { - const session = await getServerSession(authOptions); - if (!session?.user?.id) { - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // First try to get from Redis cache - let credentials = await getCachedEmailCredentials(session.user.id); - - // If not in cache, get from database - if (!credentials) { - credentials = await prisma.mailCredentials.findUnique({ - where: { - userId: session.user.id - }, - select: { - email: true, - host: true, - port: true - } - }); - } else { - // Remove password from response - const { password, ...safeCredentials } = credentials; - credentials = safeCredentials; - } - - if (!credentials) { - return NextResponse.json( - { error: 'No stored credentials found' }, - { status: 404 } - ); - } - - return NextResponse.json(credentials); - } catch (error) { - return NextResponse.json( - { error: 'Failed to retrieve credentials' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/mail/mark-read/route.ts b/app/api/mail/mark-read/route.ts deleted file mode 100644 index d306b896..00000000 --- a/app/api/mail/mark-read/route.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '@/app/api/auth/[...nextauth]/route'; -import { getImapClient } from '@/lib/imap'; -import { ImapFlow } from 'imapflow'; - -export async function POST(request: Request) { - let client: ImapFlow | null = null; - try { - // Get the session and validate it - const session = await getServerSession(authOptions); - console.log('Session:', session); // Debug log - - if (!session?.user?.id) { - console.error('No session or user ID found'); - return NextResponse.json( - { error: 'Unauthorized' }, - { status: 401 } - ); - } - - // Get the request body - const { emailId, isRead } = await request.json(); - console.log('Request body:', { emailId, isRead }); // Debug log - - if (!emailId || typeof isRead !== 'boolean') { - console.error('Invalid request parameters:', { emailId, isRead }); - return NextResponse.json( - { error: 'Invalid request parameters' }, - { status: 400 } - ); - } - - // Get the current folder from the request URL - const url = new URL(request.url); - const folder = url.searchParams.get('folder') || 'INBOX'; - console.log('Folder:', folder); // Debug log - - try { - // Initialize IMAP client with user credentials - client = await getImapClient(); - if (!client) { - console.error('Failed to initialize IMAP client'); - return NextResponse.json( - { error: 'Failed to initialize email client' }, - { status: 500 } - ); - } - - await client.connect(); - await client.mailboxOpen(folder); - - // Fetch the email to get its UID - const message = await client.fetchOne(emailId.toString(), { - uid: true, - flags: true - }); - - if (!message) { - console.error('Email not found:', emailId); - return NextResponse.json( - { error: 'Email not found' }, - { status: 404 } - ); - } - - // Update the flags - if (isRead) { - await client.messageFlagsAdd(message.uid.toString(), ['\\Seen'], { uid: true }); - } else { - await client.messageFlagsRemove(message.uid.toString(), ['\\Seen'], { uid: true }); - } - - return NextResponse.json({ success: true }); - } finally { - if (client) { - try { - await client.logout(); - } catch (e) { - console.error('Error during logout:', e); - } - } - } - } catch (error) { - console.error('Error marking email as read:', error); - return NextResponse.json( - { error: 'Failed to mark email as read' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/app/api/mail/test-connection/route.ts b/app/api/mail/test-connection/route.ts deleted file mode 100644 index ed6de504..00000000 --- a/app/api/mail/test-connection/route.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { NextResponse } from 'next/server'; -import Imap from 'imap'; - -export async function POST(request: Request) { - try { - const { email, password, host, port } = await request.json(); - - if (!email || !password || !host || !port) { - return NextResponse.json( - { error: 'Missing required fields' }, - { status: 400 } - ); - } - - const imapConfig = { - user: email, - password, - host, - port: parseInt(port), - tls: true, - authTimeout: 10000, - connTimeout: 10000, - debug: (info: string) => console.log('IMAP Debug:', info) - }; - - console.log('Testing IMAP connection with config:', { - ...imapConfig, - password: '***', - email - }); - - const imap = new Imap(imapConfig); - - const connectPromise = new Promise((resolve, reject) => { - imap.once('ready', () => { - imap.end(); - resolve(true); - }); - imap.once('error', (err: Error) => { - imap.end(); - reject(err); - }); - imap.connect(); - }); - - await connectPromise; - - return NextResponse.json({ success: true }); - } catch (error) { - console.error('Error testing connection:', error); - return NextResponse.json( - { error: error instanceof Error ? error.message : 'Failed to connect to email server' }, - { status: 500 } - ); - } -} \ No newline at end of file diff --git a/components/mail/mail-list.tsx b/components/mail/mail-list.tsx deleted file mode 100644 index 7b63a854..00000000 --- a/components/mail/mail-list.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Mail } from "@/types/mail"; -import { Star, StarOff, Paperclip } from "lucide-react"; -import { format } from "date-fns"; - -interface MailListProps { - mails: Mail[]; - onMailClick: (mail: Mail) => void; -} - -export function MailList({ mails, onMailClick }: MailListProps) { - if (!mails || mails.length === 0) { - return ( -
No emails found
-- {mail.body} -
-