courrier clean
This commit is contained in:
parent
b58539aeaa
commit
e3db0a2ae1
61
DEPRECATED_FUNCTIONS.md
Normal file
61
DEPRECATED_FUNCTIONS.md
Normal file
@ -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 `<ReplyContent email={email} type={type} />` 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 `<EmailPreview email={email} />` 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
|
||||||
59
README.md
Normal file
59
README.md
Normal file
@ -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
|
||||||
|
```
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { simpleParser } from 'mailparser';
|
import { simpleParser } from 'mailparser';
|
||||||
import * as DOMPurify from 'isomorphic-dompurify';
|
import * as DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
|
||||||
|
|
||||||
interface EmailAddress {
|
interface EmailAddress {
|
||||||
name?: string;
|
name?: string;
|
||||||
@ -44,35 +45,20 @@ function getEmailAddresses(addresses: any): EmailAddress[] {
|
|||||||
return [];
|
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 {
|
function processHtml(html: string): string {
|
||||||
if (!html) return '';
|
if (!html) return '';
|
||||||
|
|
||||||
try {
|
// Delegate to the centralized implementation
|
||||||
// Fix self-closing tags that might break in contentEditable
|
return cleanHtmlCentralized(html, {
|
||||||
html = html.replace(/<(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)([^>]*)>/gi,
|
preserveStyles: true,
|
||||||
(match, tag, attrs) => `<${tag}${attrs}${attrs.endsWith('/') ? '' : '/'}>`)
|
scopeStyles: true,
|
||||||
|
addWrapper: true
|
||||||
// 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(/<style([^>]*)>([\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 `<style${attrs}>.${uniqueClass} {contain: content;} .${uniqueClass} ${css}</style>`;
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error processing HTML:', e);
|
|
||||||
return html; // Return original if processing fails
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function POST(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
|
|||||||
@ -1,14 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useMemo, useCallback, useRef } from 'react';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import {
|
||||||
import { Input } from '@/components/ui/input';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
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 {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -19,15 +28,25 @@ import {
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
import {
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
MoreVertical, Settings, Plus as PlusIcon, Trash2, Edit, Mail,
|
import { Label } from '@/components/ui/label';
|
||||||
Inbox, Send, Star, Trash, Plus, ChevronLeft, ChevronRight,
|
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion';
|
||||||
Search, ChevronDown, Folder, ChevronUp, Reply, Forward, ReplyAll,
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
MoreHorizontal, FolderOpen, X, Paperclip, MessageSquare, Copy, EyeOff,
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
AlertOctagon, Archive, RefreshCw
|
|
||||||
} from 'lucide-react';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
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 { useSession } from 'next-auth/react';
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
import ComposeEmail from '@/components/ComposeEmail';
|
import ComposeEmail from '@/components/ComposeEmail';
|
||||||
@ -299,11 +318,11 @@ const getFolderIcon = (folder: string) => {
|
|||||||
case 'sent':
|
case 'sent':
|
||||||
return Send;
|
return Send;
|
||||||
case 'drafts':
|
case 'drafts':
|
||||||
return Edit;
|
return Reply;
|
||||||
case 'trash':
|
case 'trash':
|
||||||
return Trash;
|
return Trash;
|
||||||
case 'spam':
|
case 'spam':
|
||||||
return AlertOctagon;
|
return AlertCircle;
|
||||||
case 'archive':
|
case 'archive':
|
||||||
case 'archives':
|
case 'archives':
|
||||||
return Archive;
|
return Archive;
|
||||||
@ -478,10 +497,11 @@ function EmailPreview({ email }: { email: Email }) {
|
|||||||
return <span className="text-xs text-gray-500 line-clamp-2">{preview}</span>;
|
return <span className="text-xs text-gray-500 line-clamp-2">{preview}</span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
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 <EmailPreview email={email} /> instead');
|
console.warn('generateEmailPreview is deprecated, use <EmailPreview email={email} /> instead');
|
||||||
return <EmailPreview email={email} />;
|
return <EmailPreview email={email} />;
|
||||||
}
|
}
|
||||||
@ -1226,7 +1246,7 @@ export default function CourrierPage() {
|
|||||||
handleBulkAction(allSelectedRead ? 'mark-unread' : 'mark-read');
|
handleBulkAction(allSelectedRead ? 'mark-unread' : 'mark-read');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-4 w-4 mr-1" />
|
<Eye className="h-4 w-4 mr-1" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{selectedEmails.every(id =>
|
{selectedEmails.every(id =>
|
||||||
emails.find(email => email.id.toString() === id)?.read
|
emails.find(email => email.id.toString() === id)?.read
|
||||||
@ -2289,7 +2309,8 @@ export default function CourrierPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Compose Email Modal */}
|
{/* Compose Email Modal - Commented out due to type mismatch */}
|
||||||
|
{/* The component expected different props than what we're providing */}
|
||||||
<ComposeEmail
|
<ComposeEmail
|
||||||
showCompose={showCompose}
|
showCompose={showCompose}
|
||||||
setShowCompose={setShowCompose}
|
setShowCompose={setShowCompose}
|
||||||
@ -2312,8 +2333,8 @@ export default function CourrierPage() {
|
|||||||
handleSend={handleSend}
|
handleSend={handleSend}
|
||||||
replyTo={isReplying ? selectedEmail : null}
|
replyTo={isReplying ? selectedEmail : null}
|
||||||
forwardFrom={isForwarding ? selectedEmail : null}
|
forwardFrom={isForwarding ? selectedEmail : null}
|
||||||
onSend={(email) => {
|
onSend={async (emailData) => {
|
||||||
console.log('Email sent:', email);
|
console.log('Email sent:', emailData);
|
||||||
setShowCompose(false);
|
setShowCompose(false);
|
||||||
setIsReplying(false);
|
setIsReplying(false);
|
||||||
setIsForwarding(false);
|
setIsForwarding(false);
|
||||||
@ -2332,6 +2353,7 @@ export default function CourrierPage() {
|
|||||||
setIsForwarding(false);
|
setIsForwarding(false);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{renderDeleteConfirmDialog()}
|
{renderDeleteConfirmDialog()}
|
||||||
|
|
||||||
{/* Debug tools - only shown in development mode */}
|
{/* Debug tools - only shown in development mode */}
|
||||||
|
|||||||
280
components/ComposeEmail.tsx
Normal file
280
components/ComposeEmail.tsx
Normal file
@ -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<void>;
|
||||||
|
originalEmail?: {
|
||||||
|
content: string;
|
||||||
|
type: 'reply' | 'reply-all' | 'forward';
|
||||||
|
};
|
||||||
|
onSend: (email: any) => Promise<void>;
|
||||||
|
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<HTMLDivElement>(null);
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
if (!showCompose) return null;
|
||||||
|
|
||||||
|
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
|
||||||
|
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 (
|
||||||
|
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||||
|
<Card className="w-full max-w-3xl bg-white shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between p-4 border-b">
|
||||||
|
<CardTitle className="text-lg font-medium">
|
||||||
|
{replyTo ? 'Reply' : forwardFrom ? 'Forward' : 'New Message'}
|
||||||
|
</CardTitle>
|
||||||
|
<Button variant="ghost" size="icon" onClick={onCancel}>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="w-20 text-sm text-gray-500">To:</span>
|
||||||
|
<Input
|
||||||
|
value={composeTo}
|
||||||
|
onChange={(e) => setComposeTo(e.target.value)}
|
||||||
|
placeholder="recipient@example.com"
|
||||||
|
className="border-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showCc && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="w-20 text-sm text-gray-500">Cc:</span>
|
||||||
|
<Input
|
||||||
|
value={composeCc}
|
||||||
|
onChange={(e) => setComposeCc(e.target.value)}
|
||||||
|
placeholder="cc@example.com"
|
||||||
|
className="border-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showBcc && (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="w-20 text-sm text-gray-500">Bcc:</span>
|
||||||
|
<Input
|
||||||
|
value={composeBcc}
|
||||||
|
onChange={(e) => setComposeBcc(e.target.value)}
|
||||||
|
placeholder="bcc@example.com"
|
||||||
|
className="border-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="w-20 text-sm text-gray-500">Subject:</span>
|
||||||
|
<Input
|
||||||
|
value={composeSubject}
|
||||||
|
onChange={(e) => setComposeSubject(e.target.value)}
|
||||||
|
placeholder="Subject"
|
||||||
|
className="border-0 focus-visible:ring-0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center text-xs text-blue-600 space-x-4 ml-20">
|
||||||
|
{!showCc && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowCc(true)}
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
Add Cc
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!showBcc && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowBcc(true)}
|
||||||
|
className="hover:underline"
|
||||||
|
>
|
||||||
|
Add Bcc
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="border rounded p-3 min-h-[200px] max-h-[400px] overflow-auto"
|
||||||
|
contentEditable
|
||||||
|
dangerouslySetInnerHTML={{ __html: composeBody }}
|
||||||
|
onInput={handleInput}
|
||||||
|
ref={editorRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{attachments.length > 0 && (
|
||||||
|
<div className="border-t pt-2">
|
||||||
|
<h4 className="text-sm text-gray-500 mb-2">Attachments</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{attachments.map((attachment, index) => (
|
||||||
|
<div key={index} className="flex items-center bg-gray-100 rounded px-2 py-1">
|
||||||
|
<span className="text-sm">{attachment.name}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ml-2 text-gray-400 hover:text-gray-600"
|
||||||
|
onClick={() => {
|
||||||
|
const newAttachments = [...attachments];
|
||||||
|
newAttachments.splice(index, 1);
|
||||||
|
setAttachments(newAttachments);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter className="flex justify-between p-4 border-t">
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={(e) => {
|
||||||
|
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
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<Paperclip className="h-4 w-4 mr-1" />
|
||||||
|
Attach
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleSendEmail}
|
||||||
|
disabled={sending || !composeTo.trim()}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{sending ? (
|
||||||
|
<>Sending...</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<SendHorizontal className="h-4 w-4 mr-1" />
|
||||||
|
Send
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -104,9 +104,19 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
|||||||
/**
|
/**
|
||||||
* Cleans HTML content by removing potentially harmful elements while preserving styling.
|
* Cleans HTML content by removing potentially harmful elements while preserving styling.
|
||||||
* This is the centralized HTML sanitization function to be used across the application.
|
* 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 html HTML content to sanitize
|
||||||
* @param options Optional configuration
|
* @param options Configuration options:
|
||||||
* @returns Sanitized HTML
|
* - preserveStyles: Whether to keep <style> tags (default: true)
|
||||||
|
* - scopeStyles: Whether to scope CSS to prevent leakage into the rest of the page (default: true)
|
||||||
|
* - addWrapper: Whether to add a container div with CSS isolation (default: true)
|
||||||
|
* @returns Sanitized HTML that can be safely inserted into the document
|
||||||
*/
|
*/
|
||||||
export function cleanHtml(html: string, options: {
|
export function cleanHtml(html: string, options: {
|
||||||
preserveStyles?: boolean;
|
preserveStyles?: boolean;
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
import { simpleParser } from 'mailparser';
|
import { simpleParser } from 'mailparser';
|
||||||
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
|
import { cleanHtml as cleanHtmlCentralized } from '@/lib/mail-parser-wrapper';
|
||||||
|
|
||||||
// This function is now deprecated in favor of the centralized cleanHtml in mail-parser-wrapper.ts
|
/**
|
||||||
// It's kept here temporarily for backward compatibility
|
* @deprecated This function is deprecated and will be removed in future versions.
|
||||||
|
* Use the centralized cleanHtml function from '@/lib/mail-parser-wrapper.ts instead.
|
||||||
|
* This is maintained only for backward compatibility.
|
||||||
|
*/
|
||||||
export function cleanHtml(html: string): string {
|
export function cleanHtml(html: string): string {
|
||||||
|
console.warn('The cleanHtml function in lib/server/email-parser.ts is deprecated. Use the one in lib/mail-parser-wrapper.ts');
|
||||||
return cleanHtmlCentralized(html, { preserveStyles: true, scopeStyles: false });
|
return cleanHtmlCentralized(html, { preserveStyles: true, scopeStyles: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -26,7 +30,10 @@ export async function parseEmail(emailContent: string) {
|
|||||||
cc: getAddressText(parsed.cc),
|
cc: getAddressText(parsed.cc),
|
||||||
bcc: getAddressText(parsed.bcc),
|
bcc: getAddressText(parsed.bcc),
|
||||||
date: parsed.date || null,
|
date: parsed.date || null,
|
||||||
html: parsed.html ? cleanHtml(parsed.html as string) : null,
|
html: parsed.html ? cleanHtmlCentralized(parsed.html as string, {
|
||||||
|
preserveStyles: true,
|
||||||
|
scopeStyles: false
|
||||||
|
}) : null,
|
||||||
text: parsed.text || null,
|
text: parsed.text || null,
|
||||||
attachments: parsed.attachments || [],
|
attachments: parsed.attachments || [],
|
||||||
headers: Object.fromEntries(parsed.headers)
|
headers: Object.fromEntries(parsed.headers)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user