courrier multi account restore compose
This commit is contained in:
parent
df1570395d
commit
fdd91c4b3d
@ -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 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -39,7 +39,7 @@ import EmailList from '@/components/email/EmailList';
|
|||||||
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
|
import EmailSidebarContent from '@/components/email/EmailSidebarContent';
|
||||||
import EmailDetailView from '@/components/email/EmailDetailView';
|
import EmailDetailView from '@/components/email/EmailDetailView';
|
||||||
import ComposeEmail from '@/components/email/ComposeEmail';
|
import ComposeEmail from '@/components/email/ComposeEmail';
|
||||||
import { DeleteConfirmDialog, LoginNeededAlert } from '@/components/email/EmailDialogs';
|
import { DeleteConfirmDialog } from '@/components/email/EmailDialogs';
|
||||||
|
|
||||||
// Import the custom hook
|
// Import the custom hook
|
||||||
import { useCourrier, EmailData } from '@/hooks/use-courrier';
|
import { useCourrier, EmailData } from '@/hooks/use-courrier';
|
||||||
@ -322,7 +322,6 @@ export default function CourrierPage() {
|
|||||||
} else {
|
} else {
|
||||||
console.error('Max retries reached for session request');
|
console.error('Max retries reached for session request');
|
||||||
// Instead of throwing, redirect to login
|
// Instead of throwing, redirect to login
|
||||||
router.push('/courrier/login');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -595,9 +594,7 @@ export default function CourrierPage() {
|
|||||||
const handleMailboxChange = (folder: string, accountId?: string) => {
|
const handleMailboxChange = (folder: string, accountId?: string) => {
|
||||||
if (accountId && accountId !== 'loading-account') {
|
if (accountId && accountId !== 'loading-account') {
|
||||||
const account = accounts.find(a => a.id === accountId);
|
const account = accounts.find(a => a.id === accountId);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
console.warn(`Account ${accountId} not found`);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Account not found",
|
title: "Account not found",
|
||||||
description: `The account ${accountId} could not be found.`,
|
description: `The account ${accountId} could not be found.`,
|
||||||
@ -605,43 +602,18 @@ export default function CourrierPage() {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Only allow navigation to folders in selectedAccount.folders
|
||||||
// Ensure the account has initialized folders
|
if (!account.folders.includes(folder)) {
|
||||||
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`);
|
|
||||||
toast({
|
toast({
|
||||||
title: "Folder not found",
|
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",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
// Default to INBOX with prefix
|
|
||||||
setSelectedFolders(prev => ({
|
|
||||||
...prev,
|
|
||||||
[accountId]: `${accountId}:INBOX`
|
|
||||||
}));
|
|
||||||
changeFolder(`${accountId}:INBOX`, accountId);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setSelectedFolders(prev => ({ ...prev, [accountId]: folder }));
|
||||||
// Update selected folders state with the full prefixed folder name
|
changeFolder(folder, accountId);
|
||||||
setSelectedFolders(prev => ({
|
|
||||||
...prev,
|
|
||||||
[accountId]: fullFolder
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Change to the selected folder with account prefix
|
|
||||||
changeFolder(fullFolder, accountId);
|
|
||||||
} else {
|
} else {
|
||||||
// For all accounts view, use the original folder name
|
|
||||||
changeFolder(folder, accountId);
|
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>
|
<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>
|
<span className="truncate text-gray-700 flex-1">{account.name}</span>
|
||||||
{/* More options button */}
|
|
||||||
{account.id !== 'loading-account' && (
|
{account.id !== 'loading-account' && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@ -1071,16 +1042,12 @@ export default function CourrierPage() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Show folders for this account if expanded */}
|
{/* Show only selectedAccount.folders for the selected account if expanded */}
|
||||||
{(() => {
|
{selectedAccount && account.id === selectedAccount.id && expandedAccounts[account.id] && account.folders && account.folders.length > 0 && (
|
||||||
const isExpanded = expandedAccounts[account.id];
|
<div className="pl-4">
|
||||||
const hasFolders = account.folders && account.folders.length > 0;
|
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
|
||||||
return isExpanded && account.id !== 'loading-account' && hasFolders && (
|
</div>
|
||||||
<div className="pl-4">
|
)}
|
||||||
{account.folders.map((folder) => renderFolderButton(folder, account.id))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@ -1161,16 +1128,6 @@ export default function CourrierPage() {
|
|||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{error}
|
{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>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
</div>
|
</div>
|
||||||
@ -1260,12 +1217,6 @@ export default function CourrierPage() {
|
|||||||
onConfirm={handleDeleteConfirm}
|
onConfirm={handleDeleteConfirm}
|
||||||
onCancel={() => setShowDeleteConfirm(false)}
|
onCancel={() => setShowDeleteConfirm(false)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoginNeededAlert
|
|
||||||
show={showLoginNeeded}
|
|
||||||
onLogin={handleGoToLogin}
|
|
||||||
onClose={() => setShowLoginNeeded(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Compose Email Dialog */}
|
{/* Compose Email Dialog */}
|
||||||
<Dialog open={showComposeModal} onOpenChange={(open) => !open && setShowComposeModal(false)}>
|
<Dialog open={showComposeModal} onOpenChange={(open) => !open && setShowComposeModal(false)}>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user