diff --git a/components/email/ComposeEmail.tsx b/components/email/ComposeEmail.tsx index 5ffd684e..5138914e 100644 --- a/components/email/ComposeEmail.tsx +++ b/components/email/ComposeEmail.tsx @@ -16,14 +16,15 @@ import ComposeEmailHeader from './ComposeEmailHeader'; import ComposeEmailForm from './ComposeEmailForm'; import ComposeEmailFooter from './ComposeEmailFooter'; import RichEmailEditor from './RichEmailEditor'; +import QuotedEmailContent from './QuotedEmailContent'; // Import ONLY from the centralized formatter import { + formatReplyEmail, formatForwardedEmail, - formatReplyEmail, - formatEmailForReplyOrForward, - EmailMessage as FormatterEmailMessage, - sanitizeHtml + formatEmailAddresses, + type EmailMessage, + type EmailAddress } from '@/lib/utils/email-formatter'; /** @@ -39,40 +40,7 @@ import { * for consistent handling of email content and text direction. */ -// Define EmailMessage interface locally instead of importing from server-only file -interface EmailAddress { - name: string; - address: string; -} - -interface EmailMessage { - id: string; - messageId?: string; - subject: string; - from: EmailAddress[]; - to: EmailAddress[]; - cc?: EmailAddress[]; - bcc?: EmailAddress[]; - date: Date | string; - flags?: { - seen: boolean; - flagged: boolean; - answered: boolean; - deleted: boolean; - draft: boolean; - }; - preview?: string; - content?: string; - html?: string; - text?: string; - hasAttachments?: boolean; - attachments?: any[]; - folder?: string; - size?: number; - contentFetched?: boolean; -} - -// Legacy interface for backward compatibility with old ComposeEmail component +// Define interface for the legacy props interface LegacyComposeEmailProps { showCompose: boolean; setShowCompose: (show: boolean) => void; @@ -103,7 +71,7 @@ interface LegacyComposeEmailProps { forwardFrom?: any | null; } -// New interface for the modern ComposeEmail component +// Define interface for the modern props interface ComposeEmailProps { initialEmail?: EmailMessage | null; type?: 'new' | 'reply' | 'reply-all' | 'forward'; @@ -122,12 +90,46 @@ interface ComposeEmailProps { }) => Promise; } -// Union type to handle both new and legacy props +// Union type for handling both types of props type ComposeEmailAllProps = ComposeEmailProps | LegacyComposeEmailProps; // Type guard to check if props are legacy -function isLegacyProps(props: ComposeEmailAllProps): props is LegacyComposeEmailProps { - return 'showCompose' in props && 'setShowCompose' in props; +function isLegacyProps( + props: ComposeEmailAllProps +): props is LegacyComposeEmailProps { + return 'showCompose' in props; +} + +// Helper function to adapt EmailMessage to QuotedEmailContent props format +function EmailMessageToQuotedContentAdapter({ + email, + type +}: { + email: EmailMessage, + type: 'reply' | 'reply-all' | 'forward' +}) { + // Get the email content + const content = email.content || email.html || email.text || ''; + + // Get the sender + const sender = email.from && email.from.length > 0 + ? { + name: email.from[0].name, + email: email.from[0].address + } + : { email: 'unknown@example.com' }; + + // Map the type to what QuotedEmailContent expects + const mappedType = type === 'reply-all' ? 'reply' : type; + + return ( + + ); } export default function ComposeEmail(props: ComposeEmailAllProps) { @@ -158,39 +160,56 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { useEffect(() => { if (initialEmail && type !== 'new') { try { - const formatterEmail: FormatterEmailMessage = { - id: initialEmail.id, - messageId: initialEmail.messageId, - subject: initialEmail.subject, - from: initialEmail.from || [], - to: initialEmail.to || [], - cc: initialEmail.cc || [], - bcc: initialEmail.bcc || [], - date: initialEmail.date, - content: initialEmail.content, - html: initialEmail.html, - text: initialEmail.text, - hasAttachments: initialEmail.hasAttachments || false - }; - - if (type === 'forward') { - // For forwarding, use the dedicated formatter - const { subject, content } = formatForwardedEmail(formatterEmail); + // Set recipients based on type + if (type === 'reply' || type === 'reply-all') { + // Reply goes to the original sender + setTo(formatEmailAddresses(initialEmail.from || [])); + + // For reply-all, include all original recipients in CC + if (type === 'reply-all') { + const allRecipients = [ + ...(initialEmail.to || []), + ...(initialEmail.cc || []) + ]; + // Filter out the current user if they were a recipient + // This would need some user context to properly implement + setCc(formatEmailAddresses(allRecipients)); + } + + // Set subject with Re: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^Re:/i) ? subjectBase : `Re: ${subjectBase}`; setSubject(subject); - setEmailContent(content); - } else { - // For reply/reply-all, use the reply formatter - const { to, cc, subject, content } = formatReplyEmail(formatterEmail, type as 'reply' | 'reply-all'); - setTo(to); - if (cc) { - setCc(cc); + + // Set an empty content with proper spacing before the quoted content + setEmailContent('


