courrier preview
This commit is contained in:
parent
b71336e749
commit
b5411945b3
278
lib/utils/email-adapters.ts
Normal file
278
lib/utils/email-adapters.ts
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
import { EmailMessage, EmailContent, EmailAddress, LegacyEmailMessage } from '@/types/email';
|
||||||
|
import { sanitizeHtml } from './email-formatter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts a legacy email format to the standardized EmailMessage format
|
||||||
|
*/
|
||||||
|
export function adaptLegacyEmail(email: LegacyEmailMessage): EmailMessage {
|
||||||
|
if (!email) {
|
||||||
|
throw new Error('Cannot adapt null or undefined email');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process content
|
||||||
|
const content: EmailContent = normalizeContent(email);
|
||||||
|
|
||||||
|
// Convert email addresses to string format as required by EmailMessage interface
|
||||||
|
const from = formatAddressesToString(normalizeAddresses(email.from));
|
||||||
|
const to = formatAddressesToString(normalizeAddresses(email.to));
|
||||||
|
const cc = email.cc ? formatAddressesToString(normalizeAddresses(email.cc)) : undefined;
|
||||||
|
const bcc = email.bcc ? formatAddressesToString(normalizeAddresses(email.bcc)) : undefined;
|
||||||
|
|
||||||
|
// Convert flags if needed
|
||||||
|
const flags: string[] = normalizeFlags(email.flags);
|
||||||
|
|
||||||
|
// Create standardized email message
|
||||||
|
return {
|
||||||
|
id: email.id || '',
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
cc,
|
||||||
|
bcc,
|
||||||
|
subject: email.subject || '',
|
||||||
|
content,
|
||||||
|
date: email.date || new Date().toISOString(),
|
||||||
|
flags,
|
||||||
|
attachments: normalizeAttachments(email.attachments),
|
||||||
|
_originalFormat: email // Store original for debugging
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects if an email is in MIME format
|
||||||
|
*/
|
||||||
|
export function isMimeFormat(email: any): boolean {
|
||||||
|
// Simple check for MIME format indicators
|
||||||
|
if (!email) return false;
|
||||||
|
|
||||||
|
// Check for typical MIME format properties
|
||||||
|
return !!(
|
||||||
|
email.mimeContent ||
|
||||||
|
(email.headers && (email.bodyParts || email.body)) ||
|
||||||
|
(typeof email.content === 'string' && email.content.includes('MIME-Version'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapts a MIME format email to the standardized EmailMessage format
|
||||||
|
* This is a placeholder - actual implementation would depend on the MIME library
|
||||||
|
*/
|
||||||
|
export function adaptMimeEmail(mimeEmail: any): EmailMessage {
|
||||||
|
// Placeholder implementation
|
||||||
|
const content: EmailContent = {
|
||||||
|
text: mimeEmail.text || mimeEmail.plainText || '',
|
||||||
|
html: mimeEmail.html || undefined,
|
||||||
|
isHtml: !!mimeEmail.html,
|
||||||
|
direction: 'ltr'
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: mimeEmail.id || '',
|
||||||
|
from: mimeEmail.from || '',
|
||||||
|
to: mimeEmail.to || '',
|
||||||
|
cc: mimeEmail.cc,
|
||||||
|
bcc: mimeEmail.bcc,
|
||||||
|
subject: mimeEmail.subject || '',
|
||||||
|
content,
|
||||||
|
date: mimeEmail.date || new Date().toISOString(),
|
||||||
|
flags: [],
|
||||||
|
_originalFormat: mimeEmail
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats an array of EmailAddress objects to string format
|
||||||
|
*/
|
||||||
|
function formatAddressesToString(addresses: EmailAddress[]): string {
|
||||||
|
return addresses.map(addr => {
|
||||||
|
if (addr.name && addr.name !== addr.address) {
|
||||||
|
return `${addr.name} <${addr.address}>`;
|
||||||
|
}
|
||||||
|
return addr.address;
|
||||||
|
}).join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes content from various formats into the standard EmailContent format
|
||||||
|
*/
|
||||||
|
function normalizeContent(email: LegacyEmailMessage): EmailContent {
|
||||||
|
// Default content structure
|
||||||
|
const normalizedContent: EmailContent = {
|
||||||
|
html: undefined,
|
||||||
|
text: '',
|
||||||
|
isHtml: false,
|
||||||
|
direction: 'ltr'
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract content based on standardized property hierarchy
|
||||||
|
let htmlContent = '';
|
||||||
|
let textContent = '';
|
||||||
|
let isHtml = false;
|
||||||
|
|
||||||
|
// Step 1: Extract content from the various possible formats
|
||||||
|
if (email.content && typeof email.content === 'object') {
|
||||||
|
isHtml = !!email.content.html;
|
||||||
|
htmlContent = email.content.html || '';
|
||||||
|
textContent = email.content.text || '';
|
||||||
|
} else if (typeof email.content === 'string') {
|
||||||
|
// Check if the string content is HTML
|
||||||
|
isHtml = email.content.trim().startsWith('<') &&
|
||||||
|
(email.content.includes('<html') ||
|
||||||
|
email.content.includes('<body') ||
|
||||||
|
email.content.includes('<div') ||
|
||||||
|
email.content.includes('<p>'));
|
||||||
|
htmlContent = isHtml ? email.content : '';
|
||||||
|
textContent = isHtml ? '' : email.content;
|
||||||
|
} else if (email.html) {
|
||||||
|
isHtml = true;
|
||||||
|
htmlContent = email.html;
|
||||||
|
textContent = email.text || email.plainText || '';
|
||||||
|
} else if (email.text || email.plainText) {
|
||||||
|
isHtml = false;
|
||||||
|
htmlContent = '';
|
||||||
|
textContent = email.text || email.plainText || '';
|
||||||
|
} else if (email.formattedContent) {
|
||||||
|
// Assume formattedContent is already HTML
|
||||||
|
isHtml = true;
|
||||||
|
htmlContent = email.formattedContent;
|
||||||
|
textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Set the normalized content properties
|
||||||
|
normalizedContent.isHtml = isHtml;
|
||||||
|
|
||||||
|
// Always ensure we have text content
|
||||||
|
if (textContent) {
|
||||||
|
normalizedContent.text = textContent;
|
||||||
|
} else if (htmlContent) {
|
||||||
|
// Extract text from HTML if we don't have plain text
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
// Browser environment
|
||||||
|
const tempDiv = document.createElement('div');
|
||||||
|
tempDiv.innerHTML = htmlContent;
|
||||||
|
normalizedContent.text = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
|
} else {
|
||||||
|
// Server environment - do simple strip
|
||||||
|
normalizedContent.text = htmlContent
|
||||||
|
.replace(/<[^>]*>/g, '')
|
||||||
|
.replace(/ /g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have HTML content, sanitize it
|
||||||
|
if (isHtml && htmlContent) {
|
||||||
|
normalizedContent.html = sanitizeHtml(htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine text direction
|
||||||
|
normalizedContent.direction = detectTextDirection(normalizedContent.text);
|
||||||
|
|
||||||
|
return normalizedContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error normalizing email content:', error);
|
||||||
|
|
||||||
|
// Return minimal valid content in case of error
|
||||||
|
return {
|
||||||
|
text: 'Error loading email content',
|
||||||
|
isHtml: false,
|
||||||
|
direction: 'ltr'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detects the text direction (LTR or RTL) based on the content
|
||||||
|
*/
|
||||||
|
function detectTextDirection(text: string): 'ltr' | 'rtl' {
|
||||||
|
// Simple RTL detection for common RTL languages
|
||||||
|
// This is a basic implementation and can be enhanced
|
||||||
|
const rtlChars = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/;
|
||||||
|
return rtlChars.test(text) ? 'rtl' : 'ltr';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes email addresses to the EmailAddress format
|
||||||
|
*/
|
||||||
|
function normalizeAddresses(addresses: string | EmailAddress[] | undefined): EmailAddress[] {
|
||||||
|
if (!addresses) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(addresses)) {
|
||||||
|
// If already in EmailAddress format, return as is
|
||||||
|
if (addresses.length > 0 && typeof addresses[0] === 'object') {
|
||||||
|
return addresses as EmailAddress[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise convert string elements to EmailAddress objects
|
||||||
|
return addresses.map((addr: any) => {
|
||||||
|
if (typeof addr === 'string') {
|
||||||
|
return {
|
||||||
|
name: addr.split('@')[0] || '',
|
||||||
|
address: addr
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return addr;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle single address as string
|
||||||
|
if (typeof addresses === 'string') {
|
||||||
|
// Check if format is "Name <email@example.com>"
|
||||||
|
const match = addresses.match(/^([^<]+)<([^>]+)>$/);
|
||||||
|
if (match) {
|
||||||
|
return [{
|
||||||
|
name: match[1].trim(),
|
||||||
|
address: match[2].trim()
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [{
|
||||||
|
name: addresses.split('@')[0] || '',
|
||||||
|
address: addresses
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes email flags to string array format
|
||||||
|
*/
|
||||||
|
function normalizeFlags(flags: string[] | Record<string, boolean> | undefined): string[] {
|
||||||
|
if (!flags) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(flags)) {
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert object format to array
|
||||||
|
return Object.entries(flags)
|
||||||
|
.filter(([_, value]) => value === true)
|
||||||
|
.map(([key]) => key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes attachments to the expected format
|
||||||
|
*/
|
||||||
|
function normalizeAttachments(attachments: any[] | undefined): Array<{
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
encoding?: string;
|
||||||
|
content?: string;
|
||||||
|
}> {
|
||||||
|
if (!attachments || !Array.isArray(attachments)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return attachments.map(att => ({
|
||||||
|
filename: att.filename || att.name || 'unknown',
|
||||||
|
contentType: att.contentType || 'application/octet-stream',
|
||||||
|
encoding: att.encoding,
|
||||||
|
content: att.content
|
||||||
|
}));
|
||||||
|
}
|
||||||
@ -1,275 +1,412 @@
|
|||||||
/**
|
/**
|
||||||
* Email MIME Decoder
|
* Infomaniak Email MIME Decoder
|
||||||
*
|
*
|
||||||
* This module provides functions to decode MIME-encoded email content
|
* This module provides specialized functions to decode MIME-encoded email content
|
||||||
* for proper display in a frontend application.
|
* from Infomaniak servers for proper display in a frontend application.
|
||||||
|
* It handles multipart messages, different encodings, and character set conversions.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { LegacyEmailMessage } from '@/types/email';
|
||||||
|
|
||||||
|
export interface DecodedEmail {
|
||||||
|
subject: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
bcc?: string;
|
||||||
|
date: string;
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
attachments?: Array<{
|
||||||
|
filename: string;
|
||||||
|
contentType: string;
|
||||||
|
encoding?: string;
|
||||||
|
content?: string;
|
||||||
|
}>;
|
||||||
|
headers?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailHeaderInfo {
|
||||||
|
contentType: string;
|
||||||
|
encoding: string;
|
||||||
|
charset: string;
|
||||||
|
boundary?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a MIME encoded string (quoted-printable or base64)
|
* Main function to decode Infomaniak Email in MIME format
|
||||||
* @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 {
|
export function decodeInfomaniakEmail(rawEmailContent: string): DecodedEmail {
|
||||||
if (!text) return '';
|
// Check if it's a multipart email
|
||||||
|
const headers = extractHeaders(rawEmailContent);
|
||||||
|
const headerInfo = parseEmailHeaders(headers);
|
||||||
|
|
||||||
// Normalize encoding to lowercase
|
if (headerInfo.contentType.includes('multipart')) {
|
||||||
encoding = (encoding || '').toLowerCase();
|
return processMultipartEmail(rawEmailContent, headerInfo);
|
||||||
charset = (charset || 'utf-8').toLowerCase();
|
} else {
|
||||||
|
return processSinglePartEmail(rawEmailContent, headerInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a single part email
|
||||||
|
*/
|
||||||
|
function processSinglePartEmail(rawEmail: string, headerInfo: EmailHeaderInfo): DecodedEmail {
|
||||||
|
const splitEmail = rawEmail.split('\r\n\r\n');
|
||||||
|
const headers = splitEmail[0];
|
||||||
|
const body = splitEmail.slice(1).join('\r\n\r\n');
|
||||||
|
|
||||||
try {
|
const parsedHeaders = parseHeadersToObject(headers);
|
||||||
// Handle different encoding types
|
const decodedBody = decodeMimeContent(body, headerInfo.encoding);
|
||||||
if (encoding === 'quoted-printable') {
|
const content = convertCharset(decodedBody, headerInfo.charset);
|
||||||
return decodeQuotedPrintable(text, charset);
|
|
||||||
} else if (encoding === 'base64') {
|
const result: DecodedEmail = {
|
||||||
return decodeBase64(text, charset);
|
subject: decodeHeaderValue(parsedHeaders['subject'] || ''),
|
||||||
|
from: decodeHeaderValue(parsedHeaders['from'] || ''),
|
||||||
|
to: decodeHeaderValue(parsedHeaders['to'] || ''),
|
||||||
|
cc: parsedHeaders['cc'] ? decodeHeaderValue(parsedHeaders['cc']) : undefined,
|
||||||
|
bcc: parsedHeaders['bcc'] ? decodeHeaderValue(parsedHeaders['bcc']) : undefined,
|
||||||
|
date: parsedHeaders['date'] || '',
|
||||||
|
headers: parsedHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
if (headerInfo.contentType.includes('text/plain')) {
|
||||||
|
result.text = content;
|
||||||
|
} else if (headerInfo.contentType.includes('text/html')) {
|
||||||
|
result.html = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process a multipart email
|
||||||
|
*/
|
||||||
|
function processMultipartEmail(rawEmail: string, headerInfo: EmailHeaderInfo): DecodedEmail {
|
||||||
|
if (!headerInfo.boundary) {
|
||||||
|
throw new Error('Multipart email missing boundary');
|
||||||
|
}
|
||||||
|
|
||||||
|
const boundary = headerInfo.boundary;
|
||||||
|
const splitEmail = rawEmail.split('\r\n\r\n');
|
||||||
|
const headers = splitEmail[0];
|
||||||
|
const parsedHeaders = parseHeadersToObject(headers);
|
||||||
|
|
||||||
|
const result: DecodedEmail = {
|
||||||
|
subject: decodeHeaderValue(parsedHeaders['subject'] || ''),
|
||||||
|
from: decodeHeaderValue(parsedHeaders['from'] || ''),
|
||||||
|
to: decodeHeaderValue(parsedHeaders['to'] || ''),
|
||||||
|
cc: parsedHeaders['cc'] ? decodeHeaderValue(parsedHeaders['cc']) : undefined,
|
||||||
|
bcc: parsedHeaders['bcc'] ? decodeHeaderValue(parsedHeaders['bcc']) : undefined,
|
||||||
|
date: parsedHeaders['date'] || '',
|
||||||
|
attachments: [],
|
||||||
|
headers: parsedHeaders
|
||||||
|
};
|
||||||
|
|
||||||
|
// Split by boundary
|
||||||
|
const bodyContent = rawEmail.split('--' + boundary);
|
||||||
|
|
||||||
|
// Process each part (skip first as it's headers and last as it's boundary end)
|
||||||
|
for (let i = 1; i < bodyContent.length - 1; i++) {
|
||||||
|
const part = bodyContent[i];
|
||||||
|
const partHeaders = extractHeaders(part);
|
||||||
|
const partHeaderInfo = parseEmailHeaders(partHeaders);
|
||||||
|
|
||||||
|
// Handle sub-multipart (nested multipart)
|
||||||
|
if (partHeaderInfo.contentType.includes('multipart') && partHeaderInfo.boundary) {
|
||||||
|
const subMultipart = processMultipartEmail(part, partHeaderInfo);
|
||||||
|
if (subMultipart.html) result.html = subMultipart.html;
|
||||||
|
if (subMultipart.text) result.text = subMultipart.text;
|
||||||
|
if (subMultipart.attachments) {
|
||||||
|
result.attachments = [...(result.attachments || []), ...subMultipart.attachments];
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get content after headers
|
||||||
|
const partContent = part.split('\r\n\r\n').slice(1).join('\r\n\r\n');
|
||||||
|
const decodedContent = decodeMimeContent(partContent, partHeaderInfo.encoding);
|
||||||
|
const content = convertCharset(decodedContent, partHeaderInfo.charset);
|
||||||
|
|
||||||
|
// Check content disposition
|
||||||
|
const contentDisposition = getHeaderValue(partHeaders, 'Content-Disposition') || '';
|
||||||
|
|
||||||
|
if (contentDisposition.includes('attachment')) {
|
||||||
|
// This is an attachment
|
||||||
|
const filename = extractFilename(contentDisposition);
|
||||||
|
if (result.attachments && filename) {
|
||||||
|
result.attachments.push({
|
||||||
|
filename,
|
||||||
|
contentType: partHeaderInfo.contentType,
|
||||||
|
encoding: partHeaderInfo.encoding,
|
||||||
|
content: decodedContent
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Plain text or other encoding
|
// This is a content part
|
||||||
return text;
|
if (partHeaderInfo.contentType.includes('text/plain')) {
|
||||||
|
result.text = content;
|
||||||
|
} else if (partHeaderInfo.contentType.includes('text/html')) {
|
||||||
|
result.html = content;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
console.error('Error decoding MIME:', error);
|
|
||||||
return text; // Return original text if decoding fails
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a quoted-printable encoded string
|
* Extract headers from an email or part
|
||||||
* @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 {
|
function extractHeaders(content: string): string {
|
||||||
// Replace soft line breaks (=\r\n or =\n)
|
const headerEnd = content.indexOf('\r\n\r\n');
|
||||||
let decoded = text.replace(/=(?:\r\n|\n)/g, '');
|
if (headerEnd === -1) return content;
|
||||||
|
return content.substring(0, headerEnd);
|
||||||
// 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
|
* Parse email headers into an object
|
||||||
* @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 {
|
function parseHeadersToObject(headers: string): Record<string, string> {
|
||||||
// Remove whitespace that might be present in the base64 string
|
const result: Record<string, string> = {};
|
||||||
const cleanText = text.replace(/\s/g, '');
|
const lines = headers.split('\r\n');
|
||||||
|
|
||||||
try {
|
let currentHeader = '';
|
||||||
// Use built-in atob function and TextDecoder for charset handling
|
let currentValue = '';
|
||||||
const binary = atob(cleanText);
|
|
||||||
if (charset !== 'utf-8' && typeof TextDecoder !== 'undefined') {
|
for (const line of lines) {
|
||||||
// If TextDecoder is available and the charset is not utf-8
|
// If line starts with a space or tab, it's a continuation
|
||||||
const bytes = new Uint8Array(binary.length);
|
if (line.startsWith(' ') || line.startsWith('\t')) {
|
||||||
for (let i = 0; i < binary.length; i++) {
|
currentValue += ' ' + line.trim();
|
||||||
bytes[i] = binary.charCodeAt(i);
|
} else {
|
||||||
|
// Save previous header if exists
|
||||||
|
if (currentHeader) {
|
||||||
|
result[currentHeader.toLowerCase()] = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const colonIndex = line.indexOf(':');
|
||||||
|
if (colonIndex !== -1) {
|
||||||
|
currentHeader = line.substring(0, colonIndex).trim();
|
||||||
|
currentValue = line.substring(colonIndex + 1).trim();
|
||||||
}
|
}
|
||||||
return new TextDecoder(charset).decode(bytes);
|
|
||||||
}
|
}
|
||||||
return binary;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Base64 decoding error:', e);
|
|
||||||
return text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the last header
|
||||||
|
if (currentHeader) {
|
||||||
|
result[currentHeader.toLowerCase()] = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse email headers to extract content type, encoding and charset
|
* 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): {
|
function parseEmailHeaders(headers: string): EmailHeaderInfo {
|
||||||
contentType: string;
|
const contentType = getHeaderValue(headers, 'Content-Type') || 'text/plain';
|
||||||
encoding: string;
|
const encoding = getHeaderValue(headers, 'Content-Transfer-Encoding') || '7bit';
|
||||||
charset: string;
|
|
||||||
} {
|
|
||||||
const result = {
|
|
||||||
contentType: 'text/plain',
|
|
||||||
encoding: 'quoted-printable',
|
|
||||||
charset: 'utf-8'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Extract content type
|
// Extract charset
|
||||||
const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;]+))?/i);
|
let charset = 'utf-8';
|
||||||
if (contentTypeMatch) {
|
const charsetMatch = contentType.match(/charset\s*=\s*["']?([^"';\s]+)/i);
|
||||||
result.contentType = contentTypeMatch[1].trim().toLowerCase();
|
if (charsetMatch) {
|
||||||
if (contentTypeMatch[2]) {
|
charset = charsetMatch[1];
|
||||||
result.charset = contentTypeMatch[2].trim().replace(/"/g, '').toLowerCase();
|
}
|
||||||
|
|
||||||
|
// Extract boundary for multipart emails
|
||||||
|
let boundary;
|
||||||
|
const boundaryMatch = contentType.match(/boundary\s*=\s*["']?([^"';\s]+)/i);
|
||||||
|
if (boundaryMatch) {
|
||||||
|
boundary = boundaryMatch[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { contentType, encoding, charset, boundary };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific header value
|
||||||
|
*/
|
||||||
|
function getHeaderValue(headers: string, name: string): string | null {
|
||||||
|
const regex = new RegExp(`${name}:\\s*([^\\r\\n]+)`, 'i');
|
||||||
|
const match = headers.match(regex);
|
||||||
|
return match ? match[1].trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract filename from Content-Disposition header
|
||||||
|
*/
|
||||||
|
function extractFilename(contentDisposition: string): string {
|
||||||
|
const filenameMatch = contentDisposition.match(/filename\s*=\s*["']?([^"';\s]+)/i);
|
||||||
|
if (filenameMatch) return filenameMatch[1];
|
||||||
|
|
||||||
|
// For encoded filenames
|
||||||
|
const encodedFilenameMatch = contentDisposition.match(/filename\*=([^']*)'[^']*'([^;]+)/i);
|
||||||
|
if (encodedFilenameMatch) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(encodedFilenameMatch[2].replace(/%([\dA-F]{2})/g, '%$1'));
|
||||||
|
} catch (e) {
|
||||||
|
return encodedFilenameMatch[2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract content transfer encoding
|
return 'attachment';
|
||||||
const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s]+)/i);
|
}
|
||||||
if (encodingMatch) {
|
|
||||||
result.encoding = encodingMatch[1].trim().toLowerCase();
|
/**
|
||||||
|
* Decode MIME content based on encoding
|
||||||
|
*/
|
||||||
|
function decodeMimeContent(content: string, encoding: string): string {
|
||||||
|
switch (encoding.toLowerCase()) {
|
||||||
|
case 'quoted-printable':
|
||||||
|
return decodeQuotedPrintable(content);
|
||||||
|
case 'base64':
|
||||||
|
return decodeBase64(content);
|
||||||
|
case '7bit':
|
||||||
|
case '8bit':
|
||||||
|
case 'binary':
|
||||||
|
default:
|
||||||
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode an email body based on its headers
|
* Decode quoted-printable content
|
||||||
* @param {string} emailRaw - The raw email content (headers + body)
|
|
||||||
* @returns {Object} - Object containing decoded text and html parts
|
|
||||||
*/
|
*/
|
||||||
export function decodeEmail(emailRaw: string): {
|
function decodeQuotedPrintable(content: string): string {
|
||||||
contentType: string;
|
return content
|
||||||
charset: string;
|
.replace(/=\r\n/g, '') // Remove soft line breaks
|
||||||
encoding: string;
|
.replace(/=([a-fA-F0-9]{2})/g, (match, p1) => { // Replace hex codes with chars
|
||||||
decodedBody: string;
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
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
|
* Decode base64 content
|
||||||
* @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 {
|
function decodeBase64(content: string): string {
|
||||||
const result: EmailContent = {
|
// Remove any whitespace
|
||||||
text: '',
|
const cleanContent = content.replace(/\s+/g, '');
|
||||||
html: '',
|
try {
|
||||||
attachments: []
|
return atob(cleanContent);
|
||||||
};
|
} catch (e) {
|
||||||
|
console.error('Error decoding base64', e);
|
||||||
// Split by boundary
|
return content;
|
||||||
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
|
* Convert content from specified charset to UTF-8
|
||||||
* @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 {
|
function convertCharset(content: string, charset: string): string {
|
||||||
const boundaryMatch = contentType.match(/boundary=["']?([^"';]+)/i);
|
// Basic charset conversion - for more complex cases, consider TextDecoder
|
||||||
return boundaryMatch ? boundaryMatch[1] : null;
|
if (charset.toLowerCase() === 'utf-8' || charset.toLowerCase() === 'utf8') {
|
||||||
}
|
return content;
|
||||||
|
}
|
||||||
/**
|
|
||||||
* 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 {
|
try {
|
||||||
// Split headers and body
|
// For browsers that support TextDecoder
|
||||||
const headerBodySplit = rawEmail.split(/\r?\n\r?\n/);
|
if (typeof TextDecoder !== 'undefined') {
|
||||||
const headers = headerBodySplit[0];
|
// Convert string to ArrayBuffer
|
||||||
const body = headerBodySplit.slice(1).join('\n\n');
|
const buffer = new Uint8Array(content.length);
|
||||||
|
for (let i = 0; i < content.length; i++) {
|
||||||
// Check if multipart
|
buffer[i] = content.charCodeAt(i);
|
||||||
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) {
|
const decoder = new TextDecoder(charset);
|
||||||
// Process multipart email
|
return decoder.decode(buffer);
|
||||||
return processMultipartEmail(rawEmail, boundary);
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('TextDecoder not supported or failed for charset:', charset);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for simpler encodings
|
||||||
|
if (charset.toLowerCase() === 'iso-8859-1' || charset.toLowerCase() === 'latin1') {
|
||||||
|
return content; // Browser will handle this reasonably
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Unsupported charset:', charset);
|
||||||
|
return content; // Return as-is if we can't convert
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode encoded header values (RFC 2047)
|
||||||
|
*/
|
||||||
|
function decodeHeaderValue(value: string): string {
|
||||||
|
// Decode headers like =?UTF-8?Q?Subject?=
|
||||||
|
return value.replace(/=\?([^?]+)\?([BQ])\?([^?]*)\?=/gi, (match, charset, encoding, text) => {
|
||||||
|
if (encoding.toUpperCase() === 'B') {
|
||||||
|
// Base64 encoding
|
||||||
|
try {
|
||||||
|
const decoded = atob(text);
|
||||||
|
return convertCharset(decoded, charset);
|
||||||
|
} catch (e) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
} else if (encoding.toUpperCase() === 'Q') {
|
||||||
|
// Quoted-printable
|
||||||
|
try {
|
||||||
|
const decoded = text
|
||||||
|
.replace(/_/g, ' ')
|
||||||
|
.replace(/=([\da-fA-F]{2})/g, (m: string, hex: string) =>
|
||||||
|
String.fromCharCode(parseInt(hex, 16))
|
||||||
|
);
|
||||||
|
return convertCharset(decoded, charset);
|
||||||
|
} catch (e) {
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean HTML content for safe rendering
|
||||||
|
*/
|
||||||
|
export function cleanHtml(html: string): string {
|
||||||
|
// Basic sanitization - consider using DOMPurify in a real app
|
||||||
|
return html
|
||||||
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
||||||
|
.replace(/on\w+="[^"]*"/g, '')
|
||||||
|
.replace(/on\w+='[^']*'/g, '')
|
||||||
|
.replace(/on\w+=\w+/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if email content is likely in MIME format
|
||||||
|
*/
|
||||||
|
export function isMimeFormat(content: string | undefined): boolean {
|
||||||
|
if (!content) return false;
|
||||||
|
|
||||||
|
// Check for common MIME headers
|
||||||
|
return (
|
||||||
|
content.includes('Content-Type:') &&
|
||||||
|
content.includes('MIME-Version:') &&
|
||||||
|
/\r\n\r\n/.test(content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapt legacy email to use the decoded MIME content
|
||||||
|
*/
|
||||||
|
export function adaptMimeEmail(legacyEmail: LegacyEmailMessage): LegacyEmailMessage {
|
||||||
|
if (!legacyEmail.content || typeof legacyEmail.content !== 'string' || !isMimeFormat(legacyEmail.content)) {
|
||||||
|
return legacyEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const decoded = decodeInfomaniakEmail(legacyEmail.content);
|
||||||
|
|
||||||
// Not multipart, decode as a single part
|
return {
|
||||||
const { contentType, encoding, charset, decodedBody } = decodeEmail(rawEmail);
|
...legacyEmail,
|
||||||
|
html: decoded.html,
|
||||||
// Set content based on type
|
text: decoded.text || '',
|
||||||
if (contentType.includes('text/html')) {
|
subject: decoded.subject || legacyEmail.subject,
|
||||||
result.html = decodedBody;
|
// Keep original content for reference
|
||||||
} else {
|
content: decoded.html || decoded.text || ''
|
||||||
result.text = decodedBody;
|
};
|
||||||
}
|
} catch (e) {
|
||||||
|
console.error('Failed to decode MIME email:', e);
|
||||||
return result;
|
return legacyEmail;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error parsing raw email:', error);
|
|
||||||
// Return raw content as text on error
|
|
||||||
result.text = rawEmail;
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -14,6 +14,8 @@ import {
|
|||||||
EmailContent,
|
EmailContent,
|
||||||
EmailAddress
|
EmailAddress
|
||||||
} from '@/types/email';
|
} from '@/types/email';
|
||||||
|
import { adaptLegacyEmail } from './email-adapters';
|
||||||
|
import { decodeInfomaniakEmail, adaptMimeEmail, isMimeFormat } from './email-mime-decoder';
|
||||||
|
|
||||||
// Reset any existing hooks to start clean
|
// Reset any existing hooks to start clean
|
||||||
DOMPurify.removeAllHooks();
|
DOMPurify.removeAllHooks();
|
||||||
@ -154,92 +156,31 @@ export function formatPlainTextToHtml(text: string): string {
|
|||||||
* Normalize email content to our standard format regardless of input format
|
* Normalize email content to our standard format regardless of input format
|
||||||
* This is the key function that handles all the different email content formats
|
* This is the key function that handles all the different email content formats
|
||||||
*/
|
*/
|
||||||
export function normalizeEmailContent(email: any): EmailContent {
|
export function normalizeEmailContent(email: any): EmailMessage {
|
||||||
// Default content structure
|
if (!email) {
|
||||||
const normalizedContent: EmailContent = {
|
throw new Error('Cannot normalize null or undefined email');
|
||||||
html: undefined,
|
|
||||||
text: '',
|
|
||||||
isHtml: false,
|
|
||||||
direction: 'ltr'
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Extract content based on standardized property hierarchy
|
|
||||||
let htmlContent = '';
|
|
||||||
let textContent = '';
|
|
||||||
let isHtml = false;
|
|
||||||
|
|
||||||
// Step 1: Extract content from the various possible formats
|
|
||||||
if (email.content && typeof email.content === 'object') {
|
|
||||||
isHtml = !!email.content.html;
|
|
||||||
htmlContent = email.content.html || '';
|
|
||||||
textContent = email.content.text || '';
|
|
||||||
} else if (typeof email.content === 'string') {
|
|
||||||
// Check if the string content is HTML
|
|
||||||
isHtml = email.content.trim().startsWith('<') &&
|
|
||||||
(email.content.includes('<html') ||
|
|
||||||
email.content.includes('<body') ||
|
|
||||||
email.content.includes('<div') ||
|
|
||||||
email.content.includes('<p>'));
|
|
||||||
htmlContent = isHtml ? email.content : '';
|
|
||||||
textContent = isHtml ? '' : email.content;
|
|
||||||
} else if (email.html) {
|
|
||||||
isHtml = true;
|
|
||||||
htmlContent = email.html;
|
|
||||||
textContent = email.text || '';
|
|
||||||
} else if (email.text) {
|
|
||||||
isHtml = false;
|
|
||||||
htmlContent = '';
|
|
||||||
textContent = email.text;
|
|
||||||
} else if (email.formattedContent) {
|
|
||||||
// Assume formattedContent is already HTML
|
|
||||||
isHtml = true;
|
|
||||||
htmlContent = email.formattedContent;
|
|
||||||
textContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: Set the normalized content properties
|
|
||||||
normalizedContent.isHtml = isHtml;
|
|
||||||
|
|
||||||
// Always ensure we have text content
|
|
||||||
if (textContent) {
|
|
||||||
normalizedContent.text = textContent;
|
|
||||||
} else if (htmlContent) {
|
|
||||||
// Extract text from HTML if we don't have plain text
|
|
||||||
if (typeof document !== 'undefined') {
|
|
||||||
// Browser environment
|
|
||||||
const tempDiv = document.createElement('div');
|
|
||||||
tempDiv.innerHTML = htmlContent;
|
|
||||||
normalizedContent.text = tempDiv.textContent || tempDiv.innerText || '';
|
|
||||||
} else {
|
|
||||||
// Server environment - do simple strip
|
|
||||||
normalizedContent.text = htmlContent
|
|
||||||
.replace(/<[^>]*>/g, '')
|
|
||||||
.replace(/ /g, ' ')
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have HTML content, sanitize it
|
|
||||||
if (isHtml && htmlContent) {
|
|
||||||
normalizedContent.html = sanitizeHtml(htmlContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine text direction
|
|
||||||
normalizedContent.direction = detectTextDirection(normalizedContent.text);
|
|
||||||
|
|
||||||
return normalizedContent;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error normalizing email content:', error);
|
|
||||||
|
|
||||||
// Return minimal valid content in case of error
|
|
||||||
return {
|
|
||||||
text: 'Error loading email content',
|
|
||||||
isHtml: false,
|
|
||||||
direction: 'ltr'
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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...');
|
||||||
|
return adaptMimeEmail(email);
|
||||||
|
} 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
|
||||||
|
return adaptLegacyEmail(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -20,10 +20,10 @@ export interface EmailAttachment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailContent {
|
export interface EmailContent {
|
||||||
html?: string; // HTML content if available
|
text: string;
|
||||||
text: string; // Plain text (always present)
|
html?: string;
|
||||||
isHtml: boolean; // Flag to indicate primary content type
|
isHtml: boolean;
|
||||||
direction?: 'ltr'|'rtl' // Text direction
|
direction: 'ltr' | 'rtl';
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EmailFlags {
|
export interface EmailFlags {
|
||||||
@ -36,18 +36,43 @@ export interface EmailFlags {
|
|||||||
|
|
||||||
export interface EmailMessage {
|
export interface EmailMessage {
|
||||||
id: string;
|
id: string;
|
||||||
messageId?: string;
|
|
||||||
uid?: number;
|
|
||||||
subject: string;
|
subject: string;
|
||||||
from: EmailAddress[];
|
from: string;
|
||||||
to: EmailAddress[];
|
to: string;
|
||||||
cc?: EmailAddress[];
|
cc?: string;
|
||||||
bcc?: EmailAddress[];
|
bcc?: string;
|
||||||
date: Date | string;
|
date: string;
|
||||||
flags: EmailFlags;
|
flags: string[];
|
||||||
preview?: string;
|
content: EmailContent;
|
||||||
content: EmailContent; // Standardized content structure
|
attachments?: Array<{
|
||||||
attachments: EmailAttachment[];
|
filename: string;
|
||||||
folder?: string;
|
contentType: string;
|
||||||
size?: number;
|
encoding?: string;
|
||||||
|
content?: string;
|
||||||
|
}>;
|
||||||
|
// For debugging and transition
|
||||||
|
_originalFormat?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This represents the legacy email format that might come from API
|
||||||
|
export interface LegacyEmailMessage {
|
||||||
|
id: string;
|
||||||
|
subject: string;
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
cc?: string;
|
||||||
|
bcc?: string;
|
||||||
|
date: string;
|
||||||
|
flags?: string[] | Record<string, boolean>;
|
||||||
|
content?: string | EmailContent;
|
||||||
|
html?: string; // Some APIs provide HTML directly
|
||||||
|
text?: string; // Some APIs provide text directly
|
||||||
|
plainText?: string; // Alternative to text
|
||||||
|
formattedContent?: string; // Legacy property
|
||||||
|
attachments?: Array<{
|
||||||
|
filename?: string;
|
||||||
|
name?: string;
|
||||||
|
contentType?: string;
|
||||||
|
content?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user