224 lines
7.2 KiB
TypeScript
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;
|
|
}
|
|
}
|