courrier preview
This commit is contained in:
parent
3f017f82f8
commit
d12a6c4670
109
components/email/EmailContentDisplay.tsx
Normal file
109
components/email/EmailContentDisplay.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useMemo, CSSProperties } from 'react';
|
||||||
|
import { renderEmailContent, normalizeEmailContent } from '@/lib/utils/email-utils';
|
||||||
|
import { EmailContent } from '@/types/email';
|
||||||
|
|
||||||
|
interface EmailContentDisplayProps {
|
||||||
|
content: EmailContent | any;
|
||||||
|
className?: string;
|
||||||
|
showQuotedText?: boolean;
|
||||||
|
type?: 'html' | 'text' | 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unified component for displaying email content in a consistent way
|
||||||
|
* This handles both HTML and plain text content with proper styling
|
||||||
|
*/
|
||||||
|
const EmailContentDisplay: React.FC<EmailContentDisplayProps> = ({
|
||||||
|
content,
|
||||||
|
className = '',
|
||||||
|
showQuotedText = true,
|
||||||
|
type = 'auto'
|
||||||
|
}) => {
|
||||||
|
// Normalize the content to our standard format if needed
|
||||||
|
const normalizedContent = useMemo(() => {
|
||||||
|
// If content is already in our EmailContent format
|
||||||
|
if (content &&
|
||||||
|
typeof content === 'object' &&
|
||||||
|
'text' in content &&
|
||||||
|
'isHtml' in content) {
|
||||||
|
return content as EmailContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise normalize it
|
||||||
|
return normalizeEmailContent(content);
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
// Render the normalized content
|
||||||
|
const htmlContent = useMemo(() => {
|
||||||
|
if (!normalizedContent) return '';
|
||||||
|
|
||||||
|
// Override content type if specified
|
||||||
|
let contentToRender: EmailContent = { ...normalizedContent };
|
||||||
|
|
||||||
|
if (type === 'html' && !contentToRender.isHtml) {
|
||||||
|
// Force HTML rendering for text content
|
||||||
|
contentToRender = {
|
||||||
|
...contentToRender,
|
||||||
|
isHtml: true,
|
||||||
|
html: `<p>${contentToRender.text.replace(/\n/g, '<br>')}</p>`
|
||||||
|
};
|
||||||
|
} else if (type === 'text' && contentToRender.isHtml) {
|
||||||
|
// Force text rendering
|
||||||
|
contentToRender = {
|
||||||
|
...contentToRender,
|
||||||
|
isHtml: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderEmailContent(contentToRender);
|
||||||
|
}, [normalizedContent, type]);
|
||||||
|
|
||||||
|
// Apply quoted text styling if needed
|
||||||
|
const containerStyle: CSSProperties = showQuotedText
|
||||||
|
? {}
|
||||||
|
: { maxHeight: '400px', overflowY: 'auto' };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`email-content-display ${className} ${showQuotedText ? 'quoted-text' : ''}`}
|
||||||
|
style={containerStyle}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="email-content-inner"
|
||||||
|
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<style jsx>{`
|
||||||
|
.email-content-display {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content-display.quoted-text {
|
||||||
|
opacity: 0.85;
|
||||||
|
font-size: 0.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content-inner :global(img) {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content-inner :global(table) {
|
||||||
|
max-width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-content-inner :global(td),
|
||||||
|
.email-content-inner :global(th) {
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EmailContentDisplay;
|
||||||
@ -1,58 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef, useEffect, useMemo } from 'react';
|
import { useRef } from 'react';
|
||||||
import { Loader2, Paperclip, User } from 'lucide-react';
|
import { Loader2 } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import {
|
|
||||||
formatReplyEmail,
|
|
||||||
formatForwardedEmail,
|
|
||||||
formatEmailForReplyOrForward,
|
|
||||||
EmailMessage as FormatterEmailMessage,
|
|
||||||
sanitizeHtml
|
|
||||||
} from '@/lib/utils/email-formatter';
|
|
||||||
import { formatEmailContent } from '@/lib/utils/email-content';
|
|
||||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||||
import { AvatarImage } from '@/components/ui/avatar';
|
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { cn } from '@/lib/utils';
|
import { EmailMessage, EmailAddress } from '@/types/email';
|
||||||
import { CalendarIcon, PaperclipIcon } from 'lucide-react';
|
import { formatEmailAddresses, formatEmailDate } from '@/lib/utils/email-utils';
|
||||||
import Link from 'next/link';
|
import EmailContentDisplay from './EmailContentDisplay';
|
||||||
import DOMPurify from 'dompurify';
|
|
||||||
|
|
||||||
interface EmailAddress {
|
|
||||||
name: string;
|
|
||||||
address: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmailAttachment {
|
|
||||||
filename: string;
|
|
||||||
contentType: string;
|
|
||||||
size: number;
|
|
||||||
path?: string;
|
|
||||||
content?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmailMessage {
|
|
||||||
id: string;
|
|
||||||
uid: number;
|
|
||||||
from: EmailAddress[];
|
|
||||||
to: EmailAddress[];
|
|
||||||
cc?: EmailAddress[];
|
|
||||||
bcc?: EmailAddress[];
|
|
||||||
subject: string;
|
|
||||||
date: string;
|
|
||||||
flags: string[];
|
|
||||||
attachments: EmailAttachment[];
|
|
||||||
content?: string | {
|
|
||||||
text?: string;
|
|
||||||
html?: string;
|
|
||||||
};
|
|
||||||
html?: string;
|
|
||||||
text?: string;
|
|
||||||
formattedContent?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EmailPreviewProps {
|
interface EmailPreviewProps {
|
||||||
email: EmailMessage | null;
|
email: EmailMessage | null;
|
||||||
@ -64,32 +20,6 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
// Add editorRef to match ComposeEmail exactly
|
// Add editorRef to match ComposeEmail exactly
|
||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Format the date
|
|
||||||
const formatDate = (date: Date | string) => {
|
|
||||||
if (!date) return '';
|
|
||||||
|
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
|
||||||
return dateObj.toLocaleString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format email addresses
|
|
||||||
const formatEmailAddresses = (addresses: Array<{name: string, address: string}> | undefined) => {
|
|
||||||
if (!addresses || addresses.length === 0) return '';
|
|
||||||
|
|
||||||
return addresses.map(addr =>
|
|
||||||
addr.name && addr.name !== addr.address
|
|
||||||
? `${addr.name} <${addr.address}>`
|
|
||||||
: addr.address
|
|
||||||
).join(', ');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get sender initials for avatar
|
// Get sender initials for avatar
|
||||||
const getSenderInitials = (name: string) => {
|
const getSenderInitials = (name: string) => {
|
||||||
if (!name) return '';
|
if (!name) return '';
|
||||||
@ -101,33 +31,6 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
.slice(0, 2);
|
.slice(0, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Format the email content
|
|
||||||
const formattedContent = useMemo(() => {
|
|
||||||
if (!email) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// CRITICAL FIX: Send consistent input format to formatEmailContent
|
|
||||||
try {
|
|
||||||
// Log what we're sending to formatEmailContent for debugging
|
|
||||||
console.log('EmailPreview: Calling formatEmailContent with email:',
|
|
||||||
JSON.stringify({
|
|
||||||
id: email.id,
|
|
||||||
contentType: typeof email.content,
|
|
||||||
hasHtml: typeof email.content === 'object' ? !!email.content.html : false,
|
|
||||||
hasText: typeof email.content === 'object' ? !!email.content.text : false,
|
|
||||||
hasHtmlProp: !!email.html,
|
|
||||||
hasTextProp: !!email.text
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
return formatEmailContent(email);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error formatting email content:', error);
|
|
||||||
return `<div class="error-message p-4 text-red-500">Error rendering email content: ${error instanceof Error ? error.message : 'Unknown error'}</div>`;
|
|
||||||
}
|
|
||||||
}, [email]);
|
|
||||||
|
|
||||||
// Display loading state
|
// Display loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@ -153,7 +56,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
|
|
||||||
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined;
|
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined;
|
||||||
|
|
||||||
// Update the array access to use proper type checking
|
// Check for attachments
|
||||||
const hasAttachments = email.attachments && email.attachments.length > 0;
|
const hasAttachments = email.attachments && email.attachments.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -171,7 +74,7 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="font-medium">{sender?.name || sender?.address}</div>
|
<div className="font-medium">{sender?.name || sender?.address}</div>
|
||||||
<span className="text-sm text-muted-foreground">{formatDate(email.date)}</span>
|
<span className="text-sm text-muted-foreground">{formatEmailDate(email.date)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-muted-foreground truncate mt-1">
|
<div className="text-sm text-muted-foreground truncate mt-1">
|
||||||
@ -214,29 +117,31 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Attachments */}
|
{/* Attachments list */}
|
||||||
{hasAttachments && (
|
{hasAttachments && (
|
||||||
<div className="px-6 py-3 border-b bg-muted/30">
|
<div className="px-6 py-3 border-b bg-muted/20">
|
||||||
<div className="text-sm font-medium mb-2">Attachments</div>
|
<h3 className="text-sm font-medium mb-2">Attachments ({email.attachments.length})</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{email.attachments.map((attachment, index) => (
|
{email.attachments.map((attachment, index) => (
|
||||||
<Badge key={index} variant="outline" className="flex items-center gap-1 px-2 py-1">
|
<div
|
||||||
<Paperclip className="h-3.5 w-3.5" />
|
key={index}
|
||||||
|
className="flex items-center gap-2 bg-background rounded-md px-3 py-1.5 text-sm border"
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<svg className="h-4 w-4 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"></path></svg>
|
||||||
|
</div>
|
||||||
<span>{attachment.filename}</span>
|
<span>{attachment.filename}</span>
|
||||||
<span className="text-xs text-muted-foreground ml-1">
|
</div>
|
||||||
({Math.round(attachment.size / 1024)}KB)
|
|
||||||
</span>
|
|
||||||
</Badge>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Email content */}
|
{/* Email body */}
|
||||||
<ScrollArea className="flex-1">
|
<ScrollArea className="flex-1">
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* IMPROVED: Simplified email content container with better styling */}
|
{/* Render the email content using the new standardized component */}
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
className="email-content-container rounded-lg overflow-hidden bg-white shadow-sm"
|
className="email-content-container rounded-lg overflow-hidden bg-white shadow-sm"
|
||||||
@ -245,17 +150,11 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
border: '1px solid #e2e8f0'
|
border: '1px solid #e2e8f0'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Render the formatted content directly */}
|
<EmailContentDisplay
|
||||||
{formattedContent ? (
|
content={email.content}
|
||||||
<div
|
type="auto"
|
||||||
className="email-body"
|
className="p-6"
|
||||||
dangerouslySetInnerHTML={{ __html: formattedContent }}
|
/>
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="p-8 text-center text-muted-foreground">
|
|
||||||
<p>This email does not contain any content.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Only in development mode: Show debugging info */}
|
{/* Only in development mode: Show debugging info */}
|
||||||
@ -264,16 +163,12 @@ export default function EmailPreview({ email, loading = false, onReply }: EmailP
|
|||||||
<summary className="cursor-pointer">Email Debug Info</summary>
|
<summary className="cursor-pointer">Email Debug Info</summary>
|
||||||
<div className="mt-2 overflow-auto max-h-40 p-2 bg-gray-50 rounded">
|
<div className="mt-2 overflow-auto max-h-40 p-2 bg-gray-50 rounded">
|
||||||
<p><strong>Email ID:</strong> {email.id}</p>
|
<p><strong>Email ID:</strong> {email.id}</p>
|
||||||
<p><strong>Content Type:</strong> {
|
<p><strong>Content Type:</strong> {email.content.isHtml ? 'HTML' : 'Plain Text'}</p>
|
||||||
typeof email.content === 'object' && email.content?.html
|
<p><strong>Text Direction:</strong> {email.content.direction || 'ltr'}</p>
|
||||||
? 'HTML'
|
<p><strong>Content Size:</strong>
|
||||||
: 'Plain Text'
|
HTML: {email.content.html?.length || 0} chars,
|
||||||
}</p>
|
Text: {email.content.text?.length || 0} chars
|
||||||
<p><strong>Content Size:</strong> {
|
</p>
|
||||||
typeof email.content === 'object'
|
|
||||||
? `HTML: ${email.content?.html?.length || 0} chars, Text: ${email.content?.text?.length || 0} chars`
|
|
||||||
: `${typeof email.content === 'string' ? email.content.length : 0} chars`
|
|
||||||
}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,15 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import EmailContentDisplay from './EmailContentDisplay';
|
import EmailContentDisplay from '@/components/email/EmailContentDisplay';
|
||||||
|
import { formatEmailDate } from '@/lib/utils/email-utils';
|
||||||
|
import { EmailContent } from '@/types/email';
|
||||||
|
|
||||||
interface QuotedEmailContentProps {
|
interface QuotedEmailContentProps {
|
||||||
content: string;
|
content: EmailContent | string;
|
||||||
sender: {
|
sender: {
|
||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
};
|
};
|
||||||
date: Date | string;
|
date: Date | string;
|
||||||
|
subject?: string;
|
||||||
|
recipients?: string;
|
||||||
type: 'reply' | 'forward';
|
type: 'reply' | 'forward';
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
@ -21,32 +25,14 @@ const QuotedEmailContent: React.FC<QuotedEmailContentProps> = ({
|
|||||||
content,
|
content,
|
||||||
sender,
|
sender,
|
||||||
date,
|
date,
|
||||||
|
subject,
|
||||||
|
recipients,
|
||||||
type,
|
type,
|
||||||
className = ''
|
className = ''
|
||||||
}) => {
|
}) => {
|
||||||
// Format the date
|
|
||||||
const formatDate = (date: Date | string) => {
|
|
||||||
if (!date) return '';
|
|
||||||
|
|
||||||
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
||||||
|
|
||||||
try {
|
|
||||||
return dateObj.toLocaleString('en-US', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return typeof date === 'string' ? date : date.toString();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format sender info
|
// Format sender info
|
||||||
const senderName = sender.name || sender.email;
|
const senderName = sender.name || sender.email;
|
||||||
const formattedDate = formatDate(date);
|
const formattedDate = formatEmailDate(date);
|
||||||
|
|
||||||
// Create header based on type
|
// Create header based on type
|
||||||
const renderQuoteHeader = () => {
|
const renderQuoteHeader = () => {
|
||||||
@ -62,8 +48,8 @@ const QuotedEmailContent: React.FC<QuotedEmailContentProps> = ({
|
|||||||
<div>---------- Forwarded message ---------</div>
|
<div>---------- Forwarded message ---------</div>
|
||||||
<div><b>From:</b> {senderName} <{sender.email}></div>
|
<div><b>From:</b> {senderName} <{sender.email}></div>
|
||||||
<div><b>Date:</b> {formattedDate}</div>
|
<div><b>Date:</b> {formattedDate}</div>
|
||||||
<div><b>Subject:</b> {/* Subject would be passed as a prop if needed */}</div>
|
<div><b>Subject:</b> {subject || '(No subject)'}</div>
|
||||||
<div><b>To:</b> {/* Recipients would be passed as a prop if needed */}</div>
|
<div><b>To:</b> {recipients || 'Undisclosed recipients'}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
147
lib/utils/email-adapter.ts
Normal file
147
lib/utils/email-adapter.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* Email Adapter Utility
|
||||||
|
*
|
||||||
|
* This utility provides adapter functions to convert legacy email
|
||||||
|
* formats to the standardized EmailMessage format.
|
||||||
|
*
|
||||||
|
* Use these functions to migrate code gradually without breaking changes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
EmailMessage,
|
||||||
|
EmailContent,
|
||||||
|
EmailAddress,
|
||||||
|
EmailAttachment,
|
||||||
|
EmailFlags
|
||||||
|
} from '@/types/email';
|
||||||
|
import { normalizeEmailContent } from './email-utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a legacy email format to our standardized EmailMessage format
|
||||||
|
*
|
||||||
|
* This adapter function handles all the various ways email data might be structured
|
||||||
|
* in the legacy codebase and converts it to our new standardized format.
|
||||||
|
*/
|
||||||
|
export function adaptLegacyEmail(legacyEmail: any): EmailMessage {
|
||||||
|
if (!legacyEmail) {
|
||||||
|
throw new Error('Cannot adapt a null or undefined email');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle case where it's already in the right format
|
||||||
|
if (
|
||||||
|
legacyEmail.content &&
|
||||||
|
typeof legacyEmail.content === 'object' &&
|
||||||
|
'isHtml' in legacyEmail.content &&
|
||||||
|
'text' in legacyEmail.content
|
||||||
|
) {
|
||||||
|
return legacyEmail as EmailMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create normalized content
|
||||||
|
const normalizedContent = normalizeEmailContent(legacyEmail);
|
||||||
|
|
||||||
|
// Normalize flags to standard format
|
||||||
|
let normalizedFlags: EmailFlags = {
|
||||||
|
seen: false,
|
||||||
|
flagged: false,
|
||||||
|
answered: false,
|
||||||
|
deleted: false,
|
||||||
|
draft: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle different possible formats for flags
|
||||||
|
if (legacyEmail.flags) {
|
||||||
|
if (typeof legacyEmail.flags === 'object' && !Array.isArray(legacyEmail.flags)) {
|
||||||
|
// Object format: { seen: true, flagged: false, ... }
|
||||||
|
normalizedFlags = {
|
||||||
|
seen: !!legacyEmail.flags.seen,
|
||||||
|
flagged: !!legacyEmail.flags.flagged,
|
||||||
|
answered: !!legacyEmail.flags.answered,
|
||||||
|
deleted: !!legacyEmail.flags.deleted,
|
||||||
|
draft: !!legacyEmail.flags.draft
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(legacyEmail.flags)) {
|
||||||
|
// Array format: ['\\Seen', '\\Flagged', ...]
|
||||||
|
normalizedFlags.seen = legacyEmail.flags.includes('\\Seen');
|
||||||
|
normalizedFlags.flagged = legacyEmail.flags.includes('\\Flagged');
|
||||||
|
normalizedFlags.answered = legacyEmail.flags.includes('\\Answered');
|
||||||
|
normalizedFlags.deleted = legacyEmail.flags.includes('\\Deleted');
|
||||||
|
normalizedFlags.draft = legacyEmail.flags.includes('\\Draft');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize attachments to standard format
|
||||||
|
const normalizedAttachments: EmailAttachment[] = Array.isArray(legacyEmail.attachments)
|
||||||
|
? legacyEmail.attachments.map((att: any) => ({
|
||||||
|
filename: att.filename || att.name || 'attachment',
|
||||||
|
contentType: att.contentType || att.type || 'application/octet-stream',
|
||||||
|
content: att.content || att.data || undefined,
|
||||||
|
size: att.size || 0,
|
||||||
|
contentId: att.contentId || att.cid || undefined
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// Return a normalized EmailMessage
|
||||||
|
return {
|
||||||
|
id: legacyEmail.id || legacyEmail.uid?.toString() || `email-${Date.now()}`,
|
||||||
|
messageId: legacyEmail.messageId,
|
||||||
|
uid: typeof legacyEmail.uid === 'number' ? legacyEmail.uid : undefined,
|
||||||
|
subject: legacyEmail.subject || '(No subject)',
|
||||||
|
from: Array.isArray(legacyEmail.from) ? legacyEmail.from : [],
|
||||||
|
to: Array.isArray(legacyEmail.to) ? legacyEmail.to : [],
|
||||||
|
cc: Array.isArray(legacyEmail.cc) ? legacyEmail.cc : undefined,
|
||||||
|
bcc: Array.isArray(legacyEmail.bcc) ? legacyEmail.bcc : undefined,
|
||||||
|
date: legacyEmail.date || new Date(),
|
||||||
|
flags: normalizedFlags,
|
||||||
|
preview: legacyEmail.preview || '',
|
||||||
|
content: normalizedContent,
|
||||||
|
attachments: normalizedAttachments,
|
||||||
|
folder: legacyEmail.folder || undefined,
|
||||||
|
size: typeof legacyEmail.size === 'number' ? legacyEmail.size : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to detect if an object is an EmailAddress
|
||||||
|
*/
|
||||||
|
export function isEmailAddress(obj: any): obj is EmailAddress {
|
||||||
|
return obj &&
|
||||||
|
typeof obj === 'object' &&
|
||||||
|
'address' in obj &&
|
||||||
|
typeof obj.address === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert legacy email address format to standardized EmailAddress format
|
||||||
|
*/
|
||||||
|
export function adaptEmailAddress(address: any): EmailAddress {
|
||||||
|
if (isEmailAddress(address)) {
|
||||||
|
return {
|
||||||
|
name: address.name || '',
|
||||||
|
address: address.address
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof address === 'string') {
|
||||||
|
// Try to extract name and address from string like "Name <email@example.com>"
|
||||||
|
const match = address.match(/^(?:"?([^"]*)"?\s)?<?([^\s>]+@[^\s>]+)>?$/);
|
||||||
|
if (match) {
|
||||||
|
return {
|
||||||
|
name: match[1] || '',
|
||||||
|
address: match[2]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match, assume it's just an email address
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
address
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return a placeholder if we can't parse the address
|
||||||
|
return {
|
||||||
|
name: '',
|
||||||
|
address: 'unknown@example.com'
|
||||||
|
};
|
||||||
|
}
|
||||||
448
lib/utils/email-utils.ts
Normal file
448
lib/utils/email-utils.ts
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
/**
|
||||||
|
* Unified Email Utilities
|
||||||
|
*
|
||||||
|
* This file contains all email-related utility functions:
|
||||||
|
* - Content normalization
|
||||||
|
* - Content sanitization
|
||||||
|
* - Email formatting (replies, forwards)
|
||||||
|
* - Text direction detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import DOMPurify from 'isomorphic-dompurify';
|
||||||
|
import {
|
||||||
|
EmailMessage,
|
||||||
|
EmailContent,
|
||||||
|
EmailAddress
|
||||||
|
} from '@/types/email';
|
||||||
|
|
||||||
|
// Reset any existing hooks to start clean
|
||||||
|
DOMPurify.removeAllHooks();
|
||||||
|
|
||||||
|
// Configure DOMPurify for auto text direction
|
||||||
|
DOMPurify.addHook('afterSanitizeAttributes', function(node) {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
// Only set direction if not already specified
|
||||||
|
if (!node.hasAttribute('dir')) {
|
||||||
|
// Add dir attribute only if not present
|
||||||
|
node.setAttribute('dir', 'auto');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure DOMPurify to preserve direction attributes
|
||||||
|
DOMPurify.setConfig({
|
||||||
|
ADD_ATTR: ['dir'],
|
||||||
|
ALLOWED_ATTR: ['style', 'class', 'id', 'dir']
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect if text contains RTL characters
|
||||||
|
*/
|
||||||
|
export function detectTextDirection(text: string): 'ltr' | 'rtl' {
|
||||||
|
// Pattern for RTL characters (Arabic, Hebrew, etc.)
|
||||||
|
const rtlLangPattern = /[\u0591-\u07FF\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||||
|
return rtlLangPattern.test(text) ? 'rtl' : 'ltr';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format email addresses for display
|
||||||
|
*/
|
||||||
|
export function formatEmailAddresses(addresses: EmailAddress[]): string {
|
||||||
|
if (!addresses || addresses.length === 0) return '';
|
||||||
|
|
||||||
|
return addresses.map(addr =>
|
||||||
|
addr.name && addr.name !== addr.address
|
||||||
|
? `${addr.name} <${addr.address}>`
|
||||||
|
: addr.address
|
||||||
|
).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format date for display
|
||||||
|
*/
|
||||||
|
export function formatEmailDate(date: Date | string | undefined): string {
|
||||||
|
if (!date) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
||||||
|
return dateObj.toLocaleString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return typeof date === 'string' ? date : date.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize HTML content before processing or displaying
|
||||||
|
* Uses email industry standards for proper, consistent, and secure rendering
|
||||||
|
*/
|
||||||
|
export function sanitizeHtml(html: string): string {
|
||||||
|
if (!html) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use DOMPurify with comprehensive email HTML standards
|
||||||
|
const clean = DOMPurify.sanitize(html, {
|
||||||
|
ADD_TAGS: [
|
||||||
|
'html', 'head', 'body', 'style', 'link', 'meta', 'title',
|
||||||
|
'table', 'caption', 'col', 'colgroup', 'thead', 'tbody', 'tfoot', 'tr', 'td', 'th',
|
||||||
|
'div', 'span', 'img', 'br', 'hr', 'section', 'article', 'header', 'footer',
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'blockquote', 'pre', 'code',
|
||||||
|
'ul', 'ol', 'li', 'dl', 'dt', 'dd', 'a', 'b', 'i', 'u', 'em',
|
||||||
|
'strong', 'del', 'ins', 'mark', 'small', 'sub', 'sup', 'q', 'abbr'
|
||||||
|
],
|
||||||
|
ADD_ATTR: [
|
||||||
|
'style', 'class', 'id', 'name', 'href', 'src', 'alt', 'title', 'width', 'height',
|
||||||
|
'border', 'cellspacing', 'cellpadding', 'bgcolor', 'background', 'color',
|
||||||
|
'align', 'valign', 'dir', 'lang', 'target', 'rel', 'charset', 'media',
|
||||||
|
'colspan', 'rowspan', 'scope', 'span', 'size', 'face', 'hspace', 'vspace',
|
||||||
|
'data-*'
|
||||||
|
],
|
||||||
|
KEEP_CONTENT: true,
|
||||||
|
WHOLE_DOCUMENT: false,
|
||||||
|
ALLOW_DATA_ATTR: true,
|
||||||
|
ALLOW_UNKNOWN_PROTOCOLS: true, // Needed for some email clients
|
||||||
|
FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'form', 'input', 'button', 'select', 'textarea'],
|
||||||
|
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover', 'onmouseout'],
|
||||||
|
FORCE_BODY: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix common email rendering issues
|
||||||
|
return clean
|
||||||
|
// Fix for Outlook WebVML content
|
||||||
|
.replace(/<!--\[if\s+gte\s+mso/g, '<!--[if gte mso')
|
||||||
|
// Fix for broken image paths that might be relative
|
||||||
|
.replace(/(src|background)="(?!http|data|https|cid)/gi, '$1="https://');
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error sanitizing HTML:', e);
|
||||||
|
// Fall back to a basic sanitization approach
|
||||||
|
return html
|
||||||
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
.replace(/on\w+="[^"]*"/g, '')
|
||||||
|
.replace(/(javascript|jscript|vbscript|mocha):/gi, 'removed:');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format plain text for HTML display with proper line breaks
|
||||||
|
*/
|
||||||
|
export function formatPlainTextToHtml(text: string): string {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
// Escape HTML characters to prevent XSS
|
||||||
|
const escapedText = text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
|
||||||
|
// Format plain text with proper line breaks and paragraphs
|
||||||
|
return escapedText
|
||||||
|
.replace(/\r\n|\r|\n/g, '<br>') // Convert all newlines to <br>
|
||||||
|
.replace(/((?:<br>){2,})/g, '</p><p>') // Convert multiple newlines to paragraphs
|
||||||
|
.replace(/<br><\/p>/g, '</p>') // Fix any <br></p> combinations
|
||||||
|
.replace(/<p><br>/g, '<p>'); // Fix any <p><br> combinations
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize email content to our standard format regardless of input format
|
||||||
|
* This is the key function that handles all the different email content formats
|
||||||
|
*/
|
||||||
|
export function normalizeEmailContent(email: any): EmailContent {
|
||||||
|
// Default content structure
|
||||||
|
const normalizedContent: EmailContent = {
|
||||||
|
html: undefined,
|
||||||
|
text: '',
|
||||||
|
isHtml: false,
|
||||||
|
direction: 'ltr'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract content based on standardized property hierarchy
|
||||||
|
let htmlContent = '';
|
||||||
|
let textContent = '';
|
||||||
|
let isHtml = false;
|
||||||
|
|
||||||
|
// Step 1: Extract content from the various possible formats
|
||||||
|
if (email.content && typeof email.content === 'object') {
|
||||||
|
isHtml = !!email.content.html;
|
||||||
|
htmlContent = email.content.html || '';
|
||||||
|
textContent = email.content.text || '';
|
||||||
|
} else if (typeof email.content === 'string') {
|
||||||
|
// Check if the string content is HTML
|
||||||
|
isHtml = email.content.trim().startsWith('<') &&
|
||||||
|
(email.content.includes('<html') ||
|
||||||
|
email.content.includes('<body') ||
|
||||||
|
email.content.includes('<div') ||
|
||||||
|
email.content.includes('<p>'));
|
||||||
|
htmlContent = isHtml ? email.content : '';
|
||||||
|
textContent = isHtml ? '' : email.content;
|
||||||
|
} else if (email.html) {
|
||||||
|
isHtml = true;
|
||||||
|
htmlContent = email.html;
|
||||||
|
textContent = email.text || '';
|
||||||
|
} else if (email.text) {
|
||||||
|
isHtml = false;
|
||||||
|
htmlContent = '';
|
||||||
|
textContent = email.text;
|
||||||
|
} else if (email.formattedContent) {
|
||||||
|
// Assume formattedContent is already HTML
|
||||||
|
isHtml = true;
|
||||||
|
htmlContent = email.formattedContent;
|
||||||
|
textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Set the normalized content properties
|
||||||
|
normalizedContent.isHtml = isHtml;
|
||||||
|
|
||||||
|
// Always ensure we have text content
|
||||||
|
if (textContent) {
|
||||||
|
normalizedContent.text = textContent;
|
||||||
|
} else if (htmlContent) {
|
||||||
|
// Extract text from HTML if we don't have plain text
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
// Browser environment
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
normalizedContent.text = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
} else {
|
||||||
|
// Server environment - do simple strip
|
||||||
|
normalizedContent.text = htmlContent
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have HTML content, sanitize it
|
||||||
|
if (isHtml && htmlContent) {
|
||||||
|
normalizedContent.html = sanitizeHtml(htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine text direction
|
||||||
|
normalizedContent.direction = detectTextDirection(normalizedContent.text);
|
||||||
|
|
||||||
|
return normalizedContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error normalizing email content:', error);
|
||||||
|
|
||||||
|
// Return minimal valid content in case of error
|
||||||
|
return {
|
||||||
|
text: 'Error loading email content',
|
||||||
|
isHtml: false,
|
||||||
|
direction: 'ltr'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render normalized email content into HTML for display
|
||||||
|
*/
|
||||||
|
export function renderEmailContent(content: EmailContent): string {
|
||||||
|
if (!content) {
|
||||||
|
return '<div class="email-content-empty">No content available</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine if we're rendering HTML or plain text
|
||||||
|
if (content.isHtml && content.html) {
|
||||||
|
// For HTML content, wrap it with proper styling
|
||||||
|
return `<div class="email-content" dir="${content.direction}" style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; max-width: 100%; overflow-x: auto; overflow-wrap: break-word; word-wrap: break-word;">${content.html}</div>`;
|
||||||
|
} else {
|
||||||
|
// For plain text, format it as HTML and wrap with monospace styling
|
||||||
|
const formattedText = formatPlainTextToHtml(content.text);
|
||||||
|
return `<div class="email-content plain-text" dir="${content.direction}" style="font-family: -apple-system, BlinkMacSystemFont, Menlo, Monaco, Consolas, 'Courier New', monospace; white-space: pre-wrap; line-height: 1.5; color: #333; padding: 15px; max-width: 100%; overflow-wrap: break-word;"><p>${formattedText}</p></div>`;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering email content:', error);
|
||||||
|
return `<div class="email-content-error" style="padding: 15px; color: #721c24; background-color: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px;"><p>Error displaying email content</p><p style="font-size: 12px; margin-top: 10px;">${error instanceof Error ? error.message : 'Unknown error'}</p></div>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an email for forwarding
|
||||||
|
*/
|
||||||
|
export function formatForwardedEmail(email: EmailMessage): {
|
||||||
|
subject: string;
|
||||||
|
content: EmailContent;
|
||||||
|
} {
|
||||||
|
// Format subject with Fwd: prefix if needed
|
||||||
|
const subjectBase = email.subject || '(No subject)';
|
||||||
|
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i)
|
||||||
|
? subjectBase
|
||||||
|
: `Fwd: ${subjectBase}`;
|
||||||
|
|
||||||
|
// Get sender and recipient information
|
||||||
|
const fromString = formatEmailAddresses(email.from || []);
|
||||||
|
const toString = formatEmailAddresses(email.to || []);
|
||||||
|
const dateString = formatEmailDate(email.date);
|
||||||
|
|
||||||
|
// Get original content as HTML
|
||||||
|
const originalContent = email.content.isHtml && email.content.html
|
||||||
|
? email.content.html
|
||||||
|
: formatPlainTextToHtml(email.content.text);
|
||||||
|
|
||||||
|
// Check if the content already has a forwarded message header
|
||||||
|
const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------');
|
||||||
|
|
||||||
|
// If there's already a forwarded message header, don't add another one
|
||||||
|
let htmlContent = '';
|
||||||
|
if (hasExistingHeader) {
|
||||||
|
// Just wrap the content without additional formatting
|
||||||
|
htmlContent = `
|
||||||
|
<div style="min-height: 20px;"></div>
|
||||||
|
<div class="email-original-content">
|
||||||
|
${originalContent}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Create formatted content for forwarded email
|
||||||
|
htmlContent = `
|
||||||
|
<div style="min-height: 20px;">
|
||||||
|
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
||||||
|
<div style="font-family: Arial, sans-serif; color: #333;">
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div>---------- Forwarded message ---------</div>
|
||||||
|
<div><b>From:</b> ${fromString}</div>
|
||||||
|
<div><b>Date:</b> ${dateString}</div>
|
||||||
|
<div><b>Subject:</b> ${email.subject || ''}</div>
|
||||||
|
<div><b>To:</b> ${toString}</div>
|
||||||
|
</div>
|
||||||
|
<div class="email-original-content">
|
||||||
|
${originalContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create normalized content with HTML and extracted text
|
||||||
|
const content: EmailContent = {
|
||||||
|
html: sanitizeHtml(htmlContent),
|
||||||
|
text: '', // Will be extracted when composing
|
||||||
|
isHtml: true,
|
||||||
|
direction: email.content.direction || 'ltr'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract text from HTML if in browser environment
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
content.text = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
} else {
|
||||||
|
// Simple text extraction in server environment
|
||||||
|
content.text = htmlContent
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { subject, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an email for reply or reply-all
|
||||||
|
*/
|
||||||
|
export function formatReplyEmail(email: EmailMessage, type: 'reply' | 'reply-all'): {
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
subject: string;
|
||||||
|
content: EmailContent;
|
||||||
|
} {
|
||||||
|
// Format email addresses
|
||||||
|
const to = formatEmailAddresses(email.from || []);
|
||||||
|
|
||||||
|
// For reply-all, include all recipients in CC except our own address
|
||||||
|
let cc = undefined;
|
||||||
|
if (type === 'reply-all' && (email.to || email.cc)) {
|
||||||
|
const allRecipients = [
|
||||||
|
...(email.to || []),
|
||||||
|
...(email.cc || [])
|
||||||
|
];
|
||||||
|
|
||||||
|
// Remove duplicates, then convert to string
|
||||||
|
const uniqueRecipients = [...new Map(allRecipients.map(addr =>
|
||||||
|
[addr.address, addr]
|
||||||
|
)).values()];
|
||||||
|
|
||||||
|
cc = formatEmailAddresses(uniqueRecipients);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format subject with Re: prefix if needed
|
||||||
|
const subjectBase = email.subject || '(No subject)';
|
||||||
|
const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`;
|
||||||
|
|
||||||
|
// Get original content as HTML
|
||||||
|
const originalContent = email.content.isHtml && email.content.html
|
||||||
|
? email.content.html
|
||||||
|
: formatPlainTextToHtml(email.content.text);
|
||||||
|
|
||||||
|
// Format sender info
|
||||||
|
const sender = email.from && email.from.length > 0 ? email.from[0] : undefined;
|
||||||
|
const senderName = sender ? (sender.name || sender.address) : 'Unknown Sender';
|
||||||
|
const formattedDate = formatEmailDate(email.date);
|
||||||
|
|
||||||
|
// Create the reply content with attribution line
|
||||||
|
const htmlContent = `
|
||||||
|
<div style="min-height: 20px;"></div>
|
||||||
|
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
||||||
|
<div style="font-family: Arial, sans-serif; color: #666; margin-bottom: 10px;">
|
||||||
|
On ${formattedDate}, ${senderName} wrote:
|
||||||
|
</div>
|
||||||
|
<blockquote style="margin: 0 0 0 10px; padding: 0 0 0 10px; border-left: 2px solid #ccc;">
|
||||||
|
${originalContent}
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Create normalized content with HTML and extracted text
|
||||||
|
const content: EmailContent = {
|
||||||
|
html: sanitizeHtml(htmlContent),
|
||||||
|
text: '', // Will be extracted when composing
|
||||||
|
isHtml: true,
|
||||||
|
direction: email.content.direction || 'ltr'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Extract text from HTML if in browser environment
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
content.text = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
} else {
|
||||||
|
// Simple text extraction in server environment
|
||||||
|
content.text = htmlContent
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return { to, cc, subject, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format an email for reply or forward - Unified API
|
||||||
|
*/
|
||||||
|
export function formatEmailForReplyOrForward(
|
||||||
|
email: EmailMessage,
|
||||||
|
type: 'reply' | 'reply-all' | 'forward'
|
||||||
|
): {
|
||||||
|
to?: string;
|
||||||
|
cc?: string;
|
||||||
|
subject: string;
|
||||||
|
content: EmailContent;
|
||||||
|
} {
|
||||||
|
if (type === 'forward') {
|
||||||
|
const { subject, content } = formatForwardedEmail(email);
|
||||||
|
return { subject, content };
|
||||||
|
} else {
|
||||||
|
return formatReplyEmail(email, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
types/email.ts
Normal file
53
types/email.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Standardized Email Interfaces
|
||||||
|
*
|
||||||
|
* This file contains the core interfaces for email data structures
|
||||||
|
* used throughout the application. All components should use these
|
||||||
|
* interfaces for consistency.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface EmailAddress {
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailAttachment {
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
content?: string;
|
||||||
|
size?: number;
|
||||||
|
contentId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailContent {
|
||||||
|
html?: string; // HTML content if available
|
||||||
|
text: string; // Plain text (always present)
|
||||||
|
isHtml: boolean; // Flag to indicate primary content type
|
||||||
|
direction?: 'ltr'|'rtl' // Text direction
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailFlags {
|
||||||
|
seen: boolean;
|
||||||
|
flagged: boolean;
|
||||||
|
answered: boolean;
|
||||||
|
deleted: boolean;
|
||||||
|
draft: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailMessage {
|
||||||
|
id: string;
|
||||||
|
messageId?: string;
|
||||||
|
uid?: number;
|
||||||
|
subject: string;
|
||||||
|
from: EmailAddress[];
|
||||||
|
to: EmailAddress[];
|
||||||
|
cc?: EmailAddress[];
|
||||||
|
bcc?: EmailAddress[];
|
||||||
|
date: Date | string;
|
||||||
|
flags: EmailFlags;
|
||||||
|
preview?: string;
|
||||||
|
content: EmailContent; // Standardized content structure
|
||||||
|
attachments: EmailAttachment[];
|
||||||
|
folder?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user