Neah/lib/utils/email-utils.ts
2025-05-01 12:47:36 +02:00

429 lines
13 KiB
TypeScript

/**
* Unified Email Utilities
*
* This file provides backward compatibility for email utilities.
* New code should import directly from the specialized modules:
* - email-content.ts (content processing)
* - text-direction.ts (direction handling)
* - dom-purify-config.ts (sanitization)
*/
// Import from specialized modules
import { sanitizeHtml } from './dom-purify-config';
import { detectTextDirection, applyTextDirection } from './text-direction';
import {
extractEmailContent,
formatEmailContent,
processHtmlContent,
formatPlainTextToHtml,
isHtmlContent,
extractTextFromHtml
} from './email-content';
import {
EmailMessage,
EmailContent,
EmailAddress,
LegacyEmailMessage
} from '@/types/email';
import { adaptLegacyEmail } from '@/lib/utils/email-adapters';
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
import { format } from 'date-fns';
// Re-export important functions for backward compatibility
export {
sanitizeHtml,
extractEmailContent,
formatEmailContent,
processHtmlContent,
formatPlainTextToHtml,
detectTextDirection,
applyTextDirection
};
/**
* Standard interface for formatted email responses
*/
export interface FormattedEmail {
to: string;
cc?: string;
subject: string;
content: EmailContent;
attachments?: Array<{
filename: string;
contentType: string;
content?: string;
}>;
}
/**
* Format email addresses for display
* Can handle both array of EmailAddress objects or a string
*/
export function formatEmailAddresses(addresses: EmailAddress[] | string | undefined): string {
if (!addresses) return '';
// If already a string, return as is
if (typeof addresses === 'string') {
return addresses;
}
// If array, format each address
if (Array.isArray(addresses) && addresses.length > 0) {
return addresses.map(addr =>
addr.name && addr.name !== addr.address
? `${addr.name} <${addr.address}>`
: addr.address
).join(', ');
}
return '';
}
/**
* Format date for display
*/
export function formatEmailDate(date: Date | string | undefined): string {
if (!date) return '';
try {
const dateObj = typeof date === 'string' ? new Date(date) : date;
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();
}
}
/**
* Normalize email content to our standard format regardless of input format
*/
export function normalizeEmailContent(email: any): EmailMessage {
if (!email) {
throw new Error('Cannot normalize null or undefined email');
}
// First check if this is a MIME format email that needs decoding
if (email.content && isMimeFormat(email.content)) {
try {
console.log('Detected MIME format email, decoding...');
// We need to force cast here due to type incompatibility between EmailMessage and the mime result
const adaptedEmail = adaptMimeEmail(email);
return {
...adaptedEmail,
flags: adaptedEmail.flags || [] // Ensure flags is always an array
} as EmailMessage;
} catch (error) {
console.error('Error decoding MIME email:', error);
// Continue with regular normalization if MIME decoding fails
}
}
// Check if it's already in the standardized format
if (email.content && typeof email.content === 'object' &&
(email.content.html !== undefined || email.content.text !== undefined)) {
// Already in the correct format
return email as EmailMessage;
}
// Otherwise, adapt from legacy format
// We need to force cast here due to type incompatibility
const adaptedEmail = adaptLegacyEmail(email);
return {
...adaptedEmail,
flags: adaptedEmail.flags || [] // Ensure flags is always an array
} as EmailMessage;
}
/**
* Render normalized email content into HTML for display
*/
export function renderEmailContent(content: EmailContent | null): string {
if (!content) {
return '<div class="email-content-empty">No content available</div>';
}
// Create a simple object that can be processed by formatEmailContent
const emailObj = { content };
// Use the centralized formatting function
return formatEmailContent(emailObj);
}
/**
* Get recipient addresses from an email for reply or forward
*/
function getRecipientAddresses(email: any, type: 'reply' | 'reply-all'): { to: string; cc: string } {
// Format the recipients
const to = Array.isArray(email.from)
? email.from.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean).join(', ')
: typeof email.from === 'string'
? email.from
: '';
// For reply-all, include other recipients in CC
let cc = '';
if (type === 'reply-all') {
const toRecipients = Array.isArray(email.to)
? email.to.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean)
: typeof email.to === 'string'
? [email.to]
: [];
const ccRecipients = Array.isArray(email.cc)
? email.cc.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.address ? addr.address : '';
}).filter(Boolean)
: typeof email.cc === 'string'
? [email.cc]
: [];
cc = [...toRecipients, ...ccRecipients].join(', ');
}
return { to, cc };
}
/**
* Get formatted header information for reply or forward
*/
function getFormattedHeaderInfo(email: any): {
fromStr: string;
toStr: string;
ccStr: string;
dateStr: string;
subject: string;
} {
// Format the subject
const subject = email.subject && !email.subject.startsWith('Re:') && !email.subject.startsWith('Fwd:')
? email.subject
: email.subject || '';
// Format the date
const dateStr = email.date ? new Date(email.date).toLocaleString() : 'Unknown Date';
// Format sender
const fromStr = Array.isArray(email.from)
? email.from.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.from === 'string'
? email.from
: 'Unknown Sender';
// Format recipients
const toStr = Array.isArray(email.to)
? email.to.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.to === 'string'
? email.to
: '';
// Format CC
const ccStr = Array.isArray(email.cc)
? email.cc.map((addr: any) => {
if (typeof addr === 'string') return addr;
return addr.name ? `${addr.name} <${addr.address}>` : addr.address;
}).join(', ')
: typeof email.cc === 'string'
? email.cc
: '';
return { fromStr, toStr, ccStr, dateStr, subject };
}
/**
* Format email for reply
*/
export function formatReplyEmail(originalEmail: EmailMessage | LegacyEmailMessage | null, type: 'reply' | 'reply-all' = 'reply'): FormattedEmail {
if (!originalEmail) {
return {
to: '',
cc: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Extract recipient addresses
const { to, cc } = getRecipientAddresses(originalEmail, type);
// Get header information
const { fromStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
// Create a clearer reply header with separator line
const replyHeader = `
<div style="margin: 20px 0 10px 0; color: #666; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
On ${dateStr}, ${fromStr} wrote:
</div>
`;
// Use the original HTML content if available, otherwise format the text
const contentHtml = html || (text ? `<p>${text.replace(/\n/g, '</p><p>')}</p>` : '<p>No content available</p>');
// Wrap the original content in proper styling without losing the HTML structure
const cleanHtml = `
${replyHeader}
<blockquote style="margin: 0; padding-left: 10px; border-left: 3px solid #ddd; color: #505050; background-color: #f9f9f9; padding: 10px;">
${contentHtml}
</blockquote>
`;
// Plain text version
const plainText = `
On ${dateStr}, ${fromStr} wrote:
-------------------------------------------------------------------
${text.split('\n').join('\n> ')}
`;
return {
to,
cc,
subject: subject.startsWith('Re:') ? subject : `Re: ${subject}`,
content: {
text: plainText.trim(),
html: cleanHtml,
isHtml: true,
direction: 'ltr'
}
};
}
/**
* Format email for forwarding
*/
export function formatForwardedEmail(originalEmail: EmailMessage | LegacyEmailMessage | null): FormattedEmail {
if (!originalEmail) {
return {
to: '',
subject: '',
content: {
text: '',
html: '',
isHtml: false,
direction: 'ltr' as const
}
};
}
// Get header information
const { fromStr, toStr, ccStr, dateStr, subject } = getFormattedHeaderInfo(originalEmail);
// Extract content using the centralized extraction function
const { text, html } = extractEmailContent(originalEmail);
// Use the original HTML content if available, otherwise format the text
const contentHtml = html || (text ? `<p>${text.replace(/\n/g, '</p><p>')}</p>` : '<p>No content available</p>');
// Create a traditional forward format with dashed separator
// Wrap everything in a single containing element to prevent structure loss
const cleanHtml = `
<div class="forwarded-email-container">
<div class="forwarded-email-header" style="margin: 20px 0 10px 0; color: #666; font-family: Arial, sans-serif;">
<div style="border-bottom: 1px solid #ccc; margin-bottom: 10px; padding-bottom: 5px;">
<div>---------------------------- Forwarded Message ----------------------------</div>
</div>
<table style="margin-bottom: 10px; font-size: 14px; width: 100%;">
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">From:</td>
<td style="padding: 3px 0;">${fromStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">Date:</td>
<td style="padding: 3px 0;">${dateStr}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">Subject:</td>
<td style="padding: 3px 0;">${subject || ''}</td>
</tr>
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">To:</td>
<td style="padding: 3px 0;">${toStr}</td>
</tr>
${ccStr ? `
<tr>
<td style="padding: 3px 10px 3px 0; font-weight: bold; text-align: right; vertical-align: top; white-space: nowrap;">Cc:</td>
<td style="padding: 3px 0;">${ccStr}</td>
</tr>` : ''}
</table>
<div style="border-bottom: 1px solid #ccc; margin-top: 5px; margin-bottom: 15px; padding-bottom: 5px;">
<div>----------------------------------------------------------------------</div>
</div>
</div>
<div class="forwarded-email-content">${contentHtml}</div>
</div>
`;
// Plain text version - with clearer formatting
const plainText = `
---------------------------- Forwarded Message ----------------------------
From: ${fromStr}
Date: ${dateStr}
Subject: ${subject || ''}
To: ${toStr}
${ccStr ? `Cc: ${ccStr}` : ''}
----------------------------------------------------------------------
${text}
`;
// Check if original has attachments
const attachments = originalEmail.attachments || [];
return {
to: '',
subject: subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`,
content: {
text: plainText.trim(),
html: cleanHtml,
isHtml: true,
direction: 'ltr'
},
// Only include attachments if they exist
attachments: attachments.length > 0 ? attachments.map(att => ({
filename: att.filename || 'attachment',
contentType: att.contentType || 'application/octet-stream',
content: att.content
})) : undefined
};
}
/**
* Format an email for reply or reply-all - canonical implementation
*/
export function formatEmailForReplyOrForward(
email: EmailMessage | LegacyEmailMessage | null,
type: 'reply' | 'reply-all' | 'forward'
): FormattedEmail {
// Use our dedicated formatters
if (type === 'forward') {
return formatForwardedEmail(email);
} else {
return formatReplyEmail(email, type as 'reply' | 'reply-all');
}
}