From 450121ff2316956de92d360547f889f255dba05f Mon Sep 17 00:00:00 2001 From: alma Date: Wed, 16 Apr 2025 10:06:27 +0200 Subject: [PATCH] mail page imap connection mime 5 bis rest 16 login page 22 --- app/api/mail/route.ts | 90 +++++++++++++++++++++++++++++++--------- lib/email-parser.ts | 96 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 153 insertions(+), 33 deletions(-) diff --git a/app/api/mail/route.ts b/app/api/mail/route.ts index 807606f..f6fdf50 100644 --- a/app/api/mail/route.ts +++ b/app/api/mail/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from 'next/server'; import Imap from 'imap'; import nodemailer from 'nodemailer'; -import { parseEmailHeaders } from '@/lib/email-parser'; +import { parseEmailHeaders, decodeEmailBody } from '@/lib/email-parser'; import { cookies } from 'next/headers'; interface StoredCredentials { @@ -153,30 +153,73 @@ export async function GET() { const fetchPromise = (imap: Imap, seqno: number): Promise => { return new Promise((resolve, reject) => { - const f = imap.fetch(seqno, { bodies: ['HEADER', 'TEXT'] }); + const f = imap.fetch(seqno, { + bodies: ['HEADER', 'TEXT'], + struct: true, + markSeen: false + }); + f.on('message', (msg: ImapMessage) => { resolve(msg); }); - f.once('error', reject); + + f.once('error', (err: Error) => { + console.error('Fetch error:', err); + reject(err); + }); }); }; const processMessage = (msg: ImapMessage): Promise => { return new Promise((resolve, reject) => { - let body = ''; - msg.body['TEXT'].on('data', (chunk: Buffer) => { - body += chunk.toString('utf8'); + let headerContent = ''; + let bodyContent = ''; + let headersParsed = false; + let bodyParsed = false; + + // Process headers + msg.body['HEADER']?.on('data', (chunk: Buffer) => { + headerContent += chunk.toString('utf8'); }); - msg.body['TEXT'].on('end', () => { - const headers = parseEmailHeaders(body); - resolve({ - uid: msg.attributes.uid, - flags: msg.attributes.flags, - size: msg.attributes.size, - ...headers - }); + + // Process body + msg.body['TEXT']?.on('data', (chunk: Buffer) => { + bodyContent += chunk.toString('utf8'); }); - msg.body['TEXT'].on('error', reject); + + const tryResolve = () => { + if (headersParsed && bodyParsed) { + try { + const headers = parseEmailHeaders(headerContent); + const contentType = headerContent.match(/Content-Type:\s*([^;\r\n]+)/i)?.[1] || 'text/plain'; + const decodedBody = decodeEmailBody(bodyContent, contentType); + + resolve({ + uid: msg.attributes.uid, + flags: msg.attributes.flags, + size: msg.attributes.size, + body: decodedBody, + ...headers + }); + } catch (error) { + console.error('Error processing message:', error); + reject(error); + } + } + }; + + msg.body['HEADER']?.on('end', () => { + headersParsed = true; + tryResolve(); + }); + + msg.body['TEXT']?.on('end', () => { + bodyParsed = true; + tryResolve(); + }); + + msg.body['HEADER']?.on('error', reject); + msg.body['TEXT']?.on('error', reject); }); }; @@ -197,16 +240,23 @@ export async function GET() { return { id: msg.uid.toString(), from: msg.from, - subject: msg.subject, - date: msg.date, + subject: msg.subject || '(No subject)', + date: new Date(msg.date), read: !msg.flags?.includes('\\Unseen'), starred: msg.flags?.includes('\\Flagged') || false, - body: msg.body, - to: msg.to // add this if available + body: msg.body || '', + to: msg.to }; }); - return NextResponse.json(emails); + return NextResponse.json({ + emails, + mailUrl: process.env.NEXTCLOUD_URL ? `${process.env.NEXTCLOUD_URL}/apps/mail/` : null + }, { + headers: { + 'Content-Type': 'application/json' + } + }); } catch (error) { console.error('Error fetching emails:', error); const status = error instanceof Error && error.message.includes('Invalid login') diff --git a/lib/email-parser.ts b/lib/email-parser.ts index 134db95..6a3d0bf 100644 --- a/lib/email-parser.ts +++ b/lib/email-parser.ts @@ -1,23 +1,93 @@ -export function parseEmailHeaders(emailBody: string): { from: string; subject: string; date: string } { +interface EmailHeaders { + from: string; + subject: string; + date: string; + to?: string; +} + +export function parseEmailHeaders(headerContent: string): EmailHeaders { const headers: { [key: string]: string } = {}; - - // Split the email body into lines - const lines = emailBody.split('\r\n'); - - // Parse headers - for (const line of lines) { - if (line.trim() === '') break; // Stop at the first empty line (end of headers) + let currentHeader = ''; + let currentValue = ''; + + // Split the header content into lines + const lines = headerContent.split(/\r?\n/); + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; - const match = line.match(/^([^:]+):\s*(.+)$/); + // If line starts with whitespace, it's a continuation of the previous header + if (/^\s+/.test(line)) { + currentValue += ' ' + line.trim(); + continue; + } + + // If we have a current header being processed, save it + if (currentHeader && currentValue) { + headers[currentHeader.toLowerCase()] = currentValue.trim(); + } + + // Start processing new header + const match = line.match(/^([^:]+):\s*(.*)$/); if (match) { - const [, key, value] = match; - headers[key.toLowerCase()] = value; + currentHeader = match[1]; + currentValue = match[2]; } } - + + // Save the last header + if (currentHeader && currentValue) { + headers[currentHeader.toLowerCase()] = currentValue.trim(); + } + return { from: headers['from'] || '', subject: headers['subject'] || '', - date: headers['date'] || new Date().toISOString() + date: headers['date'] || new Date().toISOString(), + to: headers['to'] }; +} + +export function decodeEmailBody(content: string, contentType: string): string { + try { + // Remove email client-specific markers + content = content.replace(/\r\n/g, '\n') + .replace(/=\n/g, '') + .replace(/=3D/g, '=') + .replace(/=09/g, '\t'); + + // If it's HTML content + if (contentType.includes('text/html')) { + return extractTextFromHtml(content); + } + + return content; + } catch (error) { + console.error('Error decoding email body:', error); + return content; + } +} + +function extractTextFromHtml(html: string): string { + // Remove scripts and style tags + html = html.replace(/]*>[\s\S]*?<\/script>/gi, '') + .replace(/]*>[\s\S]*?<\/style>/gi, ''); + + // Convert
and

to newlines + html = html.replace(/]*>/gi, '\n') + .replace(/]*>/gi, '\n') + .replace(/<\/p>/gi, '\n'); + + // Remove all other HTML tags + html = html.replace(/<[^>]+>/g, ''); + + // Decode HTML entities + html = html.replace(/ /g, ' ') + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"'); + + // Clean up whitespace + return html.replace(/\n\s*\n/g, '\n\n').trim(); } \ No newline at end of file