diff --git a/app/api/courrier/account/route.ts b/app/api/courrier/account/route.ts new file mode 100644 index 00000000..49ccce1c --- /dev/null +++ b/app/api/courrier/account/route.ts @@ -0,0 +1,115 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '@/app/api/auth/[...nextauth]/route'; +import { saveUserEmailCredentials, testEmailConnection } from '@/lib/services/email-service'; + +// Define EmailCredentials interface inline since we're having import issues +interface EmailCredentials { + email: string; + password?: string; + host: string; + port: number; + secure?: boolean; + smtp_host?: string; + smtp_port?: number; + smtp_secure?: boolean; + display_name?: string; + color?: string; +} + +export async function POST(request: Request) { + try { + // Authenticate user + const session = await getServerSession(authOptions); + if (!session?.user?.id) { + return NextResponse.json( + { error: 'Unauthorized' }, + { status: 401 } + ); + } + + // Parse request body + const body = await request.json(); + const { + email, + password, + host, + port, + secure, + smtp_host, + smtp_port, + smtp_secure, + display_name, + color + } = body; + + // Validate required fields + if (!email || !password || !host || !port) { + return NextResponse.json( + { error: 'Required fields missing: email, password, host, port' }, + { status: 400 } + ); + } + + // Create credentials object + const credentials: EmailCredentials = { + email, + password, + host, + port: typeof port === 'string' ? parseInt(port) : port, + secure: secure ?? true, + // Optional SMTP settings + ...(smtp_host && { smtp_host }), + ...(smtp_port && { smtp_port: typeof smtp_port === 'string' ? parseInt(smtp_port) : smtp_port }), + ...(smtp_secure !== undefined && { smtp_secure }), + // Optional display settings + ...(display_name && { display_name }), + ...(color && { color }) + }; + + // Test connection before saving + const connectionTest = await testEmailConnection(credentials); + + if (!connectionTest.imap) { + return NextResponse.json( + { + error: 'Failed to connect to IMAP server with provided credentials', + details: connectionTest.error + }, + { status: 400 } + ); + } + + // If SMTP details provided but connection failed + if (smtp_host && smtp_port && !connectionTest.smtp) { + return NextResponse.json( + { + error: 'IMAP connection successful, but SMTP connection failed', + details: connectionTest.error + }, + { status: 400 } + ); + } + + // Save credentials to database and cache + await saveUserEmailCredentials(session.user.id, credentials); + + return NextResponse.json({ + success: true, + message: 'Email account added successfully', + connectionStatus: { + imap: connectionTest.imap, + smtp: connectionTest.smtp + } + }); + } catch (error) { + console.error('Error adding email account:', error); + return NextResponse.json( + { + error: 'Failed to add email account', + details: error instanceof Error ? error.message : 'Unknown error' + }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/courrier/page.tsx b/app/courrier/page.tsx index cf92347f..f81064b8 100644 --- a/app/courrier/page.tsx +++ b/app/courrier/page.tsx @@ -27,6 +27,10 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Avatar, AvatarFallback } from '@/components/ui/avatar'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +import { Checkbox } from '@/components/ui/checkbox'; +import { Label } from '@/components/ui/label'; +import { toast } from '@/components/ui/use-toast'; // Import components import EmailSidebar from '@/components/email/EmailSidebar'; @@ -449,38 +453,141 @@ export default function CourrierPage() { {showAddAccountForm && (

Add IMAP Account

