panel 2 courier
This commit is contained in:
parent
806ddaa73e
commit
e255ed4c5a
@ -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({
|
||||||
|
|||||||
@ -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"><{selectedEmail.from}></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"><{email.from}></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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user