diff --git a/src/modules/ai-assistant/DTO/summarize-email.dto.ts b/src/modules/ai-assistant/DTO/summarize-email.dto.ts index efdba82..0bd5d64 100644 --- a/src/modules/ai-assistant/DTO/summarize-email.dto.ts +++ b/src/modules/ai-assistant/DTO/summarize-email.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; import { IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString } from "class-validator"; -import { SummaryLength, SummaryTone } from "../enums/ai.enum"; +import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum"; export class SummarizeEmailDto { @IsNotEmpty({ message: "Message ID is required" }) @@ -15,12 +15,12 @@ export class SummarizeEmailDto { mailbox: string; @IsOptional() - @IsEnum(SummaryLength, { message: "Summary length must be one of: short, medium, detailed" }) - @ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLength, default: SummaryLength.MEDIUM }) - length?: SummaryLength; + @IsEnum(SummaryLengthEnum, { message: "Summary length must be one of: short, medium, detailed" }) + @ApiPropertyOptional({ description: "Preferred summary length", enum: SummaryLengthEnum, default: SummaryLengthEnum.MEDIUM }) + length?: SummaryLengthEnum; @IsOptional() - @IsEnum(SummaryTone, { message: "Summary tone must be one of: formal, casual, professional" }) - @ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryTone, default: SummaryTone.PROFESSIONAL }) - tone?: SummaryTone; + @IsEnum(SummaryToneEnum, { message: "Summary tone must be one of: formal, casual, professional" }) + @ApiPropertyOptional({ description: "Preferred summary tone", enum: SummaryToneEnum, default: SummaryToneEnum.PROFESSIONAL }) + tone?: SummaryToneEnum; } diff --git a/src/modules/ai-assistant/enums/ai.enum.ts b/src/modules/ai-assistant/enums/ai.enum.ts index 2312835..09145da 100644 --- a/src/modules/ai-assistant/enums/ai.enum.ts +++ b/src/modules/ai-assistant/enums/ai.enum.ts @@ -1,23 +1,35 @@ -export enum SummaryLength { +export enum SummaryLengthEnum { SHORT = "short", MEDIUM = "medium", DETAILED = "detailed", } -export enum SummaryTone { +export enum SentimentEnum { + POSITIVE = "positive", + NEUTRAL = "neutral", + NEGATIVE = "negative", +} + +export enum LanguageEnum { + ENGLISH = "English", + PERSIAN = "Persian", +} + +export enum SummaryToneEnum { FORMAL = "formal", CASUAL = "casual", PROFESSIONAL = "professional", } -export enum EmailPurpose { +export enum EmailPurposeEnum { + GENERAL = "general", REPLY = "reply", FORWARD = "forward", NEW = "new", FOLLOW_UP = "follow_up", } -export enum WritingTone { +export enum WritingToneEnum { FORMAL = "formal", CASUAL = "casual", PROFESSIONAL = "professional", @@ -25,7 +37,7 @@ export enum WritingTone { URGENT = "urgent", } -export enum EmailLength { +export enum EmailLengthEnum { SHORT = "short", MEDIUM = "medium", LONG = "long", diff --git a/src/modules/ai-assistant/interfaces/ai-service.interface.ts b/src/modules/ai-assistant/interfaces/ai-service.interface.ts index 3eb4912..c4db202 100644 --- a/src/modules/ai-assistant/interfaces/ai-service.interface.ts +++ b/src/modules/ai-assistant/interfaces/ai-service.interface.ts @@ -1,25 +1,29 @@ -export interface EmailData { +import { PriorityEnum } from "../../email/enums/email-header.enum"; +import { MessageAttachment } from "../../mail-server/interfaces/messages-response.interface"; +import { LanguageEnum, SentimentEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum"; + +export interface IEmailData { subject: string; content: string; htmlContent?: string; from: string; to: string[]; - date: Date; - attachments?: unknown[]; + date: string; + attachments?: boolean | MessageAttachment[]; } export interface SummarizationOptions { - length: "short" | "medium" | "detailed"; - tone: "formal" | "casual" | "professional"; - language?: "Persian" | "English"; + length: SummaryLengthEnum; + tone: SummaryToneEnum; + language?: LanguageEnum; } export interface AISummaryResponse { summary: string; keyPoints: string[]; suggestedActions: string[]; - priority: "low" | "medium" | "high"; - sentiment: "positive" | "neutral" | "negative"; + priority: PriorityEnum; + sentiment: SentimentEnum; } export interface AIWritingResponse { @@ -31,6 +35,6 @@ export interface AIWritingResponse { } export interface AIServiceInterface { - summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise; + summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise; assistEmailWriting(prompt: string): Promise; } diff --git a/src/modules/ai-assistant/services/ai-assistant.service.ts b/src/modules/ai-assistant/services/ai-assistant.service.ts index 2416e51..5a2f81e 100644 --- a/src/modules/ai-assistant/services/ai-assistant.service.ts +++ b/src/modules/ai-assistant/services/ai-assistant.service.ts @@ -7,8 +7,8 @@ import { MessageDetails } from "../../mail-server/interfaces/messages-response.i import { WildDuckMessagesService } from "../../mail-server/services/wildduck-messages.service"; import { SummarizeEmailDto } from "../DTO/summarize-email.dto"; import { WriteEmailAssistanceDto } from "../DTO/write-email-assistance.dto"; -import { SummaryLength, SummaryTone } from "../enums/ai.enum"; -import { EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; +import { SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum"; +import { IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; @Injectable() export class AiAssistantService { @@ -45,7 +45,7 @@ export class AiAssistantService { htmlContent: this.extractHtmlContent(message), from: this.extractSenderInfo(message), to: this.extractRecipientInfo(message), - date: message.date || new Date(), + date: message.date || new Date().toISOString(), attachments: message.attachments || [], }; } @@ -99,12 +99,12 @@ export class AiAssistantService { } //************************************************* */ - private async generateEmailSummary(emailData: any, length: SummaryLength = SummaryLength.MEDIUM, _tone: SummaryTone = SummaryTone.PROFESSIONAL) { + private async generateEmailSummary(emailData: IEmailData, length?: SummaryLengthEnum, tone?: SummaryToneEnum) { const content = emailData.content || emailData.htmlContent; - if (!content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED); + if (!content || !content.trim()) throw new BadRequestException(AiMessage.EMAIL_CONTENT_IS_EMPTY_OR_COULD_NOT_BE_EXTRACTED); - const aiEmailData: EmailData = { + const aiEmailData: IEmailData = { subject: emailData.subject, content: emailData.content, htmlContent: emailData.htmlContent, @@ -115,8 +115,8 @@ export class AiAssistantService { }; const summarizationOptions: SummarizationOptions = { - length: length.toLowerCase() as "short" | "medium" | "detailed", - tone: _tone.toLowerCase() as "formal" | "casual" | "professional", + length: length || SummaryLengthEnum.MEDIUM, + tone: tone || SummaryToneEnum.PROFESSIONAL, }; const aiResponse = await this.aiService.summarizeEmail(aiEmailData, summarizationOptions); diff --git a/src/modules/ai-assistant/services/ai.service.ts b/src/modules/ai-assistant/services/ai.service.ts index aeca6bc..1174499 100644 --- a/src/modules/ai-assistant/services/ai.service.ts +++ b/src/modules/ai-assistant/services/ai.service.ts @@ -4,15 +4,16 @@ import OpenAI from "openai"; import { AI_CONFIG } from "../../../common/constants"; import { AiMessage } from "../../../common/enums/message.enum"; import { AIConfig } from "../../../configs/ai.config"; -import { AIServiceInterface, AISummaryResponse, AIWritingResponse, EmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; +import { EmailPurposeEnum, LanguageEnum, SummaryLengthEnum, SummaryToneEnum } from "../enums/ai.enum"; +import { AIServiceInterface, AISummaryResponse, AIWritingResponse, IEmailData, SummarizationOptions } from "../interfaces/ai-service.interface"; @Injectable() export class AiService implements AIServiceInterface { private readonly openai: OpenAI; - private readonly language: "Persian" | "English" = "Persian"; - private readonly defaultTone: "formal" | "casual" | "professional" = "professional"; - private readonly defaultLength: "short" | "medium" | "detailed" = "medium"; - private readonly defaultPurpose: "general" | "reply" | "forward" | "new" | "follow_up" = "general"; + private readonly language: LanguageEnum = LanguageEnum.PERSIAN; + private readonly defaultTone: SummaryToneEnum = SummaryToneEnum.PROFESSIONAL; + private readonly defaultLength: SummaryLengthEnum = SummaryLengthEnum.MEDIUM; + private readonly defaultPurpose: EmailPurposeEnum = EmailPurposeEnum.GENERAL; constructor(@Inject(AI_CONFIG) private readonly aiConfig: AIConfig) { this.openai = new OpenAI({ @@ -23,7 +24,7 @@ export class AiService implements AIServiceInterface { } //************************************************* */ - async summarizeEmail(emailData: EmailData, options: SummarizationOptions): Promise { + async summarizeEmail(emailData: IEmailData, options: SummarizationOptions): Promise { const prompt = this.buildSummarizationPrompt(emailData, options); const completion = await this.openai.chat.completions.create({ @@ -98,7 +99,7 @@ export class AiService implements AIServiceInterface { } //************************************************* */ - private buildSummarizationPrompt(emailData: EmailData, options: SummarizationOptions): string { + private buildSummarizationPrompt(emailData: IEmailData, options: SummarizationOptions): string { const template = this.aiConfig.farsiPrompts.summarization.promptTemplate; let prompt = `${template.intro}\n\n`; @@ -108,7 +109,7 @@ export class AiService implements AIServiceInterface { prompt += `${template.dateLabel} ${emailData.date}\n\n`; prompt += `${template.contentLabel}\n${emailData.content}\n\n`; - if (emailData.attachments && emailData.attachments.length > 0) { + if (emailData.attachments && Array.isArray(emailData.attachments) && emailData.attachments.length > 0) { prompt += `${template.attachmentsLabel} ${emailData.attachments.length} فایل\n\n`; } diff --git a/src/modules/email/enums/email-header.enum.ts b/src/modules/email/enums/email-header.enum.ts index ede326a..6fc5994 100644 --- a/src/modules/email/enums/email-header.enum.ts +++ b/src/modules/email/enums/email-header.enum.ts @@ -34,7 +34,7 @@ export const enum EmailHeaderKey { XTrustedSources = "X-Trusted-Sources", } -export enum Priority { +export enum PriorityEnum { High = "high", Normal = "normal", Low = "low", diff --git a/src/modules/email/interfaces/email-header.interface.ts b/src/modules/email/interfaces/email-header.interface.ts index 3c1ba97..0ecaee1 100644 --- a/src/modules/email/interfaces/email-header.interface.ts +++ b/src/modules/email/interfaces/email-header.interface.ts @@ -1,4 +1,4 @@ -import { Priority } from "../enums/email-header.enum"; +import { PriorityEnum } from "../enums/email-header.enum"; export interface IGenerateAntiThreadingHeadersOptions { messageId?: string; @@ -8,7 +8,7 @@ export interface IGenerateAntiThreadingHeadersOptions { businessDomain: string; isTransactional?: boolean; isMarketing?: boolean; - priority?: Priority; + priority?: PriorityEnum; // Image-related options hasHostedImages?: boolean; imageCount?: number; diff --git a/src/modules/email/services/email-headers.service.ts b/src/modules/email/services/email-headers.service.ts index f3ce32d..386f2f5 100644 --- a/src/modules/email/services/email-headers.service.ts +++ b/src/modules/email/services/email-headers.service.ts @@ -2,7 +2,7 @@ import { Injectable } from "@nestjs/common"; import { v4 as uuidv4 } from "uuid"; import { EmailHeaderDto } from "../DTO/send-email.dto"; -import { EmailHeaderKey, Priority } from "../enums/email-header.enum"; +import { EmailHeaderKey, PriorityEnum } from "../enums/email-header.enum"; import { IGenerateAntiThreadingHeadersOptions, IGenerateGmailAntiThreadingHeadersOptions, @@ -36,12 +36,12 @@ export class EmailHeadersService { headers.push({ key: EmailHeaderKey.XPriority, - value: this.getPriorityValue(options.priority || Priority.Normal), + value: this.getPriorityValue(options.priority || PriorityEnum.Normal), }); headers.push({ key: EmailHeaderKey.XMSMailPriority, - value: options.priority || Priority.Normal, + value: options.priority || PriorityEnum.Normal, }); headers.push({ @@ -344,7 +344,7 @@ export class EmailHeadersService { } //****************************************** */ - private getPriorityValue(priority: Priority): string { - return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3"; + private getPriorityValue(priority: PriorityEnum): string { + return priority === PriorityEnum.High ? "1" : priority === PriorityEnum.Low ? "5" : "3"; } } diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index c16d541..bfdb3f2 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -15,7 +15,7 @@ import { UserRepository } from "../../users/repositories/user.repository"; import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; -import { Priority } from "../enums/email-header.enum"; +import { PriorityEnum } from "../enums/email-header.enum"; import { IEmailMap } from "../interfaces/email-map.interface"; @Injectable() @@ -108,18 +108,12 @@ export class EmailService { limit: query.limit || 20, page: query.page || 1, order: query.order || "desc", - mailbox: query.mailbox || "", + query: query.search, + from: query.from || query.search, + // mailbox: query.mailbox, }; - delete wildDuckQuery.mailbox; - if (query.search) { - wildDuckQuery.query = query.search; - wildDuckQuery.from = query.search; - } - - if (query.from) wildDuckQuery.from = query.from; - if (query.to) wildDuckQuery.to = query.to; - if (query.subject) wildDuckQuery.subject = query.subject; + delete wildDuckQuery?.mailbox; const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); @@ -727,6 +721,7 @@ export class EmailService { //============================================== private transformPaginationQuery(query: MessageListQueryDto): Record { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const transformedQuery: Record = { ...query }; if (!transformedQuery.page) transformedQuery.page = 1; @@ -767,7 +762,7 @@ export class EmailService { businessName, businessDomain, isTransactional: true, - priority: Priority.Normal, + priority: PriorityEnum.Normal, hasHostedImages: imageDetection.hasHostedImages, imageCount: imageDetection.imageCount, trustedImageSources: imageDetection.trustedImageSources,