chore: add bulk operation
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user