chore: send email and recive

This commit is contained in:
mahyargdz
2025-07-09 16:18:54 +03:30
parent b3ab6c8a10
commit 092abd2895
13 changed files with 212 additions and 98 deletions
+2 -2
View File
@@ -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 })
+70 -60
View File
@@ -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;
}
+18 -17
View File
@@ -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);
}
}
+2 -1
View File
@@ -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],
+15 -9
View File
@@ -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;
}