courrier preview

This commit is contained in:
alma 2025-04-30 23:25:41 +02:00
parent 3584f72bf5
commit ef1923baa6
2 changed files with 421 additions and 413 deletions

View File

@ -2,37 +2,23 @@
import { useState, useRef, useEffect } from 'react';
import {
X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2
X, Paperclip, SendHorizontal, Loader2, Plus
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import DOMPurify from 'isomorphic-dompurify';
import { Label } from '@/components/ui/label';
// Import sub-components
import ComposeEmailHeader from './ComposeEmailHeader';
import RichEmailEditor from './RichEmailEditor';
// Import from the centralized utils
import {
formatReplyEmail,
formatForwardedEmail,
formatEmailAddresses
formatForwardedEmail
} from '@/lib/utils/email-utils';
import { EmailMessage, EmailAddress } from '@/types/email';
import { EmailMessage } from '@/types/email';
/**
* CENTRAL EMAIL COMPOSER COMPONENT
*
* This is the unified, centralized email composer component used throughout the application.
* It handles new emails, replies, and forwards with proper text direction.
*
* All code that needs to compose emails should import this component from:
* @/components/email/ComposeEmail
* Email composer component
* Handles new emails, replies, and forwards with a clean UI
*/
// Define interface for the modern props
interface ComposeEmailProps {
initialEmail?: EmailMessage | null;
type?: 'new' | 'reply' | 'reply-all' | 'forward';
@ -60,7 +46,6 @@ export default function ComposeEmail(props: ComposeEmailProps) {
const [bcc, setBcc] = useState<string>('');
const [subject, setSubject] = useState<string>('');
const [emailContent, setEmailContent] = useState<string>('');
const [quotedContent, setQuotedContent] = useState<string>('');
const [showCc, setShowCc] = useState<boolean>(false);
const [showBcc, setShowBcc] = useState<boolean>(false);
const [sending, setSending] = useState<boolean>(false);
@ -91,11 +76,9 @@ export default function ComposeEmail(props: ComposeEmailProps) {
// Set subject
setSubject(formatted.subject);
// Set the quoted content (original email)
setQuotedContent(formatted.content.html || formatted.content.text);
// Start with empty content for the reply
setEmailContent('');
// Set content with original email
const content = formatted.content.html || formatted.content.text;
setEmailContent(content);
}
else if (type === 'forward') {
// Get formatted data for forward
@ -104,13 +87,11 @@ export default function ComposeEmail(props: ComposeEmailProps) {
// Set subject
setSubject(formatted.subject);
// Set the quoted content (original email)
setQuotedContent(formatted.content.html || formatted.content.text);
// Set content with original email
const content = formatted.content.html || formatted.content.text;
setEmailContent(content);
// Start with empty content for the forward
setEmailContent('');
// If the original email has attachments, we should include them
// If the original email has attachments, include them
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
const formattedAttachments = initialEmail.attachments.map(att => ({
name: att.filename || 'attachment',
@ -126,6 +107,39 @@ export default function ComposeEmail(props: ComposeEmailProps) {
}
}, [initialEmail, type]);
// Place cursor at beginning and ensure content is scrolled to top
useEffect(() => {
if (editorRef.current && type !== 'new') {
// Small delay to ensure DOM is ready
setTimeout(() => {
if (editorRef.current) {
// Focus the editor
editorRef.current.focus();
// Put cursor at the beginning
const selection = window.getSelection();
const range = document.createRange();
range.setStart(editorRef.current, 0);
range.collapse(true);
selection?.removeAllRanges();
selection?.addRange(range);
// Also make sure editor container is scrolled to top
editorRef.current.scrollTop = 0;
// Find parent scrollable containers and scroll them to top
let parent = editorRef.current.parentElement;
while (parent) {
if (parent.classList.contains('overflow-y-auto')) {
parent.scrollTop = 0;
}
parent = parent.parentElement;
}
}
}, 100);
}
}, [emailContent, type]);
// Handle file attachments
const handleAttachmentAdd = async (files: FileList) => {
const newAttachments = Array.from(files).map(file => ({
@ -151,17 +165,12 @@ export default function ComposeEmail(props: ComposeEmailProps) {
setSending(true);
try {
// Combine the new content with the quoted content
const fullContent = type !== 'new'
? `${emailContent}<div class="quoted-content">${quotedContent}</div>`
: emailContent;
await onSend({
to,
cc: cc || undefined,
bcc: bcc || undefined,
subject,
body: fullContent,
body: emailContent,
attachments
});
@ -175,158 +184,136 @@ export default function ComposeEmail(props: ComposeEmailProps) {
}
};
// Focus and scroll to top when opened
useEffect(() => {
setTimeout(() => {
if (editorRef.current) {
editorRef.current.focus();
// Scroll to top
const scrollElements = [
editorRef.current,
document.querySelector('.overflow-y-auto'),
document.querySelector('.compose-email-body')
];
scrollElements.forEach(el => {
if (el instanceof HTMLElement) {
el.scrollTop = 0;
}
});
}
}, 100);
}, []);
// Get compose title based on type
const getComposeTitle = () => {
switch(type) {
case 'reply': return 'Reply';
case 'reply-all': return 'Reply All';
case 'forward': return 'Forward';
default: return 'New Message';
}
};
return (
<div className="flex flex-col h-full max-h-[80vh]">
<ComposeEmailHeader
type={type}
onClose={onClose}
/>
<div className="flex-1 overflow-y-auto p-4 compose-email-body">
<div className="h-full flex flex-col p-4 space-y-2 overflow-y-auto">
{/* To Field */}
<div className="flex-none">
<Label htmlFor="to" className="block text-sm font-medium text-gray-700">To</Label>
<Input
id="to"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="recipient@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* CC/BCC Toggle Buttons */}
<div className="flex-none flex items-center gap-4">
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowCc(!showCc)}
>
{showCc ? 'Hide Cc' : 'Add Cc'}
</button>
<button
type="button"
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
onClick={() => setShowBcc(!showBcc)}
>
{showBcc ? 'Hide Bcc' : 'Add Bcc'}
</button>
</div>
{/* CC Field */}
{showCc && (
<div className="flex-none">
<Label htmlFor="cc" className="block text-sm font-medium text-gray-700">Cc</Label>
<Input
id="cc"
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="cc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
{/* Header */}
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-medium">{getComposeTitle()}</h2>
<Button variant="ghost" size="icon" onClick={onClose}>
<X className="h-5 w-5" />
</Button>
</div>
{/* Email Form */}
<div className="flex-1 overflow-y-auto">
<div className="p-4 space-y-4">
{/* Recipients */}
<div className="border-b pb-2">
<div className="flex items-center mb-2">
<span className="w-16 text-gray-500">To:</span>
<Input
type="text"
value={to}
onChange={(e) => setTo(e.target.value)}
placeholder="recipient@example.com"
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
/>
</div>
)}
{/* BCC Field */}
{showBcc && (
<div className="flex-none">
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
<Input
id="bcc"
value={bcc}
onChange={(e) => setBcc(e.target.value)}
placeholder="bcc@example.com"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
)}
{/* Subject Field */}
<div className="flex-none">
<Label htmlFor="subject" className="block text-sm font-medium text-gray-700">Subject</Label>
<Input
id="subject"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Enter subject"
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
/>
</div>
{/* Message Body - Simplified Editor */}
<div className="flex-1 min-h-[200px] flex flex-col overflow-hidden">
<Label htmlFor="message" className="flex-none block text-sm font-medium text-gray-700 mb-2">Message</Label>
<div className="flex-1 border border-gray-300 rounded-md overflow-hidden">
<div className="email-editor-container flex flex-col h-full">
{/* Simple editor for new content */}
<div
ref={editorRef}
className="simple-editor p-3 min-h-[100px] outline-none"
contentEditable={true}
dangerouslySetInnerHTML={{ __html: emailContent }}
onInput={(e) => setEmailContent(e.currentTarget.innerHTML)}
{showCc && (
<div className="flex items-center mb-2">
<span className="w-16 text-gray-500">Cc:</span>
<Input
type="text"
value={cc}
onChange={(e) => setCc(e.target.value)}
placeholder="cc@example.com"
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
/>
{/* Quoted content from original email */}
{quotedContent && (
<div className="quoted-email-content p-3 border-t border-gray-200 bg-gray-50">
<div
className="email-quoted-content"
dangerouslySetInnerHTML={{ __html: quotedContent }}
/>
</div>
)}
</div>
)}
{showBcc && (
<div className="flex items-center mb-2">
<span className="w-16 text-gray-500">Bcc:</span>
<Input
type="text"
value={bcc}
onChange={(e) => setBcc(e.target.value)}
placeholder="bcc@example.com"
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
/>
</div>
)}
{/* CC/BCC Toggle Links */}
<div className="flex gap-3 ml-16 mb-1">
{!showCc && (
<button
className="text-blue-600 text-sm"
onClick={() => setShowCc(true)}
>
Add Cc
</button>
)}
{!showBcc && (
<button
className="text-blue-600 text-sm"
onClick={() => setShowBcc(true)}
>
Add Bcc
</button>
)}
</div>
</div>
{/* Subject */}
<div className="border-b pb-2">
<div className="flex items-center">
<span className="w-16 text-gray-500">Subject:</span>
<Input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Subject"
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
/>
</div>
</div>
{/* Message Body */}
<div
ref={editorRef}
className="min-h-[300px] outline-none p-2"
contentEditable={true}
dangerouslySetInnerHTML={{ __html: emailContent }}
onInput={(e) => setEmailContent(e.currentTarget.innerHTML)}
/>
{/* Attachments */}
{attachments.length > 0 && (
<div className="border rounded-md p-3 mt-4">
<h3 className="text-sm font-medium mb-2 text-gray-700">Attachments</h3>
<div className="space-y-2">
{attachments.map((file, index) => (
<div key={index} className="flex items-center justify-between text-sm border rounded p-2">
<span className="truncate max-w-[200px] text-gray-800">{file.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleAttachmentRemove(index)}
className="h-6 w-6 p-0 text-gray-500 hover:text-gray-700"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
<div className="p-2 border rounded-md">
{attachments.map((file, index) => (
<div key={index} className="flex items-center justify-between text-sm py-1">
<span className="truncate mr-2">{file.name}</span>
<Button
variant="ghost"
size="sm"
onClick={() => handleAttachmentRemove(index)}
className="h-6 w-6 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
</div>
{/* Modal Footer - now inside the main modal container and visually attached */}
<div className="flex-none flex items-center justify-between px-6 py-3 border-t border-gray-200 bg-white">
{/* Footer */}
<div className="border-t p-3 flex items-center justify-between">
<div className="flex items-center gap-2">
{/* File Input for Attachments */}
<input
@ -342,96 +329,91 @@ export default function ComposeEmail(props: ComposeEmailProps) {
/>
<label htmlFor="file-attachment">
<Button
variant="outline"
variant="ghost"
size="icon"
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
onClick={(e) => {
e.preventDefault();
document.getElementById('file-attachment')?.click();
}}
className="h-8 w-8"
>
<Paperclip className="h-4 w-4 text-gray-600" />
<Paperclip className="h-4 w-4" />
</Button>
</label>
{sending && <span className="text-xs text-gray-500 ml-2">Preparing attachment...</span>}
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<Button
variant="ghost"
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
variant="outline"
onClick={onClose}
disabled={sending}
>
Cancel
</Button>
<Button
className="bg-blue-600 text-white hover:bg-blue-700"
variant="default"
onClick={handleSend}
disabled={sending}
className="bg-blue-600 hover:bg-blue-700"
>
{sending ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Sending...
Sending
</>
) : (
<>
<SendHorizontal className="mr-2 h-4 w-4" />
Send
</>
)}
) : 'Send'}
</Button>
</div>
</div>
{/* Styles for email display */}
{/* Styles for email content */}
<style jsx global>{`
.simple-editor {
[contenteditable] {
-webkit-user-modify: read-write-plaintext-only;
overflow-wrap: break-word;
-webkit-line-break: after-white-space;
-webkit-user-select: text;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
font-size: 14px;
line-height: 1.6;
width: 100%;
flex: 1;
}
.email-quoted-content {
color: #505050;
font-size: 13px;
line-height: 1.5;
border-left: 2px solid #ddd;
padding-left: 10px;
margin-top: 5px;
}
.email-quoted-content blockquote {
margin: 5px 0;
padding-left: 10px;
border-left: 2px solid #ddd;
[contenteditable]:focus {
outline: none;
}
.email-quoted-content img {
[contenteditable] blockquote {
margin: 10px 0;
padding-left: 15px;
border-left: 2px solid #ddd;
color: #666;
}
[contenteditable] img {
max-width: 100%;
height: auto;
}
.email-quoted-content table {
[contenteditable] table {
border-collapse: collapse;
width: 100%;
max-width: 100%;
margin-bottom: 1rem;
}
.email-quoted-content th,
.email-quoted-content td {
[contenteditable] th,
[contenteditable] td {
padding: 5px;
border: 1px solid #ddd;
}
.email-quoted-content th {
[contenteditable] th {
background-color: #f8f9fa;
font-weight: 600;
text-align: left;
}
/* Forwarded message styles */
.email-original-content {
margin-top: 20px;
color: #505050;
}
`}</style>
</div>
);

View File

@ -225,216 +225,214 @@ export function renderEmailContent(content: EmailContent): string {
}
/**
* Format an email for forwarding
* Format email for reply
*/
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}`;
export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all' = 'reply') {
if (!originalEmail) {
return {
to: '',
cc: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Format the recipients
const to = Array.isArray(originalEmail.from)
? originalEmail.from.map(addr => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean).join(', ')
: typeof originalEmail.from === 'string'
? originalEmail.from
: '';
// For reply-all, include other recipients in CC
let cc = '';
if (type === 'reply-all') {
const toRecipients = Array.isArray(originalEmail.to)
? originalEmail.to.map(addr => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean)
: typeof originalEmail.to === 'string'
? [originalEmail.to]
: [];
const ccRecipients = Array.isArray(originalEmail.cc)
? originalEmail.cc.map(addr => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean)
: typeof originalEmail.cc === 'string'
? [originalEmail.cc]
: [];
cc = [...toRecipients, ...ccRecipients].join(', ');
}
// Format the subject
const subject = originalEmail.subject && !originalEmail.subject.startsWith('Re:')
? `Re: ${originalEmail.subject}`
: originalEmail.subject || '';
// Format the content
const originalDate = originalEmail.date ? new Date(originalEmail.date) : new Date();
const dateStr = originalDate.toLocaleString();
// Get sender and recipient information
const fromString = formatEmailAddresses(email.from || []);
const toString = formatEmailAddresses(email.to || []);
const dateString = formatEmailDate(email.date);
const fromStr = Array.isArray(originalEmail.from)
? originalEmail.from.map(addr => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof originalEmail.from === 'string'
? originalEmail.from
: 'Unknown Sender';
// Get original content - use the raw content if possible to preserve formatting
let originalContent = '';
if (email.content) {
if (email.content.isHtml && email.content.html) {
originalContent = email.content.html;
} else if (email.content.text) {
// Format plain text with basic HTML formatting
originalContent = formatPlainTextToHtml(email.content.text);
const toStr = Array.isArray(originalEmail.to)
? originalEmail.to.map(addr => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof originalEmail.to === 'string'
? originalEmail.to
: '';
// Create HTML content
const htmlContent = `
<br/>
<br/>
<div class="email-original-content">
<blockquote style="border-left: 2px solid #ddd; padding-left: 10px; margin: 10px 0; color: #505050;">
<p>On ${dateStr}, ${fromStr} wrote:</p>
${originalEmail.content?.html || originalEmail.content?.text || ''}
</blockquote>
</div>
`;
// Create plain text content
const plainText = originalEmail.content?.text || '';
const textContent = `
On ${dateStr}, ${fromStr} wrote:
> ${plainText.split('\n').join('\n> ')}
`;
return {
to,
cc,
subject,
content: {
text: textContent,
html: htmlContent,
isHtml: true,
direction: 'ltr' as const
}
} else if (email.html) {
originalContent = email.html;
} else if (email.text) {
originalContent = formatPlainTextToHtml(email.text);
} else {
originalContent = '<p>No content</p>';
}
// 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>
<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>
`;
}
// Ensure we have clean HTML
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
ADD_TAGS: ['style'],
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
ALLOW_DATA_ATTR: true
});
// Create normalized content with HTML and extracted text
const content: EmailContent = {
html: cleanedHtml,
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(/&nbsp;/g, ' ')
.trim();
}
/**
* Format email for forwarding
*/
export function formatForwardedEmail(originalEmail: any) {
if (!originalEmail) {
return {
to: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Format the subject
const subject = originalEmail.subject && !originalEmail.subject.startsWith('Fwd:')
? `Fwd: ${originalEmail.subject}`
: originalEmail.subject || '';
// Format from, to, cc for the header
const fromStr = Array.isArray(originalEmail.from)
? originalEmail.from.map(addr => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof originalEmail.from === 'string'
? originalEmail.from
: 'Unknown Sender';
return { subject, content };
const toStr = Array.isArray(originalEmail.to)
? originalEmail.to.map(addr => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof originalEmail.to === 'string'
? originalEmail.to
: '';
const ccStr = Array.isArray(originalEmail.cc)
? originalEmail.cc.map(addr => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof originalEmail.cc === 'string'
? originalEmail.cc
: '';
const dateStr = originalEmail.date ? new Date(originalEmail.date).toLocaleString() : 'Unknown Date';
// Create HTML content
const htmlContent = `
<br/>
<br/>
<div class="email-original-content">
<p>---------- Forwarded message ---------</p>
<p><strong>From:</strong> ${fromStr}</p>
<p><strong>Date:</strong> ${dateStr}</p>
<p><strong>Subject:</strong> ${originalEmail.subject || ''}</p>
<p><strong>To:</strong> ${toStr}</p>
${ccStr ? `<p><strong>Cc:</strong> ${ccStr}</p>` : ''}
<div style="margin-top: 15px; border-top: 1px solid #eee; padding-top: 15px;">
${originalEmail.content?.html || originalEmail.content?.text || ''}
</div>
</div>
`;
// Create plain text content
const textContent = `
---------- Forwarded message ---------
From: ${fromStr}
Date: ${dateStr}
Subject: ${originalEmail.subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}\n` : ''}
${originalEmail.content?.text || ''}
`;
return {
to: '',
subject,
content: {
text: textContent,
html: htmlContent,
isHtml: true,
direction: 'ltr' as const
}
};
}
/**
* 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 = [
...(typeof email.to === 'string' ? [{name: '', address: email.to}] : (email.to || [])),
...(typeof email.cc === 'string' ? [{name: '', address: email.cc}] : (email.cc || []))
];
// Remove duplicates, then convert to string
const uniqueRecipients = [...new Map(allRecipients.map(addr =>
[typeof addr === 'string' ? 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 - use the raw content if possible to preserve formatting
let originalContent = '';
if (email.content) {
if (email.content.isHtml && email.content.html) {
originalContent = email.content.html;
} else if (email.content.text) {
// Format plain text with basic HTML formatting
originalContent = formatPlainTextToHtml(email.content.text);
}
} else if (email.html) {
originalContent = email.html;
} else if (email.text) {
originalContent = formatPlainTextToHtml(email.text);
} else {
originalContent = '<p>No content</p>';
}
// Format sender info
let senderName = 'Unknown Sender';
if (email.from) {
if (Array.isArray(email.from) && email.from.length > 0) {
const sender = email.from[0];
senderName = typeof sender === 'string' ? sender : (sender.name || sender.address);
} else if (typeof email.from === 'string') {
senderName = email.from;
}
}
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>
`;
// Ensure we have clean HTML
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
ADD_TAGS: ['style'],
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
ALLOW_DATA_ATTR: true
});
// Create normalized content with HTML and extracted text
const content: EmailContent = {
html: cleanedHtml,
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(/&nbsp;/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'
@ -450,4 +448,32 @@ export function formatEmailForReplyOrForward(
} else {
return formatReplyEmail(email, type);
}
}
/**
* Helper to properly format email addresses
*/
export function formatEmailAddress(addr: any) {
// ... existing code ...
}
/**
* Helper to format multiple email addresses
*/
export function formatEmailAddresses(addrs: any) {
// ... existing code ...
}
/**
* Helper to format recipient
*/
export function formatRecipient(addr: any) {
// ... existing code ...
}
/**
* Helper to format multiple recipients
*/
export function formatRecipients(addrs: any) {
// ... existing code ...
}