From b01d77e36eb5f46a7b532f5d5830508fc04d4710 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 12 Jul 2025 15:59:47 +0330 Subject: [PATCH] chore: add bulk operation --- src/common/enums/message.enum.ts | 6 + src/modules/email/DTO/bulk-actions.dto.ts | 39 ++++++ src/modules/email/email.controller.ts | 23 ++++ src/modules/email/services/email.service.ts | 127 ++++++++++++++++++ .../quota-sync/services/quota-sync.service.ts | 7 +- 5 files changed, 198 insertions(+), 4 deletions(-) create mode 100644 src/modules/email/DTO/bulk-actions.dto.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 13b5745..c23edd3 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -201,10 +201,16 @@ export const enum EmailMessage { MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت به آرشیو منتقل شد", MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد", MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = "پیام با موفقیت به سطل زباله منتقل شد", + MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = "پیام با موفقیت به جانک منتقل شد", MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد", MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد", MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = "پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد", MESSAGE_NOT_FOUND = "پیام یافت نشد", + + // Bulk action messages + BULK_ACTION_COMPLETED_SUCCESSFULLY = "عملیات گروهی با موفقیت انجام شد", + BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد", + BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد", } export const enum WebSocketMessage { diff --git a/src/modules/email/DTO/bulk-actions.dto.ts b/src/modules/email/DTO/bulk-actions.dto.ts new file mode 100644 index 0000000..b69d4f0 --- /dev/null +++ b/src/modules/email/DTO/bulk-actions.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Transform, Type } from "class-transformer"; +import { ArrayMinSize, IsArray, IsEnum, IsNotEmpty, IsNumber } from "class-validator"; + +export enum BulkActionType { + SEEN = "seen", + DELETE = "delete", + ARCHIVE = "archive", + FAVORITE = "favorite", + UNFAVORITE = "unfavorite", + UNARCHIVE = "unarchive", + RESTORE = "restore", + JUNK = "junk", + TRASH = "trash", +} + +export class BulkActionDto { + @ApiProperty({ + description: "Array of message IDs to perform bulk action on", + example: [123, 456, 789], + type: [Number], + isArray: true, + }) + @IsArray({ message: "Message IDs must be an array" }) + @ArrayMinSize(1, { message: "At least one message ID is required" }) + @Type(() => Number) + @Transform(({ value }) => value.map((id: any) => parseInt(id))) + @IsNumber({}, { each: true, message: "Each message ID must be a number" }) + messageIds: number[]; + + @ApiProperty({ + description: "Action to perform on the messages", + example: BulkActionType.SEEN, + enum: BulkActionType, + }) + @IsNotEmpty({ message: "Action type is required" }) + @IsEnum(BulkActionType, { message: "Action must be one of: seen, delete, archive, favorite, junk, trash" }) + action: BulkActionType; +} diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 0e4cc6f..1e4b82a 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { BulkActionDto } from "./DTO/bulk-actions.dto"; import { MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto"; import { MessageListQueryDto } from "./DTO/email-query.dto"; import { SearchMessagesQueryDto } from "./DTO/email-query.dto"; @@ -199,4 +200,26 @@ export class EmailController { moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId)); } + + @Patch("messages/:messageId/junk") + @ApiOperation({ summary: "Move a message to junk mailbox" }) + @ApiResponse({ status: 200, description: "Message moved to junk successfully" }) + @ApiResponse({ status: 404, description: "Message not found" }) + @ApiResponse({ status: 403, description: "Not authorized to move message" }) + moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { + return this.emailService.moveMessageToJunk(userId, Number(params.messageId)); + } + + @Post("messages/bulk-action") + @ApiOperation({ + summary: "Perform bulk actions on multiple messages", + description: "Apply actions (seen, delete, archive, favorite, junk, trash) to multiple messages at once", + }) + @ApiResponse({ status: 200, description: "Bulk action completed successfully" }) + @ApiResponse({ status: 206, description: "Bulk action partially completed" }) + @ApiResponse({ status: 400, description: "Invalid bulk action data" }) + @ApiResponse({ status: 403, description: "Not authorized to perform bulk action" }) + performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto) { + return this.emailService.performBulkAction(userId, bulkActionDto); + } } diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index e65c67c..523a039 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -5,6 +5,7 @@ import { MailboxResolverService } from "./mailbox-resolver.service"; import { EmailMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; +import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; import { SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; import { EmailGateway } from "../email.gateway"; @@ -704,4 +705,130 @@ export class EmailService { throw error; } } + + //============================================== + async moveMessageToJunk(userId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to junk for user: ${userId}`); + + try { + const messageLocation = await this.findMessageMailbox(userId, messageId); + + if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); + + const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(userId); + + this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.Junk} for user: ${userId}`); + + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { + moveTo: junkMailboxId, + }), + ); + + // Emit WebSocket notification + const payload: EmailMovePayload = { + messageId, + userId, + fromMailboxId: messageLocation.mailboxId, + toMailboxId: junkMailboxId, + fromMailboxName: messageLocation.mailboxName, + toMailboxName: MailboxEnum.Junk, + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageMoved(payload); + + this.logger.log(`Message ${messageId} moved to junk successfully from ${messageLocation.mailboxName}`); + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY, + result, + }; + } catch (error) { + this.logger.error( + `Failed to move message ${messageId} to junk for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + throw error; + } + } + + //============================================== + // BULK ACTIONS + //============================================== + async performBulkAction(userId: string, bulkActionDto: BulkActionDto) { + this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${userId}`); + + const results = { + successful: [] as number[], + failed: [] as { messageId: number; error: string }[], + }; + + // Process each message ID + for (const messageId of bulkActionDto.messageIds) { + try { + switch (bulkActionDto.action) { + case BulkActionType.SEEN: + await this.markMessageAsSeen(userId, messageId); + break; + case BulkActionType.DELETE: + await this.deleteMessage(userId, messageId); + break; + case BulkActionType.ARCHIVE: + await this.moveMessageToArchive(userId, messageId); + break; + case BulkActionType.UNARCHIVE: + await this.moveMessageToInboxFromArchive(userId, messageId); + break; + case BulkActionType.FAVORITE: + await this.moveMessageToFavorite(userId, messageId); + break; + case BulkActionType.UNFAVORITE: + await this.moveMessageToInboxFromFavorite(userId, messageId); + break; + case BulkActionType.JUNK: + await this.moveMessageToJunk(userId, messageId); + break; + case BulkActionType.TRASH: + await this.moveMessageToTrash(userId, messageId); + break; + case BulkActionType.RESTORE: + await this.moveMessageToInboxFromTrash(userId, messageId); + break; + default: + throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`); + } + + results.successful.push(messageId); + this.logger.log(`Successfully performed ${bulkActionDto.action} on message ${messageId}`); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : "Unknown error"; + results.failed.push({ messageId, error: errorMessage }); + this.logger.error(`Failed to perform ${bulkActionDto.action} on message ${messageId}: ${errorMessage}`); + } + } + + // Determine response message based on results + let message: string; + if (results.failed.length === 0) { + message = EmailMessage.BULK_ACTION_COMPLETED_SUCCESSFULLY; + } else if (results.successful.length > 0) { + message = EmailMessage.BULK_ACTION_PARTIALLY_COMPLETED; + } else { + message = EmailMessage.BULK_ACTION_FAILED; + } + + this.logger.log(`Bulk action ${bulkActionDto.action} completed: ${results.successful.length} successful, ${results.failed.length} failed`); + + return { + success: results.failed.length === 0, + message, + results, + summary: { + total: bulkActionDto.messageIds.length, + successful: results.successful.length, + failed: results.failed.length, + action: bulkActionDto.action, + }, + }; + } } diff --git a/src/modules/quota-sync/services/quota-sync.service.ts b/src/modules/quota-sync/services/quota-sync.service.ts index 4b61a05..563e69d 100644 --- a/src/modules/quota-sync/services/quota-sync.service.ts +++ b/src/modules/quota-sync/services/quota-sync.service.ts @@ -25,10 +25,9 @@ export class QuotaSyncService { ) {} /** - * Sync quota usage every 15 minutes + * Sync quota usage every 2 hours */ - @Cron(CronExpression.EVERY_10_SECONDS) - // @Cron("0 */15 * * * *") + @Cron(CronExpression.EVERY_2_HOURS) async syncAllUsersQuota(): Promise { const em = this.em.fork(); this.logger.log("Starting quota synchronization for all users"); @@ -67,7 +66,7 @@ export class QuotaSyncService { /** * Sync business quota usage every 4 hours */ - @Cron(CronExpression.EVERY_10_SECONDS) + @Cron(CronExpression.EVERY_4_HOURS) async syncAllBusinessQuotas(): Promise { const em = this.em.fork(); this.logger.log("Starting business quota synchronization");