From 092abd2895a32e0bb5f77ed82e1a72da337dec4d Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Wed, 9 Jul 2025 16:18:54 +0330 Subject: [PATCH] chore: send email and recive --- src/app.module.ts | 9 +- src/common/enums/message.enum.ts | 41 ++++++ src/modules/auth/auth.module.ts | 11 +- src/modules/auth/interfaces/IToken-payload.ts | 6 + src/modules/auth/services/auth.service.ts | 27 +++- src/modules/auth/services/tokens.service.ts | 10 +- src/modules/email/DTO/email-params.dto.ts | 4 +- src/modules/email/DTO/send-email.dto.ts | 130 ++++++++++-------- src/modules/email/email.controller.ts | 35 ++--- src/modules/email/email.module.ts | 3 +- src/modules/email/services/email.service.ts | 24 ++-- src/modules/users/entities/user.entity.ts | 4 +- src/modules/users/services/users.service.ts | 6 + 13 files changed, 212 insertions(+), 98 deletions(-) diff --git a/src/app.module.ts b/src/app.module.ts index d98eed5..83b8f97 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,7 +2,7 @@ import { MikroOrmModule } from "@mikro-orm/nestjs"; import { HttpModule } from "@nestjs/axios"; import { BullModule } from "@nestjs/bullmq"; import { CacheModule } from "@nestjs/cache-manager"; -import { Module } from "@nestjs/common"; +import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; import { ConfigModule } from "@nestjs/config"; import { ScheduleModule } from "@nestjs/schedule"; import { ThrottlerModule } from "@nestjs/throttler"; @@ -12,6 +12,7 @@ import { cacheConfig } from "./configs/cache.config"; import { httpConfig } from "./configs/http.config"; import { databaseConfig } from "./configs/mikro-orm.config"; import { rateLimitConfig } from "./configs/rateLimit.config"; +import { HTTPLogger } from "./core/middlewares/logger.middleware"; import { AuthModule } from "./modules/auth/auth.module"; import { DomainsModule } from "./modules/domains/domains.module"; import { EmailModule } from "./modules/email/email.module"; @@ -40,4 +41,8 @@ import { UtilsModule } from "./modules/utils/utils.module"; QuotaSyncModule, ], }) -export class AppModule {} +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(HTTPLogger).forRoutes("*"); + } +} diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index 437ce49..e76a688 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -145,6 +145,47 @@ export const enum EmailMessage { QUEUE_ID_MUST_BE_STRING = "شناسه پیش‌نویس ایمیل باید یک رشته باشد", MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد", FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است", + + // Validation Messages for Email DTOs + RECIPIENT_NAME_MUST_BE_STRING = "نام گیرنده باید یک رشته باشد", + RECIPIENT_ADDRESS_REQUIRED = "آدرس گیرنده مورد نیاز است", + RECIPIENT_ADDRESS_MUST_BE_EMAIL = "آدرس گیرنده باید یک ایمیل معتبر باشد", + HEADER_KEY_REQUIRED = "کلید هدر مورد نیاز است", + HEADER_KEY_MUST_BE_STRING = "کلید هدر باید یک رشته باشد", + HEADER_VALUE_REQUIRED = "مقدار هدر مورد نیاز است", + HEADER_VALUE_MUST_BE_STRING = "مقدار هدر باید یک رشته باشد", + ATTACHMENT_FILENAME_MUST_BE_STRING = "نام فایل پیوست باید یک رشته باشد", + ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING = "نوع محتوای پیوست باید یک رشته باشد", + ATTACHMENT_ENCODING_MUST_BE_STRING = "کدگذاری پیوست باید یک رشته باشد", + ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING = "کدگذاری انتقال محتوای پیوست باید یک رشته باشد", + ATTACHMENT_CONTENT_DISPOSITION_INVALID = "نحوه نمایش پیوست باید inline یا attachment باشد", + ATTACHMENT_CONTENT_REQUIRED = "محتوای پیوست مورد نیاز است", + ATTACHMENT_CONTENT_MUST_BE_STRING = "محتوای پیوست باید یک رشته باشد", + ATTACHMENT_CID_MUST_BE_STRING = "شناسه محتوای پیوست باید یک رشته باشد", + DRAFT_MAILBOX_REQUIRED = "شناسه صندوق پیش‌نویس مورد نیاز است", + DRAFT_MAILBOX_MUST_BE_STRING = "شناسه صندوق پیش‌نویس باید یک رشته باشد", + DRAFT_ID_REQUIRED = "شناسه پیش‌نویس مورد نیاز است", + DRAFT_ID_MUST_BE_NUMBER = "شناسه پیش‌نویس باید یک عدد باشد", + ENVELOPE_FROM_REQUIRED = "آدرس فرستنده در envelope مورد نیاز است", + ENVELOPE_TO_REQUIRED = "آدرس گیرنده در envelope مورد نیاز است", + ENVELOPE_TO_MUST_BE_ARRAY = "آدرس گیرنده در envelope باید یک آرایه باشد", + MAILBOX_MUST_BE_STRING = "شناسه صندوق باید یک رشته باشد", + REPLY_TO_MUST_BE_VALID = "آدرس پاسخ باید معتبر باشد", + TO_ADDRESSES_REQUIRED = "آدرس‌های گیرنده مورد نیاز است", + TO_ADDRESSES_MUST_BE_ARRAY = "آدرس‌های گیرنده باید یک آرایه باشد", + CC_ADDRESSES_MUST_BE_ARRAY = "آدرس‌های کپی کربن باید یک آرایه باشد", + BCC_ADDRESSES_MUST_BE_ARRAY = "آدرس‌های کپی مخفی باید یک آرایه باشد", + HEADERS_MUST_BE_ARRAY = "هدرهای سفارشی باید یک آرایه باشد", + SUBJECT_MUST_BE_STRING = "موضوع ایمیل باید یک رشته باشد", + TEXT_CONTENT_MUST_BE_STRING = "محتوای متنی باید یک رشته باشد", + HTML_CONTENT_MUST_BE_STRING = "محتوای HTML باید یک رشته باشد", + ATTACHMENTS_MUST_BE_ARRAY = "پیوست‌ها باید یک آرایه باشد", + SESSION_ID_MUST_BE_STRING = "شناسه جلسه باید یک رشته باشد", + IP_ADDRESS_MUST_BE_STRING = "آدرس IP باید یک رشته باشد", + IS_DRAFT_MUST_BE_BOOLEAN = "وضعیت پیش‌نویس باید بولین باشد", + SEND_TIME_MUST_BE_DATE = "زمان ارسال باید یک تاریخ معتبر باشد", + UPLOAD_ONLY_MUST_BE_BOOLEAN = "حالت فقط آپلود باید بولین باشد", + ENVELOPE_MUST_BE_VALID = "envelope باید معتبر باشد", } export const enum SmsMessage { diff --git a/src/modules/auth/auth.module.ts b/src/modules/auth/auth.module.ts index d1a187c..0be210f 100755 --- a/src/modules/auth/auth.module.ts +++ b/src/modules/auth/auth.module.ts @@ -5,6 +5,7 @@ import { PassportModule } from "@nestjs/passport"; import { AuthController } from "./auth.controller"; import { jwtConfig } from "../../configs/jwt.config"; +import { MailServerModule } from "../mail-server/mail-server.module"; import { UtilsModule } from "../utils/utils.module"; import { AuthService } from "./services/auth.service"; import { TokensService } from "./services/tokens.service"; @@ -15,7 +16,15 @@ import { BusinessesModule } from "../businesses/businesses.module"; import { User } from "../users/entities/user.entity"; @Module({ - imports: [MikroOrmModule.forFeature([User]), UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), BusinessesModule], + imports: [ + MikroOrmModule.forFeature([User]), + UtilsModule, + UsersModule, + PassportModule, + JwtModule.registerAsync(jwtConfig()), + BusinessesModule, + MailServerModule, + ], controllers: [AuthController], providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy], exports: [AuthService], diff --git a/src/modules/auth/interfaces/IToken-payload.ts b/src/modules/auth/interfaces/IToken-payload.ts index c656820..2a0ce54 100755 --- a/src/modules/auth/interfaces/IToken-payload.ts +++ b/src/modules/auth/interfaces/IToken-payload.ts @@ -3,6 +3,12 @@ import { RoleEnum } from "../../users/enums/role.enum"; export interface ILocalTokenPayload { id: string; role: RoleEnum; + inboxId: string; + draftsId: string; + junkId: string; + sentId: string; + trashId: string; + wildduckUserId: string; } export interface IConsoleTokenPayload { diff --git a/src/modules/auth/services/auth.service.ts b/src/modules/auth/services/auth.service.ts index bac0333..a850579 100644 --- a/src/modules/auth/services/auth.service.ts +++ b/src/modules/auth/services/auth.service.ts @@ -1,20 +1,25 @@ import { EntityRepository } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; -import { BadRequestException, Injectable } from "@nestjs/common"; +import { BadRequestException, Injectable, Logger } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; import { TokensService } from "./tokens.service"; import { AuthMessage } from "../../../common/enums/message.enum"; +import { MailServerService } from "../../mail-server/services/mail-server.service"; import { User } from "../../users/entities/user.entity"; import { PasswordService } from "../../utils/services/password.service"; import { LoginDto } from "../DTO/login.dto"; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); + constructor( @InjectRepository(User) private readonly userRepository: EntityRepository, private readonly tokensService: TokensService, private readonly passwordService: PasswordService, + private readonly mailServerService: MailServerService, ) {} //============================================== @@ -24,7 +29,25 @@ export class AuthService { const user = await this.checkUserLoginCredentialWithEmail(email, password); - const tokens = await this.tokensService.generateTokens(user); + // Fetch user's mailboxes from WildDuck + let mailboxes: string[] = []; + if (user.wildduckUserId) { + try { + this.logger.log(`Fetching mailboxes for user ${user.id} (WildDuck ID: ${user.wildduckUserId})`); + const mailboxesResponse = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(user.wildduckUserId)); + + if (mailboxesResponse.success && mailboxesResponse.results) { + mailboxes = mailboxesResponse.results.map((mailbox) => mailbox.id); + + this.logger.log(`Successfully fetched ${mailboxes.length} mailboxes for user ${user.id}`); + } + } catch (error: any) { + this.logger.error(`Failed to fetch mailboxes for user ${user.id}: ${error.message}`); + // Continue with login even if mailbox fetch fails + } + } + + const tokens = await this.tokensService.generateTokens(user, mailboxes); return { message: AuthMessage.PASSWORD_LOGIN_SUCCESS, diff --git a/src/modules/auth/services/tokens.service.ts b/src/modules/auth/services/tokens.service.ts index e540dd0..0864d6e 100755 --- a/src/modules/auth/services/tokens.service.ts +++ b/src/modules/auth/services/tokens.service.ts @@ -20,11 +20,17 @@ export class TokensService { private readonly em: EntityManager, ) {} - async generateTokens(user: User, em?: EntityManager) { + async generateTokens(user: User, mailboxes?: string[], em?: EntityManager) { return this.generateAccessAndRefreshToken( { id: user.id, role: user.role.name, + inboxId: mailboxes?.[0] || "", + sentId: mailboxes?.[1] || "", + draftsId: mailboxes?.[2] || "", + trashId: mailboxes?.[3] || "", + junkId: mailboxes?.[4] || "", + wildduckUserId: user.wildduckUserId, }, em, ); @@ -101,7 +107,7 @@ export class TokensService { } await entityManager.removeAndFlush(token); - const tokens = await this.generateTokens(token.user, entityManager); + const tokens = await this.generateTokens(token.user, undefined, entityManager); await entityManager.commit(); return tokens; diff --git a/src/modules/email/DTO/email-params.dto.ts b/src/modules/email/DTO/email-params.dto.ts index 81f7fcf..87050b9 100644 --- a/src/modules/email/DTO/email-params.dto.ts +++ b/src/modules/email/DTO/email-params.dto.ts @@ -32,14 +32,14 @@ export class QueueIdParamDto { queueId: string; } -export class UserMailboxParamDto extends UserIdParamDto { +export class UserMailboxParamDto { @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" }) @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) @IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED }) mailboxId: string; } -export class UserMailboxMessageParamDto extends UserMailboxParamDto { +export class UserMailboxMessageParamDto { @ApiProperty({ description: "Message ID", example: "12345" }) @Transform(({ value }) => parseInt(value)) @IsNumberString({ no_symbols: true }, { message: EmailMessage.MESSAGE_ID_MUST_BE_NUMBER }) diff --git a/src/modules/email/DTO/send-email.dto.ts b/src/modules/email/DTO/send-email.dto.ts index 450cda0..d112f31 100644 --- a/src/modules/email/DTO/send-email.dto.ts +++ b/src/modules/email/DTO/send-email.dto.ts @@ -6,94 +6,103 @@ import { EmailMessage } from "../../../common/enums/message.enum"; //######################################################## export class EmailRecipientDto { - @ApiPropertyOptional({ description: "Display name", example: "John Doe" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.RECIPIENT_NAME_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Display nam (optional) ", example: "John Doe" }) name?: string; + @IsNotEmpty({ message: EmailMessage.RECIPIENT_ADDRESS_REQUIRED }) + @IsEmail({}, { message: EmailMessage.RECIPIENT_ADDRESS_MUST_BE_EMAIL }) @ApiProperty({ description: "Email address", example: "john@example.com" }) - @IsEmail() address: string; } //######################################################## export class EmailHeaderDto { @ApiProperty({ description: "Header key", example: "X-Mailer" }) - @IsString() + @IsNotEmpty({ message: EmailMessage.HEADER_KEY_REQUIRED }) + @IsString({ message: EmailMessage.HEADER_KEY_MUST_BE_STRING }) key: string; @ApiProperty({ description: "Header value", example: "My Awesome Mailing Service" }) - @IsString() + @IsNotEmpty({ message: EmailMessage.HEADER_VALUE_REQUIRED }) + @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING }) value: string; } //######################################################## export class EmailAttachmentDto { - @ApiPropertyOptional({ description: "Filename", example: "document.pdf" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.ATTACHMENT_FILENAME_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Filename (optional)", example: "document.pdf" }) filename?: string; - @ApiPropertyOptional({ description: "MIME type for the attachment file", example: "application/pdf" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.ATTACHMENT_CONTENT_TYPE_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "MIME type for the attachment file (optional)", example: "application/pdf" }) contentType?: string; - @ApiPropertyOptional({ description: "Encoding to use to store the attachments", example: "base64", default: "base64" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.ATTACHMENT_ENCODING_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Encoding to use to store the attachments (optional)", example: "base64", default: "base64" }) encoding?: string; - @ApiPropertyOptional({ description: "Transfer encoding" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.ATTACHMENT_CONTENT_TRANSFER_ENCODING_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Transfer encoding (optional)" }) contentTransferEncoding?: string; - @ApiPropertyOptional({ description: "Content Disposition", enum: ["inline", "attachment"] }) @IsOptional() - @IsEnum(["inline", "attachment"]) + @IsEnum(["inline", "attachment"], { message: EmailMessage.ATTACHMENT_CONTENT_DISPOSITION_INVALID }) + @ApiPropertyOptional({ description: "Content Disposition (optional)", enum: ["inline", "attachment"] }) contentDisposition?: "inline" | "attachment"; + @IsNotEmpty({ message: EmailMessage.ATTACHMENT_CONTENT_REQUIRED }) + @IsString({ message: EmailMessage.ATTACHMENT_CONTENT_MUST_BE_STRING }) @ApiProperty({ description: "Base64 encoded attachment content" }) - @IsString() content: string; - @ApiPropertyOptional({ description: "Content-ID value if you want to reference to this attachment from HTML formatted message" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.ATTACHMENT_CID_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Content-ID value if you want to reference to this attachment from HTML formatted message (optional)" }) cid?: string; } //######################################################## export class DraftReferenceDto { + @IsNotEmpty({ message: EmailMessage.DRAFT_MAILBOX_REQUIRED }) + @IsString({ message: EmailMessage.DRAFT_MAILBOX_MUST_BE_STRING }) @ApiProperty({ description: "ID of the Mailbox", example: "507f1f77bcf86cd799439012" }) - @IsString() mailbox: string; + @IsNotEmpty({ message: EmailMessage.DRAFT_ID_REQUIRED }) + @IsNumber({}, { message: EmailMessage.DRAFT_ID_MUST_BE_NUMBER }) @ApiProperty({ description: "Message ID", example: 12345 }) - @IsNumber() id: number; } //######################################################## export class EmailEnvelopeDto { - @ApiProperty({ description: "From address for envelope" }) + @IsNotEmpty({ message: EmailMessage.ENVELOPE_FROM_REQUIRED }) @ValidateNested() @Type(() => EmailRecipientDto) + @ApiProperty({ description: "From address for envelope" }) from: EmailRecipientDto; - @ApiProperty({ description: "To addresses for envelope", type: [EmailRecipientDto] }) - @IsArray() + @IsNotEmpty({ message: EmailMessage.ENVELOPE_TO_REQUIRED }) + @IsArray({ message: EmailMessage.ENVELOPE_TO_MUST_BE_ARRAY }) @ValidateNested({ each: true }) @Type(() => EmailRecipientDto) + @ApiProperty({ description: "To addresses for envelope", type: [EmailRecipientDto] }) to: EmailRecipientDto[]; } //######################################################## export class SendEmailDto { - @ApiPropertyOptional({ description: "ID of the Mailbox", example: "507f1f77bcf86cd799439012" }) @IsOptional() - @IsString() + @IsNotEmpty() + @IsString({ message: EmailMessage.MAILBOX_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "ID of the Mailbox (optional)", example: "507f1f77bcf86cd799439012" }) mailbox?: string; @IsNotEmpty({ message: EmailMessage.FROM_ADDRESS_REQUIRED }) @@ -102,106 +111,107 @@ export class SendEmailDto { @ApiProperty({ description: "Address for the From: header" }) from: EmailRecipientDto; - @ApiPropertyOptional({ description: "Address for the Reply-To: header" }) @IsOptional() - @ValidateNested() + @ValidateNested({ message: EmailMessage.REPLY_TO_MUST_BE_VALID }) @Type(() => EmailRecipientDto) + @ApiPropertyOptional({ description: "Address for the Reply-To: header (optional)" }) replyTo?: EmailRecipientDto; - @ApiProperty({ description: "Addresses for the To: header", type: [EmailRecipientDto] }) - @IsArray() + @IsNotEmpty({ message: EmailMessage.TO_ADDRESSES_REQUIRED }) + @IsArray({ message: EmailMessage.TO_ADDRESSES_MUST_BE_ARRAY }) @ValidateNested({ each: true }) @Type(() => EmailRecipientDto) + @ApiProperty({ description: "Addresses for the To: header", type: [EmailRecipientDto] }) to: EmailRecipientDto[]; - @ApiPropertyOptional({ description: "Addresses for the Cc: header", type: [EmailRecipientDto] }) @IsOptional() - @IsArray() + @IsArray({ message: EmailMessage.CC_ADDRESSES_MUST_BE_ARRAY }) @ValidateNested({ each: true }) @Type(() => EmailRecipientDto) + @ApiPropertyOptional({ description: "Addresses for the Cc: header (optional)", type: [EmailRecipientDto] }) cc?: EmailRecipientDto[]; - @ApiPropertyOptional({ description: "Addresses for the Bcc: header", type: [EmailRecipientDto] }) @IsOptional() - @IsArray() + @IsArray({ message: EmailMessage.BCC_ADDRESSES_MUST_BE_ARRAY }) @ValidateNested({ each: true }) @Type(() => EmailRecipientDto) + @ApiPropertyOptional({ description: "Addresses for the Bcc: header (optional)", type: [EmailRecipientDto] }) bcc?: EmailRecipientDto[]; + @IsOptional() + @IsArray({ message: EmailMessage.HEADERS_MUST_BE_ARRAY }) + @ValidateNested({ each: true }) + @Type(() => EmailHeaderDto) @ApiPropertyOptional({ description: "Custom headers for the message. If reference message is set then In-Reply-To and References headers are set automatically", type: [EmailHeaderDto], }) - @IsOptional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => EmailHeaderDto) headers?: EmailHeaderDto[]; - @ApiPropertyOptional({ description: "Message subject. If not then resolved from Reference message", example: "Test email" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.SUBJECT_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Message subject. If not then resolved from Reference message (optional)", example: "Test email" }) subject?: string; - @ApiPropertyOptional({ description: "Plaintext message" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.TEXT_CONTENT_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Plaintext message (optional)" }) text?: string; - @ApiPropertyOptional({ description: "HTML formatted message" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.HTML_CONTENT_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "HTML formatted message (optional)" }) html?: string; - @ApiPropertyOptional({ description: "Attachments for the message", type: [EmailAttachmentDto] }) @IsOptional() - @IsArray() + @IsArray({ message: EmailMessage.ATTACHMENTS_MUST_BE_ARRAY }) @ValidateNested({ each: true }) @Type(() => EmailAttachmentDto) + @ApiPropertyOptional({ description: "Attachments for the message (optional)", type: [EmailAttachmentDto] }) attachments?: EmailAttachmentDto[]; - @ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string" }) @IsOptional() + @ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string (optional)" }) meta?: any; - @ApiPropertyOptional({ description: "Session identifier for the logs" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.SESSION_ID_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "Session identifier for the logs (optional)" }) sess?: string; - @ApiPropertyOptional({ description: "IP address for the logs" }) @IsOptional() - @IsString() + @IsString({ message: EmailMessage.IP_ADDRESS_MUST_BE_STRING }) + @ApiPropertyOptional({ description: "IP address for the logs (optional)" }) ip?: string; - @ApiPropertyOptional({ description: "Reference message data" }) @IsOptional() + @ApiPropertyOptional({ description: "Reference message data (optional)" }) reference?: any; - @ApiPropertyOptional({ description: "Is the message a draft or not", default: false }) @IsOptional() - @IsBoolean() + @IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "Is the message a draft or not (optional)", default: false }) isDraft?: boolean; - @ApiPropertyOptional({ description: "Draft message to base this one on" }) @IsOptional() @ValidateNested() @Type(() => DraftReferenceDto) + @ApiPropertyOptional({ description: "Draft message to base this one on (optional)" }) draft?: DraftReferenceDto; - @ApiPropertyOptional({ description: "Send time", example: "2023-12-01T10:00:00Z" }) @IsOptional() - @IsDateString() + @IsDateString({}, { message: EmailMessage.SEND_TIME_MUST_BE_DATE }) + @ApiPropertyOptional({ description: "Send time (optional)", example: "2023-12-01T10:00:00Z" }) sendTime?: string; - @ApiPropertyOptional({ description: "If true only uploads the message but does not send it", default: false }) @IsOptional() - @IsBoolean() + @IsBoolean({ message: EmailMessage.UPLOAD_ONLY_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "If true only uploads the message but does not send it", default: false }) uploadOnly?: boolean; - @ApiPropertyOptional({ description: "Optional envelope" }) @IsOptional() - @ValidateNested() + @ValidateNested({ message: EmailMessage.ENVELOPE_MUST_BE_VALID }) @Type(() => EmailEnvelopeDto) + @ApiPropertyOptional({ description: "Optional envelope (optional)" }) envelope?: EmailEnvelopeDto; } diff --git a/src/modules/email/email.controller.ts b/src/modules/email/email.controller.ts index a435c49..27eba10 100644 --- a/src/modules/email/email.controller.ts +++ b/src/modules/email/email.controller.ts @@ -1,25 +1,26 @@ import { Body, Controller, Delete, Get, Param, Post, Query } from "@nestjs/common"; import { ApiOperation, ApiResponse } from "@nestjs/swagger"; -import { QueueIdParamDto, UserIdParamDto, UserMailboxMessageParamDto, UserMailboxParamDto } from "./DTO/email-params.dto"; +import { QueueIdParamDto, UserMailboxMessageParamDto } from "./DTO/email-params.dto"; import { MessageListQueryDto } from "./DTO/email-query.dto"; import { SearchMessagesQueryDto } from "./DTO/email-query.dto"; import { SendEmailDto } from "./DTO/send-email.dto"; import { EmailService } from "./services/email.service"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; +import { UserDec } from "../../common/decorators/user.decorator"; @Controller("email") @AuthGuards() export class EmailController { constructor(private readonly emailService: EmailService) {} - @Post(":userId/send") + @Post("send") @ApiOperation({ summary: "Send an email" }) @ApiResponse({ status: 201, description: "Email sent successfully" }) @ApiResponse({ status: 400, description: "Invalid email data" }) @ApiResponse({ status: 403, description: "Not authorized to send email" }) - sendEmail(@Param() params: UserIdParamDto, @Body() sendEmailDto: SendEmailDto) { - return this.emailService.sendEmail(params.userId, sendEmailDto); + sendEmail(@UserDec("id") userId: string, @Body() sendEmailDto: SendEmailDto) { + return this.emailService.sendEmail(userId, sendEmailDto); } @Get("status/:queueId") @@ -30,34 +31,34 @@ export class EmailController { return this.emailService.getEmailStatus(params.queueId); } - @Get(":userId/messages/:mailboxId") - @ApiOperation({ summary: "List messages in a mailbox" }) + @Get("messages/inbox") + @ApiOperation({ summary: "List messages in a inbox" }) @ApiResponse({ status: 200, description: "List of messages" }) @ApiResponse({ status: 404, description: "Mailbox not found" }) - listMessages(@Param() params: UserMailboxParamDto, @Query() query: MessageListQueryDto) { - return this.emailService.listMessages(params.userId, params.mailboxId, query); + listMessages(@UserDec("id") userId: string, @UserDec("inboxId") inboxId: string, @Query() query: MessageListQueryDto) { + return this.emailService.listMessages(userId, inboxId, query); } - @Get(":userId/messages/:mailboxId/:messageId") + @Get("messages/inbox/:messageId") @ApiOperation({ summary: "Get a specific message" }) @ApiResponse({ status: 200, description: "Message details" }) @ApiResponse({ status: 404, description: "Message not found" }) - getMessage(@Param() params: UserMailboxMessageParamDto) { - return this.emailService.getMessage(params.userId, params.mailboxId, params.messageId); + getMessage(@UserDec("id") userId: string, @UserDec("inboxId") inboxId: string, @Param() params: UserMailboxMessageParamDto) { + return this.emailService.getMessage(userId, inboxId, params.messageId); } - @Delete(":userId/messages/:mailboxId/:messageId") + @Delete("messages/inbox/:messageId") @ApiOperation({ summary: "Delete a message" }) @ApiResponse({ status: 200, description: "Message deleted successfully" }) @ApiResponse({ status: 404, description: "Message not found" }) - deleteMessage(@Param() params: UserMailboxMessageParamDto) { - return this.emailService.deleteMessage(params.userId, params.mailboxId, params.messageId); + deleteMessage(@UserDec("id") userId: string, @UserDec("inboxId") inboxId: string, @Param() params: UserMailboxMessageParamDto) { + return this.emailService.deleteMessage(userId, inboxId, params.messageId); } - @Get(":userId/search") + @Get("search") @ApiOperation({ summary: "Search messages across all mailboxes" }) @ApiResponse({ status: 200, description: "Search results" }) - searchMessages(@Param() params: UserIdParamDto, @Query() query: SearchMessagesQueryDto) { - return this.emailService.searchMessages(params.userId, query); + searchMessages(@UserDec("id") userId: string, @Query() query: SearchMessagesQueryDto) { + return this.emailService.searchMessages(userId, query); } } diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index e97a2f8..6120fba 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -3,9 +3,10 @@ import { Module } from "@nestjs/common"; import { EmailController } from "./email.controller"; import { EmailService } from "./services/email.service"; import { MailServerModule } from "../mail-server/mail-server.module"; +import { UsersModule } from "../users/users.module"; @Module({ - imports: [MailServerModule], + imports: [MailServerModule, UsersModule], controllers: [EmailController], providers: [EmailService], exports: [EmailService], diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 55d6f69..d22b222 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 { EmailMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; +import { UsersService } from "../../users/services/users.service"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; import { SendEmailDto } from "../DTO/send-email.dto"; @@ -10,14 +11,19 @@ import { SendEmailDto } from "../DTO/send-email.dto"; export class EmailService { private readonly logger = new Logger(EmailService.name); - constructor(private readonly mailServerService: MailServerService) {} + constructor( + private readonly mailServerService: MailServerService, + private readonly userService: UsersService, + ) {} //######################################################## async sendEmail(userId: string, sendEmailDto: SendEmailDto) { - this.logger.log(`Sending email from user ${userId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`); + const userEmailId = await this.userService.getUserEmailIdFromId(userId); + + this.logger.log(`Sending email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`); try { - const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userId, sendEmailDto)); + const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userEmailId, sendEmailDto)); this.logger.log(`Email sent successfully: ${response.id} (Queue: ${response.queueId})`); @@ -31,7 +37,7 @@ export class EmailService { created: response.created, }; } catch (error) { - this.logger.error(`Failed to send email for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`); + this.logger.error(`Failed to send email for user ${userEmailId}: ${error instanceof Error ? error.message : "Unknown error"}`); throw error; } } @@ -77,13 +83,13 @@ export class EmailService { } //######################################################## - async listMessages(userId: string, mailboxId: string, query: MessageListQueryDto) { - this.logger.log(`Listing messages in mailbox ${mailboxId} for user: ${userId}`); + async listMessages(userId: string, inboxId: string, query: MessageListQueryDto) { + this.logger.log(`Listing messages in mailbox ${inboxId} for user: ${userId}`); try { - const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, mailboxId, query)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, inboxId, query)); - this.logger.log(`Found ${response.results.length} messages in mailbox ${mailboxId} for user: ${userId}`); + this.logger.log(`Found ${response.results.length} messages in mailbox ${inboxId} for user: ${userId}`); return { message: EmailMessage.MESSAGES_LISTED_SUCCESSFULLY, @@ -91,7 +97,7 @@ export class EmailService { }; } catch (error) { this.logger.error( - `Failed to list messages for mailbox ${mailboxId} and user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, + `Failed to list messages for mailbox ${inboxId} and user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`, ); throw error; } diff --git a/src/modules/users/entities/user.entity.ts b/src/modules/users/entities/user.entity.ts index b51c6bc..cb794ed 100644 --- a/src/modules/users/entities/user.entity.ts +++ b/src/modules/users/entities/user.entity.ts @@ -32,8 +32,8 @@ export class User extends BaseEntity { emailQuotaUsed?: number; // Email quota used in bytes // Email account integration fields - @Property({ type: "varchar", length: 255, nullable: true }) - wildduckUserId?: string; // WildDuck user ID + @Property({ type: "varchar", length: 255, nullable: false }) + wildduckUserId: string; // WildDuck user ID @Property({ type: "varchar", length: 255, nullable: true }) displayName?: string; // Display name for email diff --git a/src/modules/users/services/users.service.ts b/src/modules/users/services/users.service.ts index b799beb..aebb976 100644 --- a/src/modules/users/services/users.service.ts +++ b/src/modules/users/services/users.service.ts @@ -35,6 +35,12 @@ export class UsersService { private readonly domainsService: DomainsService, ) {} + async getUserEmailIdFromId(userId: string) { + const user = await this.userRepository.findOne({ id: userId }, { populate: ["role"] }); + if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND); + return user.wildduckUserId; + } + async findOneWithEmail(email: string, businessId: string) { const user = await this.userRepository.findOne({ emailAddress: email, business: { id: businessId } }, { populate: ["role"] }); return user;