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_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت به آرشیو منتقل شد",
|
||||||
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد",
|
MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد",
|
||||||
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = "پیام با موفقیت به سطل زباله منتقل شد",
|
MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = "پیام با موفقیت به سطل زباله منتقل شد",
|
||||||
|
MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY = "پیام با موفقیت به جانک منتقل شد",
|
||||||
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد",
|
MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد",
|
||||||
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد",
|
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد",
|
||||||
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = "پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد",
|
MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = "پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد",
|
||||||
MESSAGE_NOT_FOUND = "پیام یافت نشد",
|
MESSAGE_NOT_FOUND = "پیام یافت نشد",
|
||||||
|
|
||||||
|
// Bulk action messages
|
||||||
|
BULK_ACTION_COMPLETED_SUCCESSFULLY = "عملیات گروهی با موفقیت انجام شد",
|
||||||
|
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد",
|
||||||
|
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WebSocketMessage {
|
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 { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||||
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
import { ApiOperation, ApiResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { BulkActionDto } from "./DTO/bulk-actions.dto";
|
||||||
import { MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto";
|
import { MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto";
|
||||||
import { MessageListQueryDto } from "./DTO/email-query.dto";
|
import { MessageListQueryDto } from "./DTO/email-query.dto";
|
||||||
import { SearchMessagesQueryDto } 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) {
|
moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
|
||||||
return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId));
|
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 { EmailMessage } from "../../../common/enums/message.enum";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
||||||
|
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
||||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
||||||
import { SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
import { SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
||||||
import { EmailGateway } from "../email.gateway";
|
import { EmailGateway } from "../email.gateway";
|
||||||
@@ -704,4 +705,130 @@ export class EmailService {
|
|||||||
throw error;
|
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(CronExpression.EVERY_2_HOURS)
|
||||||
// @Cron("0 */15 * * * *")
|
|
||||||
async syncAllUsersQuota(): Promise<void> {
|
async syncAllUsersQuota(): Promise<void> {
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
this.logger.log("Starting quota synchronization for all users");
|
this.logger.log("Starting quota synchronization for all users");
|
||||||
@@ -67,7 +66,7 @@ export class QuotaSyncService {
|
|||||||
/**
|
/**
|
||||||
* Sync business quota usage every 4 hours
|
* Sync business quota usage every 4 hours
|
||||||
*/
|
*/
|
||||||
@Cron(CronExpression.EVERY_10_SECONDS)
|
@Cron(CronExpression.EVERY_4_HOURS)
|
||||||
async syncAllBusinessQuotas(): Promise<void> {
|
async syncAllBusinessQuotas(): Promise<void> {
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
this.logger.log("Starting business quota synchronization");
|
this.logger.log("Starting business quota synchronization");
|
||||||
|
|||||||
Reference in New Issue
Block a user