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 { 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;
+1 -19
View File
@@ -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")
+27 -12
View File
@@ -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);
}
//########################################################