courrier multi account restore compose

This commit is contained in:
alma 2025-04-28 18:52:24 +02:00
parent df1570395d
commit fdd91c4b3d
3 changed files with 12 additions and 308 deletions

View File

@ -1,131 +0,0 @@
import { NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/app/api/auth/[...nextauth]/route';
import {
saveUserEmailCredentials,
getUserEmailCredentials,
testEmailConnection
} from '@/lib/services/email-service';
import { prefetchUserEmailData } from '@/lib/services/prefetch-service';
import {
cacheEmailCredentials,
invalidateUserEmailCache,
getCachedEmailCredentials
} from '@/lib/redis';
import { prisma } from '@/lib/prisma';
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 }
);
}
// Get credentials from request
const { email, password, host, port } = await request.json();
// Validate required fields
if (!email || !password || !host || !port) {
return NextResponse.json(
{ error: 'Missing required fields' },
{ status: 400 }
);
}
// Test connection before saving
const connectionSuccess = await testEmailConnection({
email,
password,
host,
port: parseInt(port)
});
if (!connectionSuccess) {
return NextResponse.json(
{ error: 'Failed to connect to email server. Please check your credentials.' },
{ status: 401 }
);
}
// Invalidate all cached data for this user as they are changing their credentials
await invalidateUserEmailCache(session.user.id);
// Create credentials object with required fields
const credentials = {
email,
password,
host,
port: parseInt(port),
secure: true // Default to secure connection
};
// Save credentials in the database and Redis
// Use email as the accountId since it's unique per user
await saveUserEmailCredentials(session.user.id, email, credentials);
// Start prefetching email data in the background
// We don't await this to avoid blocking the response
prefetchUserEmailData(session.user.id).catch(err => {
console.error('Background prefetch error:', err);
});
return NextResponse.json({ success: true });
} catch (error) {
console.error('Error in login handler:', error);
return NextResponse.json(
{ error: 'An unexpected error occurred' },
{ status: 500 }
);
}
}
export async function GET() {
try {
const session = await getServerSession(authOptions);
if (!session?.user?.id) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
);
}
// First try to get from Redis cache
let credentials = await getCachedEmailCredentials(session.user.id, 'default');
// If not in cache, get from database
if (!credentials) {
credentials = await prisma.mailCredentials.findUnique({
where: {
userId: session.user.id
},
select: {
email: true,
host: true,
port: true
}
});
} else {
// Remove password from response
const { password, ...safeCredentials } = credentials;
credentials = safeCredentials;
}
if (!credentials) {
return NextResponse.json(
{ error: 'No stored credentials found' },
{ status: 404 }
);
}
return NextResponse.json(credentials);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to retrieve credentials' },
{ status: 500 }
);
}
}

View File

