chore: add new funcionality for the mailboxes

This commit is contained in:
mahyargdz
2025-07-12 11:36:48 +03:30
parent 344bf1de0a
commit 24e315ff46
11 changed files with 291 additions and 91 deletions
+3
View File
@@ -200,6 +200,9 @@ export const enum EmailMessage {
DRAFT_DELETED_SUCCESSFULLY = "پیش نویس با موفقیت حذف شد", DRAFT_DELETED_SUCCESSFULLY = "پیش نویس با موفقیت حذف شد",
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_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد",
MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد",
} }
export const enum WebSocketMessage { export const enum WebSocketMessage {
+16 -5
View File
@@ -24,13 +24,24 @@ export const databaseConfig: MikroOrmModuleAsyncOptions = {
ensureDatabase: { forceCheck: true, create: true, schema: "update" }, ensureDatabase: { forceCheck: true, create: true, schema: "update" },
forceUtcTimezone: true, forceUtcTimezone: true,
pool: { pool: {
min: 2, min: 5,
max: 15, max: 25,
idleTimeoutMillis: 10000, idleTimeoutMillis: 30000,
acquireTimeoutMillis: 10000, acquireTimeoutMillis: 15000,
reapIntervalMillis: 1000, reapIntervalMillis: 1000,
createTimeoutMillis: 3000, createTimeoutMillis: 8000,
destroyTimeoutMillis: 5000, 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), logger: (message) => logger.debug(message),
schemaGenerator: { schemaGenerator: {
@@ -3,11 +3,6 @@ import { RoleEnum } from "../../users/enums/role.enum";
export interface ILocalTokenPayload { export interface ILocalTokenPayload {
id: string; id: string;
role: RoleEnum; role: RoleEnum;
inboxId: string;
draftsId: string;
junkId: string;
sentId: string;
trashId: string;
wildduckUserId: string; wildduckUserId: string;
} }
+17 -22
View File
@@ -1,25 +1,23 @@
import { EntityRepository } from "@mikro-orm/core"; import { EntityRepository } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs"; import { InjectRepository } from "@mikro-orm/nestjs";
import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { BadRequestException, Injectable } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { TokensService } from "./tokens.service"; import { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum"; import { AuthMessage } from "../../../common/enums/message.enum";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { User } from "../../users/entities/user.entity"; import { User } from "../../users/entities/user.entity";
import { PasswordService } from "../../utils/services/password.service"; import { PasswordService } from "../../utils/services/password.service";
import { LoginDto } from "../DTO/login.dto"; import { LoginDto } from "../DTO/login.dto";
@Injectable() @Injectable()
export class AuthService { export class AuthService {
private readonly logger = new Logger(AuthService.name); // private readonly logger = new Logger(AuthService.name);
constructor( constructor(
@InjectRepository(User) @InjectRepository(User)
private readonly userRepository: EntityRepository<User>, private readonly userRepository: EntityRepository<User>,
private readonly tokensService: TokensService, private readonly tokensService: TokensService,
private readonly passwordService: PasswordService, 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); const user = await this.checkUserLoginCredentialWithEmail(email, password);
// Fetch user's mailboxes from WildDuck // // Fetch user's mailboxes from WildDuck
let mailboxes: string[] = []; // if (user.wildduckUserId) {
if (user.wildduckUserId) { // try {
try { // this.logger.log(`Fetching mailboxes for user ${user.id} (WildDuck ID: ${user.wildduckUserId})`);
this.logger.log(`Fetching mailboxes for user ${user.id} (WildDuck ID: ${user.wildduckUserId})`); // const mailboxesResponse = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(user.wildduckUserId));
const mailboxesResponse = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(user.wildduckUserId));
if (mailboxesResponse.success && mailboxesResponse.results) { // if (mailboxesResponse.success && mailboxesResponse.results) {
mailboxes = mailboxesResponse.results.map((mailbox) => mailbox.id); // 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}`); const tokens = await this.tokensService.generateTokens(user);
}
} 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);
return { return {
message: AuthMessage.PASSWORD_LOGIN_SUCCESS, message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
+29 -21
View File
@@ -20,16 +20,11 @@ export class TokensService {
private readonly em: EntityManager, private readonly em: EntityManager,
) {} ) {}
async generateTokens(user: User, mailboxes?: string[], em?: EntityManager) { async generateTokens(user: User, em?: EntityManager) {
return this.generateAccessAndRefreshToken( return this.generateAccessAndRefreshToken(
{ {
id: user.id, id: user.id,
role: user.role.name, role: user.role.name,
inboxId: mailboxes?.[0] || "",
draftsId: mailboxes?.[1] || "",
junkId: mailboxes?.[2] || "",
sentId: mailboxes?.[3] || "",
trashId: mailboxes?.[4] || "",
wildduckUserId: user.wildduckUserId, wildduckUserId: user.wildduckUserId,
}, },
em, em,
@@ -53,12 +48,12 @@ export class TokensService {
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) { async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
const entityManager = em || this.em.fork(); const entityManager = em || this.em.fork();
let transactionStarted = false; const isExternalTransaction = !!em && em.isInTransaction();
try { try {
if (!entityManager.isInTransaction()) { // Only start transaction if no external EntityManager with transaction is provided
if (!isExternalTransaction && !entityManager.isInTransaction()) {
await entityManager.begin(); await entityManager.begin();
transactionStarted = true;
} }
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE"); const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
@@ -73,23 +68,30 @@ export class TokensService {
expiresAt, expiresAt,
}); });
// Use persist instead of persistAndFlush when in transaction
if (isExternalTransaction) {
await entityManager.persist(token);
} else {
await entityManager.persistAndFlush(token); await entityManager.persistAndFlush(token);
if (entityManager.isInTransaction()) {
if (transactionStarted) await entityManager.commit(); await entityManager.commit();
}
}
} catch (error) { } catch (error) {
this.logger.error(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; throw error;
} }
} }
async refreshToken(oldRefreshToken: string) { async refreshToken(oldRefreshToken: string) {
const entityManager = this.em.fork(); const entityManager = this.em.fork();
let transactionStarted = false;
try { try {
await entityManager.begin(); await entityManager.begin();
transactionStarted = true;
const token = await entityManager.findOne( const token = await entityManager.findOne(
RefreshToken, RefreshToken,
@@ -102,18 +104,26 @@ export class TokensService {
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN); if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) { 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); throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
} }
await entityManager.removeAndFlush(token); // Remove the old token
const tokens = await this.generateTokens(token.user, undefined, entityManager); 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(); await entityManager.commit();
return tokens; return tokens;
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
if (transactionStarted) { if (entityManager.isInTransaction()) {
await entityManager.rollback(); await entityManager.rollback();
} }
throw error; throw error;
@@ -122,11 +132,9 @@ export class TokensService {
async invalidateRefreshToken(userId: string) { async invalidateRefreshToken(userId: string) {
const entityManager = this.em.fork(); const entityManager = this.em.fork();
let transactionStarted = false;
try { try {
await entityManager.begin(); await entityManager.begin();
transactionStarted = true;
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } }); await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`); this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
@@ -134,7 +142,7 @@ export class TokensService {
await entityManager.commit(); await entityManager.commit();
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);
if (transactionStarted) { if (entityManager.isInTransaction()) {
await entityManager.rollback(); await entityManager.rollback();
} }
throw error; throw error;
@@ -21,11 +21,6 @@ export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRAT
return { return {
id: payload.id, id: payload.id,
role: payload.role, role: payload.role,
inboxId: payload.inboxId,
draftsId: payload.draftsId,
junkId: payload.junkId,
sentId: payload.sentId,
trashId: payload.trashId,
wildduckUserId: payload.wildduckUserId, wildduckUserId: payload.wildduckUserId,
}; };
} }
+27
View File
@@ -163,4 +163,31 @@ export class EmailController {
moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToFavorite(userId, Number(params.messageId)); 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));
}
} }
+110
View File
@@ -470,4 +470,114 @@ export class EmailService {
throw error; 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;
}
}
} }
+28 -20
View File
@@ -2,46 +2,54 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator"; import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator";
export class CreateMailboxDto { export class CreateMailboxDto {
@ApiProperty({ @ApiProperty({ description: "Mailbox path with slashes as folder separators", example: "Important/Work" })
description: "Mailbox path with slashes as folder separators",
example: "Important/Work",
})
@IsString() @IsString()
path: string; path: string;
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "Retention time in milliseconds for messages in this mailbox", example: 2592000000 })
description: "Retention time in milliseconds for messages in this mailbox",
example: 2592000000,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
retention?: number; retention?: number;
} }
export class UpdateMailboxDto { export class UpdateMailboxDto {
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "New mailbox path if renaming", example: "Renamed Folder" })
description: "New mailbox path if renaming",
example: "Renamed Folder",
})
@IsOptional() @IsOptional()
@IsString() @IsString()
path?: string; path?: string;
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "New retention time in milliseconds", example: 5184000000 })
description: "New retention time in milliseconds",
example: 5184000000,
})
@IsOptional() @IsOptional()
@IsNumber() @IsNumber()
retention?: number; retention?: number;
} }
export class MailboxQueryDto { export class MailboxQueryDto {
@ApiPropertyOptional({ @ApiPropertyOptional({ description: "Include message counters in results", default: false })
description: "Include message counters in results",
default: false,
})
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
counters?: boolean; 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;
}
+20 -12
View File
@@ -1,63 +1,71 @@
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, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger"; import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger";
import { Observable } from "rxjs";
import { MailboxQueryDto } from "./DTO/mailbox.dto"; import { MailboxQueryDto } from "./DTO/mailbox.dto";
import { MailboxService } from "./services/mailbox.service"; import { MailboxService } from "./services/mailbox.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
import { CreateMailboxDto, UpdateMailboxDto } from "../mail-server/DTO"; import { CreateMailboxDto, UpdateMailboxDto } from "../mail-server/DTO";
@Controller("mailbox")
@AuthGuards() @AuthGuards()
@Controller("mailbox")
export class MailboxController { export class MailboxController {
constructor(private readonly mailboxService: MailboxService) {} 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" }) @ApiOperation({ summary: "List mailboxes for a user" })
@ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "userId", description: "User ID" })
@ApiQuery({ name: "counters", required: false, description: "Include message counters" }) @ApiQuery({ name: "counters", required: false, description: "Include message counters" })
@ApiResponse({ status: 200, description: "List of mailboxes" }) @ApiResponse({ status: 200, description: "List of mailboxes" })
@ApiResponse({ status: 404, description: "User not found" }) @ApiResponse({ status: 404, description: "User not found" })
listMailboxes(@Param("userId") userId: string, @Query() query: MailboxQueryDto): Observable<any[]> { listMailboxes(@UserDec("wildduckUserId") userId: string, @Query() query: MailboxQueryDto) {
return this.mailboxService.listMailboxes(userId, query); return this.mailboxService.listMailboxes(userId, query);
} }
@Get(":userId/mailboxes/:mailboxId") @Get("mailboxes/:mailboxId")
@ApiOperation({ summary: "Get mailbox details" }) @ApiOperation({ summary: "Get mailbox details" })
@ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "userId", description: "User ID" })
@ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" })
@ApiResponse({ status: 200, description: "Mailbox details" }) @ApiResponse({ status: 200, description: "Mailbox details" })
@ApiResponse({ status: 404, description: "Mailbox not found" }) @ApiResponse({ status: 404, description: "Mailbox not found" })
getMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable<any> { getMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string) {
return this.mailboxService.getMailbox(userId, mailboxId); return this.mailboxService.getMailbox(userId, mailboxId);
} }
@Post(":userId/mailboxes") @Post("mailboxes")
@ApiOperation({ summary: "Create a new mailbox" }) @ApiOperation({ summary: "Create a new mailbox" })
@ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "userId", description: "User ID" })
@ApiResponse({ status: 201, description: "Mailbox created successfully" }) @ApiResponse({ status: 201, description: "Mailbox created successfully" })
@ApiResponse({ status: 400, description: "Invalid mailbox data" }) @ApiResponse({ status: 400, description: "Invalid mailbox data" })
createMailbox(@Param("userId") userId: string, @Body() createMailboxDto: CreateMailboxDto): Observable<any> { createMailbox(@UserDec("wildduckUserId") userId: string, @Body() createMailboxDto: CreateMailboxDto) {
return this.mailboxService.createMailbox(userId, createMailboxDto); return this.mailboxService.createMailbox(userId, createMailboxDto);
} }
@Patch(":userId/mailboxes/:mailboxId") @Patch("mailboxes/:mailboxId")
@ApiOperation({ summary: "Update mailbox" }) @ApiOperation({ summary: "Update mailbox" })
@ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "userId", description: "User ID" })
@ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" })
@ApiResponse({ status: 200, description: "Mailbox updated successfully" }) @ApiResponse({ status: 200, description: "Mailbox updated successfully" })
@ApiResponse({ status: 404, description: "Mailbox not found" }) @ApiResponse({ status: 404, description: "Mailbox not found" })
updateMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string, @Body() updateMailboxDto: UpdateMailboxDto): Observable<any> { updateMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string, @Body() updateMailboxDto: UpdateMailboxDto) {
return this.mailboxService.updateMailbox(userId, mailboxId, updateMailboxDto); return this.mailboxService.updateMailbox(userId, mailboxId, updateMailboxDto);
} }
@Delete(":userId/mailboxes/:mailboxId") @Delete("mailboxes/:mailboxId")
@ApiOperation({ summary: "Delete a mailbox" }) @ApiOperation({ summary: "Delete a mailbox" })
@ApiParam({ name: "userId", description: "User ID" }) @ApiParam({ name: "userId", description: "User ID" })
@ApiParam({ name: "mailboxId", description: "Mailbox ID" }) @ApiParam({ name: "mailboxId", description: "Mailbox ID" })
@ApiResponse({ status: 200, description: "Mailbox deleted successfully" }) @ApiResponse({ status: 200, description: "Mailbox deleted successfully" })
@ApiResponse({ status: 404, description: "Mailbox not found" }) @ApiResponse({ status: 404, description: "Mailbox not found" })
deleteMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable<any> { deleteMailbox(@UserDec("wildduckUserId") userId: string, @Param("mailboxId") mailboxId: string) {
return this.mailboxService.deleteMailbox(userId, mailboxId); return this.mailboxService.deleteMailbox(userId, mailboxId);
} }
} }
@@ -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 * Get a specific mailbox
*/ */