chore: add bulk operation

This commit is contained in:
mahyargdz
2025-07-12 15:59:47 +03:30
parent 7a0c337cda
commit b01d77e36e
5 changed files with 198 additions and 4 deletions
+6
View File
@@ -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 {
+39
View File
@@ -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;
}
+23
View File
@@ -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);
}
}
+127
View File
@@ -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,
},
};
}
}
@@ -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<void> {
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<void> {
const em = this.em.fork();
this.logger.log("Starting business quota synchronization");