@ -1,116 +0,0 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
export default function MailLoginPage() {
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [host, setHost] = useState('mail.infomaniak.com');
const [port, setPort] = useState('993');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setLoading(true);
try {
const response = await fetch('/api/courrier/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
email,
password,
host,
port,
}),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to connect to email server');
}
// Redirect to mail page
router.push('/mail');
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-gray-50">
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>Email Configuration</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<Label htmlFor="email">Email</Label>
<Input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="password">Password</Label>
<Input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="host">IMAP Host</Label>
<Input
id="host"
type="text"
value={host}
onChange={(e) => setHost(e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="port">IMAP Port</Label>
<Input
id="port"
type="text"
value={port}
onChange={(e) => setPort(e.target.value)}
required
/>
</div>
{error && (
<div className="text-red-500 text-sm">{error}</div>
)}
<Button
type="submit"
className="w-full"
disabled={loading}
>
{loading ? 'Connecting...' : 'Connect'}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -39,7 +39,7 @@ import EmailList from '@/components/email/EmailList';
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
import EmailDetailView from '@/components/email/EmailDetailView';
import ComposeEmail from '@/components/email/ComposeEmail';
import { DeleteConfirmDialog, LoginNeededAlert } from '@/components/email/EmailDialogs';
import { DeleteConfirmDialog } from '@/components/email/EmailDialogs';
// Import the custom hook
import { useCourrier, EmailData } from '@/hooks/use-courrier';
@ -322,7 +322,6 @@ export default function CourrierPage() {
} else {
console.error('Max retries reached for session request');
// Instead of throwing, redirect to login
router.push('/courrier/login');
return;
}
}
@ -595,9 +594,7 @@ export default function CourrierPage() {
const handleMailboxChange = (folder: string, accountId?: string) => {
if (accountId && accountId !== 'loading-account') {
const account = accounts.find(a => a.id === accountId);
if (!account) {
console.warn(`Account ${accountId} not found`);
toast({
title: "Account not found",
description: `The account ${accountId} could not be found.`,
@ -605,43 +602,18 @@ export default function CourrierPage() {
});
return;
}
// Ensure the account has initialized folders
if (!account.folders || account.folders.length === 0) {
console.warn(`No folders initialized for account ${accountId}`);
// Set default folders if none are initialized
account.folders = ['INBOX', 'SENT', 'DRAFTS', 'TRASH'];
}
// Use the full prefixed folder name for existence check
const fullFolder = folder.includes(':') ? folder : `${accountId}:${folder}`;
if (!account.folders.includes(fullFolder)) {
console.warn(`Folder ${fullFolder} not found in account ${accountId}, defaulting to INBOX`);
// Only allow navigation to folders in selectedAccount.folders
if (!account.folders.includes(folder)) {
toast({
title: "Folder not found",
description: `The folder ${fullFolder} does not exist for this account. Defaulting to INBOX.`,
description: `The folder ${folder} does not exist for this account.`,
variant: "destructive",
});
// Default to INBOX with prefix
setSelectedFolders(prev => ({
...prev,
[accountId]: `${accountId}:INBOX`
}));
changeFolder(`${accountId}:INBOX`, accountId);
return;
}
// Update selected folders state with the full prefixed folder name
setSelectedFolders(prev => ({
...prev,
[accountId]: fullFolder
}));
// Change to the selected folder with account prefix
changeFolder(fullFolder, accountId);
setSelectedFolders(prev => ({ ...prev, [accountId]: folder }));
changeFolder(folder, accountId);
} else {
// For all accounts view, use the original folder name
changeFolder(folder, accountId);
}
};
@ -1046,7 +1018,6 @@ export default function CourrierPage() {
>
<div className={`w-3 h-3 rounded-full ${account.color?.startsWith('#') ? 'bg-blue-500' : account.color || 'bg-blue-500'} mr-2`}></div>
<span className="truncate text-gray-700 flex-1">{account.name}</span>
{/* More options button */}
{account.id !== 'loading-account' && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -1071,16 +1042,12 @@ export default function CourrierPage() {
</button>
)}
</div>
{/* Show folders for this account if expanded */}
{(() => {
const isExpanded = expandedAccounts[account.id];
const hasFolders = account.folders && account.folders.length > 0;
return isExpanded && account.id !== 'loading-account' && hasFolders && (
<div className="pl-4">
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
</div>
);
})()}
{/* Show only selectedAccount.folders for the selected account if expanded */}
{selectedAccount && account.id === selectedAccount.id && expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
<div className="pl-4">
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
</div>
)}
</div>
))}
</div>
@ -1161,16 +1128,6 @@ export default function CourrierPage() {
<AlertTitle>Error</AlertTitle>
<AlertDescription>
{error}
{(error?.includes('Not authenticated') || error?.includes('No email credentials found')) && (
<Button
variant="outline"
size="sm"
className="mt-2"
onClick={handleGoToLogin}
>
Go to login
</Button>
)}
</AlertDescription>
</Alert>
</div>
@ -1260,12 +1217,6 @@ export default function CourrierPage() {
onConfirm={handleDeleteConfirm}
onCancel={() => setShowDeleteConfirm(false)}
/>
<LoginNeededAlert
show={showLoginNeeded}
onLogin={handleGoToLogin}
onClose={() => setShowLoginNeeded(false)}
/>
{/* Compose Email Dialog */}
<Dialog open={showComposeModal} onOpenChange={(open) => !open && setShowComposeModal(false)}>