mime change
This commit is contained in:
parent
1a35e16357
commit
7dcd0c92f8
@ -6,8 +6,11 @@ import nodemailer from 'nodemailer';
|
|||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
try {
|
try {
|
||||||
|
console.log('Starting email send process...');
|
||||||
|
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
|
console.log('No session found');
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Unauthorized' },
|
{ error: 'Unauthorized' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
@ -15,6 +18,7 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get credentials from database
|
// Get credentials from database
|
||||||
|
console.log('Fetching credentials for user:', session.user.id);
|
||||||
const credentials = await prisma.mailCredentials.findUnique({
|
const credentials = await prisma.mailCredentials.findUnique({
|
||||||
where: {
|
where: {
|
||||||
userId: session.user.id
|
userId: session.user.id
|
||||||
@ -22,6 +26,7 @@ export async function POST(request: Request) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!credentials) {
|
if (!credentials) {
|
||||||
|
console.log('No credentials found for user');
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'No mail credentials found. Please configure your email account.' },
|
{ error: 'No mail credentials found. Please configure your email account.' },
|
||||||
{ status: 401 }
|
{ status: 401 }
|
||||||
@ -30,8 +35,10 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
// Get the email data from the request
|
// Get the email data from the request
|
||||||
const { to, cc, bcc, subject, body, attachments } = await request.json();
|
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) {
|
if (!to) {
|
||||||
|
console.log('No recipient specified');
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Recipient is required' },
|
{ error: 'Recipient is required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
@ -39,20 +46,33 @@ export async function POST(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create SMTP transporter with Infomaniak SMTP settings
|
// Create SMTP transporter with Infomaniak SMTP settings
|
||||||
|
console.log('Creating SMTP transporter...');
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: 'smtp.infomaniak.com', // Infomaniak's SMTP server
|
host: 'smtp.infomaniak.com',
|
||||||
port: 587, // Standard SMTP port with STARTTLS
|
port: 587,
|
||||||
secure: false, // Use STARTTLS
|
secure: false,
|
||||||
auth: {
|
auth: {
|
||||||
user: credentials.email,
|
user: credentials.email,
|
||||||
pass: credentials.password,
|
pass: credentials.password,
|
||||||
},
|
},
|
||||||
tls: {
|
tls: {
|
||||||
rejectUnauthorized: false
|
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
|
// Prepare email options
|
||||||
|
console.log('Preparing email options...');
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: credentials.email,
|
from: credentials.email,
|
||||||
to: to,
|
to: to,
|
||||||
@ -68,13 +88,21 @@ export async function POST(request: Request) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Send the email
|
// 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) {
|
} catch (error) {
|
||||||
console.error('Error sending email:', error);
|
console.error('Error sending email:', error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: 'Failed to send email' },
|
{
|
||||||
|
error: 'Failed to send email',
|
||||||
|
details: error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,19 +28,9 @@ import {
|
|||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
import { useSession } from 'next-auth/react';
|
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 DOMPurify from 'isomorphic-dompurify';
|
||||||
import ComposeEmail from '@/components/ComposeEmail';
|
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';
|
import { Attachment as MailParserAttachment } from 'mailparser';
|
||||||
|
|
||||||
export interface Account {
|
export interface Account {
|
||||||
|
|||||||
@ -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(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
|
||||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
|
||||||
.replace(/<meta[^>]*>/gi, '')
|
|
||||||
.replace(/<link[^>]*>/gi, '')
|
|
||||||
.replace(/<base[^>]*>/gi, '')
|
|
||||||
.replace(/<title[^>]*>[\s\S]*?<\/title>/gi, '')
|
|
||||||
.replace(/<head[^>]*>[\s\S]*?<\/head>/gi, '')
|
|
||||||
// Preserve body attributes
|
|
||||||
.replace(/<body[^>]*>/gi, (match) => {
|
|
||||||
const dir = match.match(/dir=["'](rtl|ltr)["']/i)?.[1] || defaultDir;
|
|
||||||
return `<body dir="${dir}">`;
|
|
||||||
})
|
|
||||||
.replace(/<\/body>/gi, '')
|
|
||||||
.replace(/<html[^>]*>/gi, '')
|
|
||||||
.replace(/<\/html>/gi, '')
|
|
||||||
// Handle tables
|
|
||||||
.replace(/<table[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/table>/gi, '\n')
|
|
||||||
.replace(/<tr[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/tr>/gi, '\n')
|
|
||||||
.replace(/<td[^>]*>/gi, ' ')
|
|
||||||
.replace(/<\/td>/gi, ' ')
|
|
||||||
.replace(/<th[^>]*>/gi, ' ')
|
|
||||||
.replace(/<\/th>/gi, ' ')
|
|
||||||
// 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 other block elements
|
|
||||||
.replace(/<div[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/div>/gi, '\n')
|
|
||||||
.replace(/<p[^>]*>/gi, '\n')
|
|
||||||
.replace(/<\/p>/gi, '\n')
|
|
||||||
.replace(/<br[^>]*>/gi, '\n')
|
|
||||||
.replace(/<hr[^>]*>/gi, '\n')
|
|
||||||
// Handle inline elements
|
|
||||||
.replace(/<span[^>]*>/gi, '')
|
|
||||||
.replace(/<\/span>/gi, '')
|
|
||||||
.replace(/<a[^>]*>/gi, '')
|
|
||||||
.replace(/<\/a>/gi, '')
|
|
||||||
.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 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 `<div dir="${defaultDir}">${html}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() : '';
|
|
||||||
}
|
|
||||||
@ -51,7 +51,7 @@ export async function decodeEmail(emailContent: string): Promise<ParsedEmail> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanHtml(html: string): string {
|
export function cleanHtml(html: string): string {
|
||||||
try {
|
try {
|
||||||
return DOMPurify.sanitize(html, {
|
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'],
|
ALLOWED_TAGS: ['p', 'br', 'div', 'span', 'a', 'img', 'strong', 'em', 'u', 'ul', 'ol', 'li', 'blockquote', 'pre', 'code', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user