chore: update webscoket connetion and polling service

This commit is contained in:
mahyargdz
2025-07-30 09:37:11 +03:30
parent 5315fae914
commit f46c2ced99
13 changed files with 140 additions and 113 deletions
+1
View File
@@ -280,6 +280,7 @@ export const enum EmailMessage {
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد", MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد",
TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد", TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد",
SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند", SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند",
SAVE_DRAFT_SUCCESS = "پیش نویس با موفقیت ذخیره شد",
} }
export const enum WebSocketMessage { export const enum WebSocketMessage {
+2 -6
View File
@@ -28,11 +28,7 @@ import { EmailPollingService } from "../email-utils/services/email-polling.servi
@UsePipes(new ValidationPipe({ transform: true })) @UsePipes(new ValidationPipe({ transform: true }))
@UseFilters(WsExceptionFilter) @UseFilters(WsExceptionFilter)
@WebSocketGateway({ @WebSocketGateway({ namespace: WEBSOCKET_NAMESPACES.EMAIL, cors: { origin: true, credentials: true }, transports: ["websocket", "polling"] })
namespace: WEBSOCKET_NAMESPACES.EMAIL,
cors: { origin: true, credentials: true },
transports: ["websocket", "polling"],
})
export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() @WebSocketServer()
server: Server; server: Server;
@@ -86,7 +82,7 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
// Start email polling for this user // Start email polling for this user
try { try {
await this.emailPollingService.startPollingForUser(user.id, client.id); await this.emailPollingService.startPollingForUser(user.wildduckUserId, user.id, client.id);
this.logger.log(`Email polling started for user ${user.id}`); this.logger.log(`Email polling started for user ${user.id}`);
} catch (pollingError) { } catch (pollingError) {
this.logger.error(`Failed to start email polling for user ${user.id}:`, pollingError); this.logger.error(`Failed to start email polling for user ${user.id}:`, pollingError);
@@ -1,4 +1,5 @@
export interface EmailPollingJobData { export interface EmailPollingJobData {
wildduckUserId: string;
userId: string; userId: string;
lastCheckTimestamp?: string; lastCheckTimestamp?: string;
connectionId: string; connectionId: string;
@@ -23,47 +23,41 @@ export class EmailPollingProcessor extends WorkerProcessor {
} }
async process(job: Job<EmailPollingJobData>): Promise<void> { async process(job: Job<EmailPollingJobData>): Promise<void> {
const { userId, lastCheckTimestamp, connectionId } = job.data; const { wildduckUserId, userId, lastCheckTimestamp, connectionId } = job.data;
this.logger.debug(`Processing email polling job for user ${userId} (connection: ${connectionId})`); this.logger.debug(`Processing email polling job for user ${userId} (connection: ${connectionId})`);
try { try {
// Check if user is still connected before processing
if (!this.emailGateway.isUserConnected(userId)) { if (!this.emailGateway.isUserConnected(userId)) {
this.logger.debug(`User ${userId} no longer connected, skipping email check`); this.logger.debug(`User ${userId} no longer connected, skipping email check`);
return; return;
} }
// Get user's mailbox information const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(wildduckUserId);
const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId);
if (!mailboxIds.inbox) { if (!mailboxIds.inbox) {
this.logger.warn(`No inbox found for user ${userId}`); this.logger.warn(`No inbox found for user ${userId}`);
return; return;
} }
// Calculate time range for checking new emails
const checkSince = lastCheckTimestamp ? new Date(lastCheckTimestamp) : new Date(Date.now() - 5 * 60 * 1000); const checkSince = lastCheckTimestamp ? new Date(lastCheckTimestamp) : new Date(Date.now() - 5 * 60 * 1000);
const now = new Date(); const now = new Date();
this.logger.debug(`Checking emails for user ${userId} since ${checkSince.toISOString()}`); this.logger.debug(`Checking emails for user ${userId} since ${checkSince.toISOString()}`);
// Get new emails from inbox since last check const newEmails = await this.getRecentEmails(wildduckUserId, mailboxIds.inbox, checkSince);
const newEmails = await this.getRecentEmails(userId, mailboxIds.inbox, checkSince);
if (newEmails.length > 0) { if (newEmails.length > 0) {
this.logger.log(`Found ${newEmails.length} new emails for user ${userId}`); this.logger.log(`Found ${newEmails.length} new emails for user ${wildduckUserId}`);
// Notify user of each new email via websocket
const lastEmail = newEmails[newEmails.length - 1]; const lastEmail = newEmails[newEmails.length - 1];
await this.emailGateway.notifyNewEmail(userId, { await this.emailGateway.notifyNewEmail(userId, {
wildduckUserId: userId, wildduckUserId,
userId,
messageId: lastEmail.id, messageId: lastEmail.id,
date: lastEmail.date || new Date().toISOString(), date: lastEmail.date || new Date().toISOString(),
from: lastEmail.from, from: lastEmail.from,
to: lastEmail.to || [], to: lastEmail.to || [],
subject: lastEmail.subject, subject: lastEmail.subject,
userId,
timestamp: lastEmail.date || new Date().toISOString(), timestamp: lastEmail.date || new Date().toISOString(),
mailboxId: lastEmail.mailbox, mailboxId: lastEmail.mailbox,
mailboxName: "Inbox", mailboxName: "Inbox",
@@ -72,7 +66,7 @@ export class EmailPollingProcessor extends WorkerProcessor {
}); });
// Update unread count // Update unread count
const unreadStats = await this.getUnreadCounts(userId, mailboxIds); const unreadStats = await this.getUnreadCounts(wildduckUserId, mailboxIds);
await this.emailGateway.notifyUnreadCountUpdate(userId, { await this.emailGateway.notifyUnreadCountUpdate(userId, {
userId, userId,
totalUnread: unreadStats.total, totalUnread: unreadStats.total,
@@ -96,15 +90,15 @@ export class EmailPollingProcessor extends WorkerProcessor {
throw error; // Re-throw to trigger retry mechanism throw error; // Re-throw to trigger retry mechanism
} }
} }
//=======================================================
// Helper method to get recent emails from a mailbox private async getRecentEmails(wildduckUserId: string, mailboxId: string, since: Date) {
private async getRecentEmails(userId: string, mailboxId: string, since: Date) {
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
this.mailServerService.messages.searchMessages(userId, { this.mailServerService.messages.searchMessages(wildduckUserId, {
mailbox: mailboxId, mailbox: mailboxId,
limit: 50, // Limit to avoid too many results limit: 50,
order: "desc", // Most recent first order: "desc",
unseen: true,
}), }),
); );
@@ -117,22 +111,20 @@ export class EmailPollingProcessor extends WorkerProcessor {
return emailDate >= since; return emailDate >= since;
}); });
} catch (error) { } catch (error) {
this.logger.error(`Error fetching recent emails for user ${userId}:`, error); this.logger.error(`Error fetching recent emails for user ${wildduckUserId}:`, error);
return []; return [];
} }
} }
//=======================================================
// Helper method to get unread counts from all mailboxes private async getUnreadCounts(wildduckUserId: string, mailboxIds: UserMailboxIds) {
private async getUnreadCounts(userId: string, mailboxIds: UserMailboxIds) {
try { try {
const mailboxCounts: Record<string, number> = {}; const mailboxCounts: Record<string, number> = {};
let total = 0; let total = 0;
// Get unread count from each mailbox
for (const [mailboxName, mailboxId] of Object.entries(mailboxIds)) { for (const [mailboxName, mailboxId] of Object.entries(mailboxIds)) {
if (mailboxId) { if (mailboxId) {
try { try {
const response = await firstValueFrom(this.mailServerService.mailboxes.getMailbox(userId, mailboxId)); const response = await firstValueFrom(this.mailServerService.mailboxes.getMailbox(wildduckUserId, mailboxId));
if (response.success && response.unseen !== undefined) { if (response.success && response.unseen !== undefined) {
mailboxCounts[mailboxName] = response.unseen; mailboxCounts[mailboxName] = response.unseen;
@@ -150,7 +142,7 @@ export class EmailPollingProcessor extends WorkerProcessor {
mailboxCounts, mailboxCounts,
}; };
} catch (error) { } catch (error) {
this.logger.error(`Error calculating unread counts for user ${userId}:`, error); this.logger.error(`Error calculating unread counts for user ${wildduckUserId}:`, error);
return { return {
total: 0, total: 0,
mailboxCounts: {}, mailboxCounts: {},
@@ -14,11 +14,12 @@ export class EmailPollingService {
constructor(@InjectQueue(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE) private readonly emailPollingQueue: Queue<EmailPollingJobData>) {} constructor(@InjectQueue(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE) private readonly emailPollingQueue: Queue<EmailPollingJobData>) {}
//======================================================= //=======================================================
async startPollingForUser(userId: string, connectionId: string): Promise<void> { async startPollingForUser(wildduckUserId: string, userId: string, connectionId: string): Promise<void> {
try { try {
await this.stopPollingForUser(userId); await this.stopPollingForUser(userId);
const jobData: EmailPollingJobData = { const jobData: EmailPollingJobData = {
wildduckUserId,
userId, userId,
connectionId, connectionId,
lastCheckTimestamp: new Date().toISOString(), lastCheckTimestamp: new Date().toISOString(),
@@ -54,6 +55,7 @@ export class EmailPollingService {
async stopPollingForUser(userId: string): Promise<void> { async stopPollingForUser(userId: string): Promise<void> {
try { try {
const activeJob = this.activeJobs.get(userId); const activeJob = this.activeJobs.get(userId);
console.log(activeJob);
if (!activeJob) { if (!activeJob) {
this.logger.debug(`No active polling job found for user ${userId}`); this.logger.debug(`No active polling job found for user ${userId}`);
return; return;
@@ -140,9 +142,9 @@ export class EmailPollingService {
} }
//======================================================= //=======================================================
async restartPollingForUser(userId: string, connectionId: string): Promise<void> { async restartPollingForUser(wildduckUserId: string, userId: string, connectionId: string): Promise<void> {
this.logger.log(`Restarting email polling for user ${userId}`); this.logger.log(`Restarting email polling for user ${userId}`);
await this.stopPollingForUser(userId); await this.stopPollingForUser(userId);
await this.startPollingForUser(userId, connectionId); await this.startPollingForUser(wildduckUserId, userId, connectionId);
} }
} }
@@ -18,14 +18,14 @@ export class MailboxResolverService {
/** /**
* Get user mailbox IDs with caching * Get user mailbox IDs with caching
*/ */
async getUserMailboxIds(userId: string): Promise<UserMailboxIds> { async getUserMailboxIds(wildduckUserId: string): Promise<UserMailboxIds> {
const cached = this.getCachedMailboxIds(userId); const cached = this.getCachedMailboxIds(wildduckUserId);
if (cached) return cached; if (cached) return cached;
this.logger.log(`Fetching mailbox IDs for user: ${userId}`); this.logger.log(`Fetching mailbox IDs for user: ${wildduckUserId}`);
try { try {
const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(userId)); const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(wildduckUserId));
const mailboxIds: UserMailboxIds = { const mailboxIds: UserMailboxIds = {
inbox: this.findMailboxId(mailboxes.results, MailboxEnum.INBOX), inbox: this.findMailboxId(mailboxes.results, MailboxEnum.INBOX),
@@ -37,12 +37,12 @@ export class MailboxResolverService {
// favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false), // favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false),
}; };
this.cacheMailboxIds(userId, mailboxIds); this.cacheMailboxIds(wildduckUserId, mailboxIds);
this.logger.log(`Mailbox IDs cached for user: ${userId}`); this.logger.log(`Mailbox IDs cached for user: ${wildduckUserId}`);
return mailboxIds; return mailboxIds;
} catch (error) { } catch (error) {
this.logger.error(`Failed to fetch mailbox IDs for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); this.logger.error(`Failed to fetch mailbox IDs for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
throw error; throw error;
} }
} }
@@ -50,12 +50,12 @@ export class MailboxResolverService {
/** /**
* Get specific mailbox ID * Get specific mailbox ID
*/ */
async getMailboxId(userId: string, mailboxType: keyof UserMailboxIds): Promise<string> { async getMailboxId(wildduckUserId: string, mailboxType: keyof UserMailboxIds): Promise<string> {
const mailboxIds = await this.getUserMailboxIds(userId); const mailboxIds = await this.getUserMailboxIds(wildduckUserId);
const mailboxId = mailboxIds[mailboxType]; const mailboxId = mailboxIds[mailboxType];
if (!mailboxId) { if (!mailboxId) {
throw new NotFoundException(`${mailboxType} mailbox not found for user ${userId}`); throw new NotFoundException(`${mailboxType} mailbox not found for user ${wildduckUserId}`);
} }
return mailboxId; return mailboxId;
@@ -73,37 +73,37 @@ export class MailboxResolverService {
/** /**
* Get drafts mailbox ID * Get drafts mailbox ID
*/ */
async getDraftsMailboxId(userId: string): Promise<string> { async getDraftsMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "drafts"); return this.getMailboxId(wildduckUserId, "drafts");
} }
/** /**
* Get inbox mailbox ID * Get inbox mailbox ID
*/ */
async getInboxMailboxId(userId: string): Promise<string> { async getInboxMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "inbox"); return this.getMailboxId(wildduckUserId, "inbox");
} }
/** /**
* Get sent mailbox ID * Get sent mailbox ID
*/ */
async getSentMailboxId(userId: string): Promise<string> { async getSentMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "sent"); return this.getMailboxId(wildduckUserId, "sent");
} }
/** /**
* Get trash mailbox ID * Get trash mailbox ID
*/ */
async getTrashMailboxId(userId: string): Promise<string> { async getTrashMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "trash"); return this.getMailboxId(wildduckUserId, "trash");
} }
async getJunkMailboxId(userId: string): Promise<string> { async getJunkMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "junk"); return this.getMailboxId(wildduckUserId, "junk");
} }
async getArchiveMailboxId(userId: string): Promise<string> { async getArchiveMailboxId(wildduckUserId: string): Promise<string> {
return this.getMailboxId(userId, "archive"); return this.getMailboxId(wildduckUserId, "archive");
} }
// async getFavoriteMailboxId(userId: string): Promise<string> { // async getFavoriteMailboxId(userId: string): Promise<string> {
@@ -113,10 +113,10 @@ export class MailboxResolverService {
/** /**
* Clear cache for a specific user * Clear cache for a specific user
*/ */
clearUserCache(userId: string): void { clearUserCache(wildduckUserId: string): void {
this.mailboxCache.delete(userId); this.mailboxCache.delete(wildduckUserId);
this.cacheExpiry.delete(userId); this.cacheExpiry.delete(wildduckUserId);
this.logger.log(`Cache cleared for user: ${userId}`); this.logger.log(`Cache cleared for user: ${wildduckUserId}`);
} }
/** /**
@@ -131,18 +131,18 @@ export class MailboxResolverService {
/** /**
* Get cached mailbox IDs if not expired * Get cached mailbox IDs if not expired
*/ */
private getCachedMailboxIds(userId: string): UserMailboxIds | null { private getCachedMailboxIds(wildduckUserId: string): UserMailboxIds | null {
const cached = this.mailboxCache.get(userId); const cached = this.mailboxCache.get(wildduckUserId);
const expiry = this.cacheExpiry.get(userId); const expiry = this.cacheExpiry.get(wildduckUserId);
if (cached && expiry && Date.now() < expiry) { if (cached && expiry && Date.now() < expiry) {
this.logger.debug(`Using cached mailbox IDs for user: ${userId}`); this.logger.debug(`Using cached mailbox IDs for user: ${wildduckUserId}`);
return cached; return cached;
} }
if (cached) { if (cached) {
this.logger.debug(`Cache expired for user: ${userId}`); this.logger.debug(`Cache expired for user: ${wildduckUserId}`);
this.clearUserCache(userId); this.clearUserCache(wildduckUserId);
} }
return null; return null;
@@ -151,9 +151,9 @@ export class MailboxResolverService {
/** /**
* Cache mailbox IDs with expiry * Cache mailbox IDs with expiry
*/ */
private cacheMailboxIds(userId: string, mailboxIds: UserMailboxIds): void { private cacheMailboxIds(wildduckUserId: string, mailboxIds: UserMailboxIds): void {
this.mailboxCache.set(userId, mailboxIds); this.mailboxCache.set(wildduckUserId, mailboxIds);
this.cacheExpiry.set(userId, Date.now() + this.CACHE_DURATION); this.cacheExpiry.set(wildduckUserId, Date.now() + this.CACHE_DURATION);
} }
/** /**
@@ -49,13 +49,13 @@ export const EMAIL_QUEUE_CONSTANTS = {
EMAIL_BULK_ACTION_DELAY: 1000, EMAIL_BULK_ACTION_DELAY: 1000,
EMAIL_BULK_ACTION_BATCH_SIZE: 250, EMAIL_BULK_ACTION_BATCH_SIZE: 250,
EMAIL_BULK_ACTION_ATTEMPTS: 3, EMAIL_BULK_ACTION_ATTEMPTS: 3,
EMAIL_BULK_ACTION_BACKOFF_DELAY: 2000, // 2 seconds EMAIL_BULK_ACTION_BACKOFF_DELAY: 2000,
EMAIL_BULK_ACTION_PRIORITY: 1, EMAIL_BULK_ACTION_PRIORITY: 1,
// //
EMAIL_POLLING_INTERVAL: 30000, // 30 seconds EMAIL_POLLING_INTERVAL: 3000,
EMAIL_POLLING_REMOVE_ON_COMPLETE: 5, // Keep last 5 completed jobs EMAIL_POLLING_REMOVE_ON_COMPLETE: true,
EMAIL_POLLING_REMOVE_ON_FAIL: 10, // Keep last 10 failed jobs EMAIL_POLLING_REMOVE_ON_FAIL: 10,
EMAIL_POLLING_ATTEMPTS: 3, // Retry failed jobs 3 times EMAIL_POLLING_ATTEMPTS: 3,
EMAIL_POLLING_BACKOFF_DELAY: 2000, // 2 seconds EMAIL_POLLING_BACKOFF_DELAY: 2000,
EMAIL_POLLING_PRIORITY: 1, EMAIL_POLLING_PRIORITY: 1,
} as const; } as const;
+6 -6
View File
@@ -115,24 +115,24 @@ export class EmailController {
@ApiResponse({ status: 206, description: "Some messages marked as read, some failed" }) @ApiResponse({ status: 206, description: "Some messages marked as read, some failed" })
@ApiResponse({ status: 404, description: "Mailbox not found" }) @ApiResponse({ status: 404, description: "Mailbox not found" })
@ApiResponse({ status: 403, description: "Not authorized to mark messages as read" }) @ApiResponse({ status: 403, description: "Not authorized to mark messages as read" })
markAllMessagesAsRead(@UserDec("wildduckUserId") userId: string, @Query() query: MailboxIdQueryDto) { markAllMessagesAsRead(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
return this.emailService.markAllMessagesAsRead(userId, query.mailbox); return this.emailService.markAllMessagesAsRead(wildduckUserId, userId, query.mailbox);
} }
@Patch("messages/empty-trash") @Patch("messages/empty-trash")
@ApiOperation({ summary: "Empty trash" }) @ApiOperation({ summary: "Empty trash" })
@ApiResponse({ status: 200, description: "Trash emptied successfully" }) @ApiResponse({ status: 200, description: "Trash emptied successfully" })
@ApiResponse({ status: 404, description: "Trash not found" }) @ApiResponse({ status: 404, description: "Trash not found" })
emptyTrash(@UserDec("wildduckUserId") userId: string, @Query() query: MailboxIdQueryDto) { emptyTrash(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
return this.emailService.emptyTrash(userId, query.mailbox); return this.emailService.emptyTrash(wildduckUserId, userId, query.mailbox);
} }
@Patch("messages/empty-spam") @Patch("messages/empty-spam")
@ApiOperation({ summary: "Empty spam" }) @ApiOperation({ summary: "Empty spam" })
@ApiResponse({ status: 200, description: "Spam emptied successfully" }) @ApiResponse({ status: 200, description: "Spam emptied successfully" })
@ApiResponse({ status: 404, description: "Spam not found" }) @ApiResponse({ status: 404, description: "Spam not found" })
emptySpam(@UserDec("wildduckUserId") userId: string, @Query() query: MailboxIdQueryDto) { emptySpam(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
return this.emailService.emptySpam(userId, query.mailbox); return this.emailService.emptySpam(wildduckUserId, userId, query.mailbox);
} }
@Get("messages/inbox") @Get("messages/inbox")
+4
View File
@@ -17,6 +17,10 @@ import { User } from "../users/entities/user.entity";
imports: [ imports: [
BullModule.registerQueue({ BullModule.registerQueue({
name: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_QUEUE, name: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_QUEUE,
defaultJobOptions: {
removeOnComplete: true,
removeOnFail: false,
},
}), }),
MailServerModule, MailServerModule,
TemplatesModule, TemplatesModule,
@@ -1,4 +1,5 @@
export interface IEmailBulkActionJob { export interface IEmailBulkActionJob {
wildduckUserId: string; wildduckUserId: string;
userId: string;
mailboxId: string; mailboxId: string;
} }
@@ -31,17 +31,17 @@ export class EmailBulkActionProcessor extends WorkerHost {
switch (name) { switch (name) {
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL: case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL:
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`); this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
await this.markAllMessagesAsRead(data.wildduckUserId, data.mailboxId); await this.markAllMessagesAsRead(data.wildduckUserId, data.userId, data.mailboxId);
break; break;
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH: case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH:
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`); this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
await this.emptyTrash(data.wildduckUserId, data.mailboxId); await this.emptyTrash(data.wildduckUserId, data.userId, data.mailboxId);
break; break;
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM: case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM:
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`); this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
await this.emptySpam(data.wildduckUserId, data.mailboxId); await this.emptySpam(data.wildduckUserId, data.userId, data.mailboxId);
break; break;
default: default:
@@ -55,12 +55,11 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
//============================================== //==============================================
private async markAllMessagesAsRead(wildduckUserId: string, mailboxId: string) { private async markAllMessagesAsRead(wildduckUserId: string, userId: string, mailboxId: string) {
this.logger.log(`Marking all messages as read for user ${wildduckUserId} in mailbox ${mailboxId}`); this.logger.log(`Marking all messages as read for user ${wildduckUserId} in mailbox ${mailboxId}`);
try { try {
// Get all unread messages from the mailbox with a reasonable limit const limit = EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BATCH_SIZE;
const limit = EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BATCH_SIZE; // Process in batches to avoid memory issues
let totalMarked = 0; let totalMarked = 0;
let hasMore = true; let hasMore = true;
let next: string | undefined; let next: string | undefined;
@@ -72,11 +71,10 @@ export class EmailBulkActionProcessor extends WorkerHost {
}; };
while (hasMore) { while (hasMore) {
// Fetch unread messages in batches
const query: Record<string, unknown> = { const query: Record<string, unknown> = {
limit, limit,
order: "desc", order: "desc",
unseen: true, // Only get unread messages unseen: true,
}; };
if (next) { if (next) {
@@ -90,7 +88,6 @@ export class EmailBulkActionProcessor extends WorkerHost {
break; break;
} }
// Mark each message as seen
for (const message of response.results) { for (const message of response.results) {
try { try {
await this.emailService.markMessageAsSeen(wildduckUserId, message.id, mailboxId); await this.emailService.markMessageAsSeen(wildduckUserId, message.id, mailboxId);
@@ -105,7 +102,6 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
} }
// Check if there are more messages
next = typeof response.nextCursor === "string" ? response.nextCursor : undefined; next = typeof response.nextCursor === "string" ? response.nextCursor : undefined;
hasMore = !!next && response.results.length === limit; hasMore = !!next && response.results.length === limit;
} }
@@ -113,12 +109,14 @@ export class EmailBulkActionProcessor extends WorkerHost {
results.totalMarked = totalMarked; results.totalMarked = totalMarked;
await this.emailNotificationService.notifyEmailRead({ await this.emailNotificationService.notifyEmailRead({
userId: wildduckUserId, userId,
messageId: results.successful[0], messageId: results.successful[0],
mailboxId, mailboxId,
}); });
this.logger.log(`Marked ${totalMarked} messages as read for user ${wildduckUserId} in mailbox ${mailboxId}, ${results.failed.length} failed`); this.logger.verbose(
`Marked ${totalMarked} messages as read for user ${wildduckUserId} in mailbox ${mailboxId}, ${results.failed.length} failed`,
);
return { return {
success: true, success: true,
@@ -133,7 +131,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
//============================================== //==============================================
private async emptyTrash(wildduckUserId: string, mailboxId: string) { private async emptyTrash(wildduckUserId: string, userId: string, mailboxId: string) {
this.logger.log(`Emptying trash for user ${wildduckUserId} in mailbox ${mailboxId}`); this.logger.log(`Emptying trash for user ${wildduckUserId} in mailbox ${mailboxId}`);
try { try {
@@ -145,7 +143,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
await this.emailNotificationService.notifyEmailDeleted({ await this.emailNotificationService.notifyEmailDeleted({
userId: wildduckUserId, userId,
messageId: randomInt(1, 500), messageId: randomInt(1, 500),
mailboxId, mailboxId,
}); });
@@ -162,7 +160,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
//============================================== //==============================================
private async emptySpam(wildduckUserId: string, mailboxId: string) { private async emptySpam(wildduckUserId: string, userId: string, mailboxId: string) {
this.logger.log(`Emptying spam for user ${wildduckUserId} in mailbox ${mailboxId}`); this.logger.log(`Emptying spam for user ${wildduckUserId} in mailbox ${mailboxId}`);
try { try {
@@ -174,7 +172,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
await this.emailNotificationService.notifyEmailDeleted({ await this.emailNotificationService.notifyEmailDeleted({
userId: wildduckUserId, userId,
messageId: randomInt(1, 500), messageId: randomInt(1, 500),
mailboxId, mailboxId,
}); });
+41 -11
View File
@@ -88,7 +88,7 @@ export class EmailService {
}); });
return { return {
message: EmailMessage.EMAIL_SENDING_SUCCESS, message: sendEmailDto.isDraft ? EmailMessage.SAVE_DRAFT_SUCCESS : EmailMessage.EMAIL_SENDING_SUCCESS,
messageId: response.message.id, messageId: response.message.id,
queueId: response.message.queueId, queueId: response.message.queueId,
}; };
@@ -109,15 +109,18 @@ export class EmailService {
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
const wildDuckQuery: SearchMessagesMailServerQueryDto = { const wildDuckQuery: SearchMessagesMailServerQueryDto = {
...query, ...query,
limit: query.limit || 20, limit: 100,
page: query.page || 1, page: query.page || 1,
order: query.order || "desc", order: query.order || "desc",
searchable: true,
query: query.search, query: query.search,
from: query.from || query.search, // subject: query.subject || query.search,
// mailbox: query.mailbox, // from: query.from || query.search,
// to: query.to || query.search,
}; };
delete wildDuckQuery?.mailbox; delete wildDuckQuery?.mailbox;
delete wildDuckQuery.search;
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
@@ -283,11 +286,12 @@ export class EmailService {
} }
//============================================== //==============================================
async markAllMessagesAsRead(wildduckUserId: string, mailboxId: string) { async markAllMessagesAsRead(wildduckUserId: string, userId: string, mailboxId: string) {
await this.emailBulkActionQueue.add( await this.emailBulkActionQueue.add(
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL, EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL,
{ {
wildduckUserId, wildduckUserId,
userId,
mailboxId, mailboxId,
}, },
{ {
@@ -308,11 +312,24 @@ export class EmailService {
} }
//============================================== //==============================================
async emptyTrash(wildduckUserId: string, mailboxId: string) { async emptyTrash(wildduckUserId: string, userId: string, mailboxId: string) {
await this.emailBulkActionQueue.add(EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH, { await this.emailBulkActionQueue.add(
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH,
{
wildduckUserId, wildduckUserId,
userId,
mailboxId, mailboxId,
}); },
{
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_DELAY,
priority: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_PRIORITY,
attempts: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_ATTEMPTS,
backoff: {
type: "exponential",
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BACKOFF_DELAY,
},
},
);
return { return {
success: true, success: true,
@@ -321,11 +338,24 @@ export class EmailService {
} }
//============================================== //==============================================
async emptySpam(wildduckUserId: string, mailboxId: string) { async emptySpam(wildduckUserId: string, userId: string, mailboxId: string) {
await this.emailBulkActionQueue.add(EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM, { await this.emailBulkActionQueue.add(
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM,
{
wildduckUserId, wildduckUserId,
userId,
mailboxId, mailboxId,
}); },
{
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_DELAY,
priority: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_PRIORITY,
attempts: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_ATTEMPTS,
backoff: {
type: "exponential",
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BACKOFF_DELAY,
},
},
);
return { return {
success: true, success: true,
@@ -27,6 +27,8 @@ export class UpdateMessageDto {
} }
export class SearchMessagesMailServerQueryDto { export class SearchMessagesMailServerQueryDto {
search?: string;
q?: string; q?: string;
mailbox?: string; mailbox?: string;