panel 2 courier

This commit is contained in:
alma 2025-04-25 11:39:08 +02:00
parent 806ddaa73e
commit e255ed4c5a
4 changed files with 75 additions and 118 deletions

View File

@ -72,23 +72,14 @@ export async function GET(request: Request) {
const adjustedStart = Math.min(start, mailbox.exists); const adjustedStart = Math.min(start, mailbox.exists);
const adjustedEnd = Math.min(end, mailbox.exists); const adjustedEnd = Math.min(end, mailbox.exists);
// Fetch messages from the current folder with content // Fetch messages from the current folder
const messages = await client.fetch(`${adjustedStart}:${adjustedEnd}`, { const messages = await client.fetch(`${adjustedStart}:${adjustedEnd}`, {
envelope: true, envelope: true,
flags: true, flags: true,
bodyStructure: true, bodyStructure: true
source: true, // Get the full email source
bodyParts: ['text/plain', 'text/html'] // Get both text and HTML content
}); });
for await (const message of messages) { for await (const message of messages) {
// Get the email content
let content = '';
if (message.bodyParts) {
// Prefer HTML content if available
content = message.bodyParts.get('text/html')?.toString() || message.bodyParts.get('text/plain')?.toString() || '';
}
result.push({ result.push({
id: message.uid, id: message.uid,
from: message.envelope.from?.[0]?.address || '', from: message.envelope.from?.[0]?.address || '',
@ -100,14 +91,9 @@ export async function GET(request: Request) {
starred: message.flags.has('\\Flagged'), starred: message.flags.has('\\Flagged'),
folder: mailbox.path, folder: mailbox.path,
hasAttachments: message.bodyStructure?.type === 'multipart', hasAttachments: message.bodyStructure?.type === 'multipart',
flags: Array.from(message.flags), flags: Array.from(message.flags)
content: content,
source: message.source?.toString() || '' // Include full email source for parsing
}); });
} }
// Sort results by date, most recent first
result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
} }
return NextResponse.json({ return NextResponse.json({

View File

@ -500,32 +500,22 @@ export default function CourrierPage() {
try { try {
console.log('Checking for stored credentials...'); console.log('Checking for stored credentials...');
const response = await fetch('/api/courrier'); const response = await fetch('/api/courrier');
const data = await response.json();
if (!response.ok) { if (!response.ok) {
console.log('API response error:', data); const errorData = await response.json();
if (data.error === 'No mail credentials found. Please configure your email account.') { console.log('API response error:', errorData);
if (errorData.error === 'No stored credentials found') {
console.log('No credentials found, redirecting to login...'); console.log('No credentials found, redirecting to login...');
router.push('/courrier/login'); router.push('/courrier/login');
return; return;
} }
if (data.error === 'Unauthorized') { throw new Error(errorData.error || 'Failed to check credentials');
console.log('User not authenticated, redirecting to auth...');
router.push('/api/auth/signin');
return;
}
setError(data.error || 'Failed to check credentials');
setLoading(false);
return;
} }
// If we get here, credentials are valid and we have emails
console.log('Credentials verified, loading emails...'); console.log('Credentials verified, loading emails...');
setLoading(false); setLoading(false);
loadEmails(); loadEmails();
} catch (err) { } catch (err) {
console.error('Error checking credentials:', err); console.error('Error checking credentials:', err);
setError('Failed to connect to mail server. Please try again later.'); setError(err instanceof Error ? err.message : 'Failed to check credentials');
setLoading(false); setLoading(false);
} }
}; };
@ -808,14 +798,7 @@ export default function CourrierPage() {
// Sort emails by date (most recent first) // Sort emails by date (most recent first)
const sortedEmails = useMemo(() => { const sortedEmails = useMemo(() => {
return [...emails].sort((a, b) => { return [...emails].sort((a, b) => {
const dateA = new Date(a.date); return new Date(b.date).getTime() - new Date(a.date).getTime();
const dateB = new Date(b.date);
// First sort by date
const dateDiff = dateB.getTime() - dateA.getTime();
if (dateDiff !== 0) return dateDiff;
// If dates are equal, maintain stable order using email ID
return Number(b.id) - Number(a.id);
}); });
}, [emails]); }, [emails]);
@ -836,7 +819,7 @@ export default function CourrierPage() {
email.subject.toLowerCase().includes(query) || email.subject.toLowerCase().includes(query) ||
email.from.toLowerCase().includes(query) || email.from.toLowerCase().includes(query) ||
email.to.toLowerCase().includes(query) || email.to.toLowerCase().includes(query) ||
(email.content || '').toLowerCase().includes(query) email.content.toLowerCase().includes(query)
); );
}, [sortedEmails, searchQuery]); }, [sortedEmails, searchQuery]);
@ -1038,7 +1021,33 @@ export default function CourrierPage() {
{/* Scrollable content area */} {/* Scrollable content area */}
<ScrollArea className="flex-1 p-6"> <ScrollArea className="flex-1 p-6">
{renderEmailPreview(selectedEmail)} <div className="flex items-center gap-4 mb-6">
<Avatar className="h-10 w-10">
<AvatarFallback>
{selectedEmail.fromName?.charAt(0) || selectedEmail.from.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-gray-900">
{selectedEmail.fromName} <span className="text-gray-500">&lt;{selectedEmail.from}&gt;</span>
</p>
<p className="text-sm text-gray-500">
to {selectedEmail.to}
</p>
{selectedEmail.cc && (
<p className="text-sm text-gray-500">
cc {selectedEmail.cc}
</p>
)}
</div>
<div className="text-sm text-gray-500 whitespace-nowrap">
{formatDate(new Date(selectedEmail.date))}
</div>
</div>
<div className="prose max-w-none">
{renderEmailContent(selectedEmail)}
</div>
</ScrollArea> </ScrollArea>
</> </>
) : ( ) : (
@ -1073,7 +1082,7 @@ export default function CourrierPage() {
const renderEmailListItem = (email: Email) => ( const renderEmailListItem = (email: Email) => (
<div <div
key={email.id} key={email.id}
className={`py-2 px-3 hover:bg-gray-50/50 transition-colors cursor-pointer flex items-start gap-3 ${ className={`p-3 hover:bg-gray-50/50 transition-colors cursor-pointer flex items-start gap-3 ${
selectedEmail?.id === email.id ? 'bg-gray-50/80' : '' selectedEmail?.id === email.id ? 'bg-gray-50/80' : ''
}`} }`}
onClick={() => handleEmailSelect(email.id)} onClick={() => handleEmailSelect(email.id)}
@ -1087,29 +1096,31 @@ export default function CourrierPage() {
/> />
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2 mb-1">
<h3 className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}> <div className="flex items-center gap-2 min-w-0">
{email.subject || '(No subject)'} <span className={`text-sm truncate ${!email.read ? 'font-semibold text-gray-900' : 'text-gray-600'}`}>
</h3> {email.fromName || email.from}
<span className="text-xs text-gray-500 whitespace-nowrap flex-shrink-0"> </span>
{!email.read && (
<span className="w-1.5 h-1.5 bg-blue-600 rounded-full flex-shrink-0"></span>
)}
</div>
<span className="text-xs text-gray-500 whitespace-nowrap">
{formatDate(new Date(email.date))} {formatDate(new Date(email.date))}
</span> </span>
</div> </div>
<div className="flex items-center gap-2 text-sm text-gray-600"> <h3 className={`text-sm truncate mb-0.5 ${!email.read ? 'text-gray-900' : 'text-gray-600'}`}>
<span className="truncate"> {email.subject || '(No subject)'}
{email.fromName || email.from} </h3>
</span> <EmailPreview email={email} />
{!email.read && ( </div>
<span className="w-1.5 h-1.5 bg-blue-600 rounded-full flex-shrink-0"></span> <div className="flex-none flex items-center gap-1">
)} {email.starred && (
</div> <Star className="h-4 w-4 text-yellow-400 fill-yellow-400" />
<div className="text-xs text-gray-500 line-clamp-1 mt-0.5"> )}
{email.content ? ( {email.attachments && email.attachments.length > 0 && (
<EmailPreview email={email} /> <Paperclip className="h-4 w-4 text-gray-400" />
) : ( )}
'No content available'
)}
</div>
</div> </div>
</div> </div>
); );
@ -1369,63 +1380,23 @@ export default function CourrierPage() {
); );
}; };
// Update searchEmails to just set the search query since filteredEmails is computed by useMemo
const searchEmails = (query: string) => { const searchEmails = (query: string) => {
setSearchQuery(query); setSearchQuery(query.trim());
}; };
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
searchEmails(e.target.value); const query = e.target.value;
setSearchQuery(query);
}; };
const renderEmailPreview = (email: Email) => { const renderEmailPreview = (email: Email) => {
if (!email) return null; if (!email) return null;
return ( return (
<div className="flex-1 flex flex-col"> <div className="p-4">
{/* Email header section */} <h2 className="text-lg font-semibold mb-2">{email.subject}</h2>
<div className="flex items-center gap-4 mb-6"> <div className="text-sm text-gray-600">
<Avatar className="h-10 w-10">
<AvatarFallback>
{email.fromName?.charAt(0) || email.from.charAt(0)}
</AvatarFallback>
</Avatar>
<div className="flex-1">
<p className="font-medium text-gray-900">
{email.fromName || email.from} <span className="text-gray-500">&lt;{email.from}&gt;</span>
</p>
<p className="text-sm text-gray-500">
to {email.to}
</p>
{email.cc && (
<p className="text-sm text-gray-500">
cc {email.cc}
</p>
)}
</div>
<div className="text-sm text-gray-500 whitespace-nowrap">
{formatDate(new Date(email.date))}
</div>
</div>
{/* Email content section */}
<div className="prose max-w-none">
{renderEmailContent(email)} {renderEmailContent(email)}
</div> </div>
{/* Attachments section */}
{email.attachments && email.attachments.length > 0 && (
<div className="mt-6 border-t border-gray-200 pt-6">
<h3 className="text-sm font-medium text-gray-900 mb-2">Attachments</h3>
<div className="space-y-2">
{email.attachments.map((attachment, index) => (
<div key={index} className="flex items-center gap-2 p-2 bg-gray-50 rounded">
<Paperclip className="h-4 w-4 text-gray-400" />
<span className="text-sm text-gray-600">{attachment.name}</span>
</div>
))}
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -47,7 +47,7 @@ export function Email() {
if (!isRefresh) setLoading(true); if (!isRefresh) setLoading(true);
try { try {
const response = await fetch('/api/courrier?folder=INBOX&page=1&limit=5'); const response = await fetch('/api/mail');
if (!response.ok) { if (!response.ok) {
const errorData = await response.json(); const errorData = await response.json();
@ -60,8 +60,8 @@ export function Email() {
throw new Error(data.error); throw new Error(data.error);
} }
const validatedEmails = (data.emails || []).map((email: any) => ({ const validatedEmails = data.emails.map((email: any) => ({
id: email.id?.toString() || Date.now().toString(), id: email.id || Date.now().toString(),
subject: email.subject || '(No subject)', subject: email.subject || '(No subject)',
from: email.from || '', from: email.from || '',
fromName: email.fromName || email.from?.split('@')[0] || 'Unknown', fromName: email.fromName || email.from?.split('@')[0] || 'Unknown',
@ -72,7 +72,7 @@ export function Email() {
})); }));
setEmails(validatedEmails); setEmails(validatedEmails);
setMailUrl('/courrier'); setMailUrl(data.mailUrl || 'https://espace.slm-lab.net/apps/courrier/');
setError(null); setError(null);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : 'Error fetching emails'); setError(err instanceof Error ? err.message : 'Error fetching emails');
@ -169,7 +169,7 @@ export function Email() {
<div <div
key={email.id} key={email.id}
className="p-2 hover:bg-gray-50/50 rounded-lg transition-colors cursor-pointer" className="p-2 hover:bg-gray-50/50 rounded-lg transition-colors cursor-pointer"
onClick={() => router.push('/courrier')} onClick={() => router.push('/mail')}
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-600 truncate max-w-[60%]" title={email.fromName || email.from}> <span className="text-sm text-gray-600 truncate max-w-[60%]" title={email.fromName || email.from}>

View File

@ -54,12 +54,12 @@ export async function markAsRead(emailIds: (string | number)[], isRead: boolean)
const imap = await getImapClient(); const imap = await getImapClient();
try { try {
await imap.connect(); await imap.connect();
const lock = await imap.getMailboxLock('INBOX'); const lock = await imap.getMailboxLock('INBOX');
try { try {
for (const id of emailIds) { for (const id of emailIds) {
const stringId = typeof id === 'number' ? id.toString() : id; const stringId = typeof id === 'number' ? id.toString() : id;
const message = await imap.fetchOne(stringId, { uid: true }); const message = await imap.fetchOne(stringId, { uid: true });
if (message) { if (message) {
const uid = typeof message.uid === 'number' ? message.uid.toString() : message.uid; const uid = typeof message.uid === 'number' ? message.uid.toString() : message.uid;
await imap.messageFlagsAdd(uid, isRead ? ['\\Seen'] : [], { uid: true }); await imap.messageFlagsAdd(uid, isRead ? ['\\Seen'] : [], { uid: true });
} }