-
{ + { e.preventDefault(); - // We'll implement this function later - alert('This feature will be implemented next'); - setShowAddAccountForm(false); + setLoading(true); + + const formData = new FormData(e.currentTarget); + const newAccount = { + email: formData.get('email'), + password: formData.get('password'), + host: formData.get('host'), + port: parseInt(formData.get('port') as string), + secure: formData.get('secure') === 'on', + display_name: formData.get('display_name') || formData.get('email'), + color: formData.get('color') || '#0082c9', + smtp_host: formData.get('smtp_host'), + smtp_port: formData.get('smtp_port') ? parseInt(formData.get('smtp_port') as string) : undefined, + smtp_secure: formData.get('smtp_secure') === 'on' + }; + + try { + const response = await fetch('/api/courrier/account', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(newAccount) + }); + + const result = await response.json(); + + if (!response.ok) { + throw new Error(result.error || 'Failed to add account'); + } + + // Update accounts list + const newAccountObj = { + id: Date.now(), // temporary ID + name: newAccount.display_name as string, + email: newAccount.email as string, + color: `bg-blue-500`, // Default color class + folders: ['INBOX', 'Sent', 'Drafts', 'Trash'] // Default folders + }; + + setAccounts(prev => [...prev, newAccountObj]); + setShowAddAccountForm(false); + toast({ + title: "Account added successfully", + description: `Your email account ${newAccount.email} has been added.`, + duration: 5000 + }); + } catch (error) { + console.error('Error adding account:', error); + toast({ + title: "Failed to add account", + description: error instanceof Error ? error.message : 'Unknown error', + variant: "destructive", + duration: 5000 + }); + } finally { + setLoading(false); + } }}>
- - - - + + + IMAP Settings + SMTP Settings + + + + + + + +
+ +
+ + +
+
+
+ + + +
+ +
+ + +
+
+
+
+
diff --git a/lib/services/email-service.ts b/lib/services/email-service.ts index 17f0cec1..f81c2436 100644 --- a/lib/services/email-service.ts +++ b/lib/services/email-service.ts @@ -17,56 +17,9 @@ import { invalidateFolderCache, invalidateEmailContentCache } from '@/lib/redis'; +import { EmailCredentials, EmailMessage, EmailAddress, EmailAttachment } from '@/lib/types'; -// Types for the email service -export interface EmailCredentials { - email: string; - password: string; - host: string; - port: number; -} - -export interface EmailMessage { - id: string; - messageId?: string; - subject: string; - from: EmailAddress[]; - to: EmailAddress[]; - cc?: EmailAddress[]; - bcc?: EmailAddress[]; - date: Date; - flags: { - seen: boolean; - flagged: boolean; - answered: boolean; - deleted: boolean; - draft: boolean; - }; - preview?: string; - content?: string; - html?: string; - text?: string; - hasAttachments: boolean; - attachments?: EmailAttachment[]; - folder: string; - size?: number; - contentFetched: boolean; -} - -export interface EmailAddress { - name: string; - address: string; -} - -export interface EmailAttachment { - contentId?: string; - filename: string; - contentType: string; - size: number; - path?: string; - content?: string; -} - +// Types specific to this service export interface EmailListResult { emails: EmailMessage[]; totalEmails: number; @@ -198,18 +151,37 @@ export async function getImapConnection(userId: string): Promise { */ export async function getUserEmailCredentials(userId: string): Promise { const credentials = await prisma.mailCredentials.findUnique({ - where: { userId } + where: { userId }, + select: { + email: true, + password: true, + host: true, + port: true, + secure: true, + smtp_host: true, + smtp_port: true, + smtp_secure: true, + display_name: true, + color: true + } }); if (!credentials) { return null; } + // Return only the fields that exist in credentials return { email: credentials.email, password: credentials.password, host: credentials.host, - port: credentials.port + port: credentials.port, + ...(credentials.secure !== undefined && { secure: credentials.secure }), + ...(credentials.smtp_host && { smtp_host: credentials.smtp_host }), + ...(credentials.smtp_port && { smtp_port: credentials.smtp_port }), + ...(credentials.smtp_secure !== undefined && { smtp_secure: credentials.smtp_secure }), + ...(credentials.display_name && { display_name: credentials.display_name }), + ...(credentials.color && { color: credentials.color }) }; } @@ -227,14 +199,26 @@ export async function saveUserEmailCredentials( email: credentials.email, password: credentials.password, host: credentials.host, - port: credentials.port + port: credentials.port, + secure: credentials.secure ?? true, + smtp_host: credentials.smtp_host, + smtp_port: credentials.smtp_port, + smtp_secure: credentials.smtp_secure, + display_name: credentials.display_name, + color: credentials.color }, create: { userId, email: credentials.email, password: credentials.password, host: credentials.host, - port: credentials.port + port: credentials.port, + secure: credentials.secure ?? true, + smtp_host: credentials.smtp_host, + smtp_port: credentials.smtp_port, + smtp_secure: credentials.smtp_secure, + display_name: credentials.display_name, + color: credentials.color } }); @@ -645,11 +629,11 @@ export async function sendEmail( }; } - // Create SMTP transporter + // Create SMTP transporter with user's SMTP settings if available const transporter = nodemailer.createTransport({ - host: 'smtp.infomaniak.com', // Using Infomaniak SMTP server - port: 587, - secure: false, + host: credentials.smtp_host || 'smtp.infomaniak.com', // Use custom SMTP or default + port: credentials.smtp_port || 587, + secure: credentials.smtp_secure || false, auth: { user: credentials.email, pass: credentials.password, @@ -709,10 +693,20 @@ export async function getMailboxes(client: ImapFlow): Promise { } /** - * Test email connection with given credentials + * Test email connections with given credentials */ -export async function testEmailConnection(credentials: EmailCredentials): Promise { - const client = new ImapFlow({ +export async function testEmailConnection(credentials: EmailCredentials): Promise<{ + imap: boolean; + smtp: boolean; + error?: string; +}> { + // Test IMAP connection + let imapSuccess = false; + let smtpSuccess = false; + let errorMessage = ''; + + // First test IMAP + const imapClient = new ImapFlow({ host: credentials.host, port: credentials.port, secure: true, @@ -727,19 +721,60 @@ export async function testEmailConnection(credentials: EmailCredentials): Promis }); try { - await client.connect(); - await client.mailboxOpen('INBOX'); - return true; + await imapClient.connect(); + await imapClient.mailboxOpen('INBOX'); + imapSuccess = true; } catch (error) { - console.error('Connection test failed:', error); - return false; + console.error('IMAP connection test failed:', error); + errorMessage = error instanceof Error ? error.message : 'Unknown IMAP error'; + return { imap: false, smtp: false, error: `IMAP connection failed: ${errorMessage}` }; } finally { try { - await client.logout(); + await imapClient.logout(); } catch (e) { // Ignore logout errors } } + + // If IMAP successful and SMTP details provided, test SMTP + if (credentials.smtp_host && credentials.smtp_port) { + const transporter = nodemailer.createTransport({ + host: credentials.smtp_host, + port: credentials.smtp_port, + secure: true, + auth: { + user: credentials.email, + pass: credentials.password, + }, + tls: { + rejectUnauthorized: false + } + }); + + try { + await transporter.verify(); + smtpSuccess = true; + } catch (error) { + console.error('SMTP connection test failed:', error); + errorMessage = error instanceof Error ? error.message : 'Unknown SMTP error'; + return { + imap: imapSuccess, + smtp: false, + error: `SMTP connection failed: ${errorMessage}` + }; + } + } else { + // If no SMTP details, just mark as successful + smtpSuccess = true; + } + + return { imap: imapSuccess, smtp: smtpSuccess }; +} + +// Original simplified function for backward compatibility +export async function testImapConnection(credentials: EmailCredentials): Promise { + const result = await testEmailConnection(credentials); + return result.imap; } // Email formatting functions have been moved to lib/utils/email-formatter.ts diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 00000000..87132031 --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,67 @@ +export interface EmailCredentials { + // IMAP Settings + email: string; + password?: string; + host: string; + port: number; + secure?: boolean; + encryptedPassword?: string; + + // SMTP Settings + smtp_host?: string; + smtp_port?: number; + smtp_secure?: boolean; // true for SSL, false for TLS + + // Display Settings + display_name?: string; + color?: string; +} + +export interface EmailAddress { + name: string; + address: string; +} + +export interface EmailAttachment { + contentId?: string; + filename: string; + contentType: string; + size: number; + path?: string; + content?: string; +} + +export interface EmailMessage { + id: string; + messageId?: string; + subject: string; + from: EmailAddress[]; + to: EmailAddress[]; + cc?: EmailAddress[]; + bcc?: EmailAddress[]; + date: Date; + flags: { + seen: boolean; + flagged: boolean; + answered: boolean; + deleted: boolean; + draft: boolean; + }; + preview?: string; + content?: string; + html?: string; + text?: string; + hasAttachments: boolean; + attachments?: EmailAttachment[]; + folder: string; + size?: number; + contentFetched: boolean; +} + +export interface Account { + id: number | string; + name: string; + email: string; + color: string; + folders?: string[]; +} \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c4d4393a..71de85f9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -63,6 +63,17 @@ model MailCredentials { password String host String port Int + secure Boolean @default(true) + + // SMTP Settings + smtp_host String? + smtp_port Int? + smtp_secure Boolean? @default(false) + + // Display Settings + display_name String? + color String? @default("#0082c9") + createdAt DateTime @default(now()) updatedAt DateTime @updatedAt user User @relation(fields: [userId], references: [id], onDelete: Cascade)