chore: refactor search mehtod

This commit is contained in:
mahyargdz
2025-07-23 15:27:22 +03:30
parent c739e0bd59
commit 95ed0605bb
13 changed files with 254 additions and 158 deletions
+20 -3
View File
@@ -241,9 +241,22 @@ export const enum EmailMessage {
MESSAGE_NOT_FOUND = "پیام یافت نشد", MESSAGE_NOT_FOUND = "پیام یافت نشد",
// Bulk action messages // Bulk action messages
BULK_ACTION_COMPLETED_SUCCESSFULLY = "عملیات گروهی با موفقیت انجام شد", BULK_ACTION_COMPLETED_SUCCESSFULLY = "[count] پیام با موفقیت [action] شد",
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد", BULK_ACTION_PARTIALLY_COMPLETED = "[successful] از [total] پیام [action] شد. [failed] پیام با خطا مواجه شد",
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد", BULK_ACTION_FAILED = "هیچ پیامی [action] نشد. تمام [total] تلاش ناموفق بود",
// Bulk action display names
BULK_ACTION_SEEN = "خوانده شده علامت‌گذاری",
BULK_ACTION_DELETE = "حذف",
BULK_ACTION_ARCHIVE = "آرشیو",
BULK_ACTION_UNARCHIVE = "از آرشیو خارج",
BULK_ACTION_FAVORITE = "به علاقه مندی ها منتقل",
BULK_ACTION_UNFAVORITE = "از علاقه مندی ها حذف",
BULK_ACTION_JUNK = "به جانک منتقل",
BULK_ACTION_NOTJUNK = "از جانک از حذف",
BULK_ACTION_TRASH = "به سطل زباله منتقل",
BULK_ACTION_RESTORE = "از سطل زباله بازیابی",
BULK_ACTION_PROCESSED = "پردازش",
USER_NOT_FOUND = "کاربر یافت نشد", USER_NOT_FOUND = "کاربر یافت نشد",
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد", PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد",
SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است", SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است",
@@ -253,6 +266,10 @@ export const enum EmailMessage {
MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد", MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد",
ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد", ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد",
IS_FORWARD_MUST_BE_BOOLEAN = "IS_FORWARD_MUST_BE_BOOLEAN", IS_FORWARD_MUST_BE_BOOLEAN = "IS_FORWARD_MUST_BE_BOOLEAN",
// Favorite/Star messages (Gmail-like behavior)
MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به عنوان مورد علاقه علامت گذاری شد",
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد",
} }
export const enum WebSocketMessage { export const enum WebSocketMessage {
@@ -62,7 +62,7 @@ export class DomainAutomationService {
try { try {
const mailboxesToCreate = [ const mailboxesToCreate = [
{ path: MailboxEnum.ARCHIVE, name: "Archive" }, { path: MailboxEnum.ARCHIVE, name: "Archive" },
{ path: MailboxEnum.FAVORITE, name: "Favorite" }, // { path: MailboxEnum.STARRED, name: "Starred" },
]; ];
for (const mailbox of mailboxesToCreate) { for (const mailbox of mailboxesToCreate) {
+1 -1
View File
@@ -35,6 +35,6 @@ export class BulkActionDto {
enum: BulkActionType, enum: BulkActionType,
}) })
@IsNotEmpty({ message: "Action type is required" }) @IsNotEmpty({ message: "Action type is required" })
@IsEnum(BulkActionType, { message: "Action must be one of: seen, delete, archive, favorite, junk, trash" }) @IsEnum(BulkActionType, { message: "Action must be one of: seen, delete, archive, favorite, unfavorite, junk, trash" })
action: BulkActionType; action: BulkActionType;
} }
+32 -70
View File
@@ -137,11 +137,11 @@ export class EmailController {
} }
@Get("messages/favorite") @Get("messages/favorite")
@ApiOperation({ summary: "List messages in favorite mailbox" }) @ApiOperation({ summary: "List messages in starred mailbox" })
@ApiResponse({ status: 200, description: "List of favorite messages" }) @ApiResponse({ status: 200, description: "List of starred messages" })
@ApiResponse({ status: 404, description: "favorite mailbox not found" }) @ApiResponse({ status: 404, description: "starred mailbox not found" })
getFavoriteMessages(@UserDec("wildduckUserId") userId: string, @Query() query: MessageListQueryDto) { getStarredMessages(@UserDec("wildduckUserId") userId: string, @Query() query: MessageListQueryDto) {
return this.emailService.getFavoriteMessages(userId, query); return this.emailService.getStarredMessages(userId, query);
} }
@Get("messages/drafts") @Get("messages/drafts")
@@ -189,15 +189,6 @@ export class EmailController {
return this.emailService.moveMessageToArchive(userId, Number(params.messageId), query.mailbox); return this.emailService.moveMessageToArchive(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/favorite")
@ApiOperation({ summary: "Move a message to favorite mailbox" })
@ApiResponse({ status: 200, description: "Message moved to favorite successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToFavorite(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/trash") @Patch("messages/:messageId/trash")
@ApiOperation({ summary: "Move a message to trash mailbox" }) @ApiOperation({ summary: "Move a message to trash mailbox" })
@ApiResponse({ status: 200, description: "Message moved to trash successfully" }) @ApiResponse({ status: 200, description: "Message moved to trash successfully" })
@@ -216,20 +207,38 @@ export class EmailController {
return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId), query.mailbox); return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/favorite")
@ApiOperation({ summary: "Star a message (Gmail-like behavior)" })
@ApiResponse({ status: 200, description: "Message starred successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to star message" })
starMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.starMessage(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/unfavorite") @Patch("messages/:messageId/unfavorite")
@ApiOperation({ summary: "Move a message from favorite back to inbox" }) @ApiOperation({ summary: "Unstar a message (Gmail-like behavior)" })
@ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" }) @ApiResponse({ status: 200, description: "Message unstarred successfully" })
@ApiResponse({ status: 404, description: "Message not found in favorite" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to unstar message" })
moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { unstarMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId), query.mailbox); return this.emailService.unstarMessage(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/toggle-favorite")
@ApiOperation({ summary: "Toggle star status of a message (Gmail-like behavior)" })
@ApiResponse({ status: 200, description: "Message star status toggled successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to modify message" })
toggleMessageStar(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.toggleMessageStar(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/restore") @Patch("messages/:messageId/restore")
@ApiOperation({ summary: "Restore a message from trash back to inbox" }) @ApiOperation({ summary: "Move a message from trash back to inbox" })
@ApiResponse({ status: 200, description: "Message restored from trash to inbox successfully" }) @ApiResponse({ status: 200, description: "Message restored to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in trash" }) @ApiResponse({ status: 404, description: "Message not found in trash" })
@ApiResponse({ status: 403, description: "Not authorized to restore message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId), query.mailbox); return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId), query.mailbox);
} }
@@ -264,51 +273,4 @@ export class EmailController {
performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) { performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox); return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox);
} }
// // Domain Access Management Endpoints
// @Get("spam/domains/allowed")
// @ApiOperation({ summary: "List domains in allowlist (never marked as spam)" })
// @ApiResponse({ status: 200, description: "List of allowed domains" })
// listAllowedDomains(@UserDec("id") userId: string, @Query() query: ListDomainAccessQueryDto) {
// return this.emailSpamService.listAllowedDomains(userId, query);
// }
// @Get("spam/domains/blocked")
// @ApiOperation({ summary: "List domains in blocklist (always marked as spam)" })
// @ApiResponse({ status: 200, description: "List of blocked domains" })
// listBlockedDomains(@UserDec("id") userId: string, @Query() query: ListDomainAccessQueryDto) {
// return this.emailSpamService.listBlockedDomains(userId, query);
// }
// @Post("spam/domains/allowed")
// @ApiOperation({ summary: "Add domain to allowlist" })
// @ApiResponse({ status: 201, description: "Domain added to allowlist successfully" })
// @ApiResponse({ status: 400, description: "Invalid domain data" })
// addDomainToAllowlist(@UserDec("id") userId: string, @Body() createDomainDto: CreateDomainAccessDto) {
// return this.emailSpamService.addDomainToAllowlist(createDomainDto.domain, createDomainDto.description, userId);
// }
// @Post("spam/domains/blocked")
// @ApiOperation({ summary: "Add domain to blocklist" })
// @ApiResponse({ status: 201, description: "Domain added to blocklist successfully" })
// @ApiResponse({ status: 400, description: "Invalid domain data" })
// addDomainToBlocklist(@UserDec("id") userId: string, @Body() createDomainDto: CreateDomainAccessDto) {
// return this.emailSpamService.addDomainToBlocklist(createDomainDto.domain, createDomainDto.description, userId);
// }
// @Delete("spam/domains/allowed/:domain")
// @ApiOperation({ summary: "Remove domain from allowlist" })
// @ApiResponse({ status: 200, description: "Domain removed from allowlist successfully" })
// @ApiResponse({ status: 404, description: "Domain not found in allowlist" })
// removeDomainFromAllowlist(@UserDec("id") userId: string, @Param("domain") domain: string) {
// return this.emailSpamService.removeDomainFromAllowlist(domain, userId);
// }
// @Delete("spam/domains/blocked/:domain")
// @ApiOperation({ summary: "Remove domain from blocklist" })
// @ApiResponse({ status: 200, description: "Domain removed from blocklist successfully" })
// @ApiResponse({ status: 404, description: "Domain not found in blocklist" })
// removeDomainFromBlocklist(@UserDec("id") userId: string, @Param("domain") domain: string) {
// return this.emailSpamService.removeDomainFromBlocklist(domain, userId);
// }
} }
@@ -140,6 +140,16 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_UNREAD, data, `Message ${data.messageId} marked as unread`); return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_UNREAD, data, `Message ${data.messageId} marked as unread`);
} }
//************************************************************************ */
async notifyEmailFlagged(userId: string, data: EmailStatusPayload) {
return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_FLAGGED, data, `Message ${data.messageId} flagged`);
}
//************************************************************************ */
async notifyEmailUnflagged(userId: string, data: EmailStatusPayload) {
return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_UNFLAGGED, data, `Message ${data.messageId} unflagged`);
}
//************************************************************************ */ //************************************************************************ */
async notifyEmailDeleted(userId: string, data: EmailDeletePayload) { async notifyEmailDeleted(userId: string, data: EmailDeletePayload) {
return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_DELETED, data, `Message ${data.messageId} deleted`); return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_DELETED, data, `Message ${data.messageId} deleted`);
@@ -5,5 +5,5 @@ export interface UserMailboxIds {
trash: string; trash: string;
junk: string; junk: string;
archive: string; archive: string;
favorite: string; // starred: string;
} }
@@ -165,6 +165,76 @@ export class EmailNotificationService {
} }
} }
//************************************************************************ */
async notifyEmailFlagged(params: EmailStatusNotificationParams) {
try {
// Find the user to get the correct User entity ID for websocket notifications
const em = this.em.fork();
const user = await em.findOne(User, {
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: EmailStatusPayload = {
userId: user.id, // Use User entity ID for websocket
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailFlagged(user.id, payload);
if (success) {
this.logger.log(`Email flagged notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email flagged notification for user ${user.id}: user not connected`);
}
} catch (error) {
this.logger.error(`Failed to notify email flagged for user ${params.userId}:`, error);
}
}
//************************************************************************ */
async notifyEmailUnflagged(params: EmailStatusNotificationParams) {
try {
// Find the user to get the correct User entity ID for websocket notifications
const em = this.em.fork();
const user = await em.findOne(User, {
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: EmailStatusPayload = {
userId: user.id, // Use User entity ID for websocket
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailUnflagged(user.id, payload);
if (success) {
this.logger.log(`Email unflagged notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email unflagged notification for user ${user.id}: user not connected`);
}
} catch (error) {
this.logger.error(`Failed to notify email unflagged for user ${params.userId}:`, error);
}
}
//************************************************************************ */ //************************************************************************ */
async notifyMailboxUpdate(params: MailboxUpdateNotificationParams) { async notifyMailboxUpdate(params: MailboxUpdateNotificationParams) {
try { try {
+111 -57
View File
@@ -7,6 +7,7 @@ import { EmailNotificationService } from "./email-notification.service";
import { EmailSpamService } from "./email-spam.service"; import { EmailSpamService } from "./email-spam.service";
import { MailboxResolverService } from "./mailbox-resolver.service"; import { MailboxResolverService } from "./mailbox-resolver.service";
import { EmailMessage } from "../../../common/enums/message.enum"; import { EmailMessage } from "../../../common/enums/message.enum";
import { SearchMessagesMailServerQueryDto } from "../../mail-server/DTO/message.dto";
import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailServerService } from "../../mail-server/services/mail-server.service";
import { TemplateProcessorService } from "../../templates/services/template-processor.service"; import { TemplateProcessorService } from "../../templates/services/template-processor.service";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
@@ -102,20 +103,27 @@ export class EmailService {
//######################################################## //########################################################
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
const wildDuckQuery = { const wildDuckQuery: SearchMessagesMailServerQueryDto = {
...query, ...query,
limit: query.limit || 20, limit: query.limit || 20,
page: query.page || 1, page: query.page || 1,
order: query.order || "desc", order: query.order || "desc",
query: query.search || query.q, mailbox: query.mailbox || "",
from: query.from || query.search,
to: query.to || query.search,
subject: query.subject || query.search,
}; };
delete wildDuckQuery.mailbox;
if (wildDuckQuery.limit > 250) wildDuckQuery.limit = 250; if (query.search) {
if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1; wildDuckQuery.query = query.search;
if (wildDuckQuery.page < 1) wildDuckQuery.page = 1; wildDuckQuery.from = query.search;
}
if (query.q) {
wildDuckQuery.query = query.q;
wildDuckQuery.from = query.q;
}
if (query.from) wildDuckQuery.from = query.from;
if (query.to) wildDuckQuery.to = query.to;
if (query.subject) wildDuckQuery.subject = query.subject;
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
@@ -315,10 +323,17 @@ export class EmailService {
} }
//============================================== //==============================================
async getFavoriteMessages(wildduckUserId: string, query: MessageListQueryDto) { //==============================================
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); async getStarredMessages(wildduckUserId: string, query: MessageListQueryDto) {
const wildDuckQuery = this.transformPaginationQuery(query); const searchQuery = {
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, favoriteMailboxId, wildDuckQuery)); ...query,
flagged: true,
limit: query.limit || 20,
page: query.page || 1,
order: query.order || "desc",
};
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, searchQuery));
return { return {
favoriteMails: response.results, favoriteMails: response.results,
@@ -451,23 +466,6 @@ export class EmailService {
}; };
} }
//==============================================
async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: favoriteMailboxId,
}),
);
return {
success: true,
message: EmailMessage.MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY,
result,
};
}
//============================================== //==============================================
async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) { async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
@@ -503,22 +501,54 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) { async starMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const result = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { flagged: true }));
const result = await firstValueFrom( await this.emailNotificationService.notifyEmailFlagged({
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { userId: wildduckUserId,
moveTo: inboxMailboxId, messageId,
}), mailboxId,
); });
return { return {
success: true, success: true,
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY, message: EmailMessage.MESSAGE_MARKED_AS_FAVORITE_SUCCESSFULLY,
result, result,
}; };
} }
//==============================================
async unstarMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
flagged: false,
}),
);
await this.emailNotificationService.notifyEmailUnflagged({
userId: wildduckUserId,
messageId,
mailboxId,
});
return {
success: true,
message: EmailMessage.MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY,
result,
};
}
//==============================================
async toggleMessageStar(wildduckUserId: string, messageId: number, mailboxId: string) {
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
if (message.flagged) {
return this.unstarMessage(wildduckUserId, messageId, mailboxId);
} else {
return this.starMessage(wildduckUserId, messageId, mailboxId);
}
}
//============================================== //==============================================
async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) { async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
@@ -588,8 +618,6 @@ export class EmailService {
}; };
} }
//==============================================
// BULK ACTIONS
//============================================== //==============================================
async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) { async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) {
const results = { const results = {
@@ -597,7 +625,6 @@ export class EmailService {
failed: [] as { messageId: number; error: string }[], failed: [] as { messageId: number; error: string }[],
}; };
// Process each message ID
for (const messageId of bulkActionDto.messageIds) { for (const messageId of bulkActionDto.messageIds) {
try { try {
switch (bulkActionDto.action) { switch (bulkActionDto.action) {
@@ -614,10 +641,10 @@ export class EmailService {
await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId); await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.FAVORITE: case BulkActionType.FAVORITE:
await this.moveMessageToFavorite(wildduckUserId, messageId, mailboxId); await this.starMessage(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.UNFAVORITE: case BulkActionType.UNFAVORITE:
await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId, mailboxId); await this.unstarMessage(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.JUNK: case BulkActionType.JUNK:
await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId); await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId);
@@ -636,7 +663,6 @@ export class EmailService {
} }
results.successful.push(messageId); results.successful.push(messageId);
this.logger.log(`Successfully performed ${bulkActionDto.action} on message ${messageId}`);
} catch (error) { } catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error"; const errorMessage = error instanceof Error ? error.message : "Unknown error";
results.failed.push({ messageId, error: errorMessage }); results.failed.push({ messageId, error: errorMessage });
@@ -644,17 +670,22 @@ export class EmailService {
} }
} }
// Determine response message based on results
let message: string; let message: string;
if (results.failed.length === 0) { const actionName = this.getActionDisplayName(bulkActionDto.action);
message = EmailMessage.BULK_ACTION_COMPLETED_SUCCESSFULLY; const totalCount = bulkActionDto.messageIds.length;
} else if (results.successful.length > 0) { const successCount = results.successful.length;
message = EmailMessage.BULK_ACTION_PARTIALLY_COMPLETED; const failedCount = results.failed.length;
} else {
message = EmailMessage.BULK_ACTION_FAILED;
}
this.logger.log(`Bulk action ${bulkActionDto.action} completed: ${results.successful.length} successful, ${results.failed.length} failed`); if (results.failed.length === 0) {
message = EmailMessage.BULK_ACTION_COMPLETED_SUCCESSFULLY.replace("[count]", totalCount.toString()).replace("[action]", actionName);
} else if (results.successful.length > 0) {
message = EmailMessage.BULK_ACTION_PARTIALLY_COMPLETED.replace("[successful]", successCount.toString())
.replace("[total]", totalCount.toString())
.replace("[action]", actionName)
.replace("[failed]", failedCount.toString());
} else {
message = EmailMessage.BULK_ACTION_FAILED.replace("[action]", actionName).replace("[total]", totalCount.toString());
}
return { return {
success: results.failed.length === 0, success: results.failed.length === 0,
@@ -671,24 +702,47 @@ export class EmailService {
//######################################################## //########################################################
private getActionDisplayName(action: BulkActionType): string {
switch (action) {
case BulkActionType.SEEN:
return EmailMessage.BULK_ACTION_SEEN;
case BulkActionType.DELETE:
return EmailMessage.BULK_ACTION_DELETE;
case BulkActionType.ARCHIVE:
return EmailMessage.BULK_ACTION_ARCHIVE;
case BulkActionType.UNARCHIVE:
return EmailMessage.BULK_ACTION_UNARCHIVE;
case BulkActionType.FAVORITE:
return EmailMessage.BULK_ACTION_FAVORITE;
case BulkActionType.UNFAVORITE:
return EmailMessage.BULK_ACTION_UNFAVORITE;
case BulkActionType.JUNK:
return EmailMessage.BULK_ACTION_JUNK;
case BulkActionType.NOTJUNK:
return EmailMessage.BULK_ACTION_NOTJUNK;
case BulkActionType.TRASH:
return EmailMessage.BULK_ACTION_TRASH;
case BulkActionType.RESTORE:
return EmailMessage.BULK_ACTION_RESTORE;
default:
return EmailMessage.BULK_ACTION_PROCESSED;
}
}
//==============================================
private transformPaginationQuery(query: MessageListQueryDto): Record<string, unknown> { private transformPaginationQuery(query: MessageListQueryDto): Record<string, unknown> {
const transformedQuery: Record<string, any> = { ...query }; const transformedQuery: Record<string, any> = { ...query };
// Set default values if not provided
if (!transformedQuery.page) transformedQuery.page = 1; if (!transformedQuery.page) transformedQuery.page = 1;
if (!transformedQuery.limit) transformedQuery.limit = 20; if (!transformedQuery.limit) transformedQuery.limit = 20;
// Ensure limit is within WildDuck's bounds (1-250)
if (transformedQuery.limit > 250) transformedQuery.limit = 250; if (transformedQuery.limit > 250) transformedQuery.limit = 250;
if (transformedQuery.limit < 1) transformedQuery.limit = 1; if (transformedQuery.limit < 1) transformedQuery.limit = 1;
// Ensure page is at least 1
if (transformedQuery.page < 1) transformedQuery.page = 1; if (transformedQuery.page < 1) transformedQuery.page = 1;
// Set default order if not provided
if (!transformedQuery.order) transformedQuery.order = "desc"; if (!transformedQuery.order) transformedQuery.order = "desc";
// Remove undefined/null values
Object.keys(transformedQuery).forEach((key) => { Object.keys(transformedQuery).forEach((key) => {
if (transformedQuery[key] === undefined || transformedQuery[key] === null) { if (transformedQuery[key] === undefined || transformedQuery[key] === null) {
delete transformedQuery[key]; delete transformedQuery[key];
@@ -34,7 +34,7 @@ export class MailboxResolverService {
trash: this.findMailboxId(mailboxes.results, MailboxEnum.TRASH), trash: this.findMailboxId(mailboxes.results, MailboxEnum.TRASH),
junk: this.findMailboxId(mailboxes.results, MailboxEnum.Junk, false), junk: this.findMailboxId(mailboxes.results, MailboxEnum.Junk, false),
archive: this.findMailboxId(mailboxes.results, MailboxEnum.ARCHIVE, false), archive: this.findMailboxId(mailboxes.results, MailboxEnum.ARCHIVE, false),
favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false), // favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false),
}; };
this.cacheMailboxIds(userId, mailboxIds); this.cacheMailboxIds(userId, mailboxIds);
@@ -106,9 +106,9 @@ export class MailboxResolverService {
return this.getMailboxId(userId, "archive"); return this.getMailboxId(userId, "archive");
} }
async getFavoriteMailboxId(userId: string): Promise<string> { // async getFavoriteMailboxId(userId: string): Promise<string> {
return this.getMailboxId(userId, "favorite"); // return this.getMailboxId(userId, "favorite");
} // }
/** /**
* Clear cache for a specific user * Clear cache for a specific user
+1 -1
View File
@@ -26,7 +26,7 @@ export class UpdateMessageDto {
expires?: string | false; expires?: string | false;
} }
export class SearchMessagesQueryDto { export class SearchMessagesMailServerQueryDto {
q?: string; q?: string;
mailbox?: string; mailbox?: string;
@@ -122,15 +122,6 @@ export interface MessageSearchResponse {
query: string; query: string;
} }
export interface MessageFlaggedResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: MessageDetails[];
}
export interface MessageUpdateResponse { export interface MessageUpdateResponse {
success: true; success: true;
id: string[]; id: string[];
@@ -4,13 +4,12 @@ import { catchError, map, throwError } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service"; import { WildDuckBaseService } from "./wildduck-base.service";
import { MailServerException } from "../../../core/exceptions/mail-server.exceptions"; import { MailServerException } from "../../../core/exceptions/mail-server.exceptions";
import { ListMessagesQueryDto, SearchMessagesQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto"; import { ListMessagesQueryDto, SearchMessagesMailServerQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto";
import { AttachmentResponse } from "../interfaces/attachment.interface"; import { AttachmentResponse } from "../interfaces/attachment.interface";
import { import {
MessageDeleteAllResponse, MessageDeleteAllResponse,
MessageDetails, MessageDetails,
MessageEventsResponse, MessageEventsResponse,
MessageFlaggedResponse,
MessageForwardResponse, MessageForwardResponse,
MessageListResponse, MessageListResponse,
MessageSearchResponse, MessageSearchResponse,
@@ -117,17 +116,10 @@ export class WildDuckMessagesService extends WildDuckBaseService {
return this.get<MessageEventsResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/events`); return this.get<MessageEventsResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/events`);
} }
/**
* List flagged messages across all mailboxes
*/
listFlaggedMessages(userId: string, query?: { order?: "asc" | "desc"; limit?: number; page?: number }): Observable<MessageFlaggedResponse> {
return this.get<MessageFlaggedResponse>(`/users/${userId}/flagged`, query);
}
/** /**
* Search for messages across all mailboxes * Search for messages across all mailboxes
*/ */
searchMessages(userId: string, query: SearchMessagesQueryDto): Observable<MessageSearchResponse> { searchMessages(userId: string, query: SearchMessagesMailServerQueryDto): Observable<MessageSearchResponse> {
return this.get<MessageSearchResponse>(`/users/${userId}/search`, query); return this.get<MessageSearchResponse>(`/users/${userId}/search`, query);
} }
+1 -1
View File
@@ -2,7 +2,7 @@ export enum MailboxEnum {
INBOX = "INBOX", INBOX = "INBOX",
DRAFTS = "Drafts", DRAFTS = "Drafts",
ARCHIVE = "Archive", //should created when user created ARCHIVE = "Archive", //should created when user created
FAVORITE = "FAVORITE", //should created when user created // STARRED = "Starred", //should created when user created
Junk = "Junk", Junk = "Junk",
SENT = "Sent Mail", SENT = "Sent Mail",
TRASH = "Trash", TRASH = "Trash",