NeahStable/lib/services/notifications/email-adapter.ts
2026-01-11 22:29:40 +01:00

224 lines
7.2 KiB
TypeScript

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<boolean> {
// 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<Record<string, Record<string, number>>> {
// 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<string, Record<string, number>> = {};
// 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<NotificationCount> {
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<string, Record<string, number>> | 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<Notification[]> {
// 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<boolean> {
// Not implemented yet - Email read status is handled by the email service
return false;
}
async markAllAsRead(userId: string): Promise<boolean> {
// Not implemented yet - Email read status is handled by the email service
return false;
}
}