chore: add search message query

This commit is contained in:
mahyargdz
2025-07-22 15:49:35 +03:30
parent 97e77d23a0
commit d31490f6a7
4 changed files with 202 additions and 89 deletions
+24
View File
@@ -156,6 +156,30 @@ export const enum EmailMessage {
FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است", FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است",
// Validation Messages for Email DTOs // 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_NAME_MUST_BE_STRING = "نام گیرنده باید یک رشته باشد",
RECIPIENT_ADDRESS_REQUIRED = "آدرس گیرنده مورد نیاز است", RECIPIENT_ADDRESS_REQUIRED = "آدرس گیرنده مورد نیاز است",
RECIPIENT_ADDRESS_MUST_BE_EMAIL = "آدرس گیرنده باید یک ایمیل معتبر باشد", RECIPIENT_ADDRESS_MUST_BE_EMAIL = "آدرس گیرنده باید یک ایمیل معتبر باشد",
+131 -5
View File
@@ -1,21 +1,147 @@
import { ApiPropertyOptional, PickType } from "@nestjs/swagger"; 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 { 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 { export class MessageListQueryDto extends PaginationDto {
@ApiPropertyOptional({ description: "Sort order", enum: ["asc", "desc"], example: "desc" }) @ApiPropertyOptional({ description: "Sort order", enum: ["asc", "desc"], example: "desc" })
@IsOptional() @IsOptional()
@IsEnum(["asc", "desc"]) @IsEnum(["asc", "desc"], { message: EmailMessage.ORDER_MUST_BE_VALID })
order?: "asc" | "desc"; order?: "asc" | "desc";
} }
export class SearchMessagesQueryDto extends PaginationDto { export class SearchMessagesQueryDto extends PaginationDto {
@IsOptional() @IsOptional()
@IsString({ message: CommonMessage.SEARCH_QUERY_STRING }) @IsString({ message: EmailMessage.SEARCH_QUERY_PARAMETER_MUST_BE_STRING })
@ApiPropertyOptional({ description: "Search query", example: "search query" }) @ApiPropertyOptional({ description: "Additional query string" })
q?: 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"]) { export class EmailSuggestionsQueryDto extends PickType(PaginationDto, ["limit"]) {
+1 -5
View File
@@ -92,11 +92,7 @@ export class EmailController {
} }
@Get("suggestions") @Get("suggestions")
@ApiOperation({ @ApiOperation({ summary: "Get email address suggestions for composing" })
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.",
})
@ApiResponse({ status: 404, description: "User not found" }) @ApiResponse({ status: 404, description: "User not found" })
getEmailSuggestions(@UserDec("wildduckUserId") userId: string, @Query() query: EmailSuggestionsQueryDto) { getEmailSuggestions(@UserDec("wildduckUserId") userId: string, @Query() query: EmailSuggestionsQueryDto) {
return this.emailService.getEmailSuggestions(userId, query.query, query.limit); return this.emailService.getEmailSuggestions(userId, query.query, query.limit);
+46 -79
View File
@@ -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 { export class ListMessagesQueryDto {
@ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" })
@IsOptional()
@IsString()
order?: "asc" | "desc"; 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; limit?: number;
@ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number; page?: number;
@ApiPropertyOptional({ description: "Cursor for next page" })
@IsOptional()
@IsString()
next?: string; next?: string;
@ApiPropertyOptional({ description: "Cursor for previous page" })
@IsOptional()
@IsString()
previous?: string; previous?: string;
@ApiPropertyOptional({ description: "Unseen messages only" })
@IsOptional()
@IsBoolean()
unseen?: boolean; unseen?: boolean;
} }
export class UpdateMessageDto { export class UpdateMessageDto {
@ApiPropertyOptional({ description: "ID of the destination mailbox if moving the message" })
@IsOptional()
@IsString()
moveTo?: string; moveTo?: string;
@ApiPropertyOptional({ description: "Mark message as seen or unseen" })
@IsOptional()
@IsBoolean()
seen?: boolean; seen?: boolean;
@ApiPropertyOptional({ description: "Mark message as flagged or not" })
@IsOptional()
@IsBoolean()
flagged?: boolean; flagged?: boolean;
@ApiPropertyOptional({ description: "Mark message as draft or not" })
@IsOptional()
@IsBoolean()
draft?: boolean; draft?: boolean;
@ApiPropertyOptional({ description: "Mark message as deleted or not" })
@IsOptional()
@IsBoolean()
deleted?: boolean; deleted?: boolean;
@ApiPropertyOptional({ description: "Expiration date for auto-deletion or false to clear", example: "2024-12-31T23:59:59.000Z" })
@IsOptional()
expires?: string | false; expires?: string | false;
} }
export class SearchMessagesQueryDto { export class SearchMessagesQueryDto {
@ApiProperty({ description: "Query string to search for", example: "important meeting" }) q?: string;
@IsString()
query: string;
@ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" }) mailbox?: string;
@IsOptional()
@IsString() id?: string;
order?: "asc" | "desc";
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; limit?: number;
@ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 }) order?: "asc" | "desc";
@IsOptional()
@Type(() => Number) next?: string;
@IsNumber()
@Min(1) previous?: string;
page?: number; page?: number;
} }
export class UploadMessageDto { 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; raw: string;
@ApiPropertyOptional({ description: "Mark message as seen", default: false })
@IsOptional()
@IsBoolean()
seen?: boolean; seen?: boolean;
@ApiPropertyOptional({ description: "Mark message as flagged", default: false })
@IsOptional()
@IsBoolean()
flagged?: boolean; flagged?: boolean;
@ApiPropertyOptional({ description: "Mark message as draft", default: false })
@IsOptional()
@IsBoolean()
draft?: boolean; draft?: boolean;
@ApiPropertyOptional({ description: "Message date", example: "2024-01-01T12:00:00.000Z" })
@IsOptional()
@IsDateString()
date?: string; date?: string;
} }