courrier preview
This commit is contained in:
parent
3584f72bf5
commit
ef1923baa6
@ -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>
|
||||
);
|
||||
|
||||
@ -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(/ /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(/ /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 ...
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user