From 24e315ff465255bf4fb39ccd1b9a388243cafd4c Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sat, 12 Jul 2025 11:36:48 +0330 Subject: [PATCH] chore: add new funcionality for the mailboxes --- src/common/enums/message.enum.ts | 3 + src/configs/mikro-orm.config.ts | 21 +++- src/modules/auth/interfaces/IToken-payload.ts | 5 - src/modules/auth/services/auth.service.ts | 39 +++---- src/modules/auth/services/tokens.service.ts | 52 +++++---- .../auth/strategies/local-jwt.strategy.ts | 5 - src/modules/email/email.controller.ts | 27 +++++ src/modules/email/services/email.service.ts | 110 ++++++++++++++++++ src/modules/mailbox/DTO/mailbox.dto.ts | 48 ++++---- src/modules/mailbox/mailbox.controller.ts | 32 +++-- .../mailbox/services/mailbox.service.ts | 40 +++++++ 11 files changed, 291 insertions(+), 91 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index b4abceb..6492610 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -200,6 +200,9 @@ export const enum EmailMessage { DRAFT_DELETED_SUCCESSFULLY = "پیش نویس با موفقیت حذف شد", MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت به آرشیو منتقل شد", MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY = "پیام با موفقیت به علاقه مندی ها منتقل شد", + MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY = "پیام با موفقیت به سطل زباله منتقل شد", + MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد", + MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد", } export const enum WebSocketMessage { diff --git a/src/configs/mikro-orm.config.ts b/src/configs/mikro-orm.config.ts index f0d692d..14d295d 100755 --- a/src/configs/mikro-orm.config.ts +++ b/src/configs/mikro-orm.config.ts @@ -24,13 +24,24 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = { ensureDatabase: { forceCheck: true, create: true, schema: "update" }, forceUtcTimezone: true, pool: { - min: 2, - max: 15, - idleTimeoutMillis: 10000, - acquireTimeoutMillis: 10000, + min: 5, + max: 25, + idleTimeoutMillis: 30000, + acquireTimeoutMillis: 15000, reapIntervalMillis: 1000, - createTimeoutMillis: 3000, + createTimeoutMillis: 8000, destroyTimeoutMillis: 5000, + createRetryIntervalMillis: 200, + propagateCreateError: false, + }, + driverOptions: { + connection: { + keepAlive: true, + keepAliveInitialDelayMillis: 0, + statement_timeout: 60000, + query_timeout: 60000, + connectionTimeoutMillis: 10000, + }, }, logger: (message) => logger.debug(message), schemaGenerator: { diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts index 2a0ce54..fe727ab 100755 --- a/src/modules/auth/interfaces/IToken-payload.ts +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -3,11 +3,6 @@ import { RoleEnum } from "../../users/enums/role.enum"; export interface ILocalTokenPayload { id: string; role: RoleEnum; - inboxId: string; - draftsId: string; - junkId: string; - sentId: string; - trashId: string; wildduckUserId: string; } diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index a850579..6d2f955 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,25 +1,23 @@ import { EntityRepository } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; -import { BadRequestException, Injectable, Logger } from "@nestjs/common"; -import { firstValueFrom } from "rxjs"; +import { BadRequestException, Injectable } from "@nestjs/common"; import { TokensService } from "./tokens.service"; import { AuthMessage } from "../../../common/enums/message.enum"; -import { MailServerService } from "../../mail-server/services/mail-server.service"; import { User } from "../../users/entities/user.entity"; import { PasswordService } from "../../utils/services/password.service"; import { LoginDto } from "../DTO/login.dto"; @Injectable() export class AuthService { - private readonly logger = new Logger(AuthService.name); + // private readonly logger = new Logger(AuthService.name); constructor( @InjectRepository(User) private readonly userRepository: EntityRepository, private readonly tokensService: TokensService, private readonly passwordService: PasswordService, - private readonly mailServerService: MailServerService, + // private readonly mailServerService: MailServerService, ) {} //============================================== @@ -29,25 +27,22 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithEmail(email, password); - // Fetch user's mailboxes from WildDuck - let mailboxes: string[] = []; - if (user.wildduckUserId) { - try { - this.logger.log(`Fetching mailboxes for user ${user.id} (WildDuck ID: ${user.wildduckUserId})`); - const mailboxesResponse = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(user.wildduckUserId)); + // // Fetch user's mailboxes from WildDuck + // if (user.wildduckUserId) { + // try { + // this.logger.log(`Fetching mailboxes for user ${user.id} (WildDuck ID: ${user.wildduckUserId})`); + // const mailboxesResponse = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(user.wildduckUserId)); - if (mailboxesResponse.success && mailboxesResponse.results) { - mailboxes = mailboxesResponse.results.map((mailbox) => mailbox.id); + // if (mailboxesResponse.success && mailboxesResponse.results) { + // this.logger.log(`Successfully fetched ${mailboxesResponse.results.length} mailboxes for user ${user.id}`); + // } + // } catch (error: any) { + // this.logger.error(`Failed to fetch mailboxes for user ${user.id}: ${error.message}`); + // // Continue with login even if mailbox fetch fails + // } + // } - this.logger.log(`Successfully fetched ${mailboxes.length} mailboxes for user ${user.id}`); - } - } catch (error: any) { - this.logger.error(`Failed to fetch mailboxes for user ${user.id}: ${error.message}`); - // Continue with login even if mailbox fetch fails - } - } - - const tokens = await this.tokensService.generateTokens(user, mailboxes); + const tokens = await this.tokensService.generateTokens(user); return { message: AuthMessage.PASSWORD_LOGIN_SUCCESS, diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index 68f8af7..f6a1747 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -20,16 +20,11 @@ export class TokensService { private readonly em: EntityManager, ) {} - async generateTokens(user: User, mailboxes?: string[], em?: EntityManager) { + async generateTokens(user: User, em?: EntityManager) { return this.generateAccessAndRefreshToken( { id: user.id, role: user.role.name, - inboxId: mailboxes?.[0] || "", - draftsId: mailboxes?.[1] || "", - junkId: mailboxes?.[2] || "", - sentId: mailboxes?.[3] || "", - trashId: mailboxes?.[4] || "", wildduckUserId: user.wildduckUserId, }, em, @@ -53,12 +48,12 @@ export class TokensService { async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) { const entityManager = em || this.em.fork(); - let transactionStarted = false; + const isExternalTransaction = !!em && em.isInTransaction(); try { - if (!entityManager.isInTransaction()) { + // Only start transaction if no external EntityManager with transaction is provided + if (!isExternalTransaction && !entityManager.isInTransaction()) { await entityManager.begin(); - transactionStarted = true; } const refreshExpire = this.configService.getOrThrow("REFRESH_TOKEN_EXPIRE"); @@ -73,23 +68,30 @@ export class TokensService { expiresAt, }); - await entityManager.persistAndFlush(token); - - if (transactionStarted) await entityManager.commit(); + // Use persist instead of persistAndFlush when in transaction + if (isExternalTransaction) { + await entityManager.persist(token); + } else { + await entityManager.persistAndFlush(token); + if (entityManager.isInTransaction()) { + await entityManager.commit(); + } + } } catch (error) { this.logger.error(error); - if (transactionStarted) await entityManager.rollback(); + // Only rollback if we started the transaction + if (!isExternalTransaction && entityManager.isInTransaction()) { + await entityManager.rollback(); + } throw error; } } async refreshToken(oldRefreshToken: string) { const entityManager = this.em.fork(); - let transactionStarted = false; try { await entityManager.begin(); - transactionStarted = true; const token = await entityManager.findOne( RefreshToken, @@ -102,18 +104,26 @@ export class TokensService { if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); if (dayjs(token.expiresAt).isBefore(dayjs())) { - await entityManager.removeAndFlush(token); + await entityManager.remove(token); + await entityManager.flush(); + await entityManager.commit(); throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED); } - await entityManager.removeAndFlush(token); - const tokens = await this.generateTokens(token.user, undefined, entityManager); + // Remove the old token + await entityManager.remove(token); + // Generate new tokens within the same transaction + const tokens = await this.generateTokens(token.user, entityManager); + + // Commit the transaction + await entityManager.flush(); await entityManager.commit(); + return tokens; } catch (error) { this.logger.error(error); - if (transactionStarted) { + if (entityManager.isInTransaction()) { await entityManager.rollback(); } throw error; @@ -122,11 +132,9 @@ export class TokensService { async invalidateRefreshToken(userId: string) { const entityManager = this.em.fork(); - let transactionStarted = false; try { await entityManager.begin(); - transactionStarted = true; await entityManager.nativeDelete(RefreshToken, { user: { id: userId } }); this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`); @@ -134,7 +142,7 @@ export class TokensService { await entityManager.commit(); } catch (error) { this.logger.error(error); - if (transactionStarted) { + if (entityManager.isInTransaction()) { await entityManager.rollback(); } throw error; diff --git a/src/modules/auth/strategies/local-jwt.strategy.ts b/src/modules/auth/strategies/local-jwt.strategy.ts index c655014..a47d175 100644 --- a/src/modules/auth/strategies/local-jwt.strategy.ts +++ b/src/modules/auth/strategies/local-jwt.strategy.ts @@ -21,11 +21,6 @@ export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRAT return { id: payload.id, role: payload.role, - inboxId: payload.inboxId, - draftsId: payload.draftsId, - junkId: payload.junkId, - sentId: payload.sentId, - trashId: payload.trashId, wildduckUserId: payload.wildduckUserId, }; } diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 5676423..f7a1681 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -163,4 +163,31 @@ export class EmailController { moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { return this.emailService.moveMessageToFavorite(userId, Number(params.messageId)); } + + @Patch("messages/:messageId/trash") + @ApiOperation({ summary: "Move a message to trash mailbox" }) + @ApiResponse({ status: 200, description: "Message moved to trash successfully" }) + @ApiResponse({ status: 404, description: "Message not found" }) + @ApiResponse({ status: 403, description: "Not authorized to move message" }) + moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { + return this.emailService.moveMessageToTrash(userId, Number(params.messageId)); + } + + @Patch("messages/:messageId/unarchive") + @ApiOperation({ summary: "Move a message from archive back to inbox" }) + @ApiResponse({ status: 200, description: "Message moved from archive to inbox successfully" }) + @ApiResponse({ status: 404, description: "Message not found in archive" }) + @ApiResponse({ status: 403, description: "Not authorized to move message" }) + moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { + return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId)); + } + + @Patch("messages/:messageId/unfavorite") + @ApiOperation({ summary: "Move a message from favorite back to inbox" }) + @ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" }) + @ApiResponse({ status: 404, description: "Message not found in favorite" }) + @ApiResponse({ status: 403, description: "Not authorized to move message" }) + moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { + return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId)); + } } diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 78a5e4b..b0f8fae 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -470,4 +470,114 @@ export class EmailService { throw error; } } + + async moveMessageToTrash(userId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to trash for user: ${userId}`); + + try { + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(userId); + + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(userId, inboxMailboxId, messageId, { + moveTo: trashMailboxId, + }), + ); + + this.logger.log(`Message ${messageId} moved to trash successfully`); + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY, + result, + }; + } catch (error) { + this.logger.error( + `Failed to move message ${messageId} to trash for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + throw error; + } + } + + //============================================== + async moveMessageToInboxFromArchive(userId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${userId}`); + + try { + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(userId); + + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(userId, archiveMailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); + + // Emit WebSocket notification + const payload: EmailMovePayload = { + messageId, + userId, + fromMailboxId: archiveMailboxId, + toMailboxId: inboxMailboxId, + fromMailboxName: "Archive", + toMailboxName: "Inbox", + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageMoved(payload); + + this.logger.log(`Message ${messageId} moved from archive to inbox successfully`); + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY, + result, + }; + } catch (error) { + this.logger.error( + `Failed to move message ${messageId} from archive to inbox for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + throw error; + } + } + + //============================================== + async moveMessageToInboxFromFavorite(userId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${userId}`); + + try { + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(userId); + + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(userId, favoriteMailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); + + // Emit WebSocket notification + const payload: EmailMovePayload = { + messageId, + userId, + fromMailboxId: favoriteMailboxId, + toMailboxId: inboxMailboxId, + fromMailboxName: "Favorite", + toMailboxName: "Inbox", + timestamp: new Date().toISOString(), + }; + await this.emailGateway.notifyMessageMoved(payload); + + this.logger.log(`Message ${messageId} moved from favorite to inbox successfully`); + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY, + result, + }; + } catch (error) { + this.logger.error( + `Failed to move message ${messageId} from favorite to inbox for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + throw error; + } + } } diff --git a/src/modules/mailbox/DTO/mailbox.dto.ts b/src/modules/mailbox/DTO/mailbox.dto.ts index b466c62..cff0ada 100644 --- a/src/modules/mailbox/DTO/mailbox.dto.ts +++ b/src/modules/mailbox/DTO/mailbox.dto.ts @@ -2,46 +2,54 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator"; export class CreateMailboxDto { - @ApiProperty({ - description: "Mailbox path with slashes as folder separators", - example: "Important/Work", - }) + @ApiProperty({ description: "Mailbox path with slashes as folder separators", example: "Important/Work" }) @IsString() path: string; - @ApiPropertyOptional({ - description: "Retention time in milliseconds for messages in this mailbox", - example: 2592000000, - }) + @ApiPropertyOptional({ description: "Retention time in milliseconds for messages in this mailbox", example: 2592000000 }) @IsOptional() @IsNumber() retention?: number; } export class UpdateMailboxDto { - @ApiPropertyOptional({ - description: "New mailbox path if renaming", - example: "Renamed Folder", - }) + @ApiPropertyOptional({ description: "New mailbox path if renaming", example: "Renamed Folder" }) @IsOptional() @IsString() path?: string; - @ApiPropertyOptional({ - description: "New retention time in milliseconds", - example: 5184000000, - }) + @ApiPropertyOptional({ description: "New retention time in milliseconds", example: 5184000000 }) @IsOptional() @IsNumber() retention?: number; } export class MailboxQueryDto { - @ApiPropertyOptional({ - description: "Include message counters in results", - default: false, - }) + @ApiPropertyOptional({ description: "Include message counters in results", default: false }) @IsOptional() @IsBoolean() counters?: boolean; } + +export class MailboxCountDto { + @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439011" }) + id: string; + + @ApiProperty({ description: "Mailbox name", example: "INBOX" }) + name: string; + + @ApiProperty({ description: "Mailbox path", example: "INBOX" }) + path: string; + + @ApiProperty({ description: "Special use flag", example: "\\Inbox", nullable: true }) + specialUse: string | null | false; + + @ApiProperty({ description: "Total number of messages in the mailbox", example: 42 }) + total: number; + + @ApiProperty({ description: "Number of unseen messages in the mailbox", example: 5 }) + unseen: number; + + @ApiProperty({ description: "Total size of messages in bytes", example: 1048576 }) + size: number; +} diff --git a/src/modules/mailbox/mailbox.controller.ts b/src/modules/mailbox/mailbox.controller.ts index 05fc907..9959801 100644 --- a/src/modules/mailbox/mailbox.controller.ts +++ b/src/modules/mailbox/mailbox.controller.ts @@ -1,63 +1,71 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger"; -import { Observable } from "rxjs"; import { MailboxQueryDto } from "./DTO/mailbox.dto"; import { MailboxService } from "./services/mailbox.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; import { CreateMailboxDto, UpdateMailboxDto } from "../mail-server/DTO"; -@Controller("mailbox") @AuthGuards() +@Controller("mailbox") export class MailboxController { constructor(private readonly mailboxService: MailboxService) {} - @Get(":userId/mailboxes") + @Get("counts") + @ApiOperation({ summary: "Get mailbox message counts for current user" }) + @ApiResponse({ status: 401, description: "Unauthorized" }) + @ApiResponse({ status: 404, description: "User not found" }) + getMailboxCounts(@UserDec("wildduckUserId") userId: string) { + return this.mailboxService.getMailboxCounts(userId); + } + + @Get("mailboxes") @ApiOperation({ summary: "List mailboxes for a user" }) @ApiParam({ name: "userId", description: "User ID" }) @ApiQuery({ name: "counters", required: false, description: "Include message counters" }) @ApiResponse({ status: 200, description: "List of mailboxes" }) @ApiResponse({ status: 404, description: "User not found" }) - listMailboxes(@Param("userId") userId: string, @Query() query: MailboxQueryDto): Observable { + listMailboxes(@UserDec("wildduckUserId") userId: string, @Query() query: MailboxQueryDto) { return this.mailboxService.listMailboxes(userId, query); } - @Get(":userId/mailboxes/:mailboxId") + @Get("mailboxes/:mailboxId") @ApiOperation({ summary: "Get mailbox details" }) @ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiResponse({ status: 200, description: "Mailbox details" }) @ApiResponse({ status: 404, description: "Mailbox not found" }) - getMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable { + getMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string) { return this.mailboxService.getMailbox(userId, mailboxId); } - @Post(":userId/mailboxes") + @Post("mailboxes") @ApiOperation({ summary: "Create a new mailbox" }) @ApiParam({ name: "userId", description: "User ID" }) @ApiResponse({ status: 201, description: "Mailbox created successfully" }) @ApiResponse({ status: 400, description: "Invalid mailbox data" }) - createMailbox(@Param("userId") userId: string, @Body() createMailboxDto: CreateMailboxDto): Observable { + createMailbox(@UserDec("wildduckUserId") userId: string, @Body() createMailboxDto: CreateMailboxDto) { return this.mailboxService.createMailbox(userId, createMailboxDto); } - @Patch(":userId/mailboxes/:mailboxId") + @Patch("mailboxes/:mailboxId") @ApiOperation({ summary: "Update mailbox" }) @ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiResponse({ status: 200, description: "Mailbox updated successfully" }) @ApiResponse({ status: 404, description: "Mailbox not found" }) - updateMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string, @Body() updateMailboxDto: UpdateMailboxDto): Observable { + updateMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string, @Body() updateMailboxDto: UpdateMailboxDto) { return this.mailboxService.updateMailbox(userId, mailboxId, updateMailboxDto); } - @Delete(":userId/mailboxes/:mailboxId") + @Delete("mailboxes/:mailboxId") @ApiOperation({ summary: "Delete a mailbox" }) @ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiResponse({ status: 200, description: "Mailbox deleted successfully" }) @ApiResponse({ status: 404, description: "Mailbox not found" }) - deleteMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable { + deleteMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string) { return this.mailboxService.deleteMailbox(userId, mailboxId); } } diff --git a/src/modules/mailbox/services/mailbox.service.ts b/src/modules/mailbox/services/mailbox.service.ts index 12e8537..49343a7 100644 --- a/src/modules/mailbox/services/mailbox.service.ts +++ b/src/modules/mailbox/services/mailbox.service.ts @@ -29,6 +29,46 @@ export class MailboxService { ); } + /** + * Get mailbox message counts for a user + */ + getMailboxCounts(userId: string) { + this.logger.log(`Getting mailbox counts for user: ${userId}`); + + return this.mailServerService.mailboxes.listMailboxes(userId, { counters: true }).pipe( + map((response) => { + this.logger.log(`Found ${response.results.length} mailboxes with counts for user: ${userId}`); + + const mailboxes = response.results.map((mailbox) => ({ + id: mailbox.id, + name: mailbox.name, + path: mailbox.path, + specialUse: mailbox.specialUse, + total: mailbox.total || 0, + unseen: mailbox.unseen || 0, + size: mailbox.size || 0, + })); + + const totalMailboxes = mailboxes.length; + const totalMessages = mailboxes.reduce((sum, mailbox) => sum + mailbox.total, 0); + const totalUnseen = mailboxes.reduce((sum, mailbox) => sum + mailbox.unseen, 0); + const totalSize = mailboxes.reduce((sum, mailbox) => sum + mailbox.size, 0); + + return { + mailboxes, + totalMailboxes, + totalMessages, + totalUnseen, + totalSize, + }; + }), + catchError((error) => { + this.logger.error(`Failed to get mailbox counts for user ${userId}: ${error.message}`); + return throwError(() => error); + }), + ); + } + /** * Get a specific mailbox */