From d31490f6a765bf6691f79ac40573f21278636214 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 22 Jul 2025 15:49:35 +0330 Subject: [PATCH] chore: add search message query --- src/common/enums/message.enum.ts | 24 ++++ src/modules/email/DTO/email-query.dto.ts | 136 ++++++++++++++++++++- src/modules/email/email.controller.ts | 6 +- src/modules/mail-server/DTO/message.dto.ts | 125 +++++++------------ 4 files changed, 202 insertions(+), 89 deletions(-) diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index b3ec101..49a82fa 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -156,6 +156,30 @@ export const enum EmailMessage { FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است", // Validation Messages for Email DTOs + FROM_ADDRESS_MUST_BE_EMAIL = "آدرس فرستنده باید یک ایمیل معتبر باشد", + + // Query validation messages + SEARCH_QUERY_PARAMETER_MUST_BE_STRING = "پارامتر جستجو باید یک رشته باشد", + MESSAGE_IDS_MUST_BE_STRING = "شناسه‌های پیام باید یک رشته باشد", + THREAD_ID_MUST_BE_STRING = "شناسه رشته گفتگو باید یک رشته باشد", + OR_QUERY_MUST_BE_OBJECT = "پارامتر OR باید یک شیء باشد", + DATE_START_MUST_BE_DATE_STRING = "تاریخ شروع باید یک رشته تاریخ معتبر باشد", + DATE_END_MUST_BE_DATE_STRING = "تاریخ پایان باید یک رشته تاریخ معتبر باشد", + TO_ADDRESS_MUST_BE_STRING = "آدرس گیرنده باید یک رشته باشد", + MIN_SIZE_MUST_BE_NUMBER = "حداقل اندازه باید یک عدد باشد", + MIN_SIZE_MUST_BE_NON_NEGATIVE = "حداقل اندازه نمی‌تواند منفی باشد", + MAX_SIZE_MUST_BE_NUMBER = "حداکثر اندازه باید یک عدد باشد", + MAX_SIZE_MUST_BE_NON_NEGATIVE = "حداکثر اندازه نمی‌تواند منفی باشد", + ATTACHMENTS_FILTER_MUST_BE_BOOLEAN = "فیلتر پیوست‌ها باید بولین باشد", + FLAGGED_FILTER_MUST_BE_BOOLEAN = "فیلتر پیام‌های علامت‌دار باید بولین باشد", + UNSEEN_FILTER_MUST_BE_BOOLEAN = "فیلتر پیام‌های خوانده‌نشده باید بولین باشد", + INCLUDE_HEADERS_MUST_BE_STRING = "لیست هدرها باید یک رشته باشد", + SEARCHABLE_FILTER_MUST_BE_BOOLEAN = "فیلتر قابل جستجو باید بولین باشد", + THREAD_COUNTERS_MUST_BE_BOOLEAN = "شمارنده رشته گفتگو باید بولین باشد", + ORDER_MUST_BE_VALID = "ترتیب باید یکی از مقادیر asc یا desc باشد", + NEXT_CURSOR_MUST_BE_STRING = "نشانگر صفحه بعد باید یک رشته باشد", + PREVIOUS_CURSOR_MUST_BE_STRING = "نشانگر صفحه قبل باید یک رشته باشد", + RECIPIENT_NAME_MUST_BE_STRING = "نام گیرنده باید یک رشته باشد", RECIPIENT_ADDRESS_REQUIRED = "آدرس گیرنده مورد نیاز است", RECIPIENT_ADDRESS_MUST_BE_EMAIL = "آدرس گیرنده باید یک ایمیل معتبر باشد", diff --git a/src/modules/email/DTO/email-query.dto.ts b/src/modules/email/DTO/email-query.dto.ts index 2295df0..a205cae 100644 --- a/src/modules/email/DTO/email-query.dto.ts +++ b/src/modules/email/DTO/email-query.dto.ts @@ -1,21 +1,147 @@ import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; -import { IsEnum, IsOptional, IsString } from "class-validator"; +import { Type } from "class-transformer"; +import { IsBoolean, IsDateString, IsEmail, IsEnum, IsNumber, IsObject, IsOptional, IsString, Min } from "class-validator"; import { PaginationDto } from "../../../common/DTO/pagination.dto"; -import { CommonMessage } from "../../../common/enums/message.enum"; +import { CommonMessage, EmailMessage } from "../../../common/enums/message.enum"; export class MessageListQueryDto extends PaginationDto { @ApiPropertyOptional({ description: "Sort order", enum: ["asc", "desc"], example: "desc" }) @IsOptional() - @IsEnum(["asc", "desc"]) + @IsEnum(["asc", "desc"], { message: EmailMessage.ORDER_MUST_BE_VALID }) order?: "asc" | "desc"; } export class SearchMessagesQueryDto extends PaginationDto { @IsOptional() - @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) - @ApiPropertyOptional({ description: "Search query", example: "search query" }) + @IsString({ message: EmailMessage.SEARCH_QUERY_PARAMETER_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Additional query string" }) q?: string; + + @IsOptional() + @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "ID of the Mailbox" }) + mailbox?: string; + + @ApiPropertyOptional({ + description: + "Message ID values, only applies when used in combination with mailbox. Either comma separated numbers (1,2,3) or colon separated range (3:15), or a range from UID to end (3:*)", + }) + @IsOptional() + @IsString({ message: EmailMessage.MESSAGE_IDS_MUST_BE_STRING }) + id?: string; + + @IsOptional() + @IsString({ message: EmailMessage.THREAD_ID_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Thread ID" }) + thread?: string; + + @IsOptional() + @IsObject({ message: EmailMessage.OR_QUERY_MUST_BE_OBJECT }) + @ApiPropertyOptional({ description: "At least one of the included terms must match" }) + or?: object; + + @IsOptional() + @IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) + @ApiPropertyOptional({ description: "Search string" }) + query?: string; + + @IsOptional() + @IsDateString({}, { message: EmailMessage.DATE_START_MUST_BE_DATE_STRING }) + @ApiPropertyOptional({ description: "Datestring for the earliest message storing time", example: "2024-01-01T12:00:00.000Z" }) + datestart?: string; + + @IsOptional() + @IsDateString({}, { message: EmailMessage.DATE_END_MUST_BE_DATE_STRING }) + @ApiPropertyOptional({ description: "Datestring for the latest message storing time", example: "2024-01-01T12:00:00.000Z" }) + dateend?: string; + + @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; + + @IsOptional() + @IsString({ message: EmailMessage.TO_ADDRESS_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Partial match for the To: and Cc: header lines", example: "john@example.com" }) + to?: string; + + @IsOptional() + @IsString({ message: EmailMessage.SUBJECT_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Partial match for the Subject: header line", example: "important meeting" }) + subject?: string; + + @IsOptional() + @Type(() => Number) + @IsNumber({}, { message: EmailMessage.MIN_SIZE_MUST_BE_NUMBER }) + @Min(0, { message: EmailMessage.MIN_SIZE_MUST_BE_NON_NEGATIVE }) + @ApiPropertyOptional({ description: "Minimal message size in bytes", example: 100 }) + minSize?: number; + + @IsOptional() + @Type(() => Number) + @IsNumber({}, { message: EmailMessage.MAX_SIZE_MUST_BE_NUMBER }) + @Min(0, { message: EmailMessage.MAX_SIZE_MUST_BE_NON_NEGATIVE }) + @ApiPropertyOptional({ description: "Maximal message size in bytes", example: 1000000 }) + maxSize?: number; + + @IsOptional() + @IsBoolean({ message: EmailMessage.ATTACHMENTS_FILTER_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "If true, then matches only messages with attachments", example: true }) + attachments?: boolean; + + @IsOptional() + @IsBoolean({ message: EmailMessage.FLAGGED_FILTER_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "If true, then matches only messages with \\Flagged flags", example: true }) + flagged?: boolean; + + @IsOptional() + @IsBoolean({ message: EmailMessage.UNSEEN_FILTER_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "If true, then matches only messages without \\Seen flags", example: true }) + unseen?: boolean; + + @ApiPropertyOptional({ + description: "Comma separated list of header keys to include in the response", + example: "List-ID, MIME-Version", + }) + @IsOptional() + @IsString({ message: EmailMessage.INCLUDE_HEADERS_MUST_BE_STRING }) + includeHeaders?: string; + + @ApiPropertyOptional({ description: "If true, then matches messages not in Junk or Trash", example: true }) + @IsOptional() + @IsBoolean({ message: EmailMessage.SEARCHABLE_FILTER_MUST_BE_BOOLEAN }) + searchable?: boolean; + + @ApiPropertyOptional({ description: "If true, then includes threadMessageCount in the response. Counters come with some overhead", default: false }) + @IsOptional() + @IsBoolean({ message: EmailMessage.THREAD_COUNTERS_MUST_BE_BOOLEAN }) + threadCounters?: boolean; + + @ApiPropertyOptional({ + description: "Ordering of the records by insert date. If no order is supplied, results are sorted by their mongoDB ObjectId", + enum: ["asc", "desc"], + }) + @IsOptional() + @IsString({ message: EmailMessage.ORDER_MUST_BE_VALID }) + order?: "asc" | "desc"; + + @ApiPropertyOptional({ + description: "Cursor value for next page, retrieved from nextCursor response value", + example: "eyIkb2lkIjoiNWRmMWZkMmQ3NzkyNTExOGI2MDdjNjg0In0", + }) + @IsOptional() + @IsString({ message: EmailMessage.NEXT_CURSOR_MUST_BE_STRING }) + next?: string; + + @ApiPropertyOptional({ + description: "Cursor value for previous page, retrieved from previousCursor response value", + example: "TMIjjIy23ZGM2kk0lIixygWomEknQDWdmzMNIkbNeO0NNjR", + }) + @IsOptional() + @IsString({ message: EmailMessage.PREVIOUS_CURSOR_MUST_BE_STRING }) + previous?: string; } export class EmailSuggestionsQueryDto extends PickType(PaginationDto, ["limit"]) { diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index db9433a..0998b63 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -92,11 +92,7 @@ export class EmailController { } @Get("suggestions") - @ApiOperation({ - summary: "Get email address suggestions for composing", - description: - "Returns email address suggestions based on previous email interactions (sent and received). Results are sorted by frequency and recency.", - }) + @ApiOperation({ summary: "Get email address suggestions for composing" }) @ApiResponse({ status: 404, description: "User not found" }) getEmailSuggestions(@UserDec("wildduckUserId") userId: string, @Query() query: EmailSuggestionsQueryDto) { return this.emailService.getEmailSuggestions(userId, query.query, query.limit); diff --git a/src/modules/mail-server/DTO/message.dto.ts b/src/modules/mail-server/DTO/message.dto.ts index 54bf0d0..49cd6ac 100644 --- a/src/modules/mail-server/DTO/message.dto.ts +++ b/src/modules/mail-server/DTO/message.dto.ts @@ -1,126 +1,93 @@ -import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; -import { Type } from "class-transformer"; -import { IsBoolean, IsDateString, IsNumber, IsOptional, IsString, Max, Min } from "class-validator"; - export class ListMessagesQueryDto { - @ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" }) - @IsOptional() - @IsString() order?: "asc" | "desc"; - @ApiPropertyOptional({ description: "Number of messages to return", minimum: 1, maximum: 250, default: 20 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) - @Max(250) limit?: number; - @ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) page?: number; - @ApiPropertyOptional({ description: "Cursor for next page" }) - @IsOptional() - @IsString() next?: string; - @ApiPropertyOptional({ description: "Cursor for previous page" }) - @IsOptional() - @IsString() previous?: string; - @ApiPropertyOptional({ description: "Unseen messages only" }) - @IsOptional() - @IsBoolean() unseen?: boolean; } export class UpdateMessageDto { - @ApiPropertyOptional({ description: "ID of the destination mailbox if moving the message" }) - @IsOptional() - @IsString() moveTo?: string; - @ApiPropertyOptional({ description: "Mark message as seen or unseen" }) - @IsOptional() - @IsBoolean() seen?: boolean; - @ApiPropertyOptional({ description: "Mark message as flagged or not" }) - @IsOptional() - @IsBoolean() flagged?: boolean; - @ApiPropertyOptional({ description: "Mark message as draft or not" }) - @IsOptional() - @IsBoolean() draft?: boolean; - @ApiPropertyOptional({ description: "Mark message as deleted or not" }) - @IsOptional() - @IsBoolean() deleted?: boolean; - @ApiPropertyOptional({ description: "Expiration date for auto-deletion or false to clear", example: "2024-12-31T23:59:59.000Z" }) - @IsOptional() expires?: string | false; } export class SearchMessagesQueryDto { - @ApiProperty({ description: "Query string to search for", example: "important meeting" }) - @IsString() - query: string; + q?: string; - @ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" }) - @IsOptional() - @IsString() - order?: "asc" | "desc"; + mailbox?: string; + + id?: string; + + thread?: string; + + or?: object; + + query?: string; + + datestart?: string; + + dateend?: string; + + from?: string; + + to?: string; + + subject?: string; + + minSize?: number; + + maxSize?: number; + + attachments?: boolean; + + flagged?: boolean; + + unseen?: boolean; + + includeHeaders?: string; + + searchable?: boolean; + + sess?: string; + + ip?: string; + + threadCounters?: boolean; - @ApiPropertyOptional({ description: "Number of results to return", minimum: 1, maximum: 250, default: 20 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) - @Max(250) limit?: number; - @ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 }) - @IsOptional() - @Type(() => Number) - @IsNumber() - @Min(1) + order?: "asc" | "desc"; + + next?: string; + + previous?: string; + page?: number; } export class UploadMessageDto { - @ApiProperty({ - description: "Raw message content in RFC822 format", - example: "From: sender@example.com\nTo: recipient@example.com\nSubject: Test\n\nMessage body", - }) - @IsString() raw: string; - @ApiPropertyOptional({ description: "Mark message as seen", default: false }) - @IsOptional() - @IsBoolean() seen?: boolean; - @ApiPropertyOptional({ description: "Mark message as flagged", default: false }) - @IsOptional() - @IsBoolean() flagged?: boolean; - @ApiPropertyOptional({ description: "Mark message as draft", default: false }) - @IsOptional() - @IsBoolean() draft?: boolean; - @ApiPropertyOptional({ description: "Message date", example: "2024-01-01T12:00:00.000Z" }) - @IsOptional() - @IsDateString() date?: string; }