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