From b2dd55b166a1b72f0dffacd99634788c5323995d Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Sun, 20 Jul 2025 14:48:02 +0330 Subject: [PATCH] chore: add new fetaure --- src/common/enums/message.enum.ts | 1 + src/modules/email/DTO/bulk-actions.dto.ts | 1 + src/modules/email/email.controller.ts | 58 ++- src/modules/email/email.module.ts | 16 +- .../email/interfaces/email-spam.interface.ts | 19 + .../email/services/email-spam.service.ts | 208 ++++++++++ src/modules/email/services/email.service.ts | 366 +++++++++++------- .../mail-server/DTO/domain-access.dto.ts | 38 ++ src/modules/mail-server/DTO/index.ts | 30 -- .../interfaces/attachment.interface.ts | 3 + .../domain-access-response.interface.ts | 23 ++ .../interfaces/messages-response.interface.ts | 12 +- src/modules/mail-server/mail-server.module.ts | 3 + .../services/mail-server.service.ts | 6 + .../wildduck-domain-access.service.ts | 52 +++ .../services/wildduck-messages.service.ts | 5 +- src/modules/mailbox/mailbox.controller.ts | 2 +- .../mailbox/services/mailbox.service.ts | 2 +- src/modules/users/services/users.service.ts | 2 +- 19 files changed, 654 insertions(+), 193 deletions(-) create mode 100644 src/modules/email/interfaces/email-spam.interface.ts create mode 100644 src/modules/email/services/email-spam.service.ts create mode 100644 src/modules/mail-server/DTO/domain-access.dto.ts delete mode 100644 src/modules/mail-server/DTO/index.ts create mode 100644 src/modules/mail-server/interfaces/attachment.interface.ts create mode 100644 src/modules/mail-server/interfaces/domain-access-response.interface.ts create mode 100644 src/modules/mail-server/services/wildduck-domain-access.service.ts diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 5a41374..75fe9d4 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -212,6 +212,7 @@ export const enum EmailMessage { MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY = "پیام با موفقیت از آرشیو به صندوق ورودی منتقل شد", MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از علاقه مندی ها به صندوق ورودی منتقل شد", MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY = "پیام با موفقیت از سطل زباله به صندوق ورودی منتقل شد", + MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY = "پیام با موفقیت از جانک به صندوق ورودی منتقل شد", MESSAGE_NOT_FOUND = "پیام یافت نشد", // Bulk action messages diff --git a/src/modules/email/DTO/bulk-actions.dto.ts b/src/modules/email/DTO/bulk-actions.dto.ts index b69d4f0..d8a4244 100644 --- a/src/modules/email/DTO/bulk-actions.dto.ts +++ b/src/modules/email/DTO/bulk-actions.dto.ts @@ -11,6 +11,7 @@ export enum BulkActionType { UNARCHIVE = "unarchive", RESTORE = "restore", JUNK = "junk", + NOTJUNK = "notjunk", TRASH = "trash", } diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index 57c36b4..047cd03 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -214,10 +214,19 @@ export class EmailController { return this.emailService.moveMessageToJunk(userId, Number(params.messageId)); } + @Patch("messages/:messageId/not-junk") + @ApiOperation({ summary: "Move a message from junk back to inbox (not spam)" }) + @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)); + } + @Post("messages/bulk-action") @ApiOperation({ summary: "Perform bulk actions on multiple messages", - description: "Apply actions (seen, delete, archive, unarchive, favorite, unfavorite, junk, trash, restore) to multiple messages at once", + description: "Apply actions (seen, delete, archive, unarchive, favorite, unfavorite, junk, notjunk, trash, restore) to multiple messages at once", }) @ApiResponse({ status: 200, description: "Bulk action completed successfully" }) @ApiResponse({ status: 206, description: "Bulk action partially completed" }) @@ -226,4 +235,51 @@ export class EmailController { performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto) { return this.emailService.performBulkAction(userId, bulkActionDto); } + + // // Domain Access Management Endpoints + // @Get("spam/domains/allowed") + // @ApiOperation({ summary: "List domains in allowlist (never marked as spam)" }) + // @ApiResponse({ status: 200, description: "List of allowed domains" }) + // listAllowedDomains(@UserDec("id") userId: string, @Query() query: ListDomainAccessQueryDto) { + // return this.emailSpamService.listAllowedDomains(userId, query); + // } + + // @Get("spam/domains/blocked") + // @ApiOperation({ summary: "List domains in blocklist (always marked as spam)" }) + // @ApiResponse({ status: 200, description: "List of blocked domains" }) + // listBlockedDomains(@UserDec("id") userId: string, @Query() query: ListDomainAccessQueryDto) { + // return this.emailSpamService.listBlockedDomains(userId, query); + // } + + // @Post("spam/domains/allowed") + // @ApiOperation({ summary: "Add domain to allowlist" }) + // @ApiResponse({ status: 201, description: "Domain added to allowlist successfully" }) + // @ApiResponse({ status: 400, description: "Invalid domain data" }) + // addDomainToAllowlist(@UserDec("id") userId: string, @Body() createDomainDto: CreateDomainAccessDto) { + // return this.emailSpamService.addDomainToAllowlist(createDomainDto.domain, createDomainDto.description, userId); + // } + + // @Post("spam/domains/blocked") + // @ApiOperation({ summary: "Add domain to blocklist" }) + // @ApiResponse({ status: 201, description: "Domain added to blocklist successfully" }) + // @ApiResponse({ status: 400, description: "Invalid domain data" }) + // addDomainToBlocklist(@UserDec("id") userId: string, @Body() createDomainDto: CreateDomainAccessDto) { + // return this.emailSpamService.addDomainToBlocklist(createDomainDto.domain, createDomainDto.description, userId); + // } + + // @Delete("spam/domains/allowed/:domain") + // @ApiOperation({ summary: "Remove domain from allowlist" }) + // @ApiResponse({ status: 200, description: "Domain removed from allowlist successfully" }) + // @ApiResponse({ status: 404, description: "Domain not found in allowlist" }) + // removeDomainFromAllowlist(@UserDec("id") userId: string, @Param("domain") domain: string) { + // return this.emailSpamService.removeDomainFromAllowlist(domain, userId); + // } + + // @Delete("spam/domains/blocked/:domain") + // @ApiOperation({ summary: "Remove domain from blocklist" }) + // @ApiResponse({ status: 200, description: "Domain removed from blocklist successfully" }) + // @ApiResponse({ status: 404, description: "Domain not found in blocklist" }) + // removeDomainFromBlocklist(@UserDec("id") userId: string, @Param("domain") domain: string) { + // return this.emailSpamService.removeDomainFromBlocklist(domain, userId); + // } } diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index e463ee9..db31202 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -4,21 +4,22 @@ import { Module } from "@nestjs/common"; import { JwtModule } from "@nestjs/jwt"; // import { EmailGatewayController } from "./controllers/email-gateway.controller"; +import { EMAIL_QUEUE_CONSTANTS } from "./constants/email-events.constant"; import { EmailController } from "./email.controller"; import { EmailGateway } from "./gateways/email.gateway"; import { EmailMonitoringProcessor } from "./queue/email-monitoring.processor"; import { EmailHeadersService } from "./services/email-headers.service"; import { EmailMonitoringService } from "./services/email-monitoring.service"; import { EmailNotificationService } from "./services/email-notification.service"; +import { EmailSpamService } from "./services/email-spam.service"; import { EmailService } from "./services/email.service"; import { MailboxResolverService } from "./services/mailbox-resolver.service"; import { jwtConfig } from "../../configs/jwt.config"; import { MailServerModule } from "../mail-server/mail-server.module"; import { TemplatesModule } from "../templates/templates.module"; +import { WebSocketAuthService } from "./services/websocket-auth.service"; import { User } from "../users/entities/user.entity"; import { UsersModule } from "../users/users.module"; -import { EMAIL_QUEUE_CONSTANTS } from "./constants/email-events.constant"; -import { WebSocketAuthService } from "./services/websocket-auth.service"; @Module({ imports: [ @@ -34,6 +35,7 @@ import { WebSocketAuthService } from "./services/websocket-auth.service"; controllers: [EmailController], providers: [ EmailService, + EmailSpamService, MailboxResolverService, EmailHeadersService, EmailGateway, @@ -42,6 +44,14 @@ import { WebSocketAuthService } from "./services/websocket-auth.service"; EmailMonitoringProcessor, WebSocketAuthService, ], - exports: [EmailService, MailboxResolverService, EmailHeadersService, EmailGateway, EmailMonitoringService, EmailNotificationService], + exports: [ + EmailService, + EmailSpamService, + MailboxResolverService, + EmailHeadersService, + EmailGateway, + EmailMonitoringService, + EmailNotificationService, + ], }) export class EmailModule {} diff --git a/src/modules/email/interfaces/email-spam.interface.ts b/src/modules/email/interfaces/email-spam.interface.ts new file mode 100644 index 0000000..95199b4 --- /dev/null +++ b/src/modules/email/interfaces/email-spam.interface.ts @@ -0,0 +1,19 @@ +export interface SpamActionOptions { + messageId: number; + wildduckUserId: string; + /** + * Whether to automatically add the sender's domain to allowlist/blocklist + * @default false + */ + manageDomainList?: boolean; + /** + * Tag to use for domain access control (defaults to user's business ID or 'default') + */ + domainTag?: string; +} + +export interface MessageSenderInfo { + senderDomain: string; + senderEmail: string; + messageId: number; +} diff --git a/src/modules/email/services/email-spam.service.ts b/src/modules/email/services/email-spam.service.ts new file mode 100644 index 0000000..10948d6 --- /dev/null +++ b/src/modules/email/services/email-spam.service.ts @@ -0,0 +1,208 @@ +import { Injectable, Logger } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; + +import { EmailMessage } from "../../../common/enums/message.enum"; +import { ListDomainAccessQueryDto } from "../../mail-server/DTO/domain-access.dto"; +import { MailServerService } from "../../mail-server/services/mail-server.service"; +import { MessageSenderInfo, SpamActionOptions } from "../interfaces/email-spam.interface"; + +@Injectable() +export class EmailSpamService { + private readonly logger = new Logger(EmailSpamService.name); + + constructor(private readonly mailServerService: MailServerService) {} + + async getMessageSenderInfo(userId: string, messageId: number): Promise { + try { + const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(userId)); + + for (const mailbox of mailboxes.results) { + try { + const message = await firstValueFrom(this.mailServerService.messages.getMessage(userId, mailbox.id, messageId)); + + if (message && message.from && message.from.address) { + const senderEmail = message.from.address; + const senderDomain = senderEmail.split("@")[1]?.toLowerCase(); + + if (senderDomain) { + return { + senderDomain, + senderEmail, + messageId, + }; + } + } + } catch (_error) { + continue; + } + } + + return null; + } catch (error) { + this.logger.error(`Failed to get sender info for message ${messageId}: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async markAsSpam(options: SpamActionOptions) { + const { messageId, wildduckUserId, manageDomainList = false, domainTag = "default" } = options; + + this.logger.log(`Marking message ${messageId} as spam for user: ${wildduckUserId}`); + + try { + let domainBlocked = false; + + if (manageDomainList) { + const senderInfo = await this.getMessageSenderInfo(wildduckUserId, messageId); + + if (senderInfo) { + try { + await firstValueFrom( + this.mailServerService.domainAccess.addBlockedDomain(domainTag, { + domain: senderInfo.senderDomain, + description: `Auto-blocked due to spam report from message ${messageId}`, + }), + ); + + domainBlocked = true; + this.logger.log(`Added domain ${senderInfo.senderDomain} to blocklist for tag: ${domainTag}`); + } catch (error) { + this.logger.warn(`Could not add domain to blocklist: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } + } + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY, + domainBlocked, + }; + } catch (error) { + this.logger.error(`Failed to mark message ${messageId} as spam: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async markAsNotSpam(options: SpamActionOptions) { + const { messageId, wildduckUserId, manageDomainList = false, domainTag = "default" } = options; + + this.logger.log(`Marking message ${messageId} as not spam for user: ${wildduckUserId}`); + + try { + let domainAllowed = false; + + if (manageDomainList) { + const senderInfo = await this.getMessageSenderInfo(wildduckUserId, messageId); + + if (senderInfo) { + try { + await firstValueFrom( + this.mailServerService.domainAccess.addAllowedDomain(domainTag, { + domain: senderInfo.senderDomain, + description: `Auto-allowed due to not-spam report from message ${messageId}`, + }), + ); + + domainAllowed = true; + this.logger.log(`Added domain ${senderInfo.senderDomain} to allowlist for tag: ${domainTag}`); + } catch (error) { + this.logger.warn(`Could not add domain to allowlist: ${error instanceof Error ? error.message : "Unknown error"}`); + } + } + } + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY, + domainAllowed, + }; + } catch (error) { + this.logger.error(`Failed to mark message ${messageId} as not spam: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async listAllowedDomains(domainTag = "default", query?: ListDomainAccessQueryDto) { + this.logger.log(`Listing allowed domains for tag: ${domainTag}`); + + try { + return await firstValueFrom(this.mailServerService.domainAccess.listAllowedDomains(domainTag, query)); + } catch (error) { + this.logger.error(`Failed to list allowed domains: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async listBlockedDomains(domainTag = "default", query?: ListDomainAccessQueryDto) { + this.logger.log(`Listing blocked domains for tag: ${domainTag}`); + + try { + return await firstValueFrom(this.mailServerService.domainAccess.listBlockedDomains(domainTag, query)); + } catch (error) { + this.logger.error(`Failed to list blocked domains: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async removeDomainFromAllowlist(domain: string, domainTag = "default") { + this.logger.log(`Removing domain ${domain} from allowlist for tag: ${domainTag}`); + + try { + return await firstValueFrom(this.mailServerService.domainAccess.removeAllowedDomain(domainTag, domain)); + } catch (error) { + this.logger.error(`Failed to remove domain from allowlist: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async removeDomainFromBlocklist(domain: string, domainTag = "default") { + this.logger.log(`Removing domain ${domain} from blocklist for tag: ${domainTag}`); + + try { + return await firstValueFrom(this.mailServerService.domainAccess.removeBlockedDomain(domainTag, domain)); + } catch (error) { + this.logger.error(`Failed to remove domain from blocklist: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async addDomainToAllowlist(domain: string, description?: string, domainTag = "default") { + this.logger.log(`Adding domain ${domain} to allowlist for tag: ${domainTag}`); + + try { + return await firstValueFrom( + this.mailServerService.domainAccess.addAllowedDomain(domainTag, { + domain, + description: description || `Manually added to allowlist`, + }), + ); + } catch (error) { + this.logger.error(`Failed to add domain to allowlist: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } + + //============================================== + async addDomainToBlocklist(domain: string, description?: string, domainTag = "default") { + this.logger.log(`Adding domain ${domain} to blocklist for tag: ${domainTag}`); + + try { + return await firstValueFrom( + this.mailServerService.domainAccess.addBlockedDomain(domainTag, { + domain, + description: description || `Manually added to blocklist`, + }), + ); + } catch (error) { + this.logger.error(`Failed to add domain to blocklist: ${error instanceof Error ? error.message : "Unknown error"}`); + throw error; + } + } +} diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 4aa1b40..e12af00 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -3,6 +3,7 @@ import { firstValueFrom } from "rxjs"; import { EmailHeadersService } from "./email-headers.service"; import { EmailNotificationService } from "./email-notification.service"; +import { EmailSpamService } from "./email-spam.service"; import { MailboxResolverService } from "./mailbox-resolver.service"; import { EmailMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; @@ -26,14 +27,15 @@ export class EmailService { private readonly templateProcessorService: TemplateProcessorService, private readonly userRepository: UserRepository, private readonly emailHeadersService: EmailHeadersService, + private readonly emailSpamService: EmailSpamService, ) {} //######################################################## - async sendEmail(userEmailId: string, userId: string, sendEmailDto: SendEmailDto) { - this.logger.log(`Sending ${EmailType.GENERAL} email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`); + async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) { + this.logger.log(`Sending ${EmailType.GENERAL} email from user ${wildduckUserId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`); try { - const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] }); + const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND); this.logger.log(`Processing email through business template for business: ${user.business.id}`); @@ -64,14 +66,14 @@ export class EmailService { sendEmailDto.headers = [...headers]; - const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userEmailId, sendEmailDto)); + const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, sendEmailDto)); this.logger.log(`Email sent successfully: ${response.message.id} (Queue: ${response.message.queueId})`); - const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(userId); + const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); await this.emailNotificationService.notifyEmailSent({ - userId, + userId: wildduckUserId, messageId: parseInt(response.message.id), mailboxId: sentMailboxId, recipients: sendEmailDto.to, @@ -85,7 +87,7 @@ export class EmailService { queueId: response.message.queueId, }; } catch (error) { - this.logger.error(`Failed to send email for user ${userEmailId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to send email for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } @@ -109,8 +111,8 @@ export class EmailService { } //######################################################## - async searchMessages(userId: string, query: SearchMessagesQueryDto) { - this.logger.log(`Searching messages for user ${userId} with query: ${query.q}`); + async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { + this.logger.log(`Searching messages for user ${wildduckUserId} with query: ${query.q}`); try { // Transform query parameters for WildDuck API compatibility @@ -128,10 +130,10 @@ export class EmailService { this.logger.debug("Search query for WildDuck:", wildDuckQuery); - const response = await firstValueFrom(this.mailServerService.messages.searchMessages(userId, 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: ${userId}`, + `Found ${response.results.length} messages (${response.total} total, page ${response.page}) matching search for user: ${wildduckUserId}`, ); return { @@ -143,22 +145,22 @@ export class EmailService { query: response.query, }; } catch (error) { - this.logger.error(`Failed to search messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to search messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //######################################################## - async listMessages(userId: string, query: MessageListQueryDto) { + async listMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); - this.logger.log(`Listing messages in mailbox ${inboxMailboxId} for user: ${userId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + this.logger.log(`Listing messages in mailbox ${inboxMailboxId} for user: ${wildduckUserId}`); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, inboxMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, inboxMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} total messages (page ${response.page}) in mailbox ${inboxMailboxId} for user: ${userId}`); + this.logger.log(`Found ${response.total} total messages (page ${response.page}) in mailbox ${inboxMailboxId} for user: ${wildduckUserId}`); return { results: response.results, @@ -169,44 +171,44 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to list messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to list messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //######################################################## - async getMessage(userId: string, messageId: number) { + async getMessage(wildduckUserId: string, messageId: number) { try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - this.logger.log(`Getting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${userId}`); + this.logger.log(`Getting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); - await this.markMessageAsSeen(userId, messageId); - const message = await firstValueFrom(this.mailServerService.messages.getMessage(userId, messageLocation.mailboxId, messageId)); + 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: ${userId}`); + this.logger.log(`Retrieved message ${messageId} from ${messageLocation.mailboxName} for user: ${wildduckUserId}`); return { message, }; } catch (error) { - this.logger.error(`Failed to get message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //######################################################## - async deleteMessage(userId: string, messageId: number) { + async deleteMessage(wildduckUserId: string, messageId: number) { try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - this.logger.log(`Deleting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${userId}`); + this.logger.log(`Deleting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); - const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(userId, messageLocation.mailboxId, messageId)); + const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, messageLocation.mailboxId, messageId)); this.logger.log(`Message ${messageId} deleted successfully from ${messageLocation.mailboxName}`); @@ -215,17 +217,19 @@ export class EmailService { result, }; } catch (error) { - this.logger.error(`Failed to delete message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error( + `Failed to delete message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); throw error; } } //============================================== - async deleteAllMessages(userId: string, mailboxId: string) { + async deleteAllMessages(wildduckUserId: string, mailboxId: string) { try { - const result = await firstValueFrom(this.mailServerService.messages.deleteAllFromMailbox(userId, mailboxId)); + const result = await firstValueFrom(this.mailServerService.messages.deleteAllFromMailbox(wildduckUserId, mailboxId)); - this.logger.log(`All messages deleted successfully from ${mailboxId} for user: ${userId}`); + this.logger.log(`All messages deleted successfully from ${mailboxId} for user: ${wildduckUserId}`); return { message: EmailMessage.ALL_MESSAGES_DELETED_SUCCESSFULLY, @@ -233,28 +237,28 @@ export class EmailService { result, }; } catch (error) { - this.logger.error(`Failed to delete all messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to delete all messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async markMessageAsSeen(userId: string, messageId: number) { + async markMessageAsSeen(wildduckUserId: string, messageId: number) { try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - this.logger.log(`Marking message ${messageId} as seen in ${messageLocation.mailboxName} mailbox for user: ${userId}`); + this.logger.log(`Marking message ${messageId} as seen in ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); const { updated } = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { seen: true }), + this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { seen: true }), ); this.logger.log(`Message ${messageId} marked as seen successfully in ${messageLocation.mailboxName}`); await this.emailNotificationService.notifyEmailRead({ - userId, + userId: wildduckUserId, messageId, mailboxId: messageLocation.mailboxId, }); @@ -266,20 +270,20 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to mark message ${messageId} as seen for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to mark message ${messageId} as seen for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async getSentMessages(userId: string, query: MessageListQueryDto) { + async getSentMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(userId); + const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, sentMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, sentMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} sent messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} sent messages (page ${response.page}) for user: ${wildduckUserId}`); return { sentMails: response.results, @@ -290,19 +294,19 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get sent messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get sent messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async getTrashMessages(userId: string, query: MessageListQueryDto) { + async getTrashMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(userId); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, trashMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, trashMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} trash messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} trash messages (page ${response.page}) for user: ${wildduckUserId}`); return { trashMails: response.results, @@ -313,19 +317,19 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get trash messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get trash messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async getJunkMessages(userId: string, query: MessageListQueryDto) { + async getJunkMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(userId); + const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, junkMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, junkMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} junk messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} junk messages (page ${response.page}) for user: ${wildduckUserId}`); return { junkMails: response.results, @@ -336,19 +340,19 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get junk messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get junk messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async getArchivedMessages(userId: string, query: MessageListQueryDto) { + async getArchivedMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(userId); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, archiveMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, archiveMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} archived messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} archived messages (page ${response.page}) for user: ${wildduckUserId}`); return { archiveMails: response.results, @@ -359,19 +363,19 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get archive messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get archive messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async getFavoriteMessages(userId: string, query: MessageListQueryDto) { + async getFavoriteMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(userId); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, favoriteMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, favoriteMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} favorite messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} favorite messages (page ${response.page}) for user: ${wildduckUserId}`); return { favoriteMails: response.results, @@ -382,20 +386,20 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get favorite messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get favorite messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async getDraftsMessages(userId: string, query: MessageListQueryDto) { + async getDraftsMessages(wildduckUserId: string, query: MessageListQueryDto) { try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(userId); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, draftsMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, draftsMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} draft messages (page ${response.page}) for user: ${userId}`); + this.logger.log(`Found ${response.total} draft messages (page ${response.page}) for user: ${wildduckUserId}`); return { draftsMail: response.results, @@ -406,18 +410,18 @@ export class EmailService { paginate: true, }; } catch (error) { - this.logger.error(`Failed to get drafts messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to get drafts messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async updateDraft(userEmailId: string, messageId: number, updateDraftDto: UpdateDraftDto) { - this.logger.log(`Updating draft ${messageId} for user ${userEmailId}`); + async updateDraft(wildduckUserId: string, messageId: number, updateDraftDto: UpdateDraftDto) { + this.logger.log(`Updating draft ${messageId} for user ${wildduckUserId}`); try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(userEmailId); - const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(userEmailId, draftsMailboxId, messageId)); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId)); const draftData: SendEmailDto = { // mailbox: updateDraftDto.mailbox, @@ -444,7 +448,7 @@ export class EmailService { }; // Process draft content through business template if available - const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] }); + const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) { const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, user.id, { @@ -466,7 +470,7 @@ export class EmailService { } } - const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userEmailId, draftData)); + const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData)); this.logger.log(`Draft updated successfully: ${response.message.mailbox}`); @@ -477,44 +481,44 @@ export class EmailService { isDraft: true, }; } catch (error) { - this.logger.error(`Failed to update draft ${messageId} for user ${userEmailId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error( + `Failed to update draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); throw error; } } //============================================== - async sendDraft(userId: string, messageId: number) { - this.logger.log(`Sending draft ${messageId} for user: ${userId}`); + async sendDraft(wildduckUserId: string, messageId: number) { + this.logger.log(`Sending draft ${messageId} for user: ${wildduckUserId}`); try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(userId); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.submitDraft(userId, draftsMailboxId, messageId, { - deleteFiles: false, - }), + this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }), ); this.logger.log(`Draft ${messageId} sent successfully`); return { message: EmailMessage.DRAFT_SENT_SUCCESSFULLY, - messageId: result.id, + messageId: result.message.id, queueId: result.queueId, result, }; } catch (error) { - this.logger.error(`Failed to send draft ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to send draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } //============================================== - async deleteDraft(userId: string, messageId: number) { - this.logger.log(`Deleting draft ${messageId} for user: ${userId}`); + async deleteDraft(wildduckUserId: string, messageId: number) { + this.logger.log(`Deleting draft ${messageId} for user: ${wildduckUserId}`); try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(userId); - const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(userId, draftsMailboxId, messageId)); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, draftsMailboxId, messageId)); this.logger.log(`Draft ${messageId} deleted successfully`); @@ -523,26 +527,28 @@ export class EmailService { result, }; } catch (error) { - this.logger.error(`Failed to delete draft ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error( + `Failed to delete draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); throw error; } } //============================================== - async moveMessageToArchive(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} to archive for user: ${userId}`); + async moveMessageToArchive(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(userId); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.ARCHIVE} for user: ${userId}`); + this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { moveTo: archiveMailboxId, }), ); @@ -556,27 +562,27 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} to archive for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} to archive for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async moveMessageToFavorite(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} to favorite for user: ${userId}`); + async moveMessageToFavorite(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(userId); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.FAVORITE} for user: ${userId}`); + this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { moveTo: favoriteMailboxId, }), ); @@ -590,26 +596,27 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} to favorite for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} to favorite for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } - async moveMessageToTrash(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} to trash for user: ${userId}`); + //============================================== + async moveMessageToTrash(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(userId); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.TRASH} for user: ${userId}`); + this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { moveTo: trashMailboxId, }), ); @@ -623,22 +630,22 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} to trash for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} to trash for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async moveMessageToInboxFromArchive(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${userId}`); + async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`); try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(userId); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, archiveMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, archiveMailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -652,22 +659,22 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} from archive to inbox for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} from archive to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async moveMessageToInboxFromFavorite(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${userId}`); + async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`); try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(userId); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, favoriteMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, favoriteMailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -681,22 +688,22 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} from favorite to inbox for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} from favorite to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async moveMessageToInboxFromTrash(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${userId}`); + async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`); try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(userId); - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(userId); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, trashMailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, trashMailboxId, messageId, { moveTo: inboxMailboxId, }), ); @@ -710,33 +717,44 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} from trash to inbox for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} from trash to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } } //============================================== - async moveMessageToJunk(userId: string, messageId: number) { - this.logger.log(`Moving message ${messageId} to junk for user: ${userId}`); + async moveMessageToJunk(wildduckUserId: string, messageId: number) { + this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`); try { - const messageLocation = await this.findMessageMailbox(userId, messageId); + const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(userId); + const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); - this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.Junk} for user: ${userId}`); + this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`); const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(userId, messageLocation.mailboxId, messageId, { + this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { moveTo: junkMailboxId, }), ); this.logger.log(`Message ${messageId} moved to junk successfully from ${messageLocation.mailboxName}`); + const domainTag = await this.getUserBusinessId(wildduckUserId); + + const spamResult = await this.emailSpamService.markAsSpam({ + messageId, + wildduckUserId, + manageDomainList: true, + domainTag, + }); + + this.logger.log(`Automatic domain management result for message ${messageId}: domainBlocked=${spamResult.domainBlocked}`); + return { success: true, message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY, @@ -744,7 +762,47 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to move message ${messageId} to junk for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to move message ${messageId} to junk for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + throw error; + } + } + + //============================================== + async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number) { + 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, { + moveTo: inboxMailboxId, + }), + ); + + this.logger.log(`Message ${messageId} moved from junk to inbox successfully`); + + const domainTag = await this.getUserBusinessId(wildduckUserId); + + const notSpamResult = await this.emailSpamService.markAsNotSpam({ + messageId, + wildduckUserId, + manageDomainList: true, + domainTag, + }); + + this.logger.log(`Automatic domain management result for message ${messageId}: domainAllowed=${notSpamResult.domainAllowed}`); + + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY, + result, + }; + } catch (error) { + this.logger.error( + `Failed to move message ${messageId} from junk to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } @@ -753,8 +811,8 @@ export class EmailService { //============================================== // BULK ACTIONS //============================================== - async performBulkAction(userId: string, bulkActionDto: BulkActionDto) { - this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${userId}`); + async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto) { + this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${wildduckUserId}`); const results = { successful: [] as number[], @@ -766,31 +824,34 @@ export class EmailService { try { switch (bulkActionDto.action) { case BulkActionType.SEEN: - await this.markMessageAsSeen(userId, messageId); + await this.markMessageAsSeen(wildduckUserId, messageId); break; case BulkActionType.DELETE: - await this.deleteMessage(userId, messageId); + await this.deleteMessage(wildduckUserId, messageId); break; case BulkActionType.ARCHIVE: - await this.moveMessageToArchive(userId, messageId); + await this.moveMessageToArchive(wildduckUserId, messageId); break; case BulkActionType.UNARCHIVE: - await this.moveMessageToInboxFromArchive(userId, messageId); + await this.moveMessageToInboxFromArchive(wildduckUserId, messageId); break; case BulkActionType.FAVORITE: - await this.moveMessageToFavorite(userId, messageId); + await this.moveMessageToFavorite(wildduckUserId, messageId); break; case BulkActionType.UNFAVORITE: - await this.moveMessageToInboxFromFavorite(userId, messageId); + await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId); break; case BulkActionType.JUNK: - await this.moveMessageToJunk(userId, messageId); + await this.moveMessageToJunk(wildduckUserId, messageId); + break; + case BulkActionType.NOTJUNK: + await this.moveMessageToInboxFromJunk(wildduckUserId, messageId); break; case BulkActionType.TRASH: - await this.moveMessageToTrash(userId, messageId); + await this.moveMessageToTrash(wildduckUserId, messageId); break; case BulkActionType.RESTORE: - await this.moveMessageToInboxFromTrash(userId, messageId); + await this.moveMessageToInboxFromTrash(wildduckUserId, messageId); break; default: throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`); @@ -832,9 +893,6 @@ export class EmailService { //######################################################## - /** - * Transform pagination query for WildDuck API compatibility - */ private transformPaginationQuery(query: MessageListQueryDto): Record { const transformedQuery: Record = { ...query }; @@ -898,6 +956,14 @@ export class EmailService { } } + //============================================== + private async getUserBusinessId(wildduckUserId: string) { + const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); + return user?.business?.id || "default"; + } + + //============================================== + //######################################################## private generateHeadersForEmailType(user: User) { // const emailType = sendEmailDto.emailType || EmailType.GENERAL; diff --git a/src/modules/mail-server/DTO/domain-access.dto.ts b/src/modules/mail-server/DTO/domain-access.dto.ts new file mode 100644 index 0000000..bea043f --- /dev/null +++ b/src/modules/mail-server/DTO/domain-access.dto.ts @@ -0,0 +1,38 @@ +import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger"; +import { IsNotEmpty, IsOptional, IsString } from "class-validator"; + +export class CreateDomainAccessDto { + @ApiProperty({ + description: "Domain name to add to the list", + example: "example.com", + }) + @IsNotEmpty() + @IsString() + domain: string; + + @ApiPropertyOptional({ + description: "Optional description for this domain entry", + example: "Trusted business partner domain", + }) + @IsOptional() + @IsString() + description?: string; +} + +export class ListDomainAccessQueryDto { + @ApiPropertyOptional({ + description: "Number of entries to return", + example: 20, + default: 20, + }) + @IsOptional() + limit?: number; + + @ApiPropertyOptional({ + description: "Page number for pagination", + example: 1, + default: 1, + }) + @IsOptional() + page?: number; +} diff --git a/src/modules/mail-server/DTO/index.ts b/src/modules/mail-server/DTO/index.ts deleted file mode 100644 index aff78ca..0000000 --- a/src/modules/mail-server/DTO/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -// User DTOs -export * from "./create-user.dto"; -export * from "./update-user.dto"; - -// Message DTOs -export * from "./message.dto"; - -// Mailbox DTOs -export * from "./mailbox.dto"; - -// Address DTOs -export * from "./address.dto"; - -// Authentication DTOs -export * from "./authenticate.dto"; - -// Filter DTOs -export * from "./filter.dto"; - -// Submission DTOs -export * from "./submission.dto"; - -// DKIM DTOs -export * from "./dkim.dto"; - -// Webhook DTOs -export * from "./webhook.dto"; - -// Two-Factor Authentication DTOs -export * from "./two-factor.dto"; diff --git a/src/modules/mail-server/interfaces/attachment.interface.ts b/src/modules/mail-server/interfaces/attachment.interface.ts new file mode 100644 index 0000000..cb2af8a --- /dev/null +++ b/src/modules/mail-server/interfaces/attachment.interface.ts @@ -0,0 +1,3 @@ +export interface AttachmentResponse { + success: File; +} diff --git a/src/modules/mail-server/interfaces/domain-access-response.interface.ts b/src/modules/mail-server/interfaces/domain-access-response.interface.ts new file mode 100644 index 0000000..f6da452 --- /dev/null +++ b/src/modules/mail-server/interfaces/domain-access-response.interface.ts @@ -0,0 +1,23 @@ +import { WildDuckPaginatedResponse, WildDuckSuccessResponse } from "./wildduck-response.interface"; + +export interface DomainAccessEntry { + id: string; + domain: string; + description?: string; + created: string; +} + +export interface DomainAccessCreateResponse extends WildDuckSuccessResponse { + id: string; +} + +export interface DomainAccessListResponse extends WildDuckPaginatedResponse { + results: DomainAccessEntry[]; +} + +export interface DomainAccessDetailsResponse extends WildDuckSuccessResponse, DomainAccessEntry {} + +export interface DomainAccessListQueryDto { + limit?: number; + page?: number; +} diff --git a/src/modules/mail-server/interfaces/messages-response.interface.ts b/src/modules/mail-server/interfaces/messages-response.interface.ts index 1759779..c8669b7 100644 --- a/src/modules/mail-server/interfaces/messages-response.interface.ts +++ b/src/modules/mail-server/interfaces/messages-response.interface.ts @@ -1,3 +1,9 @@ +interface IMessage { + id: string; + mailbox: string; + size: number; +} + export interface MessageAttachment { id: string; filename: string; @@ -70,9 +76,7 @@ export interface MessageListResponse { export interface MessageUploadResponse { success: true; - id: number; - mailbox: string; - size: number; + message: IMessage; } export interface MessageEventsEntry { @@ -104,7 +108,7 @@ export interface MessageForwardResponse { export interface MessageSubmitResponse { success: true; - id: string; + message: IMessage; queueId: string; } diff --git a/src/modules/mail-server/mail-server.module.ts b/src/modules/mail-server/mail-server.module.ts index 312291e..59f9f6a 100644 --- a/src/modules/mail-server/mail-server.module.ts +++ b/src/modules/mail-server/mail-server.module.ts @@ -11,6 +11,7 @@ import { WildDuckAuthService } from "./services/wildduck-auth.service"; import { WildDuckAutorepliesService } from "./services/wildduck-autoreplies.service"; import { WildDuckBaseService } from "./services/wildduck-base.service"; import { WildDuckDKIMService } from "./services/wildduck-dkim.service"; +import { WildDuckDomainAccessService } from "./services/wildduck-domain-access.service"; import { WildDuckExportService } from "./services/wildduck-export.service"; import { WildDuckFiltersService } from "./services/wildduck-filters.service"; import { WildDuckHealthService } from "./services/wildduck-health.service"; @@ -48,6 +49,7 @@ import { wildduckConfig } from "../../configs/wildduck.config"; WildDuckAutorepliesService, WildDuckExportService, WildDuckTwoFactorService, + WildDuckDomainAccessService, ], exports: [ MailServerService, @@ -69,6 +71,7 @@ import { wildduckConfig } from "../../configs/wildduck.config"; WildDuckAutorepliesService, WildDuckExportService, WildDuckTwoFactorService, + WildDuckDomainAccessService, ], }) export class MailServerModule {} diff --git a/src/modules/mail-server/services/mail-server.service.ts b/src/modules/mail-server/services/mail-server.service.ts index e93095b..c6ca04d 100644 --- a/src/modules/mail-server/services/mail-server.service.ts +++ b/src/modules/mail-server/services/mail-server.service.ts @@ -7,6 +7,7 @@ import { WildDuckAuditService } from "./wildduck-audit.service"; import { WildDuckAuthService } from "./wildduck-auth.service"; import { WildDuckAutorepliesService } from "./wildduck-autoreplies.service"; import { WildDuckDKIMService } from "./wildduck-dkim.service"; +import { WildDuckDomainAccessService } from "./wildduck-domain-access.service"; import { WildDuckExportService } from "./wildduck-export.service"; import { WildDuckFiltersService } from "./wildduck-filters.service"; import { WildDuckHealthService } from "./wildduck-health.service"; @@ -42,6 +43,7 @@ export class MailServerService { private readonly autorepliesService: WildDuckAutorepliesService, private readonly exportService: WildDuckExportService, private readonly twoFactorService: WildDuckTwoFactorService, + private readonly domainAccessService: WildDuckDomainAccessService, ) {} getMailServerInfo() { @@ -133,4 +135,8 @@ export class MailServerService { get twoFactor() { return this.twoFactorService; } + + get domainAccess() { + return this.domainAccessService; + } } diff --git a/src/modules/mail-server/services/wildduck-domain-access.service.ts b/src/modules/mail-server/services/wildduck-domain-access.service.ts new file mode 100644 index 0000000..b5175b1 --- /dev/null +++ b/src/modules/mail-server/services/wildduck-domain-access.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { Observable } from "rxjs"; + +import { WildDuckBaseService } from "./wildduck-base.service"; +import { CreateDomainAccessDto, ListDomainAccessQueryDto } from "../DTO/domain-access.dto"; +import { DomainAccessCreateResponse, DomainAccessListResponse } from "../interfaces/domain-access-response.interface"; +import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface"; + +@Injectable() +export class WildDuckDomainAccessService extends WildDuckBaseService { + /** + * List allowlisted domains for a tag + */ + listAllowedDomains(tag: string, query?: ListDomainAccessQueryDto): Observable { + return this.get(`/domainaccess/${tag}/allow`, query); + } + + /** + * Add domain to allowlist + */ + addAllowedDomain(tag: string, data: CreateDomainAccessDto): Observable { + return this.post(`/domainaccess/${tag}/allow`, data); + } + + /** + * Remove domain from allowlist + */ + removeAllowedDomain(tag: string, domain: string): Observable { + return this.delete(`/domainaccess/${tag}/allow/${domain}`); + } + + /** + * List blocked domains for a tag + */ + listBlockedDomains(tag: string, query?: ListDomainAccessQueryDto): Observable { + return this.get(`/domainaccess/${tag}/block`, query); + } + + /** + * Add domain to blocklist + */ + addBlockedDomain(tag: string, data: CreateDomainAccessDto): Observable { + return this.post(`/domainaccess/${tag}/block`, data); + } + + /** + * Remove domain from blocklist + */ + removeBlockedDomain(tag: string, domain: string): Observable { + return this.delete(`/domainaccess/${tag}/block/${domain}`); + } +} diff --git a/src/modules/mail-server/services/wildduck-messages.service.ts b/src/modules/mail-server/services/wildduck-messages.service.ts index fe76026..775aa49 100644 --- a/src/modules/mail-server/services/wildduck-messages.service.ts +++ b/src/modules/mail-server/services/wildduck-messages.service.ts @@ -3,6 +3,7 @@ import { Observable } from "rxjs"; import { WildDuckBaseService } from "./wildduck-base.service"; import { ListMessagesQueryDto, SearchMessagesQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto"; +import { AttachmentResponse } from "../interfaces/attachment.interface"; import { MessageDeleteAllResponse, MessageDetails, @@ -72,9 +73,9 @@ export class WildDuckMessagesService extends WildDuckBaseService { /** * Get message attachment */ - getMessageAttachment(userId: string, mailboxId: string, messageId: number, attachmentId: string): Observable { + getMessageAttachment(userId: string, mailboxId: string, messageId: number, attachmentId: string): Observable { // This returns binary content - return this.get(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/attachments/${attachmentId}`); + return this.get(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/attachments/${attachmentId}`); } /** diff --git a/src/modules/mailbox/mailbox.controller.ts b/src/modules/mailbox/mailbox.controller.ts index 6e276bf..2569401 100644 --- a/src/modules/mailbox/mailbox.controller.ts +++ b/src/modules/mailbox/mailbox.controller.ts @@ -2,11 +2,11 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestj import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger"; import { MailboxQueryDto } from "./DTO/mailbox.dto"; +import { CreateMailboxDto, UpdateMailboxDto } from "./DTO/mailbox.dto"; import { MailboxService } from "./services/mailbox.service"; import { AdminRoute } from "../../common/decorators/admin.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { UserDec } from "../../common/decorators/user.decorator"; -import { CreateMailboxDto, UpdateMailboxDto } from "../mail-server/DTO"; @AuthGuards() @Controller("mailbox") diff --git a/src/modules/mailbox/services/mailbox.service.ts b/src/modules/mailbox/services/mailbox.service.ts index 49343a7..addf24b 100644 --- a/src/modules/mailbox/services/mailbox.service.ts +++ b/src/modules/mailbox/services/mailbox.service.ts @@ -1,9 +1,9 @@ import { Injectable, Logger, NotFoundException } from "@nestjs/common"; import { Observable, catchError, map, throwError } from "rxjs"; -import { CreateMailboxDto, UpdateMailboxDto } from "../../mail-server/DTO"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailboxQueryDto } from "../DTO/mailbox.dto"; +import { CreateMailboxDto, UpdateMailboxDto } from "../DTO/mailbox.dto"; @Injectable() export class MailboxService { diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index ea26b21..4077752 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -8,7 +8,7 @@ import { Business } from "../../businesses/entities/business.entity"; import { DomainStatus } from "../../domains/enums/domain-status.enum"; import { DomainAutomationService } from "../../domains/services/domain-automation.service"; import { DomainsService } from "../../domains/services/domains.service"; -import { UpdateUserDto } from "../../mail-server/DTO"; +import { UpdateUserDto } from "../../mail-server/DTO/update-user.dto"; import { MailServerService } from "../../mail-server/services/mail-server.service"; import { QuotaSyncService } from "../../quota-sync/services/quota-sync.service"; import { UserSettingsService } from "../../settings/services/user-settings.service";