chore: add filter and search
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
//########################################################
|
||||
|
||||
Reference in New Issue
Block a user