chore: add new funcionality for the mailboxes
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
await entityManager.persistAndFlush(token);
|
// Use persist instead of persistAndFlush when in transaction
|
||||||
|
if (isExternalTransaction) {
|
||||||
if (transactionStarted) await entityManager.commit();
|
await entityManager.persist(token);
|
||||||
|
} else {
|
||||||
|
await entityManager.persistAndFlush(token);
|
||||||
|
if (entityManager.isInTransaction()) {
|
||||||
|
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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user