chore: add filter and search

This commit is contained in:
mahyargdz
2025-07-23 12:17:38 +03:30
parent d31490f6a7
commit 24783634fd
3 changed files with 33 additions and 35 deletions
+5 -4
View File
@@ -1,6 +1,6 @@
import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; import { ApiPropertyOptional, PickType } from "@nestjs/swagger";
import { Type } from "class-transformer"; import { Transform, Type } from "class-transformer";
import { IsBoolean, IsDateString, IsEmail, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from "class-validator"; import { IsBoolean, IsDateString, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from "class-validator";
import { PaginationDto } from "../../../common/DTO/pagination.dto"; import { PaginationDto } from "../../../common/DTO/pagination.dto";
import { CommonMessage, EmailMessage } from "../../../common/enums/message.enum"; import { CommonMessage, EmailMessage } from "../../../common/enums/message.enum";
@@ -44,7 +44,7 @@ export class SearchMessagesQueryDto extends PaginationDto {
@IsOptional() @IsOptional()
@IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) @IsString({ message: CommonMessage.SEARCH_QUERY_STRING })
@ApiPropertyOptional({ description: "Search string" }) @ApiPropertyOptional({ description: "Search string" })
query?: string; search?: string;
@IsOptional() @IsOptional()
@IsDateString({}, { message: EmailMessage.DATE_START_MUST_BE_DATE_STRING }) @IsDateString({}, { message: EmailMessage.DATE_START_MUST_BE_DATE_STRING })
@@ -58,7 +58,6 @@ export class SearchMessagesQueryDto extends PaginationDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsEmail({}, { message: EmailMessage.FROM_ADDRESS_MUST_BE_EMAIL })
@ApiPropertyOptional({ description: "Partial match for the From: header line", example: "john@example.com" }) @ApiPropertyOptional({ description: "Partial match for the From: header line", example: "john@example.com" })
from?: string; from?: string;
@@ -92,11 +91,13 @@ export class SearchMessagesQueryDto extends PaginationDto {
attachments?: boolean; attachments?: boolean;
@IsOptional() @IsOptional()
@Transform(({ value }) => value === "true")
@IsBoolean({ message: EmailMessage.FLAGGED_FILTER_MUST_BE_BOOLEAN }) @IsBoolean({ message: EmailMessage.FLAGGED_FILTER_MUST_BE_BOOLEAN })
@ApiPropertyOptional({ description: "If true, then matches only messages with \\Flagged flags", example: true }) @ApiPropertyOptional({ description: "If true, then matches only messages with \\Flagged flags", example: true })
flagged?: boolean; flagged?: boolean;
@IsOptional() @IsOptional()
@Transform(({ value }) => value === "true")
@IsBoolean({ message: EmailMessage.UNSEEN_FILTER_MUST_BE_BOOLEAN }) @IsBoolean({ message: EmailMessage.UNSEEN_FILTER_MUST_BE_BOOLEAN })
@ApiPropertyOptional({ description: "If true, then matches only messages without \\Seen flags", example: true }) @ApiPropertyOptional({ description: "If true, then matches only messages without \\Seen flags", example: true })
unseen?: boolean; unseen?: boolean;
+1 -19
View File
@@ -63,25 +63,7 @@ export class EmailController {
@Query() query: AttachmentIdQueryDto, @Query() query: AttachmentIdQueryDto,
@Res({ passthrough: false }) reply: FastifyReply, @Res({ passthrough: false }) reply: FastifyReply,
) { ) {
const attachmentData = await this.emailService.downloadMessageAttachment(userId, params.messageId, query.mailbox, query.attachmentId); this.emailService.downloadMessageAttachment(userId, params.messageId, query.mailbox, query.attachmentId, reply);
// 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);
} }
@Get("search") @Get("search")
+27 -12
View File
@@ -1,4 +1,5 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common"; import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { FastifyReply } from "fastify";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EmailHeadersService } from "./email-headers.service"; import { EmailHeadersService } from "./email-headers.service";
@@ -101,27 +102,23 @@ export class EmailService {
//######################################################## //########################################################
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
// Transform query parameters for WildDuck API compatibility
const wildDuckQuery = { const wildDuckQuery = {
query: query.q || "", ...query,
limit: query.limit || 20, limit: query.limit || 20,
page: query.page || 1, 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 > 250) wildDuckQuery.limit = 250;
if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1; if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1;
if (wildDuckQuery.page < 1) wildDuckQuery.page = 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)); 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 { return {
results: response.results, results: response.results,
total: response.total, 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( const attachmentResponse = await firstValueFrom(
this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId), this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId),
); );
return { const attachmentData = {
stream: attachmentResponse.success, stream: attachmentResponse.success,
contentType: attachmentResponse.contentType || "application/octet-stream", contentType: attachmentResponse.contentType || "application/octet-stream",
contentDisposition: attachmentResponse.contentDisposition, contentDisposition: attachmentResponse.contentDisposition,
contentLength: attachmentResponse.contentLength, contentLength: attachmentResponse.contentLength,
headers: attachmentResponse.headers, 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);
} }
//######################################################## //########################################################