'); + + // Show CC field if there are CC recipients + if (initialEmail.cc && initialEmail.cc.length > 0) { setShowCc(true); } + } + else if (type === 'forward') { + // Set subject with Fwd: prefix + const subjectBase = initialEmail.subject || '(No subject)'; + const subject = subjectBase.match(/^(Fwd|FW|Forward):/i) ? subjectBase : `Fwd: ${subjectBase}`; setSubject(subject); - setEmailContent(content); + + // Set an empty content with proper spacing before the quoted content + setEmailContent('


'); + + // If the original email has attachments, we should include them + if (initialEmail.attachments && initialEmail.attachments.length > 0) { + const formattedAttachments = initialEmail.attachments.map(att => ({ + name: att.filename || 'attachment', + type: att.contentType || 'application/octet-stream', + content: att.content || '' + })); + setAttachments(formattedAttachments); + } } - } catch (err) { - console.error('Error formatting email for reply/forward:', err); + } catch (error) { + console.error('Error initializing compose form:', error); } } }, [initialEmail, type]); @@ -339,10 +358,21 @@ export default function ComposeEmail(props: ComposeEmailAllProps) { onChange={setEmailContent} minHeight="200px" maxHeight="none" + preserveFormatting={true} /> + {/* Add QuotedEmailContent for replies and forwards */} + {initialEmail && (type === 'reply' || type === 'reply-all' || type === 'forward') && ( +
+ +
+ )} + {/* Attachments */} {attachments.length > 0 && (
@@ -625,10 +655,23 @@ function LegacyAdapter({ onChange={setComposeBody} minHeight="200px" maxHeight="none" + preserveFormatting={true} />
+ {/* Add QuotedEmailContent for replies and forwards */} + {(originalEmail || replyTo || forwardFrom) && + (determineType() === 'reply' || determineType() === 'reply-all' || determineType() === 'forward') && ( +
+ {/* For legacy adapter, we'd need to convert the different formats */} + {/* Since we don't have the full implementation for this, we'll add a placeholder */} +
+

Original message content would appear here

+
+
+ )} + {/* Attachments */} {attachments.length > 0 && (
diff --git a/components/email/EmailContentDisplay.tsx b/components/email/EmailContentDisplay.tsx new file mode 100644 index 00000000..50986449 --- /dev/null +++ b/components/email/EmailContentDisplay.tsx @@ -0,0 +1,197 @@ +'use client'; + +import React, { useEffect, useState, useRef } from 'react'; +import DOMPurify from 'isomorphic-dompurify'; +import { parseRawEmail } from '@/lib/utils/email-mime-decoder'; + +interface EmailContentDisplayProps { + content: string; + type?: 'html' | 'text' | 'auto'; + className?: string; + showQuotedText?: boolean; +} + +/** + * Component for displaying properly formatted email content + * Handles MIME decoding, sanitization, and proper rendering + */ +const EmailContentDisplay: React.FC = ({ + content, + type = 'auto', + className = '', + showQuotedText = true +}) => { + const [processedContent, setProcessedContent] = useState<{ + html: string; + text: string; + isHtml: boolean; + }>({ + html: '', + text: '', + isHtml: false + }); + + const containerRef = useRef(null); + + // Process and sanitize email content + useEffect(() => { + if (!content) { + setProcessedContent({ html: '', text: '', isHtml: false }); + return; + } + + try { + // Check if this is raw email content + const isRawEmail = content.includes('Content-Type:') || + content.includes('MIME-Version:') || + content.includes('From:') && content.includes('To:'); + + if (isRawEmail) { + // Parse raw email content + const parsed = parseRawEmail(content); + + // Check which content to use based on type and availability + const useHtml = (type === 'html' || (type === 'auto' && parsed.html)) && !!parsed.html; + + if (useHtml) { + // Sanitize HTML content + const sanitizedHtml = DOMPurify.sanitize(parsed.html, { + ADD_TAGS: ['table', 'thead', 'tbody', 'tr', 'td', 'th'], + ADD_ATTR: ['target', 'rel', 'colspan', 'rowspan'], + ALLOW_DATA_ATTR: false + }); + + setProcessedContent({ + html: sanitizedHtml, + text: parsed.text, + isHtml: true + }); + } else { + // Format plain text with line breaks + const formattedText = parsed.text.replace(/\n/g, '
'); + setProcessedContent({ + html: formattedText, + text: parsed.text, + isHtml: false + }); + } + } else { + // Treat as direct content (not raw email) + const isHtmlContent = content.includes('') || + content.includes(''); + setProcessedContent({ + html: formattedText, + text: content, + isHtml: false + }); + } + } + } catch (err) { + console.error('Error processing email content:', err); + // Fallback to plain text + setProcessedContent({ + html: content.replace(/\n/g, '
'), + text: content, + isHtml: false + }); + } + }, [content, type]); + + // Process quoted content visibility and fix table styling + useEffect(() => { + if (!containerRef.current || !processedContent.html) return; + + const container = containerRef.current; + + // Handle quoted text visibility + if (!showQuotedText) { + // Add toggle buttons for quoted text sections + const quotedSections = container.querySelectorAll('blockquote'); + + quotedSections.forEach((quote, index) => { + // Check if this quoted section already has a toggle + if (quote.previousElementSibling?.classList.contains('quoted-toggle-btn')) { + return; + } + + // Create toggle button + const toggleBtn = document.createElement('button'); + toggleBtn.innerText = '▼ Show quoted text'; + toggleBtn.className = 'quoted-toggle-btn'; + toggleBtn.style.cssText = 'background: none; border: none; color: #666; font-size: 12px; cursor: pointer; padding: 4px 0; display: block;'; + + // Hide quoted section initially + quote.style.display = 'none'; + + // Add click handler + toggleBtn.addEventListener('click', () => { + const isHidden = quote.style.display === 'none'; + quote.style.display = isHidden ? 'block' : 'none'; + toggleBtn.innerText = isHidden ? '▲ Hide quoted text' : '▼ Show quoted text'; + }); + + // Insert before the blockquote + quote.parentNode?.insertBefore(toggleBtn, quote); + }); + } + + // Process tables and ensure they're properly formatted + const tables = container.querySelectorAll('table'); + tables.forEach(table => { + // Cast to HTMLTableElement to access style property + const tableElement = table as HTMLTableElement; + + // Only apply styling if the table doesn't already have border styles + if (!tableElement.hasAttribute('border') && + (!tableElement.style.border || tableElement.style.border === '')) { + // Apply proper table styling + tableElement.style.width = '100%'; + tableElement.style.borderCollapse = 'collapse'; + tableElement.style.margin = '10px 0'; + tableElement.style.border = '1px solid #ddd'; + } + + const cells = table.querySelectorAll('td, th'); + cells.forEach(cell => { + // Cast to HTMLTableCellElement to access style property + const cellElement = cell as HTMLTableCellElement; + + // Only apply styling if the cell doesn't already have border styles + if (!cellElement.style.border || cellElement.style.border === '') { + cellElement.style.border = '1px solid #ddd'; + cellElement.style.padding = '6px'; + } + }); + }); + }, [processedContent.html, showQuotedText]); + + return ( +
+ ); +}; + +export default EmailContentDisplay; \ No newline at end of file diff --git a/components/email/QuotedEmailContent.tsx b/components/email/QuotedEmailContent.tsx new file mode 100644 index 00000000..87f49820 --- /dev/null +++ b/components/email/QuotedEmailContent.tsx @@ -0,0 +1,144 @@ +'use client'; + +import React from 'react'; +import EmailContentDisplay from './EmailContentDisplay'; + +interface QuotedEmailContentProps { + content: string; + sender: { + name?: string; + email: string; + }; + date: Date | string; + type: 'reply' | 'forward'; + className?: string; +} + +/** + * Component for displaying properly formatted quoted email content in replies and forwards + */ +const QuotedEmailContent: React.FC = ({ + content, + sender, + date, + type, + className = '' +}) => { + // Format the date + const formatDate = (date: Date | string) => { + if (!date) return ''; + + const dateObj = typeof date === 'string' ? new Date(date) : date; + + try { + return dateObj.toLocaleString('en-US', { + weekday: 'short', + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + } catch (e) { + return typeof date === 'string' ? date : date.toString(); + } + }; + + // Format sender info + const senderName = sender.name || sender.email; + const formattedDate = formatDate(date); + + // Create header based on type + const renderQuoteHeader = () => { + if (type === 'reply') { + return ( +
+ On {formattedDate}, {senderName} wrote: +
+ ); + } else { + return ( +
+
---------- Forwarded message ---------
+
From: {senderName} <{sender.email}>
+
Date: {formattedDate}
+
Subject: {/* Subject would be passed as a prop if needed */}
+
To: {/* Recipients would be passed as a prop if needed */}
+
+ ); + } + }; + + return ( +
+ {renderQuoteHeader()} +
+ +
+ + +
+ ); +}; + +export default QuotedEmailContent; \ No newline at end of file diff --git a/components/email/RichEmailEditor.tsx b/components/email/RichEmailEditor.tsx index d83566de..0118d26f 100644 --- a/components/email/RichEmailEditor.tsx +++ b/components/email/RichEmailEditor.tsx @@ -10,6 +10,7 @@ interface RichEmailEditorProps { placeholder?: string; minHeight?: string; maxHeight?: string; + preserveFormatting?: boolean; } const RichEmailEditor: React.FC = ({ @@ -18,6 +19,7 @@ const RichEmailEditor: React.FC = ({ placeholder = 'Write your message here...', minHeight = '200px', maxHeight = 'calc(100vh - 400px)', + preserveFormatting = false, }) => { const editorRef = useRef(null); const toolbarRef = useRef(null); @@ -32,6 +34,21 @@ const RichEmailEditor: React.FC = ({ const Quill = (await import('quill')).default; + // Import quill-better-table + try { + const QuillBetterTable = await import('quill-better-table'); + + // Register the table module if available + if (QuillBetterTable && QuillBetterTable.default) { + Quill.register({ + 'modules/better-table': QuillBetterTable.default + }, true); + console.log('Better Table module registered successfully'); + } + } catch (err) { + console.warn('Table module not available:', err); + } + // Define custom formats/modules with table support const emailToolbarOptions = [ ['bold', 'italic', 'underline', 'strike'], @@ -53,6 +70,15 @@ const RichEmailEditor: React.FC = ({ // Add any custom toolbar handlers here } }, + 'better-table': preserveFormatting ? { + operationMenu: { + items: { + unmergeCells: { + text: 'Unmerge cells' + } + } + } + } : false, }, placeholder: placeholder, theme: 'snow', @@ -64,14 +90,29 @@ const RichEmailEditor: React.FC = ({ // First, ensure we preserve the raw HTML structure const preservedContent = sanitizeHtml(initialContent); - // Use root's innerHTML for complete reset to avoid Quill's automatic formatting - quillRef.current.root.innerHTML = ''; - - // Now use clipboard API to insert the content with proper Quill delta conversion + // Set editor content with paste method which preserves most formatting quillRef.current.clipboard.dangerouslyPasteHTML(0, preservedContent); - // Force update to ensure content is rendered - quillRef.current.update(); + // If we're specifically trying to preserve complex HTML like tables + if (preserveFormatting) { + // For tables and complex formatting, we may need to manually preserve some elements + // Get all table elements from the original content + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = preservedContent; + + // Force better table rendering in Quill + setTimeout(() => { + // This ensures tables are properly rendered by forcing a refresh + quillRef.current.update(); + + // Additional step: directly set HTML if tables aren't rendering properly + if (tempDiv.querySelectorAll('table').length > 0 && + !quillRef.current.root.querySelectorAll('table').length) { + console.log('Using HTML fallback for tables'); + quillRef.current.root.innerHTML = preservedContent; + } + }, 50); + } } catch (err) { console.error('Error setting initial content:', err); // Fallback method if the above fails diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..9eb25264 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,2 @@ +// Global type declarations +declare module 'quill-better-table'; \ No newline at end of file diff --git a/lib/utils/email-mime-decoder.ts b/lib/utils/email-mime-decoder.ts new file mode 100644 index 00000000..c70900c1 --- /dev/null +++ b/lib/utils/email-mime-decoder.ts @@ -0,0 +1,275 @@ +/** + * Email MIME Decoder + * + * This module provides functions to decode MIME-encoded email content + * for proper display in a frontend application. + */ + +/** + * Decode a MIME encoded string (quoted-printable or base64) + * @param {string} text - The encoded text + * @param {string} encoding - The encoding type ('quoted-printable', 'base64', etc) + * @param {string} charset - The character set (utf-8, iso-8859-1, etc) + * @returns {string} - The decoded text + */ +export function decodeMIME(text: string, encoding?: string, charset = 'utf-8'): string { + if (!text) return ''; + + // Normalize encoding to lowercase + encoding = (encoding || '').toLowerCase(); + charset = (charset || 'utf-8').toLowerCase(); + + try { + // Handle different encoding types + if (encoding === 'quoted-printable') { + return decodeQuotedPrintable(text, charset); + } else if (encoding === 'base64') { + return decodeBase64(text, charset); + } else { + // Plain text or other encoding + return text; + } + } catch (error) { + console.error('Error decoding MIME:', error); + return text; // Return original text if decoding fails + } +} + +/** + * Decode a quoted-printable encoded string + * @param {string} text - The quoted-printable encoded text + * @param {string} charset - The character set + * @returns {string} - The decoded text + */ +export function decodeQuotedPrintable(text: string, charset: string): string { + // Replace soft line breaks (=\r\n or =\n) + let decoded = text.replace(/=(?:\r\n|\n)/g, ''); + + // Replace quoted-printable encoded characters + decoded = decoded.replace(/=([0-9A-F]{2})/gi, (match, p1) => { + return String.fromCharCode(parseInt(p1, 16)); + }); + + // Handle character encoding + if (charset !== 'utf-8' && typeof TextDecoder !== 'undefined') { + try { + const bytes = new Uint8Array(decoded.length); + for (let i = 0; i < decoded.length; i++) { + bytes[i] = decoded.charCodeAt(i); + } + return new TextDecoder(charset).decode(bytes); + } catch (e) { + console.warn('TextDecoder error:', e); + } + } + + return decoded; +} + +/** + * Decode a base64 encoded string + * @param {string} text - The base64 encoded text + * @param {string} charset - The character set + * @returns {string} - The decoded text + */ +export function decodeBase64(text: string, charset: string): string { + // Remove whitespace that might be present in the base64 string + const cleanText = text.replace(/\s/g, ''); + + try { + // Use built-in atob function and TextDecoder for charset handling + const binary = atob(cleanText); + if (charset !== 'utf-8' && typeof TextDecoder !== 'undefined') { + // If TextDecoder is available and the charset is not utf-8 + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return new TextDecoder(charset).decode(bytes); + } + return binary; + } catch (e) { + console.error('Base64 decoding error:', e); + return text; + } +} + +/** + * Parse email headers to extract content type, encoding and charset + * @param {string} headers - The raw email headers + * @returns {Object} - Object containing content type, encoding and charset + */ +export function parseEmailHeaders(headers: string): { + contentType: string; + encoding: string; + charset: string; +} { + const result = { + contentType: 'text/plain', + encoding: 'quoted-printable', + charset: 'utf-8' + }; + + // Extract content type + const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;]+))?/i); + if (contentTypeMatch) { + result.contentType = contentTypeMatch[1].trim().toLowerCase(); + if (contentTypeMatch[2]) { + result.charset = contentTypeMatch[2].trim().replace(/"/g, '').toLowerCase(); + } + } + + // Extract content transfer encoding + const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s]+)/i); + if (encodingMatch) { + result.encoding = encodingMatch[1].trim().toLowerCase(); + } + + return result; +} + +/** + * Decode an email body based on its headers + * @param {string} emailRaw - The raw email content (headers + body) + * @returns {Object} - Object containing decoded text and html parts + */ +export function decodeEmail(emailRaw: string): { + contentType: string; + charset: string; + encoding: string; + decodedBody: string; + headers: string; +} { + // Separate headers and body + const parts = emailRaw.split(/\r?\n\r?\n/); + const headers = parts[0]; + const body = parts.slice(1).join('\n\n'); + + // Parse headers + const { contentType, encoding, charset } = parseEmailHeaders(headers); + + // Decode the body + const decodedBody = decodeMIME(body, encoding, charset); + + return { + contentType, + charset, + encoding, + decodedBody, + headers + }; +} + +interface EmailContent { + text: string; + html: string; + attachments: Array<{ + contentType: string; + content: string; + filename?: string; + }>; +} + +/** + * Process a multipart email to extract text and HTML parts + * @param {string} emailRaw - The raw email content + * @param {string} boundary - The multipart boundary + * @returns {Object} - Object containing text and html parts + */ +export function processMultipartEmail(emailRaw: string, boundary: string): EmailContent { + const result: EmailContent = { + text: '', + html: '', + attachments: [] + }; + + // Split by boundary + const boundaryRegex = new RegExp(`--${boundary}\\r?\\n|--${boundary}--\\r?\\n?`, 'g'); + const parts = emailRaw.split(boundaryRegex).filter(part => part.trim()); + + // Process each part + parts.forEach(part => { + const decoded = decodeEmail(part); + + if (decoded.contentType === 'text/plain') { + result.text = decoded.decodedBody; + } else if (decoded.contentType === 'text/html') { + result.html = decoded.decodedBody; + } else if (decoded.contentType.startsWith('image/') || + decoded.contentType.startsWith('application/')) { + // Extract filename if available + const filenameMatch = decoded.headers.match(/filename=["']?([^"';\r\n]+)/i); + const filename = filenameMatch ? filenameMatch[1] : 'attachment'; + + // Handle attachments + result.attachments.push({ + contentType: decoded.contentType, + content: decoded.decodedBody, + filename + }); + } + }); + + return result; +} + +/** + * Extract boundary from Content-Type header + * @param {string} contentType - The Content-Type header value + * @returns {string|null} - The boundary string or null if not found + */ +export function extractBoundary(contentType: string): string | null { + const boundaryMatch = contentType.match(/boundary=["']?([^"';]+)/i); + return boundaryMatch ? boundaryMatch[1] : null; +} + +/** + * Parse an email from its raw content + * @param {string} rawEmail - The raw email content + * @returns {Object} - The parsed email with text and html parts + */ +export function parseRawEmail(rawEmail: string): EmailContent { + // Default result structure + const result: EmailContent = { + text: '', + html: '', + attachments: [] + }; + + try { + // Split headers and body + const headerBodySplit = rawEmail.split(/\r?\n\r?\n/); + const headers = headerBodySplit[0]; + const body = headerBodySplit.slice(1).join('\n\n'); + + // Check if multipart + const contentTypeHeader = headers.match(/Content-Type:\s*([^\r\n]+)/i); + + if (contentTypeHeader && contentTypeHeader[1].includes('multipart/')) { + // Get boundary + const boundary = extractBoundary(contentTypeHeader[1]); + + if (boundary) { + // Process multipart email + return processMultipartEmail(rawEmail, boundary); + } + } + + // Not multipart, decode as a single part + const { contentType, encoding, charset, decodedBody } = decodeEmail(rawEmail); + + // Set content based on type + if (contentType.includes('text/html')) { + result.html = decodedBody; + } else { + result.text = decodedBody; + } + + return result; + } catch (error) { + console.error('Error parsing raw email:', error); + // Return raw content as text on error + result.text = rawEmail; + return result; + } +} \ No newline at end of file diff --git a/node_modules/.package-lock.json b/node_modules/.package-lock.json index 6be0d58e..96cab6b8 100644 --- a/node_modules/.package-lock.json +++ b/node_modules/.package-lock.json @@ -5622,6 +5622,12 @@ "npm": ">=8.2.3" } }, + "node_modules/quill-better-table": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/quill-better-table/-/quill-better-table-1.2.10.tgz", + "integrity": "sha512-CFwxAQzt4EPCQuynQ65R/FU7Yu//kcDBb/rmBBOsFfO758+q50zvG/PDt4Lenv9DcrSgwnyNkfo4yeA5fzzVYQ==", + "license": "MIT" + }, "node_modules/quill-delta": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", @@ -6832,6 +6838,12 @@ "utf8": "^2.1.1" } }, + "node_modules/vcard-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vcard-parser/-/vcard-parser-1.0.0.tgz", + "integrity": "sha512-rSEjrjBK3of4VimMR5vBjLLcN5ZCSp9yuVzyx5i4Fwx74Yd0s+DnHtSit/wAAtj1a7/T/qQc0ykwXADoD0+fTQ==", + "license": "MIT" + }, "node_modules/vcd-parser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vcd-parser/-/vcd-parser-1.0.1.tgz", diff --git a/package-lock.json b/package-lock.json index 4073f4e5..d6154382 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "nodemailer": "^6.10.1", "pg": "^8.14.1", "quill": "^2.0.3", + "quill-better-table": "^1.2.10", "react": "^18", "react-datepicker": "^8.3.0", "react-day-picker": "8.10.1", @@ -84,6 +85,7 @@ "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.6", "vcard-js": "^1.2.2", + "vcard-parser": "^1.0.0", "vcd-parser": "^1.0.1", "webdav": "^5.8.0" }, @@ -6591,6 +6593,12 @@ "npm": ">=8.2.3" } }, + "node_modules/quill-better-table": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/quill-better-table/-/quill-better-table-1.2.10.tgz", + "integrity": "sha512-CFwxAQzt4EPCQuynQ65R/FU7Yu//kcDBb/rmBBOsFfO758+q50zvG/PDt4Lenv9DcrSgwnyNkfo4yeA5fzzVYQ==", + "license": "MIT" + }, "node_modules/quill-delta": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", @@ -7801,6 +7809,12 @@ "utf8": "^2.1.1" } }, + "node_modules/vcard-parser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vcard-parser/-/vcard-parser-1.0.0.tgz", + "integrity": "sha512-rSEjrjBK3of4VimMR5vBjLLcN5ZCSp9yuVzyx5i4Fwx74Yd0s+DnHtSit/wAAtj1a7/T/qQc0ykwXADoD0+fTQ==", + "license": "MIT" + }, "node_modules/vcd-parser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/vcd-parser/-/vcd-parser-1.0.1.tgz", diff --git a/package.json b/package.json index 3e4e28aa..47048ffd 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "nodemailer": "^6.10.1", "pg": "^8.14.1", "quill": "^2.0.3", + "quill-better-table": "^1.2.10", "react": "^18", "react-datepicker": "^8.3.0", "react-day-picker": "8.10.1", @@ -85,6 +86,7 @@ "tailwindcss-animate": "^1.0.7", "vaul": "^0.9.6", "vcard-js": "^1.2.2", + "vcard-parser": "^1.0.0", "vcd-parser": "^1.0.1", "webdav": "^5.8.0" }, diff --git a/yarn.lock b/yarn.lock index cb3a9d19..6dff5e8a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3031,6 +3031,11 @@ quick-format-unescaped@^4.0.3: resolved "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz" integrity sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg== +quill-better-table@^1.2.10: + version "1.2.10" + resolved "https://registry.npmjs.org/quill-better-table/-/quill-better-table-1.2.10.tgz" + integrity sha512-CFwxAQzt4EPCQuynQ65R/FU7Yu//kcDBb/rmBBOsFfO758+q50zvG/PDt4Lenv9DcrSgwnyNkfo4yeA5fzzVYQ== + quill-delta@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz" @@ -3742,6 +3747,11 @@ vcard-js@^1.2.2: quoted-printable "^1.0.0" utf8 "^2.1.1" +vcard-parser@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/vcard-parser/-/vcard-parser-1.0.0.tgz" + integrity sha512-rSEjrjBK3of4VimMR5vBjLLcN5ZCSp9yuVzyx5i4Fwx74Yd0s+DnHtSit/wAAtj1a7/T/qQc0ykwXADoD0+fTQ== + vcd-parser@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/vcd-parser/-/vcd-parser-1.0.1.tgz"