courrier multi account

This commit is contained in:
alma 2025-04-27 16:00:26 +02:00
parent cf1509540e
commit b66081643f
5 changed files with 437 additions and 96 deletions

View File

@ -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 }
);
}
}

View File

@ -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 && (
<div className="mb-3 p-2 border border-gray-200 rounded-md bg-gray-50">
<h4 className="text-xs font-medium mb-2 text-gray-700">Add IMAP Account</h4>
<form onSubmit={(e) => {
<form onSubmit={async (e) => {
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);
}
}}>
<div className="space-y-2">
<Input
type="email"
placeholder="Email address"
className="h-8 text-xs"
required
/>
<Input
type="password"
placeholder="Password"
className="h-8 text-xs"
required
/>
<Input
type="text"
placeholder="IMAP server"
className="h-8 text-xs"
required
/>
<Input
type="number"
placeholder="Port (usually 993)"
className="h-8 text-xs"
defaultValue="993"
required
/>
<Tabs defaultValue="imap" className="w-full">
<TabsList className="grid w-full grid-cols-2 h-7">
<TabsTrigger value="imap" className="text-xs">IMAP Settings</TabsTrigger>
<TabsTrigger value="smtp" className="text-xs">SMTP Settings</TabsTrigger>
</TabsList>
<TabsContent value="imap" className="space-y-2 pt-2">
<Input
type="email"
name="email"
placeholder="Email address"
className="h-8 text-xs"
required
/>
<Input
type="password"
name="password"
placeholder="Password"
className="h-8 text-xs"
required
/>
<Input
type="text"
name="display_name"
placeholder="Display name (optional)"
className="h-8 text-xs"
/>
<Input
type="text"
name="host"
placeholder="IMAP server"
className="h-8 text-xs"
required
/>
<div className="flex space-x-2">
<Input
type="number"
name="port"
placeholder="Port"
className="h-8 text-xs w-1/2"
defaultValue="993"
required
/>
<div className="flex items-center space-x-2 w-1/2">
<Checkbox id="secure" name="secure" defaultChecked />
<Label htmlFor="secure" className="text-xs">SSL/TLS</Label>
</div>
</div>
</TabsContent>
<TabsContent value="smtp" className="space-y-2 pt-2">
<Input
type="text"
name="smtp_host"
placeholder="SMTP server (optional)"
className="h-8 text-xs"
/>
<div className="flex space-x-2">
<Input
type="number"
name="smtp_port"
placeholder="Port"
className="h-8 text-xs w-1/2"
defaultValue="587"
/>
<div className="flex items-center space-x-2 w-1/2">
<Checkbox id="smtp_secure" name="smtp_secure" />
<Label htmlFor="smtp_secure" className="text-xs">SSL/TLS</Label>
</div>
</div>
</TabsContent>
</Tabs>
<div className="flex justify-end space-x-2 pt-1">
<Button
type="button"
@ -495,8 +602,14 @@ export default function CourrierPage() {
type="submit"
size="sm"
className="h-7 text-xs"
disabled={loading}
>
Add Account
{loading ? (
<>
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
Adding...
</>
) : "Add Account"}
</Button>
</div>
</div>

View File

@ -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<ImapFlow> {
*/
export async function getUserEmailCredentials(userId: string): Promise<EmailCredentials | null> {
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<string[]> {
}
/**
* Test email connection with given credentials
* Test email connections with given credentials
*/
export async function testEmailConnection(credentials: EmailCredentials): Promise<boolean> {
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<boolean> {
const result = await testEmailConnection(credentials);
return result.imap;
}
// Email formatting functions have been moved to lib/utils/email-formatter.ts

67
lib/types.ts Normal file
View File

@ -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[];
}

View File

@ -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)