diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 75fe9d4..574d3df 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -147,8 +147,8 @@ export const enum EmailMessage { MESSAGE_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد", USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است", USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد", - MAILBOX_ID_REQUIRED = "شناسه مستندک ایمیل مورد نیاز است", - MAILBOX_ID_MUST_BE_STRING = "شناسه مستندک ایمیل باید یک رشته باشد", + MAILBOX_ID_REQUIRED = "شناسه میل باکس ایمیل مورد نیاز است", + MAILBOX_ID_MUST_BE_STRING = "شناسه میل باکس ایمیل باید یک رشته باشد", QUEUE_ID_REQUIRED = "شناسه پیش‌نویس ایمیل مورد نیاز است", QUEUE_ID_MUST_BE_STRING = "شناسه پیش‌نویس ایمیل باید یک رشته باشد", MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد", @@ -223,6 +223,7 @@ export const enum EmailMessage { PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد", SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است", ALL_MESSAGES_DELETED_SUCCESSFULLY = "همه پیام ها با موفقیت حذف شدند", + MAILBOX_ID_MUST_BE_MONGO_ID = "شناسه میل باکس ایمیل باید یک آی دی معتبر باشد", } export const enum WebSocketMessage { diff --git a/src/modules/email/DTO/email-params.dto.ts b/src/modules/email/DTO/email-params.dto.ts index 1aa8d6b..5880b29 100644 --- a/src/modules/email/DTO/email-params.dto.ts +++ b/src/modules/email/DTO/email-params.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { Transform } from "class-transformer"; -import { IsNotEmpty, IsNumber, IsNumberString, IsString } from "class-validator"; +import { IsMongoId, IsNotEmpty, IsNumber, IsNumberString, IsString } from "class-validator"; import { EmailMessage } from "../../../common/enums/message.enum"; @@ -11,11 +11,12 @@ export class UserIdParamDto { userId: string; } -export class MailboxIdParamDto { - @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" }) - @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) +export class MailboxIdQueryDto { @IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED }) - mailboxId: string; + @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) + @IsMongoId({ message: EmailMessage.MAILBOX_ID_MUST_BE_MONGO_ID }) + @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" }) + mailbox: string; } export class MessageIdParamDto { @@ -32,7 +33,7 @@ export class QueueIdParamDto { queueId: string; } -export class UserMailboxParamDto { +export class MailboxParamDto { @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" }) @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) @IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED }) diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 047cd03..e12e44e 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestj import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { BulkActionDto } from "./DTO/bulk-actions.dto"; -import { MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto"; +import { MailboxIdQueryDto, MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto"; import { MessageListQueryDto } from "./DTO/email-query.dto"; import { SearchMessagesQueryDto } from "./DTO/email-query.dto"; import { SendEmailDto, UpdateDraftDto } from "./DTO/send-email.dto"; @@ -40,16 +40,16 @@ export class EmailController { @ApiOperation({ summary: "Get a specific message" }) @ApiResponse({ status: 200, description: "Message details" }) @ApiResponse({ status: 404, description: "Message not found" }) - getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.getMessage(userId, params.messageId); + getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.getMessage(userId, params.messageId, query.mailbox); } @Delete("messages/:messageId") @ApiOperation({ summary: "Delete a message" }) @ApiResponse({ status: 200, description: "Message deleted successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) - deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.deleteMessage(userId, params.messageId); + deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.deleteMessage(userId, params.messageId, query.mailbox); } @Get("search") @@ -63,8 +63,8 @@ export class EmailController { @ApiOperation({ summary: "Mark a message as seen" }) @ApiResponse({ status: 200, description: "Message marked as seen successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) - markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.markMessageAsSeen(userId, Number(params.messageId)); + markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.markMessageAsSeen(userId, Number(params.messageId), query.mailbox); } @Get("messages/inbox") @@ -156,8 +156,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved to archive successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToArchive(userId, Number(params.messageId)); + moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToArchive(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/favorite") @@ -165,8 +165,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved to favorite successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToFavorite(userId, Number(params.messageId)); + moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToFavorite(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/trash") @@ -174,8 +174,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved to trash successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToTrash(userId, Number(params.messageId)); + moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToTrash(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/unarchive") @@ -183,8 +183,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved from archive to inbox successfully" }) @ApiResponse({ status: 404, description: "Message not found in archive" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId)); + moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/unfavorite") @@ -192,8 +192,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" }) @ApiResponse({ status: 404, description: "Message not found in favorite" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId)); + moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/restore") @@ -201,8 +201,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message restored from trash to inbox successfully" }) @ApiResponse({ status: 404, description: "Message not found in trash" }) @ApiResponse({ status: 403, description: "Not authorized to restore message" }) - moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId)); + moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/junk") @@ -210,8 +210,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved to junk successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToJunk(userId, Number(params.messageId)); + moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToJunk(userId, Number(params.messageId), query.mailbox); } @Patch("messages/:messageId/not-junk") @@ -219,8 +219,8 @@ export class EmailController { @ApiResponse({ status: 200, description: "Message moved from junk to inbox successfully" }) @ApiResponse({ status: 404, description: "Message not found in junk" }) @ApiResponse({ status: 403, description: "Not authorized to move message" }) - moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { - return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId)); + moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId), query.mailbox); } @Post("messages/bulk-action") @@ -232,8 +232,8 @@ export class EmailController { @ApiResponse({ status: 206, description: "Bulk action partially completed" }) @ApiResponse({ status: 400, description: "Invalid bulk action data" }) @ApiResponse({ status: 403, description: "Not authorized to perform bulk action" }) - performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto) { - return this.emailService.performBulkAction(userId, bulkActionDto); + performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) { + return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox); } // // Domain Access Management Endpoints diff --git a/src/modules/email/enums/email-header.enum.ts b/src/modules/email/enums/email-header.enum.ts index 431825f..ede326a 100644 --- a/src/modules/email/enums/email-header.enum.ts +++ b/src/modules/email/enums/email-header.enum.ts @@ -25,6 +25,13 @@ export const enum EmailHeaderKey { Importance = "Importance", XCampaignID = "X-Campaign-ID", XMarketingEmail = "X-Marketing-Email", + // Image-related anti-spam headers + XImageSource = "X-Image-Source", + XContentSecurity = "X-Content-Security", + XImageValidation = "X-Image-Validation", + XOriginVerified = "X-Origin-Verified", + XImageCount = "X-Image-Count", + XTrustedSources = "X-Trusted-Sources", } export enum Priority { diff --git a/src/modules/email/gateways/email.gateway.ts b/src/modules/email/gateways/email.gateway.ts index a874aec..1c3987d 100644 --- a/src/modules/email/gateways/email.gateway.ts +++ b/src/modules/email/gateways/email.gateway.ts @@ -104,7 +104,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew }); if (userInfo) { - // Leave user room const userRoom = WEBSOCKET_ROOMS.USER(userInfo.userId); client.leave(userRoom); @@ -137,12 +136,8 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew } //************************************************************************ */ async notifyEmailSent(userId: string, data: EmailSentPayload) { - return await this.sendUserNotification( - userId, - WEBSOCKET_EVENTS.EMAIL_SENT, - data, - `Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`, - ); + const content = `Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`; + return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_SENT, data, content); } //************************************************************************ */ async notifyUnreadCountUpdate(userId: string, data: UnreadCountPayload) { @@ -193,19 +188,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew return userIds.filter((userId) => this.isUserConnected(userId)); } - //************************************************************************ */ - async getBusinessConnectedUsers(businessId: string): Promise { - try { - // Note: Currently returning all connected users since business filtering - // requires additional user data lookup. This can be enhanced later. - this.logger.warn(`getBusinessConnectedUsers called for business ${businessId} - returning all connected users for now`); - return this.getConnectedUserIds(); - } catch (error) { - this.logger.error(`Failed to get business connected users for ${businessId}:`, error); - return []; - } - } - // ============================================================================ // PRIVATE HELPER METHODS // ============================================================================ diff --git a/src/modules/email/interfaces/email-header.interface.ts b/src/modules/email/interfaces/email-header.interface.ts index de1dd7b..3c1ba97 100644 --- a/src/modules/email/interfaces/email-header.interface.ts +++ b/src/modules/email/interfaces/email-header.interface.ts @@ -9,9 +9,14 @@ export interface IGenerateAntiThreadingHeadersOptions { isTransactional?: boolean; isMarketing?: boolean; priority?: Priority; + // Image-related options + hasHostedImages?: boolean; + imageCount?: number; + trustedImageSources?: string[]; } -export interface IGenerateGmailAntiThreadingHeadersOptions extends Pick { +export interface IGenerateGmailAntiThreadingHeadersOptions { + businessDomain: string; conversationBreaker?: string; } @@ -25,3 +30,10 @@ export interface IGenerateMarketingHeadersOptions campaignId?: string; unsubscribeUrl: string; } + +export interface IImageDetectionResult { + hasHostedImages: boolean; + imageCount: number; + trustedImageSources: string[]; + danakCorpImages: string[]; +} diff --git a/src/modules/email/interfaces/user-mailbox.interface.ts b/src/modules/email/interfaces/user-mailbox.interface.ts new file mode 100644 index 0000000..2f49327 --- /dev/null +++ b/src/modules/email/interfaces/user-mailbox.interface.ts @@ -0,0 +1,9 @@ +export interface UserMailboxIds { + inbox: string; + sent: string; + drafts: string; + trash: string; + junk: string; + archive: string; + favorite: string; +} diff --git a/src/modules/email/queue/email-monitoring.processor.ts b/src/modules/email/queue/email-monitoring.processor.ts index e7f6dcc..7f4f215 100644 --- a/src/modules/email/queue/email-monitoring.processor.ts +++ b/src/modules/email/queue/email-monitoring.processor.ts @@ -6,9 +6,7 @@ import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant"; import { EmailMonitoringJob, EmailSchedulerJob } from "../interfaces/email-events.interface"; import { EmailMonitoringService } from "../services/email-monitoring.service"; -@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE, { - concurrency: 5, -}) +@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE) export class EmailMonitoringProcessor extends WorkerHost { private readonly logger = new Logger(EmailMonitoringProcessor.name); diff --git a/src/modules/email/services/email-headers.service.ts b/src/modules/email/services/email-headers.service.ts index 63f5bbd..f3ce32d 100644 --- a/src/modules/email/services/email-headers.service.ts +++ b/src/modules/email/services/email-headers.service.ts @@ -8,27 +8,27 @@ import { IGenerateGmailAntiThreadingHeadersOptions, IGenerateMarketingHeadersOptions, IGenerateTransactionalHeadersOptions, + IImageDetectionResult, } from "../interfaces/email-header.interface"; @Injectable() export class EmailHeadersService { + private readonly hostDomain = "danakcorp.com"; + private readonly hostDomain2 = "storage.danakcorp.com"; //****************************************** */ generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] { const headers: EmailHeaderDto[] = []; - // Add References header with unique value to break threading headers.push({ key: EmailHeaderKey.References, value: `<${uuidv4()}@${options.businessDomain}>`, }); - // Add unique Thread-Index to prevent Outlook threading headers.push({ key: EmailHeaderKey.ThreadIndex, value: this.generateThreadIndex(), }); - // Anti-spam headers headers.push({ key: EmailHeaderKey.XMailer, value: `Danak Mail Service v1.0`, @@ -44,7 +44,6 @@ export class EmailHeadersService { value: options.priority || Priority.Normal, }); - // Add MIME version headers.push({ key: EmailHeaderKey.MIMEVersion, value: "1.0", @@ -60,19 +59,20 @@ export class EmailHeadersService { value: "0.0", }); - // Add feedback loop headers headers.push({ key: EmailHeaderKey.XFeedbackID, value: `${uuidv4()}:${options.businessDomain}`, }); - // Add entity reference ID headers.push({ key: EmailHeaderKey.XEntityRefID, value: uuidv4(), }); - // Content classification headers + if (options.hasHostedImages) { + headers.push(...this.generateImageAntiSpamHeaders(options)); + } + if (options.isTransactional) { headers.push({ key: EmailHeaderKey.XAutoResponseSuppress, @@ -106,7 +106,6 @@ export class EmailHeadersService { } } - // Add List-ID if provided if (options.listId) { headers.push({ key: EmailHeaderKey.ListId, @@ -114,30 +113,107 @@ export class EmailHeadersService { }); } - // Add authentication headers - // if (options.businessDomain) { - // headers.push({ - // key: EmailHeaderKey.AuthenticationResults, - // value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`, - // }); - // } + return headers; + } - // Generate unique Message-ID to prevent threading - // const messageId = options.messageId || this.generateMessageId(options.businessDomain); - // headers.push({ - // key: EmailHeaderKey.MessageId, - // value: messageId, - // }); + //****************************************** */ + generateImageAntiSpamHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] { + const headers: EmailHeaderDto[] = []; - // Add custom headers to help with deliverability - // headers.push({ - // key: "X-Originating-IP", - // value: "[" + this.getServerIP() + "]", - // }); + if (!options.hasHostedImages) { + return headers; + } + + headers.push({ + key: EmailHeaderKey.XImageSource, + value: this.hostDomain2, + }); + + headers.push({ + key: EmailHeaderKey.XContentSecurity, + value: "verified", + }); + + headers.push({ + key: EmailHeaderKey.XImageValidation, + value: "trusted-source", + }); + + headers.push({ + key: EmailHeaderKey.XOriginVerified, + value: `${this.hostDomain}-${Date.now()}`, + }); + + if (options.imageCount && options.imageCount > 0) { + headers.push({ + key: EmailHeaderKey.XImageCount, + value: options.imageCount.toString(), + }); + } + + if (options.trustedImageSources && options.trustedImageSources.length > 0) { + headers.push({ + key: EmailHeaderKey.XTrustedSources, + value: options.trustedImageSources.join(", "), + }); + } return headers; } + //****************************************** */ + detectImagesInContent(htmlContent: string): IImageDetectionResult { + if (!htmlContent) { + return { + hasHostedImages: false, + imageCount: 0, + trustedImageSources: [], + danakCorpImages: [], + }; + } + + // Regex to find img tags with src attributes + const imgRegex = /]+src\s*=\s*["']([^"']+)["'][^>]*>/gi; + const matches = [...htmlContent.matchAll(imgRegex)]; + + const allImages = matches.map((match) => match[1]); + const danakCorpImages = allImages.filter((src) => src.includes(this.hostDomain) || src.includes(this.hostDomain2)); + + // Also check for CSS background images + const cssImageRegex = /background-image\s*:\s*url\(["']?([^"')]+)["']?\)/gi; + const cssMatches = [...htmlContent.matchAll(cssImageRegex)]; + const cssImages = cssMatches.map((match) => match[1]); + const danakCorpCssImages = cssImages.filter((src) => src.includes(this.hostDomain) || src.includes(this.hostDomain2)); + + const allDanakCorpImages = [...danakCorpImages, ...danakCorpCssImages]; + const hasHostedImages = allDanakCorpImages.length > 0; + + // Get unique domains from danakcorp images + const trustedImageSources = [ + ...new Set( + allDanakCorpImages + .map((src) => { + try { + const url = new URL(src.startsWith("//") ? "https:" + src : src); + return url.hostname; + } catch { + // If URL parsing fails, extract domain manually + const match = src.match(/(?:https?:\/\/)?(?:www\.)?([^/]+)/); + return match ? match[1] : src; + } + }) + .filter((domain) => domain.includes(this.hostDomain)), + ), + ]; + + return { + hasHostedImages, + imageCount: allDanakCorpImages.length, + trustedImageSources, + danakCorpImages: allDanakCorpImages, + }; + } + //****************************************** */ generateGmailAntiThreadingHeaders(options: IGenerateGmailAntiThreadingHeadersOptions): EmailHeaderDto[] { const headers: EmailHeaderDto[] = []; @@ -268,20 +344,7 @@ export class EmailHeadersService { } //****************************************** */ - private getPriorityValue(priority: "high" | "normal" | "low"): string { - switch (priority) { - case "high": - return "1"; - case "low": - return "5"; - default: - return "3"; - } + private getPriorityValue(priority: Priority): string { + return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3"; } - - // private getServerIP(): string { - // // In a real implementation, you might want to get the actual server IP - // // For now, returning a placeholder - // return "127.0.0.1"; - // } } diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index e12af00..21ab32c 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -62,7 +62,10 @@ export class EmailService { name: user.displayName, }; - const headers = this.generateHeadersForEmailType(user); + const headers = this.generateHeadersForEmailType(user, { + html: sendEmailDto.html, + text: sendEmailDto.text, + }); sendEmailDto.headers = [...headers]; @@ -177,21 +180,21 @@ export class EmailService { } //######################################################## - async getMessage(wildduckUserId: string, messageId: number) { + async getMessage(wildduckUserId: string, messageId: number, mailboxId: string) { try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); + this.logger.log(`Getting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`); - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); + await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); + const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId)); - this.logger.log(`Getting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); - - await this.markMessageAsSeen(wildduckUserId, messageId); - const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, messageLocation.mailboxId, messageId)); - - this.logger.log(`Retrieved message ${messageId} from ${messageLocation.mailboxName} for user: ${wildduckUserId}`); + this.logger.log(`Retrieved message ${messageId} from ${mailboxId} for user: ${wildduckUserId}`); + const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId); return { - message, + message: { + ...message, + mailboxName, + }, }; } catch (error) { this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); @@ -200,17 +203,13 @@ export class EmailService { } //######################################################## - async deleteMessage(wildduckUserId: string, messageId: number) { + async deleteMessage(wildduckUserId: string, messageId: number, mailboxId: string) { try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); + this.logger.log(`Deleting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`); - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); + const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, mailboxId, messageId)); - this.logger.log(`Deleting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); - - const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, messageLocation.mailboxId, messageId)); - - this.logger.log(`Message ${messageId} deleted successfully from ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} deleted successfully from ${mailboxId}`); return { message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY, @@ -243,24 +242,18 @@ export class EmailService { } //============================================== - async markMessageAsSeen(wildduckUserId: string, messageId: number) { + async markMessageAsSeen(wildduckUserId: string, messageId: number, mailboxId: string) { try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); + this.logger.log(`Marking message ${messageId} as seen in ${mailboxId} mailbox for user: ${wildduckUserId}`); - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); + const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: true })); - this.logger.log(`Marking message ${messageId} as seen in ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); - - const { updated } = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { seen: true }), - ); - - this.logger.log(`Message ${messageId} marked as seen successfully in ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} marked as seen successfully in ${mailboxId}`); await this.emailNotificationService.notifyEmailRead({ userId: wildduckUserId, messageId, - mailboxId: messageLocation.mailboxId, + mailboxId, }); return { @@ -468,6 +461,12 @@ export class EmailService { if (processedTemplate.hasTemplate) { this.logger.log(`Draft content processed through template for business: ${user.business.id}`); } + + const headers = this.generateHeadersForEmailType(user, { + html: draftData.html, + text: draftData.text, + }); + draftData.headers = [...(draftData.headers || []), ...headers]; } const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData)); @@ -535,25 +534,21 @@ export class EmailService { } //============================================== - async moveMessageToArchive(wildduckUserId: string, messageId: number) { + async moveMessageToArchive(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); - - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`); + this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: archiveMailboxId, }), ); - this.logger.log(`Message ${messageId} moved to archive successfully from ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} moved to archive successfully from ${mailboxId}`); return { success: true, @@ -569,25 +564,21 @@ export class EmailService { } //============================================== - async moveMessageToFavorite(wildduckUserId: string, messageId: number) { + async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); - - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`); + this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: favoriteMailboxId, }), ); - this.logger.log(`Message ${messageId} moved to favorite successfully from ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} moved to favorite successfully from ${mailboxId}`); return { success: true, @@ -603,25 +594,21 @@ export class EmailService { } //============================================== - async moveMessageToTrash(wildduckUserId: string, messageId: number) { + async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); - - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`); + this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: trashMailboxId, }), ); - this.logger.log(`Message ${messageId} moved to trash successfully from ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} moved to trash successfully from ${mailboxId}`); return { success: true, @@ -637,15 +624,14 @@ export class EmailService { } //============================================== - async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number) { + async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`); try { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, archiveMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -666,15 +652,14 @@ export class EmailService { } //============================================== - async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number) { + async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`); try { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, favoriteMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -695,15 +680,14 @@ export class EmailService { } //============================================== - async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number) { + async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`); try { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, trashMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -724,25 +708,21 @@ export class EmailService { } //============================================== - async moveMessageToJunk(wildduckUserId: string, messageId: number) { + async moveMessageToJunk(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); - - if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`); + this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: junkMailboxId, }), ); - this.logger.log(`Message ${messageId} moved to junk successfully from ${messageLocation.mailboxName}`); + this.logger.log(`Message ${messageId} moved to junk successfully from ${mailboxId}`); const domainTag = await this.getUserBusinessId(wildduckUserId); @@ -769,15 +749,14 @@ export class EmailService { } //============================================== - async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number) { + async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number, mailboxId: string) { this.logger.log(`Moving message ${messageId} from junk to inbox for user: ${wildduckUserId}`); try { const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, junkMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -811,7 +790,7 @@ export class EmailService { //============================================== // BULK ACTIONS //============================================== - async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto) { + async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) { this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${wildduckUserId}`); const results = { @@ -824,34 +803,34 @@ export class EmailService { try { switch (bulkActionDto.action) { case BulkActionType.SEEN: - await this.markMessageAsSeen(wildduckUserId, messageId); + await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); break; case BulkActionType.DELETE: - await this.deleteMessage(wildduckUserId, messageId); + await this.deleteMessage(wildduckUserId, messageId, mailboxId); break; case BulkActionType.ARCHIVE: - await this.moveMessageToArchive(wildduckUserId, messageId); + await this.moveMessageToArchive(wildduckUserId, messageId, mailboxId); break; case BulkActionType.UNARCHIVE: - await this.moveMessageToInboxFromArchive(wildduckUserId, messageId); + await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId); break; case BulkActionType.FAVORITE: - await this.moveMessageToFavorite(wildduckUserId, messageId); + await this.moveMessageToFavorite(wildduckUserId, messageId, mailboxId); break; case BulkActionType.UNFAVORITE: - await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId); + await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId, mailboxId); break; case BulkActionType.JUNK: - await this.moveMessageToJunk(wildduckUserId, messageId); + await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId); break; case BulkActionType.NOTJUNK: - await this.moveMessageToInboxFromJunk(wildduckUserId, messageId); + await this.moveMessageToInboxFromJunk(wildduckUserId, messageId, mailboxId); break; case BulkActionType.TRASH: - await this.moveMessageToTrash(wildduckUserId, messageId); + await this.moveMessageToTrash(wildduckUserId, messageId, mailboxId); break; case BulkActionType.RESTORE: - await this.moveMessageToInboxFromTrash(wildduckUserId, messageId); + await this.moveMessageToInboxFromTrash(wildduckUserId, messageId, mailboxId); break; default: throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`); @@ -920,42 +899,6 @@ export class EmailService { return transformedQuery; } - //######################################################## - - private async findMessageMailbox(userId: string, messageId: number): Promise<{ mailboxId: string; mailboxName: string } | null> { - try { - const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId); - - const mailboxesToSearch = [ - { id: mailboxIds.inbox, name: MailboxEnum.INBOX }, - { id: mailboxIds.sent, name: MailboxEnum.SENT }, - { id: mailboxIds.drafts, name: MailboxEnum.DRAFTS }, - { id: mailboxIds.trash, name: MailboxEnum.TRASH }, - { id: mailboxIds.archive, name: MailboxEnum.ARCHIVE }, - { id: mailboxIds.favorite, name: MailboxEnum.FAVORITE }, - { id: mailboxIds.junk, name: MailboxEnum.Junk }, - ]; - - for (const mailbox of mailboxesToSearch) { - if (!mailbox.id) continue; - - try { - await firstValueFrom(this.mailServerService.messages.getMessage(userId, mailbox.id, messageId)); - this.logger.log(`Message ${messageId} found in ${mailbox.name} mailbox`); - return { mailboxId: mailbox.id, mailboxName: mailbox.name }; - } catch (_error) { - continue; - } - } - - this.logger.warn(`Message ${messageId} not found in any mailbox for user ${userId}`); - return null; - } catch (error) { - this.logger.error(`Failed to find message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } - } - //============================================== private async getUserBusinessId(wildduckUserId: string) { const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); @@ -964,68 +907,21 @@ export class EmailService { //============================================== - //######################################################## - private generateHeadersForEmailType(user: User) { - // const emailType = sendEmailDto.emailType || EmailType.GENERAL; + private generateHeadersForEmailType(user: User, emailContent?: { html?: string; text?: string }) { const businessName = user.business.name; const businessDomain = user.emailAddress.split("@")[1]; + const htmlContent = emailContent?.html || ""; + const imageDetection = this.emailHeadersService.detectImagesInContent(htmlContent); + return this.emailHeadersService.generateAntiThreadingHeaders({ businessName, businessDomain, isTransactional: true, priority: Priority.Normal, + hasHostedImages: imageDetection.hasHostedImages, + imageCount: imageDetection.imageCount, + trustedImageSources: imageDetection.trustedImageSources, }); - - // switch (emailType) { - // case EmailType.TRANSACTIONAL: - // return this.emailHeadersService.generateTransactionalHeaders({ - // businessName, - // businessDomain, - // transactionId: sendEmailDto.transactionId, - // category: sendEmailDto.category, - // }); - - // case EmailType.MARKETING: - // if (!sendEmailDto.listId || !sendEmailDto.unsubscribeUrl) { - // throw new BadRequestException("Marketing emails require listId and unsubscribeUrl"); - // } - // return this.emailHeadersService.generateMarketingHeaders({ - // businessName, - // businessDomain, - // listId: sendEmailDto.listId, - // unsubscribeUrl: sendEmailDto.unsubscribeUrl, - // campaignId: sendEmailDto.campaignId, - // }); - - // case EmailType.ANTI_THREADING: { - // const antiThreadingHeaders = this.emailHeadersService.generateAntiThreadingHeaders({ - // businessName, - // businessDomain, - // isTransactional: true, - // priority: sendEmailDto.priority as Priority, - // }); - - // Add Gmail-specific headers if requested - // if (sendEmailDto.forceGmailBreak) { - // const gmailHeaders = this.emailHeadersService.generateGmailAntiThreadingHeaders({ - // businessDomain, - // conversationBreaker: sendEmailDto.conversationBreaker, - // }); - // antiThreadingHeaders.push(...gmailHeaders); - // } - - // return antiThreadingHeaders; - // } - - // case EmailType.GENERAL: - // default: - // return this.emailHeadersService.generateAntiThreadingHeaders({ - // businessName, - // businessDomain, - // isTransactional: true, - // priority: Priority.Normal, - // }); - // } } } diff --git a/src/modules/email/services/mailbox-resolver.service.ts b/src/modules/email/services/mailbox-resolver.service.ts index 49a48ce..d0c5de5 100644 --- a/src/modules/email/services/mailbox-resolver.service.ts +++ b/src/modules/email/services/mailbox-resolver.service.ts @@ -4,16 +4,7 @@ import { firstValueFrom } from "rxjs"; import { MailboxInfo } from "../../mail-server/interfaces/mailboxes-response.interface"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; - -export interface UserMailboxIds { - inbox: string; - sent: string; - drafts: string; - trash: string; - junk: string; - archive: string; - favorite: string; -} +import { UserMailboxIds } from "../interfaces/user-mailbox.interface"; @Injectable() export class MailboxResolverService { @@ -70,6 +61,15 @@ export class MailboxResolverService { return mailboxId; } + /** + * Get mailbox name by ID + */ + async getMailboxName(wildduckUserId: string, mailboxId: string): Promise { + const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(wildduckUserId)); + const mailbox = mailboxes.results.find((mb) => mb.id === mailboxId); + return mailbox?.name || ""; + } + /** * Get drafts mailbox ID */ diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index 140ffe2..eded0f5 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -13,7 +13,6 @@ import { BusinessInterceptor } from "../../core/interceptors/business.intercepto @Controller("users") @ApiHeader({ name: "x-business-id", description: "Business ID" }) -@UseInterceptors(BusinessInterceptor) @AuthGuards() export class UsersController { constructor(private readonly usersService: UsersService) {} @@ -26,6 +25,7 @@ export class UsersController { } @Post() + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Create a new email user" }) @ApiResponse({ status: 201, description: "Email user created successfully" }) createEmailUser(@Body() createEmailUserDto: CreateEmailUserDto, @BusinessDec("id") businessId: string) { @@ -33,6 +33,7 @@ export class UsersController { } @Get() + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Get email users for business" }) @ApiResponse({ status: 200, description: "Email users retrieved successfully" }) getEmailUsers(@BusinessDec("id") businessId: string, @Query() queryDto: UserListQueryDto) { @@ -40,6 +41,7 @@ export class UsersController { } @Delete(":id") + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Delete email user" }) @ApiResponse({ status: 200, description: "Email user deleted successfully" }) deleteEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { @@ -47,6 +49,7 @@ export class UsersController { } @Patch(":id") + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Update email user" }) @ApiResponse({ status: 200, description: "Email user updated successfully" }) updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) { @@ -54,6 +57,7 @@ export class UsersController { } @Patch(":id/quota") + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Update email user quota" }) @ApiResponse({ status: 200, description: "Email user quota updated successfully" }) updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) { @@ -61,6 +65,7 @@ export class UsersController { } @Patch(":id/toggle-status") + @UseInterceptors(BusinessInterceptor) @ApiOperation({ summary: "Update email user status" }) @ApiResponse({ status: 200, description: "Email user status updated successfully" }) updateEmailUserStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {