courrier preview
This commit is contained in:
parent
3584f72bf5
commit
ef1923baa6
@ -2,37 +2,23 @@
|
|||||||
|
|
||||||
import { useState, useRef, useEffect } from 'react';
|
import { useState, useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
X, Paperclip, ChevronDown, ChevronUp, SendHorizontal, Loader2
|
X, Paperclip, SendHorizontal, Loader2, Plus
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
|
||||||
import DOMPurify from 'isomorphic-dompurify';
|
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 from the centralized utils
|
||||||
import {
|
import {
|
||||||
formatReplyEmail,
|
formatReplyEmail,
|
||||||
formatForwardedEmail,
|
formatForwardedEmail
|
||||||
formatEmailAddresses
|
|
||||||
} from '@/lib/utils/email-utils';
|
} from '@/lib/utils/email-utils';
|
||||||
import { EmailMessage, EmailAddress } from '@/types/email';
|
import { EmailMessage } from '@/types/email';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CENTRAL EMAIL COMPOSER COMPONENT
|
* Email composer component
|
||||||
*
|
* Handles new emails, replies, and forwards with a clean UI
|
||||||
* 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
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Define interface for the modern props
|
|
||||||
interface ComposeEmailProps {
|
interface ComposeEmailProps {
|
||||||
initialEmail?: EmailMessage | null;
|
initialEmail?: EmailMessage | null;
|
||||||
type?: 'new' | 'reply' | 'reply-all' | 'forward';
|
type?: 'new' | 'reply' | 'reply-all' | 'forward';
|
||||||
@ -60,7 +46,6 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
const [bcc, setBcc] = useState<string>('');
|
const [bcc, setBcc] = useState<string>('');
|
||||||
const [subject, setSubject] = useState<string>('');
|
const [subject, setSubject] = useState<string>('');
|
||||||
const [emailContent, setEmailContent] = useState<string>('');
|
const [emailContent, setEmailContent] = useState<string>('');
|
||||||
const [quotedContent, setQuotedContent] = useState<string>('');
|
|
||||||
const [showCc, setShowCc] = useState<boolean>(false);
|
const [showCc, setShowCc] = useState<boolean>(false);
|
||||||
const [showBcc, setShowBcc] = useState<boolean>(false);
|
const [showBcc, setShowBcc] = useState<boolean>(false);
|
||||||
const [sending, setSending] = useState<boolean>(false);
|
const [sending, setSending] = useState<boolean>(false);
|
||||||
@ -91,11 +76,9 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
// Set subject
|
// Set subject
|
||||||
setSubject(formatted.subject);
|
setSubject(formatted.subject);
|
||||||
|
|
||||||
// Set the quoted content (original email)
|
// Set content with original email
|
||||||
setQuotedContent(formatted.content.html || formatted.content.text);
|
const content = formatted.content.html || formatted.content.text;
|
||||||
|
setEmailContent(content);
|
||||||
// Start with empty content for the reply
|
|
||||||
setEmailContent('');
|
|
||||||
}
|
}
|
||||||
else if (type === 'forward') {
|
else if (type === 'forward') {
|
||||||
// Get formatted data for forward
|
// Get formatted data for forward
|
||||||
@ -104,13 +87,11 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
// Set subject
|
// Set subject
|
||||||
setSubject(formatted.subject);
|
setSubject(formatted.subject);
|
||||||
|
|
||||||
// Set the quoted content (original email)
|
// Set content with original email
|
||||||
setQuotedContent(formatted.content.html || formatted.content.text);
|
const content = formatted.content.html || formatted.content.text;
|
||||||
|
setEmailContent(content);
|
||||||
|
|
||||||
// Start with empty content for the forward
|
// If the original email has attachments, include them
|
||||||
setEmailContent('');
|
|
||||||
|
|
||||||
// If the original email has attachments, we should include them
|
|
||||||
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
|
if (initialEmail.attachments && initialEmail.attachments.length > 0) {
|
||||||
const formattedAttachments = initialEmail.attachments.map(att => ({
|
const formattedAttachments = initialEmail.attachments.map(att => ({
|
||||||
name: att.filename || 'attachment',
|
name: att.filename || 'attachment',
|
||||||
@ -126,6 +107,39 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
}
|
}
|
||||||
}, [initialEmail, type]);
|
}, [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
|
// Handle file attachments
|
||||||
const handleAttachmentAdd = async (files: FileList) => {
|
const handleAttachmentAdd = async (files: FileList) => {
|
||||||
const newAttachments = Array.from(files).map(file => ({
|
const newAttachments = Array.from(files).map(file => ({
|
||||||
@ -151,17 +165,12 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
setSending(true);
|
setSending(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Combine the new content with the quoted content
|
|
||||||
const fullContent = type !== 'new'
|
|
||||||
? `${emailContent}<div class="quoted-content">${quotedContent}</div>`
|
|
||||||
: emailContent;
|
|
||||||
|
|
||||||
await onSend({
|
await onSend({
|
||||||
to,
|
to,
|
||||||
cc: cc || undefined,
|
cc: cc || undefined,
|
||||||
bcc: bcc || undefined,
|
bcc: bcc || undefined,
|
||||||
subject,
|
subject,
|
||||||
body: fullContent,
|
body: emailContent,
|
||||||
attachments
|
attachments
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -175,158 +184,136 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Focus and scroll to top when opened
|
// Get compose title based on type
|
||||||
useEffect(() => {
|
const getComposeTitle = () => {
|
||||||
setTimeout(() => {
|
switch(type) {
|
||||||
if (editorRef.current) {
|
case 'reply': return 'Reply';
|
||||||
editorRef.current.focus();
|
case 'reply-all': return 'Reply All';
|
||||||
|
case 'forward': return 'Forward';
|
||||||
// Scroll to top
|
default: return 'New Message';
|
||||||
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);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full max-h-[80vh]">
|
<div className="flex flex-col h-full max-h-[80vh]">
|
||||||
<ComposeEmailHeader
|
{/* Header */}
|
||||||
type={type}
|
<div className="flex items-center justify-between p-4 border-b">
|
||||||
onClose={onClose}
|
<h2 className="text-lg font-medium">{getComposeTitle()}</h2>
|
||||||
/>
|
<Button variant="ghost" size="icon" onClick={onClose}>
|
||||||
<div className="flex-1 overflow-y-auto p-4 compose-email-body">
|
<X className="h-5 w-5" />
|
||||||
<div className="h-full flex flex-col p-4 space-y-2 overflow-y-auto">
|
</Button>
|
||||||
{/* To Field */}
|
</div>
|
||||||
<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 */}
|
{/* Email Form */}
|
||||||
<div className="flex-none flex items-center gap-4">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<button
|
<div className="p-4 space-y-4">
|
||||||
type="button"
|
{/* Recipients */}
|
||||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
<div className="border-b pb-2">
|
||||||
onClick={() => setShowCc(!showCc)}
|
<div className="flex items-center mb-2">
|
||||||
>
|
<span className="w-16 text-gray-500">To:</span>
|
||||||
{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
|
<Input
|
||||||
id="cc"
|
type="text"
|
||||||
value={cc}
|
value={to}
|
||||||
onChange={(e) => setCc(e.target.value)}
|
onChange={(e) => setTo(e.target.value)}
|
||||||
placeholder="cc@example.com"
|
placeholder="recipient@example.com"
|
||||||
className="w-full mt-1 bg-white border-gray-300 text-gray-900"
|
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* BCC Field */}
|
{showCc && (
|
||||||
{showBcc && (
|
<div className="flex items-center mb-2">
|
||||||
<div className="flex-none">
|
<span className="w-16 text-gray-500">Cc:</span>
|
||||||
<Label htmlFor="bcc" className="block text-sm font-medium text-gray-700">Bcc</Label>
|
<Input
|
||||||
<Input
|
type="text"
|
||||||
id="bcc"
|
value={cc}
|
||||||
value={bcc}
|
onChange={(e) => setCc(e.target.value)}
|
||||||
onChange={(e) => setBcc(e.target.value)}
|
placeholder="cc@example.com"
|
||||||
placeholder="bcc@example.com"
|
className="flex-1 border-0 shadow-none focus-visible:ring-0 px-0"
|
||||||
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)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 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>
|
</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>
|
||||||
</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 */}
|
||||||
{attachments.length > 0 && (
|
{attachments.length > 0 && (
|
||||||
<div className="border rounded-md p-3 mt-4">
|
<div className="p-2 border rounded-md">
|
||||||
<h3 className="text-sm font-medium mb-2 text-gray-700">Attachments</h3>
|
{attachments.map((file, index) => (
|
||||||
<div className="space-y-2">
|
<div key={index} className="flex items-center justify-between text-sm py-1">
|
||||||
{attachments.map((file, index) => (
|
<span className="truncate mr-2">{file.name}</span>
|
||||||
<div key={index} className="flex items-center justify-between text-sm border rounded p-2">
|
<Button
|
||||||
<span className="truncate max-w-[200px] text-gray-800">{file.name}</span>
|
variant="ghost"
|
||||||
<Button
|
size="sm"
|
||||||
variant="ghost"
|
onClick={() => handleAttachmentRemove(index)}
|
||||||
size="sm"
|
className="h-6 w-6 p-0"
|
||||||
onClick={() => handleAttachmentRemove(index)}
|
>
|
||||||
className="h-6 w-6 p-0 text-gray-500 hover:text-gray-700"
|
<X className="h-4 w-4" />
|
||||||
>
|
</Button>
|
||||||
<X className="h-4 w-4" />
|
</div>
|
||||||
</Button>
|
))}
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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">
|
<div className="flex items-center gap-2">
|
||||||
{/* File Input for Attachments */}
|
{/* File Input for Attachments */}
|
||||||
<input
|
<input
|
||||||
@ -342,96 +329,91 @@ export default function ComposeEmail(props: ComposeEmailProps) {
|
|||||||
/>
|
/>
|
||||||
<label htmlFor="file-attachment">
|
<label htmlFor="file-attachment">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="rounded-full bg-white hover:bg-gray-100 border-gray-300"
|
className="h-8 w-8"
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
document.getElementById('file-attachment')?.click();
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<Paperclip className="h-4 w-4 text-gray-600" />
|
<Paperclip className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
{sending && <span className="text-xs text-gray-500 ml-2">Preparing attachment...</span>}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="outline"
|
||||||
className="text-gray-600 hover:text-gray-700 hover:bg-gray-100"
|
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
disabled={sending}
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="bg-blue-600 text-white hover:bg-blue-700"
|
variant="default"
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700"
|
||||||
>
|
>
|
||||||
{sending ? (
|
{sending ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
Sending...
|
Sending
|
||||||
</>
|
</>
|
||||||
) : (
|
) : 'Send'}
|
||||||
<>
|
|
||||||
<SendHorizontal className="mr-2 h-4 w-4" />
|
|
||||||
Send
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Styles for email display */}
|
{/* Styles for email content */}
|
||||||
<style jsx global>{`
|
<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-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.6;
|
|
||||||
width: 100%;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.email-quoted-content {
|
|
||||||
color: #505050;
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
border-left: 2px solid #ddd;
|
|
||||||
padding-left: 10px;
|
|
||||||
margin-top: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-quoted-content blockquote {
|
[contenteditable]:focus {
|
||||||
margin: 5px 0;
|
outline: none;
|
||||||
padding-left: 10px;
|
|
||||||
border-left: 2px solid #ddd;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-quoted-content img {
|
[contenteditable] blockquote {
|
||||||
|
margin: 10px 0;
|
||||||
|
padding-left: 15px;
|
||||||
|
border-left: 2px solid #ddd;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
[contenteditable] img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-quoted-content table {
|
[contenteditable] table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-quoted-content th,
|
[contenteditable] th,
|
||||||
.email-quoted-content td {
|
[contenteditable] td {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
.email-quoted-content th {
|
[contenteditable] th {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Forwarded message styles */
|
||||||
|
.email-original-content {
|
||||||
|
margin-top: 20px;
|
||||||
|
color: #505050;
|
||||||
|
}
|
||||||
`}</style>
|
`}</style>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -225,215 +225,213 @@ export function renderEmailContent(content: EmailContent): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Format an email for forwarding
|
* Format email for reply
|
||||||
*/
|
*/
|
||||||
export function formatForwardedEmail(email: EmailMessage): {
|
export function formatReplyEmail(originalEmail: any, type: 'reply' | 'reply-all' = 'reply') {
|
||||||
subject: string;
|
if (!originalEmail) {
|
||||||
content: EmailContent;
|
return {
|
||||||
} {
|
to: '',
|
||||||
// Format subject with Fwd: prefix if needed
|
cc: '',
|
||||||
const subjectBase = email.subject || '(No subject)';
|
subject: '',
|
||||||
const subject = subjectBase.match(/^(Fwd|FW|Forward):/i)
|
content: {
|
||||||
? subjectBase
|
text: '',
|
||||||
: `Fwd: ${subjectBase}`;
|
html: '',
|
||||||
|
isHtml: false,
|
||||||
// Get sender and recipient information
|
direction: 'ltr' as const
|
||||||
const fromString = formatEmailAddresses(email.from || []);
|
}
|
||||||
const toString = formatEmailAddresses(email.to || []);
|
};
|
||||||
const dateString = formatEmailDate(email.date);
|
|
||||||
|
|
||||||
// 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>';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the content already has a forwarded message header
|
// Format the recipients
|
||||||
const hasExistingHeader = originalContent.includes('---------- Forwarded message ---------');
|
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
|
||||||
|
: '';
|
||||||
|
|
||||||
// If there's already a forwarded message header, don't add another one
|
// For reply-all, include other recipients in CC
|
||||||
let htmlContent = '';
|
let cc = '';
|
||||||
if (hasExistingHeader) {
|
if (type === 'reply-all') {
|
||||||
// Just wrap the content without additional formatting
|
const toRecipients = Array.isArray(originalEmail.to)
|
||||||
htmlContent = `
|
? originalEmail.to.map(addr => {
|
||||||
<div style="min-height: 20px;"></div>
|
if (typeof addr === 'string') return addr;
|
||||||
<div class="email-original-content">
|
return addr.address ? addr.address : '';
|
||||||
${originalContent}
|
}).filter(Boolean)
|
||||||
</div>
|
: typeof originalEmail.to === 'string'
|
||||||
`;
|
? [originalEmail.to]
|
||||||
} else {
|
: [];
|
||||||
// Create formatted content for forwarded email
|
|
||||||
htmlContent = `
|
const ccRecipients = Array.isArray(originalEmail.cc)
|
||||||
<div style="min-height: 20px;"></div>
|
? originalEmail.cc.map(addr => {
|
||||||
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
if (typeof addr === 'string') return addr;
|
||||||
<div style="font-family: Arial, sans-serif; color: #333;">
|
return addr.address ? addr.address : '';
|
||||||
<div style="margin-bottom: 15px;">
|
}).filter(Boolean)
|
||||||
<div>---------- Forwarded message ---------</div>
|
: typeof originalEmail.cc === 'string'
|
||||||
<div><b>From:</b> ${fromString}</div>
|
? [originalEmail.cc]
|
||||||
<div><b>Date:</b> ${dateString}</div>
|
: [];
|
||||||
<div><b>Subject:</b> ${email.subject || ''}</div>
|
|
||||||
<div><b>To:</b> ${toString}</div>
|
cc = [...toRecipients, ...ccRecipients].join(', ');
|
||||||
</div>
|
|
||||||
<div class="email-original-content">
|
|
||||||
${originalContent}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we have clean HTML
|
// Format the subject
|
||||||
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
|
const subject = originalEmail.subject && !originalEmail.subject.startsWith('Re:')
|
||||||
ADD_TAGS: ['style'],
|
? `Re: ${originalEmail.subject}`
|
||||||
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
|
: originalEmail.subject || '';
|
||||||
ALLOW_DATA_ATTR: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create normalized content with HTML and extracted text
|
// Format the content
|
||||||
const content: EmailContent = {
|
const originalDate = originalEmail.date ? new Date(originalEmail.date) : new Date();
|
||||||
html: cleanedHtml,
|
const dateStr = originalDate.toLocaleString();
|
||||||
text: '', // Will be extracted when composing
|
|
||||||
isHtml: true,
|
|
||||||
direction: email.content?.direction || 'ltr'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract text from HTML if in browser environment
|
const fromStr = Array.isArray(originalEmail.from)
|
||||||
if (typeof document !== 'undefined') {
|
? originalEmail.from.map(addr => {
|
||||||
const tempDiv = document.createElement('div');
|
if (typeof addr === 'string') return addr;
|
||||||
tempDiv.innerHTML = htmlContent;
|
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
|
||||||
content.text = tempDiv.textContent || tempDiv.innerText || '';
|
}).join(', ')
|
||||||
} else {
|
: typeof originalEmail.from === 'string'
|
||||||
// Simple text extraction in server environment
|
? originalEmail.from
|
||||||
content.text = htmlContent
|
: 'Unknown Sender';
|
||||||
.replace(/<[^>]*>/g, '')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
|
: '';
|
||||||
|
|
||||||
/**
|
// Create HTML 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 = [
|
|
||||||
...(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 = `
|
const htmlContent = `
|
||||||
<div style="min-height: 20px;"></div>
|
<br/>
|
||||||
<div style="border-top: 1px solid #ccc; margin-top: 10px; padding-top: 10px;">
|
<br/>
|
||||||
<div style="font-family: Arial, sans-serif; color: #666; margin-bottom: 10px;">
|
<div class="email-original-content">
|
||||||
On ${formattedDate}, ${senderName} wrote:
|
<blockquote style="border-left: 2px solid #ddd; padding-left: 10px; margin: 10px 0; color: #505050;">
|
||||||
</div>
|
<p>On ${dateStr}, ${fromStr} wrote:</p>
|
||||||
<blockquote style="margin: 0 0 0 10px; padding: 0 0 0 10px; border-left: 2px solid #ccc;">
|
${originalEmail.content?.html || originalEmail.content?.text || ''}
|
||||||
${originalContent}
|
|
||||||
</blockquote>
|
</blockquote>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Ensure we have clean HTML
|
// Create plain text content
|
||||||
const cleanedHtml = DOMPurify.sanitize(htmlContent, {
|
const plainText = originalEmail.content?.text || '';
|
||||||
ADD_TAGS: ['style'],
|
const textContent = `
|
||||||
ADD_ATTR: ['target', 'rel', 'href', 'src', 'style', 'class', 'id'],
|
|
||||||
ALLOW_DATA_ATTR: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create normalized content with HTML and extracted text
|
On ${dateStr}, ${fromStr} wrote:
|
||||||
const content: EmailContent = {
|
> ${plainText.split('\n').join('\n> ')}
|
||||||
html: cleanedHtml,
|
`;
|
||||||
text: '', // Will be extracted when composing
|
|
||||||
isHtml: true,
|
return {
|
||||||
direction: email.content?.direction || 'ltr'
|
to,
|
||||||
|
cc,
|
||||||
|
subject,
|
||||||
|
content: {
|
||||||
|
text: textContent,
|
||||||
|
html: htmlContent,
|
||||||
|
isHtml: true,
|
||||||
|
direction: 'ltr' as const
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
* 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';
|
||||||
|
|
||||||
|
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 formatEmailForReplyOrForward(
|
export function formatEmailForReplyOrForward(
|
||||||
email: EmailMessage,
|
email: EmailMessage,
|
||||||
@ -451,3 +449,31 @@ export function formatEmailForReplyOrForward(
|
|||||||
return formatReplyEmail(email, type);
|
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