diff --git a/components/notification-badge.tsx b/components/notification-badge.tsx index 2ffb835..969f1fa 100644 --- a/components/notification-badge.tsx +++ b/components/notification-badge.tsx @@ -1,6 +1,6 @@ import React, { memo, useState, useEffect } from 'react'; import Link from 'next/link'; -import { Bell, Check, ExternalLink, AlertCircle, LogIn, Kanban, MessageSquare } from 'lucide-react'; +import { Bell, Check, ExternalLink, AlertCircle, LogIn, Kanban, MessageSquare, Mail } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { useNotifications } from '@/hooks/use-notifications'; import { Button } from '@/components/ui/button'; @@ -215,6 +215,12 @@ export const NotificationBadge = memo(function NotificationBadge({ className }: Parole )} + {notification.source === 'email' && ( + + + Courrier + + )}

{formatDistanceToNow(new Date(notification.timestamp), { addSuffix: true })} diff --git a/lib/services/notifications/email-adapter.ts b/lib/services/notifications/email-adapter.ts index 5397150..9a5548c 100644 --- a/lib/services/notifications/email-adapter.ts +++ b/lib/services/notifications/email-adapter.ts @@ -205,10 +205,143 @@ export class EmailAdapter implements NotificationAdapter { } async getNotifications(userId: string, page = 1, limit = 20): Promise { - // For now, return empty array - we can implement this later if needed - // The notification count is what matters for the badge - // In the future, we could return unread emails as notifications - return []; + logger.debug('[EMAIL_ADAPTER] getNotifications called', { userId, page, limit }); + + try { + // Get all accounts from the database + const accounts = await prisma.mailCredentials.findMany({ + where: { userId }, + select: { + id: true, + email: true + } + }); + + if (accounts.length === 0) { + return []; + } + + const notifications: Notification[] = []; + + // For each account, get unread emails from INBOX + // Use the same flow as getEmails() but filter for unread only + for (const account of accounts) { + try { + const client = await getImapConnection(userId, account.id); + + // Use the same approach as getEmails() - open mailbox first + const mailboxInfo = await client.mailboxOpen('INBOX'); + + if (!mailboxInfo || typeof mailboxInfo === 'boolean') { + logger.debug('[EMAIL_ADAPTER] Could not open INBOX', { + userId, + accountId: account.id, + }); + continue; + } + + const totalEmails = mailboxInfo.exists || 0; + + if (totalEmails === 0) { + continue; + } + + // Search for unread emails (same as getNotificationCount logic) + const searchResult = await client.search({ seen: false }); + + if (!searchResult || searchResult.length === 0) { + continue; + } + + // Limit the number of results for performance (get more than limit to have enough after filtering) + const limitedResults = searchResult.slice(0, limit * 3); + + // Fetch email metadata using the same structure as getEmails() + const messages = await client.fetch(limitedResults, { + envelope: true, + flags: true, + uid: true + }); + + // Convert to notifications (same format as getEmails() processes emails) + for await (const message of messages) { + // Filter: only process if truly unread (double-check flags) + if (message.flags && message.flags.has('\\Seen')) { + continue; + } + + const envelope = message.envelope; + const from = envelope.from?.[0]; + const subject = envelope.subject || '(Sans objet)'; + const fromName = from?.name || from?.address || 'Expéditeur inconnu'; + const fromAddress = from?.address || ''; + + const notification: Notification = { + id: `email-${account.id}-${message.uid}`, + source: 'email', + sourceId: message.uid.toString(), + type: 'email', + title: 'Email', + message: `${fromName}${fromAddress ? ` <${fromAddress}>` : ''}: ${subject}`, + link: `/courrier?accountId=${account.id}&folder=INBOX&emailId=${message.uid}`, + isRead: false, + timestamp: envelope.date || new Date(), + priority: 'normal', + user: { + id: fromAddress, + name: fromName, + }, + metadata: { + accountId: account.id, + accountEmail: account.email, + folder: 'INBOX', + emailId: message.uid.toString(), + } + }; + + notifications.push(notification); + } + + // Close mailbox (same as getEmails() does implicitly via connection pool) + try { + await client.mailboxClose(); + } catch (closeError) { + // Non-fatal, connection pool will handle it + logger.debug('[EMAIL_ADAPTER] Error closing mailbox (non-fatal)', { + error: closeError instanceof Error ? closeError.message : String(closeError), + }); + } + } catch (accountError) { + logger.error('[EMAIL_ADAPTER] Error processing account for notifications', { + userId, + accountId: account.id, + error: accountError instanceof Error ? accountError.message : String(accountError), + }); + continue; + } + } + + // Sort by timestamp (newest first) and apply pagination + notifications.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + + const startIndex = (page - 1) * limit; + const endIndex = startIndex + limit; + const paginatedNotifications = notifications.slice(startIndex, endIndex); + + logger.debug('[EMAIL_ADAPTER] getNotifications result', { + total: notifications.length, + returned: paginatedNotifications.length, + page, + limit, + }); + + return paginatedNotifications; + } catch (error) { + logger.error('[EMAIL_ADAPTER] Error getting notifications', { + error: error instanceof Error ? error.message : String(error), + }); + return []; + } } async markAsRead(userId: string, notificationId: string): Promise {