compose mime

This commit is contained in:
alma 2025-04-25 09:25:04 +02:00
parent c70ba386ae
commit 9a762927fc
6 changed files with 457 additions and 343 deletions

View File

@ -1,29 +1,48 @@
import { NextResponse } from 'next/server';
import { parseEmail } from '@/lib/server/email-parser';
import { simpleParser, AddressObject } from 'mailparser';
function getEmailAddress(address: AddressObject | AddressObject[] | undefined): string | null {
if (!address) return null;
if (Array.isArray(address)) {
return address.map(a => a.text).join(', ');
}
return address.text;
}
export async function POST(request: Request) {
try {
const body = await request.json();
console.log('Received request body:', body);
const { email } = body;
const { emailContent } = body;
console.log('Email content type:', typeof emailContent);
console.log('Email content length:', emailContent?.length);
if (!emailContent || typeof emailContent !== 'string') {
console.log('Invalid email content:', { emailContent, type: typeof emailContent });
if (!email || typeof email !== 'string') {
return NextResponse.json(
{ error: 'Invalid email content. Expected a string.', received: { type: typeof emailContent, length: emailContent?.length } },
{ error: 'Invalid email content' },
{ status: 400 }
);
}
const parsed = await parseEmail(emailContent);
return NextResponse.json(parsed);
const parsed = await simpleParser(email);
return NextResponse.json({
subject: parsed.subject || null,
from: getEmailAddress(parsed.from),
to: getEmailAddress(parsed.to),
cc: getEmailAddress(parsed.cc),
bcc: getEmailAddress(parsed.bcc),
date: parsed.date || null,
html: parsed.html || null,
text: parsed.textAsHtml || parsed.text || null,
attachments: parsed.attachments?.map(att => ({
filename: att.filename,
contentType: att.contentType,
size: att.size
})) || [],
headers: parsed.headers || {}
});
} catch (error) {
console.error('Error parsing email:', error);
return NextResponse.json(
{ error: 'Failed to parse email', details: error instanceof Error ? error.message : 'Unknown error' },
{ error: 'Failed to parse email' },
{ status: 500 }
);
}

View File

@ -103,20 +103,30 @@ function splitEmailHeadersAndBody(emailBody: string): { headers: string; body: s
function EmailContent({ email }: { email: Email }) {
const [content, setContent] = useState<React.ReactNode>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let mounted = true;
async function loadContent() {
if (!email) return;
setIsLoading(true);
try {
if (!email.body) {
if (mounted) setContent(null);
if (mounted) {
setContent(<div className="text-gray-500">No content available</div>);
setIsLoading(false);
}
return;
}
const formattedEmail = email.body.trim();
if (!formattedEmail) {
if (mounted) setContent(null);
if (mounted) {
setContent(<div className="text-gray-500">No content available</div>);
setIsLoading(false);
}
return;
}
@ -127,7 +137,7 @@ function EmailContent({ email }: { email: Email }) {
setContent(
<div
className="email-content prose prose-sm max-w-none dark:prose-invert"
dangerouslySetInnerHTML={{ __html: parsedEmail.html }}
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(parsedEmail.html) }}
/>
);
} else if (parsedEmail.text) {
@ -137,15 +147,17 @@ function EmailContent({ email }: { email: Email }) {
</div>
);
} else {
setContent(null);
setContent(<div className="text-gray-500">No content available</div>);
}
setError(null);
setIsLoading(false);
}
} catch (err) {
console.error('Error rendering email content:', err);
if (mounted) {
setError('Error rendering email content. Please try again.');
setContent(null);
setIsLoading(false);
}
}
}
@ -155,13 +167,21 @@ function EmailContent({ email }: { email: Email }) {
return () => {
mounted = false;
};
}, [email.body]);
}, [email?.body]);
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-blue-500"></div>
</div>
);
}
if (error) {
return <div className="text-red-500">{error}</div>;
}
return content;
return content || <div className="text-gray-500">No content available</div>;
}
function renderEmailContent(email: Email) {
@ -310,15 +330,29 @@ function getReplyBody(email: Email, type: 'reply' | 'reply-all' | 'forward' = 'r
function EmailPreview({ email }: { email: Email }) {
const [preview, setPreview] = useState<string>('');
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
let mounted = true;
async function loadPreview() {
if (!email?.body) {
if (mounted) setPreview('No content available');
return;
}
setIsLoading(true);
try {
const decoded = await decodeEmail(email.body);
if (mounted) {
setPreview(decoded.text || cleanHtml(decoded.html || ''));
if (decoded.text) {
setPreview(decoded.text.substring(0, 150) + '...');
} else if (decoded.html) {
const cleanText = decoded.html.replace(/<[^>]*>/g, ' ').trim();
setPreview(cleanText.substring(0, 150) + '...');
} else {
setPreview('No preview available');
}
setError(null);
}
} catch (err) {
@ -327,6 +361,8 @@ function EmailPreview({ email }: { email: Email }) {
setError('Error generating preview');
setPreview('');
}
} finally {
if (mounted) setIsLoading(false);
}
}
@ -335,7 +371,11 @@ function EmailPreview({ email }: { email: Email }) {
return () => {
mounted = false;
};
}, [email.body]);
}, [email?.body]);
if (isLoading) {
return <span className="text-gray-400">Loading preview...</span>;
}
if (error) {
return <span className="text-red-500 text-xs">{error}</span>;
@ -507,8 +547,9 @@ export default function CourrierPage() {
setAvailableFolders(data.folders);
}
// Process emails keeping exact folder names
const processedEmails = (data.emails || []).map((email: any) => ({
// Process emails keeping exact folder names and sort by date
const processedEmails = (data.emails || [])
.map((email: any) => ({
id: Number(email.id),
accountId: 1,
from: email.from || '',
@ -526,19 +567,35 @@ export default function CourrierPage() {
raw: email.body || ''
}));
// Sort emails by date, ensuring most recent first
const sortedEmails = processedEmails.sort((a: Email, b: Email) => {
const dateA = new Date(a.date).getTime();
const dateB = new Date(b.date).getTime();
return dateB - dateA; // Most recent first
});
// Only update unread count if we're in the Inbox folder
if (currentView === 'INBOX') {
const unreadInboxEmails = processedEmails.filter(
const unreadInboxEmails = sortedEmails.filter(
(email: Email) => !email.read && email.folder === 'INBOX'
).length;
setUnreadCount(unreadInboxEmails);
}
if (isLoadMore) {
setEmails(prev => [...prev, ...processedEmails]);
// When loading more, merge with existing emails and re-sort
setEmails(prev => {
const combined = [...prev, ...sortedEmails];
return combined.sort((a: Email, b: Email) => {
const dateA = new Date(a.date).getTime();
const dateB = new Date(b.date).getTime();
return dateB - dateA; // Most recent first
});
});
setPage(prev => prev + 1);
} else {
setEmails(processedEmails);
// For initial load or refresh, just use the sorted emails
setEmails(sortedEmails);
setPage(1);
}
@ -572,8 +629,10 @@ export default function CourrierPage() {
return;
}
try {
// Set the selected email first to show preview immediately
setSelectedEmail(email);
setContentLoading(true);
// Fetch the full email content
const response = await fetch(`/api/mail/${emailId}`);
@ -586,11 +645,12 @@ export default function CourrierPage() {
// Update the email in the list and selected email with full content
setEmails(prevEmails => prevEmails.map(email =>
email.id === emailId
? { ...email, body: fullEmail.body }
? { ...email, body: fullEmail.body || email.body }
: email
));
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body } : prev);
setSelectedEmail(prev => prev ? { ...prev, body: fullEmail.body || prev.body } : prev);
setContentLoading(false);
// Try to mark as read in the background
try {
@ -620,6 +680,10 @@ export default function CourrierPage() {
} catch (error) {
console.error('Error marking email as read:', error);
}
} catch (error) {
console.error('Error fetching email content:', error);
setContentLoading(false);
}
};
// Add these improved handlers
@ -1153,9 +1217,14 @@ export default function CourrierPage() {
);
// Add back the handleReply function
const handleReply = (type: 'reply' | 'reply-all' | 'forward') => {
const handleReply = async (type: 'reply' | 'reply-all' | 'forward') => {
if (!selectedEmail) return;
try {
// Get the decoded content first
const decoded = await decodeEmail(selectedEmail.body);
// Set up the reply details
const getReplyTo = () => {
if (type === 'forward') return '';
return selectedEmail.from;
@ -1174,32 +1243,39 @@ export default function CourrierPage() {
return subject.startsWith('Re:') ? subject : `Re: ${subject}`;
};
// Get the formatted original email content
const originalContent = getReplyBody(selectedEmail, type);
// Create a clean structure with clear separation
const formattedContent = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; border: 1px solid #e5e7eb; border-radius: 4px; margin-bottom: 20px;"></div>
<div class="quoted-content" contenteditable="false" style="color: #6b7280; font-size: 0.875rem;">
${type === 'forward' ? `
<div style="margin-bottom: 10px;">
// Create the appropriate email content based on type
let formattedContent = '';
if (type === 'forward') {
formattedContent = `
<div style="min-height: 100px;">
<br/>
<div style="border-top: 1px solid #e5e7eb; margin-top: 20px; padding-top: 10px; color: #666;">
---------- Forwarded message ---------<br/>
From: ${selectedEmail.from}<br/>
Date: ${new Date(selectedEmail.date).toLocaleString()}<br/>
Subject: ${selectedEmail.subject}<br/>
To: ${selectedEmail.to}<br/>
${selectedEmail.cc ? `Cc: ${selectedEmail.cc}<br/>` : ''}
<br/>
<div style="margin-top: 10px;">
${decoded.html || decoded.text || ''}
</div>
` : `
<div style="margin-bottom: 10px;">
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:
</div>
`}
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb;">
${originalContent}
</blockquote>
</div>
`;
} else {
// For reply and reply-all
formattedContent = `
<div style="min-height: 100px;">
<br/>
<div style="border-left: 2px solid #e5e7eb; margin: 10px 0; padding-left: 10px; color: #666;">
On ${new Date(selectedEmail.date).toLocaleString()}, ${selectedEmail.from} wrote:<br/>
<br/>
${decoded.html || decoded.text || ''}
</div>
</div>
`;
}
// Update the compose form
setComposeTo(getReplyTo());
@ -1207,12 +1283,14 @@ export default function CourrierPage() {
setComposeSubject(getReplySubject());
setComposeBody(formattedContent);
setComposeBcc('');
// Show the compose form and CC field for Reply All
setShowCompose(true);
setShowCc(type === 'reply-all');
setShowBcc(false);
setAttachments([]);
} catch (error) {
console.error('Error preparing reply:', error);
}
};
// Add back the toggleStarred function
@ -1445,10 +1523,7 @@ export default function CourrierPage() {
attachments={attachments}
setAttachments={setAttachments}
handleSend={handleSend}
replyTo={selectedEmail || undefined}
forwardFrom={selectedEmail || undefined}
onSend={(email) => {
// Handle the sent email
console.log('Email sent:', email);
setShowCompose(false);
}}

View File

@ -81,157 +81,145 @@ export default function ComposeEmail({
}: ComposeEmailProps) {
const composeBodyRef = useRef<HTMLDivElement>(null);
const [localContent, setLocalContent] = useState('');
const [isInitialized, setIsInitialized] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (composeBodyRef.current && !isInitialized) {
let content = '';
if (replyTo || forwardFrom) {
const originalContent = replyTo?.body || forwardFrom?.body || '';
const initializeContent = async () => {
if (!composeBodyRef.current) return;
fetch('/api/parse-email', {
try {
const emailToProcess = replyTo || forwardFrom;
if (!emailToProcess?.body) {
console.error('No email body found to process');
return;
}
// Set initial loading state
composeBodyRef.current.innerHTML = `
<div class="compose-area" contenteditable="true">
<br/>
<div class="text-gray-500">Loading original message...</div>
</div>
`;
// Parse the original email using the API
const response = await fetch('/api/parse-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailContent: originalContent }),
})
.then(response => response.json())
.then(parsed => {
content = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;">
<br/><br/><br/>
${forwardFrom ? `
body: JSON.stringify({ email: emailToProcess.body }),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to parse email');
}
const emailContent = data.html || data.text || '';
// Format the reply/forward content
const quotedContent = forwardFrom ? `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
---------- Forwarded message ---------<br/>
From: ${forwardFrom.from}<br/>
Date: ${new Date(forwardFrom.date).toLocaleString()}<br/>
Subject: ${forwardFrom.subject}<br/>
To: ${forwardFrom.to}<br/>
${forwardFrom.cc ? `Cc: ${forwardFrom.cc}<br/>` : ''}
<br/>
${parsed.html || parsed.text}
From: ${emailToProcess.from}<br/>
Date: ${new Date(emailToProcess.date).toLocaleString()}<br/>
Subject: ${emailToProcess.subject}<br/>
To: ${emailToProcess.to}<br/>
${emailToProcess.cc ? `Cc: ${emailToProcess.cc}<br/>` : ''}
</div>
<div style="margin-top: 10px; color: #374151;">
${emailContent}
</div>
` : `
<div style="border-top: 1px solid #e5e7eb; padding-top: 20px; margin-top: 20px; color: #6b7280; font-size: 0.875rem;">
On ${new Date(replyTo?.date || '').toLocaleString()}, ${replyTo?.from} wrote:
On ${new Date(emailToProcess.date).toLocaleString()}, ${emailToProcess.from} wrote:
</div>
<blockquote style="margin: 0; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #6b7280;">
${parsed.html || parsed.text}
<blockquote style="margin: 10px 0 0 10px; padding-left: 1em; border-left: 2px solid #e5e7eb; color: #374151;">
${emailContent}
</blockquote>
`}
`;
// Set the content in the compose area with proper structure
const formattedContent = `
<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px;">
<div style="min-height: 20px;"><br/></div>
${quotedContent}
</div>
`;
if (composeBodyRef.current) {
composeBodyRef.current.innerHTML = content;
setIsInitialized(true);
composeBodyRef.current.innerHTML = formattedContent;
// Place cursor at the beginning of the compose area
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (composeArea) {
// Place cursor at the beginning before the quoted content
const selection = window.getSelection();
const range = document.createRange();
const sel = window.getSelection();
range.setStart(composeArea, 0);
const firstDiv = composeBodyRef.current.querySelector('div[style*="min-height: 20px;"]');
if (firstDiv) {
range.setStart(firstDiv, 0);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
(composeArea as HTMLElement).focus();
selection?.removeAllRanges();
selection?.addRange(range);
(firstDiv as HTMLElement).focus();
}
}
})
.catch(error => {
console.error('Error parsing email:', error);
});
} else {
content = `<div class="compose-area" contenteditable="true" style="min-height: 100px; padding: 10px; color: #000000;"></div>`;
composeBodyRef.current.innerHTML = content;
setIsInitialized(true);
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (composeArea) {
const range = document.createRange();
const sel = window.getSelection();
range.setStart(composeArea, 0);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
(composeArea as HTMLElement).focus();
// Update compose state
setComposeBody(formattedContent);
setLocalContent(formattedContent);
}
} catch (error) {
console.error('Error initializing compose content:', error);
if (composeBodyRef.current) {
const errorContent = `
<div class="compose-area" contenteditable="true">
<br/>
<div style="color: #ef4444;">Error loading original message.</div>
</div>
`;
composeBodyRef.current.innerHTML = errorContent;
setComposeBody(errorContent);
setLocalContent(errorContent);
}
}
}
}, [composeBody, replyTo, forwardFrom, isInitialized]);
};
initializeContent();
}
}, [replyTo, forwardFrom]);
// Modified input handler to work with the single contentEditable area
const handleInput = (e: React.FormEvent<HTMLDivElement>) => {
if (!composeBodyRef.current) return;
// Get the compose area content
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) return;
const content = composeArea.innerHTML;
const content = composeBodyRef.current.innerHTML;
if (!content.trim()) {
console.warn('Email content is empty');
return;
setLocalContent('');
setComposeBody('');
} else {
setLocalContent(content);
setComposeBody(content);
}
// Create MIME headers
const mimeHeaders = {
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset="utf-8"',
'Content-Transfer-Encoding': 'quoted-printable'
};
// Combine headers and content
const mimeContent = Object.entries(mimeHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n' + content;
setComposeBody(mimeContent);
if (onBodyChange) {
onBodyChange(mimeContent);
onBodyChange(content);
}
};
const handleSendEmail = async () => {
// Ensure we have content before sending
if (!composeBodyRef.current) {
console.error('Compose body ref is not available');
return;
}
if (!composeBodyRef.current) return;
const composeArea = composeBodyRef.current.querySelector('.compose-area');
if (!composeArea) {
console.error('Compose area not found');
return;
}
if (!composeArea) return;
// Get the current content
const content = composeArea.innerHTML;
if (!content.trim()) {
console.error('Email content is empty');
return;
}
// Create MIME headers
const mimeHeaders = {
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset="utf-8"',
'Content-Transfer-Encoding': 'quoted-printable'
};
// Combine headers and content
const mimeContent = Object.entries(mimeHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n' + content;
setComposeBody(mimeContent);
try {
const encodedContent = await encodeComposeContent(content);
setComposeBody(encodedContent);
await handleSend();
setShowCompose(false);
} catch (error) {

View File

@ -3,73 +3,58 @@
* Handles basic email content without creating nested structures
*/
export function decodeComposeContent(content: string): string {
if (!content) return '';
// Basic HTML cleaning without creating nested structures
let cleaned = content
// Remove script and style tags
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
// Remove meta tags
.replace(/<meta[^>]*>/gi, '')
// Remove head and title
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
// Remove body tags
.replace(/<body[^>]*>/gi, '')
.replace(/<\/body>/gi, '')
// Remove html tags
.replace(/<html[^>]*>/gi, '')
.replace(/<\/html>/gi, '')
// Handle basic formatting
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<p[^>]*>/gi, '\n')
.replace(/<\/p>/gi, '\n')
// Handle lists
.replace(/<ul[^>]*>/gi, '\n')
.replace(/<\/ul>/gi, '\n')
.replace(/<ol[^>]*>/gi, '\n')
.replace(/<\/ol>/gi, '\n')
.replace(/<li[^>]*>/gi, '• ')
.replace(/<\/li>/gi, '\n')
// Handle basic text formatting
.replace(/<strong[^>]*>/gi, '**')
.replace(/<\/strong>/gi, '**')
.replace(/<b[^>]*>/gi, '**')
.replace(/<\/b>/gi, '**')
.replace(/<em[^>]*>/gi, '*')
.replace(/<\/em>/gi, '*')
.replace(/<i[^>]*>/gi, '*')
.replace(/<\/i>/gi, '*')
// Handle links
.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, '$2 ($1)')
// Handle basic entities
.replace(/&nbsp;/g, ' ')
.replace(/&amp;/g, '&')
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
// Clean up whitespace
.replace(/\s+/g, ' ')
.trim();
// Do NOT wrap in additional divs
return cleaned;
interface ParsedContent {
html: string | null;
text: string | null;
}
export function encodeComposeContent(content: string): string {
if (!content) return '';
export async function decodeComposeContent(content: string): Promise<ParsedContent> {
if (!content.trim()) {
return { html: null, text: null };
}
// Basic HTML encoding without adding structure
const encoded = content
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/\n/g, '<br>');
try {
const response = await fetch('/api/parse-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailContent: content }),
});
return encoded;
if (!response.ok) {
throw new Error('Failed to parse email');
}
const parsed = await response.json();
return {
html: parsed.html || null,
text: parsed.text || null
};
} catch (error) {
console.error('Error parsing email content:', error);
// Fallback to basic content handling
return {
html: content,
text: content
};
}
}
export async function encodeComposeContent(content: string): Promise<string> {
if (!content.trim()) {
throw new Error('Email content is empty');
}
// Create MIME headers
const mimeHeaders = {
'MIME-Version': '1.0',
'Content-Type': 'text/html; charset="utf-8"',
'Content-Transfer-Encoding': 'quoted-printable'
};
// Combine headers and content
return Object.entries(mimeHeaders)
.map(([key, value]) => `${key}: ${value}`)
.join('\n') + '\n\n' + content;
}

View File

@ -22,9 +22,20 @@ export interface ParsedEmail {
export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
try {
// Ensure the email content is properly formatted
const formattedContent = emailContent.trim();
const formattedContent = emailContent?.trim();
if (!formattedContent) {
throw new Error('Email content is empty');
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: 'No content available',
attachments: [],
headers: {}
};
}
const response = await fetch('/api/parse-email', {
@ -32,22 +43,61 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ emailContent: formattedContent }),
body: JSON.stringify({ email: formattedContent }),
});
const data = await response.json();
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to parse email');
console.error('API Error:', data);
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: data.error || 'Failed to parse email',
attachments: [],
headers: {}
};
}
const data = await response.json();
// If we have a successful response but no content
if (!data.html && !data.text) {
return {
...data,
date: data.date ? new Date(data.date) : null
date: data.date ? new Date(data.date) : null,
html: null,
text: 'No content available',
attachments: data.attachments || [],
headers: data.headers || {}
};
}
return {
...data,
date: data.date ? new Date(data.date) : null,
text: data.text || null,
html: data.html || null,
attachments: data.attachments || [],
headers: data.headers || {}
};
} catch (error) {
console.error('Error parsing email:', error);
throw error;
return {
subject: null,
from: null,
to: null,
cc: null,
bcc: null,
date: null,
html: null,
text: 'Error parsing email content',
attachments: [],
headers: {}
};
}
}

View File

@ -1,16 +1,13 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config, { isServer }) => {
// Handle node: protocol imports
if (!isServer) {
config.resolve.fallback = {
...config.resolve.fallback,
net: false,
tls: false,
fs: false,
dns: false,
child_process: false,
http2: false,
module: false,
buffer: require.resolve('buffer/'),
stream: require.resolve('stream-browserify'),
util: require.resolve('util/'),
};
}
return config;