From 24783634fda0db7c7e8326f67c5c1dc7df369cf7 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Wed, 23 Jul 2025 12:17:38 +0330 Subject: [PATCH] chore: add filter and search --- src/modules/email/DTO/email-query.dto.ts | 9 ++--- src/modules/email/email.controller.ts | 20 +---------- src/modules/email/services/email.service.ts | 39 ++++++++++++++------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/modules/email/DTO/email-query.dto.ts b/src/modules/email/DTO/email-query.dto.ts index a205cae..90192f8 100644 --- a/src/modules/email/DTO/email-query.dto.ts +++ b/src/modules/email/DTO/email-query.dto.ts @@ -1,6 +1,6 @@ import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsBoolean, IsDateString, IsEmail, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from "class-validator"; +import { Transform, Type } from "class-transformer"; +import { IsBoolean, IsDateString, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from "class-validator"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { CommonMessage, EmailMessage } from "../../../common/enums/message.enum"; @@ -44,7 +44,7 @@ export class SearchMessagesQueryDto extends PaginationDto { @IsOptional() @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) @ApiPropertyOptional({ description: "Search string" }) - query?: string; + search?: string; @IsOptional() @IsDateString({}, { message: EmailMessage.DATE_START_MUST_BE_DATE_STRING }) @@ -58,7 +58,6 @@ export class SearchMessagesQueryDto extends PaginationDto { @IsOptional() @IsString() - @IsEmail({}, { message: EmailMessage.FROM_ADDRESS_MUST_BE_EMAIL }) @ApiPropertyOptional({ description: "Partial match for the From: header line", example: "john@example.com" }) from?: string; @@ -92,11 +91,13 @@ export class SearchMessagesQueryDto extends PaginationDto { attachments?: boolean; @IsOptional() + @Transform(({ value }) => value === "true") @IsBoolean({ message: EmailMessage.FLAGGED_FILTER_MUST_BE_BOOLEAN }) @ApiPropertyOptional({ description: "If true, then matches only messages with \\Flagged flags", example: true }) flagged?: boolean; @IsOptional() + @Transform(({ value }) => value === "true") @IsBoolean({ message: EmailMessage.UNSEEN_FILTER_MUST_BE_BOOLEAN }) @ApiPropertyOptional({ description: "If true, then matches only messages without \\Seen flags", example: true }) unseen?: boolean; diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 0998b63..ead2819 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -63,25 +63,7 @@ export class EmailController { @Query() query: AttachmentIdQueryDto, @Res({ passthrough: false }) reply: FastifyReply, ) { - const attachmentData = await this.emailService.downloadMessageAttachment(userId, params.messageId, query.mailbox, query.attachmentId); - - // Set appropriate headers for file download - reply.header("Content-Type", attachmentData.contentType || "application/octet-stream"); - - if (attachmentData.contentLength) { - reply.header("Content-Length", attachmentData.contentLength); - } - - // Set content disposition if available - if (attachmentData.contentDisposition) { - reply.header("Content-Disposition", attachmentData.contentDisposition); - } else { - // Default to attachment if no disposition specified - reply.header("Content-Disposition", `attachment; filename="attachment_${query.attachmentId}"`); - } - - // Send the stream directly - reply.send(attachmentData.stream); + this.emailService.downloadMessageAttachment(userId, params.messageId, query.mailbox, query.attachmentId, reply); } @Get("search") diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index e2814a8..b60a87a 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -1,4 +1,5 @@ import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { FastifyReply } from "fastify"; import { firstValueFrom } from "rxjs"; import { EmailHeadersService } from "./email-headers.service"; @@ -101,27 +102,23 @@ export class EmailService { //######################################################## async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { - // Transform query parameters for WildDuck API compatibility const wildDuckQuery = { - query: query.q || "", + ...query, limit: query.limit || 20, page: query.page || 1, - order: "desc" as const, + order: query.order || "desc", + query: query.search || query.q, + from: query.from || query.search, + to: query.to || query.search, + subject: query.subject || query.search, }; - // Ensure limit is within bounds if (wildDuckQuery.limit > 250) wildDuckQuery.limit = 250; if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1; if (wildDuckQuery.page < 1) wildDuckQuery.page = 1; - this.logger.debug("Search query for WildDuck:", wildDuckQuery); - const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); - this.logger.log( - `Found ${response.results.length} messages (${response.total} total, page ${response.page}) matching search for user: ${wildduckUserId}`, - ); - return { results: response.results, total: response.total, @@ -133,18 +130,36 @@ export class EmailService { } //######################################################## - async downloadMessageAttachment(wildduckUserId: string, messageId: number, mailboxId: string, attachmentId: string) { + async downloadMessageAttachment(wildduckUserId: string, messageId: number, mailboxId: string, attachmentId: string, fastifyReply: FastifyReply) { const attachmentResponse = await firstValueFrom( this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId), ); - return { + const attachmentData = { stream: attachmentResponse.success, contentType: attachmentResponse.contentType || "application/octet-stream", contentDisposition: attachmentResponse.contentDisposition, contentLength: attachmentResponse.contentLength, headers: attachmentResponse.headers, }; + + // Set appropriate headers for file download + fastifyReply.header("Content-Type", attachmentData.contentType || "application/octet-stream"); + + if (attachmentData.contentLength) { + fastifyReply.header("Content-Length", attachmentData.contentLength); + } + + // Set content disposition if available + if (attachmentData.contentDisposition) { + fastifyReply.header("Content-Disposition", attachmentData.contentDisposition); + } else { + // Default to attachment if no disposition specified + fastifyReply.header("Content-Disposition", `attachment; filename="attachment_${attachmentId}"`); + } + + // Send the stream directly + fastifyReply.send(attachmentData.stream); } //########################################################