diff --git a/app/api/courrier/send/route.ts b/app/api/courrier/send/route.ts index aaf94b02..85ee78e1 100644 --- a/app/api/courrier/send/route.ts +++ b/app/api/courrier/send/route.ts @@ -6,8 +6,11 @@ import nodemailer from 'nodemailer'; export async function POST(request: Request) { try { + console.log('Starting email send process...'); + const session = await getServerSession(authOptions); if (!session?.user?.id) { + console.log('No session found'); return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } @@ -15,6 +18,7 @@ export async function POST(request: Request) { } // Get credentials from database + console.log('Fetching credentials for user:', session.user.id); const credentials = await prisma.mailCredentials.findUnique({ where: { userId: session.user.id @@ -22,6 +26,7 @@ export async function POST(request: Request) { }); if (!credentials) { + console.log('No credentials found for user'); return NextResponse.json( { error: 'No mail credentials found. Please configure your email account.' }, { status: 401 } @@ -30,8 +35,10 @@ export async function POST(request: Request) { // Get the email data from the request const { to, cc, bcc, subject, body, attachments } = await request.json(); + console.log('Email data received:', { to, cc, bcc, subject, attachments: attachments?.length || 0 }); if (!to) { + console.log('No recipient specified'); return NextResponse.json( { error: 'Recipient is required' }, { status: 400 } @@ -39,20 +46,33 @@ export async function POST(request: Request) { } // Create SMTP transporter with Infomaniak SMTP settings + console.log('Creating SMTP transporter...'); const transporter = nodemailer.createTransport({ - host: 'smtp.infomaniak.com', // Infomaniak's SMTP server - port: 587, // Standard SMTP port with STARTTLS - secure: false, // Use STARTTLS + host: 'smtp.infomaniak.com', + port: 587, + secure: false, auth: { user: credentials.email, pass: credentials.password, }, tls: { rejectUnauthorized: false - } + }, + debug: true // Enable debug logging }); + // Verify SMTP connection + console.log('Verifying SMTP connection...'); + try { + await transporter.verify(); + console.log('SMTP connection verified successfully'); + } catch (error) { + console.error('SMTP connection verification failed:', error); + throw error; + } + // Prepare email options + console.log('Preparing email options...'); const mailOptions = { from: credentials.email, to: to, @@ -68,13 +88,21 @@ export async function POST(request: Request) { }; // Send the email - await transporter.sendMail(mailOptions); + console.log('Sending email...'); + const info = await transporter.sendMail(mailOptions); + console.log('Email sent successfully:', info.messageId); - return NextResponse.json({ success: true }); + return NextResponse.json({ + success: true, + messageId: info.messageId + }); } catch (error) { console.error('Error sending email:', error); return NextResponse.json( - { error: 'Failed to send email' }, + { + error: 'Failed to send email', + details: error instanceof Error ? error.message : 'Unknown error' + }, { status: 500 } ); } diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index 6e931607..86a24475 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -28,19 +28,9 @@ import { } from 'lucide-react'; import { ScrollArea } from '@/components/ui/scroll-area'; import { useSession } from 'next-auth/react'; -import { - decodeQuotedPrintable, - decodeBase64, - convertCharset, - cleanHtml, - parseEmailHeaders, - extractBoundary, - extractFilename, - extractHeader -} from '@/lib/infomaniak-mime-decoder'; import DOMPurify from 'isomorphic-dompurify'; import ComposeEmail from '@/components/ComposeEmail'; -import { decodeEmail } from '@/lib/mail-parser-wrapper'; +import { decodeEmail, cleanHtml } from '@/lib/mail-parser-wrapper'; import { Attachment as MailParserAttachment } from 'mailparser'; export interface Account { diff --git a/lib/infomaniak-mime-decoder.ts b/lib/infomaniak-mime-decoder.ts deleted file mode 100644 index 86ee9401..00000000 --- a/lib/infomaniak-mime-decoder.ts +++ /dev/null @@ -1,221 +0,0 @@ -// Infomaniak-specific MIME decoder functions - -export function decodeQuotedPrintable(text: string, charset: string): string { - if (!text) return ''; - - // Replace soft line breaks (=\r\n or =\n or =\r) - let decoded = text.replace(/=(?:\r\n|\n|\r)/g, ''); - - // Replace quoted-printable encoded characters - decoded = decoded - // Handle common encoded characters - .replace(/=3D/g, '=') - .replace(/=20/g, ' ') - .replace(/=09/g, '\t') - .replace(/=0A/g, '\n') - .replace(/=0D/g, '\r') - // Handle other quoted-printable encoded characters - .replace(/=([0-9A-F]{2})/gi, (match, p1) => { - return String.fromCharCode(parseInt(p1, 16)); - }); - - // Handle character encoding - try { - if (typeof TextDecoder !== 'undefined') { - const bytes = new Uint8Array(Array.from(decoded).map(c => c.charCodeAt(0))); - return new TextDecoder(charset).decode(bytes); - } - return decoded; - } catch (e) { - console.warn('Charset conversion error:', e); - return decoded; - } -} - -export function decodeBase64(text: string, charset: string): string { - if (!text) return ''; - - try { - // Remove any whitespace and line breaks - const cleanText = text.replace(/\s+/g, ''); - - // Decode base64 - const binary = atob(cleanText); - - // Convert to bytes - const bytes = new Uint8Array(binary.length); - for (let i = 0; i < binary.length; i++) { - bytes[i] = binary.charCodeAt(i); - } - - // Decode using specified charset - if (typeof TextDecoder !== 'undefined') { - return new TextDecoder(charset).decode(bytes); - } - - // Fallback - return binary; - } catch (e) { - console.warn('Base64 decoding error:', e); - return text; - } -} - -export function convertCharset(text: string, charset: string): string { - if (!text) return ''; - - try { - if (typeof TextDecoder !== 'undefined') { - // Handle common charset aliases - const normalizedCharset = charset.toLowerCase() - .replace(/^iso-8859-1$/, 'windows-1252') - .replace(/^iso-8859-15$/, 'windows-1252') - .replace(/^utf-8$/, 'utf-8') - .replace(/^us-ascii$/, 'utf-8'); - - const bytes = new Uint8Array(Array.from(text).map(c => c.charCodeAt(0))); - return new TextDecoder(normalizedCharset).decode(bytes); - } - return text; - } catch (e) { - console.warn('Charset conversion error:', e); - return text; - } -} - -export function cleanHtml(html: string): string { - if (!html) return ''; - - // Detect text direction from the content - const hasRtlChars = /[\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC]/.test(html); - const defaultDir = hasRtlChars ? 'rtl' : 'ltr'; - - // Remove or fix malformed URLs - html = html.replace(/=3D"(http[^"]+)"/g, (match, url) => { - try { - return `"${decodeURIComponent(url)}"`; - } catch { - return ''; - } - }); - - // Remove any remaining quoted-printable artifacts - html = html.replace(/=([0-9A-F]{2})/gi, (match, p1) => { - return String.fromCharCode(parseInt(p1, 16)); - }); - - // Clean up any remaining HTML issues while preserving direction - html = html - // Remove style and script tags - .replace(/]*>[\s\S]*?<\/style>/gi, '') - .replace(/]*>[\s\S]*?<\/script>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>/gi, '') - .replace(/]*>[\s\S]*?<\/title>/gi, '') - .replace(/]*>[\s\S]*?<\/head>/gi, '') - // Preserve body attributes - .replace(/]*>/gi, (match) => { - const dir = match.match(/dir=["'](rtl|ltr)["']/i)?.[1] || defaultDir; - return ``; - }) - .replace(/<\/body>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/html>/gi, '') - // Handle tables - .replace(/]*>/gi, '\n') - .replace(/<\/table>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/tr>/gi, '\n') - .replace(/]*>/gi, ' ') - .replace(/<\/td>/gi, ' ') - .replace(/]*>/gi, ' ') - .replace(/<\/th>/gi, ' ') - // Handle lists - .replace(/]*>/gi, '\n') - .replace(/<\/ul>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/ol>/gi, '\n') - .replace(/]*>/gi, '• ') - .replace(/<\/li>/gi, '\n') - // Handle other block elements - .replace(/]*>/gi, '\n') - .replace(/<\/div>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/<\/p>/gi, '\n') - .replace(/]*>/gi, '\n') - .replace(/]*>/gi, '\n') - // Handle inline elements - .replace(/]*>/gi, '') - .replace(/<\/span>/gi, '') - .replace(/]*>/gi, '') - .replace(/<\/a>/gi, '') - .replace(/]*>/gi, '**') - .replace(/<\/strong>/gi, '**') - .replace(/]*>/gi, '**') - .replace(/<\/b>/gi, '**') - .replace(/]*>/gi, '*') - .replace(/<\/em>/gi, '*') - .replace(/]*>/gi, '*') - .replace(/<\/i>/gi, '*') - // Handle special characters - .replace(/ /g, ' ') - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"') - .replace(/'/g, "'") - // Clean up whitespace - .replace(/\s+/g, ' ') - .trim(); - - // Wrap in a div with the detected direction - return `
${html}
`; -} - -export function parseEmailHeaders(headers: string): { contentType: string; encoding: string; charset: string } { - const result = { - contentType: 'text/plain', - encoding: '7bit', - charset: 'utf-8' - }; - - // Extract content type and charset - const contentTypeMatch = headers.match(/Content-Type:\s*([^;]+)(?:;\s*charset=([^;"\r\n]+)|(?:;\s*charset="([^"]+)"))?/i); - if (contentTypeMatch) { - result.contentType = contentTypeMatch[1].trim().toLowerCase(); - if (contentTypeMatch[2]) { - result.charset = contentTypeMatch[2].trim().toLowerCase(); - } else if (contentTypeMatch[3]) { - result.charset = contentTypeMatch[3].trim().toLowerCase(); - } - } - - // Extract content transfer encoding - const encodingMatch = headers.match(/Content-Transfer-Encoding:\s*([^\s;\r\n]+)/i); - if (encodingMatch) { - result.encoding = encodingMatch[1].trim().toLowerCase(); - } - - return result; -} - -export function extractBoundary(headers: string): string | null { - const boundaryMatch = headers.match(/boundary="?([^"\r\n;]+)"?/i) || - headers.match(/boundary=([^\r\n;]+)/i); - - return boundaryMatch ? boundaryMatch[1].trim() : null; -} - -export function extractFilename(headers: string): string { - const filenameMatch = headers.match(/filename="?([^"\r\n;]+)"?/i) || - headers.match(/name="?([^"\r\n;]+)"?/i); - - return filenameMatch ? filenameMatch[1] : 'attachment'; -} - -export function extractHeader(headers: string, headerName: string): string { - const regex = new RegExp(`^${headerName}:\\s*(.*)$`, 'im'); - const match = headers.match(regex); - return match ? match[1].trim() : ''; -} \ No newline at end of file diff --git a/lib/mail-parser-wrapper.ts b/lib/mail-parser-wrapper.ts index 3ed52013..c400a011 100644 --- a/lib/mail-parser-wrapper.ts +++ b/lib/mail-parser-wrapper.ts @@ -51,7 +51,7 @@ export async function decodeEmail(emailContent: string): Promise { } } -function cleanHtml(html: string): string { +export function cleanHtml(html: string): string { try { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['p', 'br', 'div', 'span', 'a', 'img', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],