import { NotificationAdapter } from './notification-adapter.interface'; import { logger } from '@/lib/logger'; import { Notification, NotificationCount } from '@/lib/types/notification'; import { getRedisClient } from '@/lib/redis'; import { prisma } from '@/lib/prisma'; import { getImapConnection } from '@/lib/services/email-service'; export class EmailAdapter implements NotificationAdapter { readonly sourceName = 'email'; private static readonly UNREAD_COUNTS_CACHE_KEY = (userId: string) => `email:unread:${userId}`; private static readonly CACHE_TTL = 120; // 2 minutes (aligned with unread-counts API) async isConfigured(): Promise { // Email service is always configured if user has email accounts return true; } /** * Fetch unread counts from IMAP (same logic as unread-counts API) */ private async fetchUnreadCounts(userId: string): Promise>> { // Get all accounts from the database const accounts = await prisma.mailCredentials.findMany({ where: { userId }, select: { id: true, email: true } }); logger.debug('[EMAIL_ADAPTER] Found accounts', { userId, accountCount: accounts.length, }); if (accounts.length === 0) { return { default: {} }; } // Mapping to hold the unread counts const unreadCounts: Record> = {}; // For each account, get the unread counts for standard folders for (const account of accounts) { const accountId = account.id; try { // Get IMAP connection for this account logger.debug('[EMAIL_ADAPTER] Processing account', { userId, accountId, email: account.email, }); const client = await getImapConnection(userId, accountId); unreadCounts[accountId] = {}; // Standard folders to check (focus on INBOX for notifications) const standardFolders = ['INBOX']; // Get mailboxes for this account to check if folders exist const mailboxes = await client.list(); const availableFolders = mailboxes.map(mb => mb.path); // Check each standard folder if it exists for (const folder of standardFolders) { // Skip if folder doesn't exist in this account if (!availableFolders.includes(folder) && !availableFolders.some(f => f.toLowerCase() === folder.toLowerCase())) { continue; } try { // Check folder status without opening it (more efficient) const status = await client.status(folder, { unseen: true }); if (status && typeof status.unseen === 'number') { // Store the unread count unreadCounts[accountId][folder] = status.unseen; logger.debug('[EMAIL_ADAPTER] Unread count', { userId, accountId, folder, unread: status.unseen, }); } } catch (folderError) { logger.error('[EMAIL_ADAPTER] Error getting unread count for folder', { userId, accountId, folder, error: folderError instanceof Error ? folderError.message : String(folderError), }); // Continue to next folder even if this one fails } } } catch (accountError) { logger.error('[EMAIL_ADAPTER] Error processing account', { userId, accountId, error: accountError instanceof Error ? accountError.message : String(accountError), }); } } return unreadCounts; } /** * Get user's email accounts and calculate total unread count */ async getNotificationCount(userId: string): Promise { logger.debug('[EMAIL_ADAPTER] getNotificationCount called', { userId }); try { // Try to get from cache first (same cache as unread-counts API) const redis = getRedisClient(); const cacheKey = EmailAdapter.UNREAD_COUNTS_CACHE_KEY(userId); let unreadCounts: Record> | null = null; try { const cachedData = await redis.get(cacheKey); if (cachedData) { unreadCounts = JSON.parse(cachedData); logger.debug('[EMAIL_ADAPTER] Using cached unread counts', { userId }); } } catch (error) { logger.debug('[EMAIL_ADAPTER] Cache miss or error', { userId, error: error instanceof Error ? error.message : String(error), }); } // If no cache, fetch directly (but don't cache here - let the API route handle caching) if (!unreadCounts) { try { // Fetch unread counts directly unreadCounts = await this.fetchUnreadCounts(userId); logger.debug('[EMAIL_ADAPTER] Fetched unread counts directly', { userId }); } catch (error) { logger.error('[EMAIL_ADAPTER] Error fetching unread counts', { userId, error: error instanceof Error ? error.message : String(error), }); return { total: 0, unread: 0, sources: { email: { total: 0, unread: 0 } } }; } } // Calculate total unread count across all accounts and folders // Focus on INBOX for notifications let totalUnread = 0; let foldersWithUnread = 0; for (const accountId in unreadCounts) { const accountFolders = unreadCounts[accountId]; // Focus on INBOX folder for notifications const inboxCount = accountFolders['INBOX'] || accountFolders[`${accountId}:INBOX`] || 0; if (inboxCount > 0) { totalUnread += inboxCount; foldersWithUnread++; } } logger.debug('[EMAIL_ADAPTER] Notification counts', { userId, total: foldersWithUnread, unread: totalUnread, }); return { total: foldersWithUnread, unread: totalUnread, sources: { email: { total: foldersWithUnread, unread: totalUnread } } }; } catch (error) { logger.error('[EMAIL_ADAPTER] Error getting notification count', { userId, error: error instanceof Error ? error.message : String(error), }); return { total: 0, unread: 0, sources: { email: { total: 0, unread: 0 } } }; } } 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 []; } async markAsRead(userId: string, notificationId: string): Promise { // Not implemented yet - Email read status is handled by the email service return false; } async markAllAsRead(userId: string): Promise { // Not implemented yet - Email read status is handled by the email service return false; } }