chore: add email and mailbox module
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsNotEmpty, IsNumberString, IsString } from "class-validator";
|
||||
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class UserIdParamDto {
|
||||
@IsNotEmpty({ message: EmailMessage.USER_ID_REQUIRED })
|
||||
@IsString({ message: EmailMessage.USER_ID_MUST_BE_STRING })
|
||||
@ApiProperty({ description: "User ID", example: "507f1f77bcf86cd799439011" })
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export class MailboxIdParamDto {
|
||||
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
|
||||
@IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
|
||||
@IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED })
|
||||
mailboxId: string;
|
||||
}
|
||||
|
||||
export class MessageIdParamDto {
|
||||
@ApiProperty({ description: "Message ID", example: "12345" })
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
@IsNumberString({ no_symbols: true }, { message: EmailMessage.MESSAGE_ID_MUST_BE_NUMBER })
|
||||
messageId: number;
|
||||
}
|
||||
|
||||
export class QueueIdParamDto {
|
||||
@ApiProperty({ description: "Email queue ID", example: "16f2d5e0b9e5c8f0a1b2c3d4" })
|
||||
@IsString({ message: EmailMessage.QUEUE_ID_MUST_BE_STRING })
|
||||
@IsNotEmpty({ message: EmailMessage.QUEUE_ID_REQUIRED })
|
||||
queueId: string;
|
||||
}
|
||||
|
||||
export class UserMailboxParamDto extends UserIdParamDto {
|
||||
@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 {
|
||||
@ApiProperty({ description: "Message ID", example: "12345" })
|
||||
@Transform(({ value }) => parseInt(value))
|
||||
@IsNumberString({ no_symbols: true }, { message: EmailMessage.MESSAGE_ID_MUST_BE_NUMBER })
|
||||
messageId: number;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
import { PaginationDto } from "../../../common/DTO/pagination.dto";
|
||||
import { CommonMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
export class MessageListQueryDto extends PaginationDto {
|
||||
@ApiPropertyOptional({ description: "Sort order", enum: ["asc", "desc"], example: "desc" })
|
||||
@IsOptional()
|
||||
@IsEnum(["asc", "desc"])
|
||||
order?: "asc" | "desc";
|
||||
}
|
||||
|
||||
export class SearchMessagesQueryDto extends PaginationDto {
|
||||
@IsOptional()
|
||||
@IsString({ message: CommonMessage.SEARCH_QUERY_STRING })
|
||||
@ApiPropertyOptional({ description: "Search query", example: "search query" })
|
||||
q?: string;
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsDateString, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
|
||||
//########################################################
|
||||
export class EmailRecipientDto {
|
||||
@ApiPropertyOptional({ description: "Display name", example: "John Doe" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@ApiProperty({ description: "Email address", example: "john@example.com" })
|
||||
@IsEmail()
|
||||
address: string;
|
||||
}
|
||||
|
||||
//########################################################
|
||||
export class EmailHeaderDto {
|
||||
@ApiProperty({ description: "Header key", example: "X-Mailer" })
|
||||
@IsString()
|
||||
key: string;
|
||||
|
||||
@ApiProperty({ description: "Header value", example: "My Awesome Mailing Service" })
|
||||
@IsString()
|
||||
value: string;
|
||||
}
|
||||
|
||||
//########################################################
|
||||
export class EmailAttachmentDto {
|
||||
@ApiPropertyOptional({ description: "Filename", example: "document.pdf" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
filename?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "MIME type for the attachment file", example: "application/pdf" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contentType?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Encoding to use to store the attachments", example: "base64", default: "base64" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
encoding?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Transfer encoding" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
contentTransferEncoding?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Content Disposition", enum: ["inline", "attachment"] })
|
||||
@IsOptional()
|
||||
@IsEnum(["inline", "attachment"])
|
||||
contentDisposition?: "inline" | "attachment";
|
||||
|
||||
@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()
|
||||
cid?: string;
|
||||
}
|
||||
|
||||
//########################################################
|
||||
export class DraftReferenceDto {
|
||||
@ApiProperty({ description: "ID of the Mailbox", example: "507f1f77bcf86cd799439012" })
|
||||
@IsString()
|
||||
mailbox: string;
|
||||
|
||||
@ApiProperty({ description: "Message ID", example: 12345 })
|
||||
@IsNumber()
|
||||
id: number;
|
||||
}
|
||||
|
||||
//########################################################
|
||||
export class EmailEnvelopeDto {
|
||||
@ApiProperty({ description: "From address for envelope" })
|
||||
@ValidateNested()
|
||||
@Type(() => EmailRecipientDto)
|
||||
from: EmailRecipientDto;
|
||||
|
||||
@ApiProperty({ description: "To addresses for envelope", type: [EmailRecipientDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailRecipientDto)
|
||||
to: EmailRecipientDto[];
|
||||
}
|
||||
|
||||
//########################################################
|
||||
export class SendEmailDto {
|
||||
@ApiPropertyOptional({ description: "ID of the Mailbox", example: "507f1f77bcf86cd799439012" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
mailbox?: string;
|
||||
|
||||
@IsNotEmpty({ message: EmailMessage.FROM_ADDRESS_REQUIRED })
|
||||
@ValidateNested()
|
||||
@Type(() => EmailRecipientDto)
|
||||
@ApiProperty({ description: "Address for the From: header" })
|
||||
from: EmailRecipientDto;
|
||||
|
||||
@ApiPropertyOptional({ description: "Address for the Reply-To: header" })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => EmailRecipientDto)
|
||||
replyTo?: EmailRecipientDto;
|
||||
|
||||
@ApiProperty({ description: "Addresses for the To: header", type: [EmailRecipientDto] })
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailRecipientDto)
|
||||
to: EmailRecipientDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: "Addresses for the Cc: header", type: [EmailRecipientDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailRecipientDto)
|
||||
cc?: EmailRecipientDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: "Addresses for the Bcc: header", type: [EmailRecipientDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailRecipientDto)
|
||||
bcc?: EmailRecipientDto[];
|
||||
|
||||
@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()
|
||||
subject?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Plaintext message" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
text?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "HTML formatted message" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
html?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Attachments for the message", type: [EmailAttachmentDto] })
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAttachmentDto)
|
||||
attachments?: EmailAttachmentDto[];
|
||||
|
||||
@ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string" })
|
||||
@IsOptional()
|
||||
meta?: any;
|
||||
|
||||
@ApiPropertyOptional({ description: "Session identifier for the logs" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
sess?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "IP address for the logs" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
ip?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "Reference message data" })
|
||||
@IsOptional()
|
||||
reference?: any;
|
||||
|
||||
@ApiPropertyOptional({ description: "Is the message a draft or not", default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isDraft?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Draft message to base this one on" })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => DraftReferenceDto)
|
||||
draft?: DraftReferenceDto;
|
||||
|
||||
@ApiPropertyOptional({ description: "Send time", example: "2023-12-01T10:00:00Z" })
|
||||
@IsOptional()
|
||||
@IsDateString()
|
||||
sendTime?: string;
|
||||
|
||||
@ApiPropertyOptional({ description: "If true only uploads the message but does not send it", default: false })
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
uploadOnly?: boolean;
|
||||
|
||||
@ApiPropertyOptional({ description: "Optional envelope" })
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => EmailEnvelopeDto)
|
||||
envelope?: EmailEnvelopeDto;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
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 { 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";
|
||||
|
||||
@Controller("email")
|
||||
@AuthGuards()
|
||||
export class EmailController {
|
||||
constructor(private readonly emailService: EmailService) {}
|
||||
|
||||
@Post(":userId/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);
|
||||
}
|
||||
|
||||
@Get("status/:queueId")
|
||||
@ApiOperation({ summary: "Get email delivery status" })
|
||||
@ApiResponse({ status: 200, description: "Email delivery status" })
|
||||
@ApiResponse({ status: 404, description: "Email not found" })
|
||||
getEmailStatus(@Param() params: QueueIdParamDto) {
|
||||
return this.emailService.getEmailStatus(params.queueId);
|
||||
}
|
||||
|
||||
@Get(":userId/messages/:mailboxId")
|
||||
@ApiOperation({ summary: "List messages in a mailbox" })
|
||||
@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);
|
||||
}
|
||||
|
||||
@Get(":userId/messages/:mailboxId/: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);
|
||||
}
|
||||
|
||||
@Delete(":userId/messages/:mailboxId/: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);
|
||||
}
|
||||
|
||||
@Get(":userId/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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { EmailController } from "./email.controller";
|
||||
import { EmailService } from "./services/email.service";
|
||||
import { MailServerModule } from "../mail-server/mail-server.module";
|
||||
|
||||
@Module({
|
||||
imports: [MailServerModule],
|
||||
controllers: [EmailController],
|
||||
providers: [EmailService],
|
||||
exports: [EmailService],
|
||||
})
|
||||
export class EmailModule {}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
||||
import { SendEmailDto } from "../DTO/send-email.dto";
|
||||
|
||||
@Injectable()
|
||||
export class EmailService {
|
||||
private readonly logger = new Logger(EmailService.name);
|
||||
|
||||
constructor(private readonly mailServerService: MailServerService) {}
|
||||
|
||||
//########################################################
|
||||
async sendEmail(userId: string, sendEmailDto: SendEmailDto) {
|
||||
this.logger.log(`Sending email from user ${userId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userId, sendEmailDto));
|
||||
|
||||
this.logger.log(`Email sent successfully: ${response.id} (Queue: ${response.queueId})`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.EMAIL_SENDING_SUCCESS,
|
||||
messageId: response.id,
|
||||
queueId: response.queueId,
|
||||
from: response.from,
|
||||
to: response.to,
|
||||
subject: response.subject,
|
||||
created: response.created,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send email for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################
|
||||
async getEmailStatus(queueId: string) {
|
||||
this.logger.log(`Getting email status for queue: ${queueId}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.mailServerService.submission.getQueueStatus(queueId));
|
||||
|
||||
this.logger.log(`Retrieved status for queue ${queueId}: ${response.status}`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.EMAIL_STATUS_RETRIEVED_SUCCESSFULLY,
|
||||
...response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get email status for queue ${queueId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################
|
||||
async searchMessages(userId: string, query: SearchMessagesQueryDto) {
|
||||
this.logger.log(`Searching messages for user ${userId} with query: ${query.q}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.mailServerService.messages.searchMessages(userId, { query: query.q || "", limit: query.limit, page: query.page }),
|
||||
);
|
||||
|
||||
this.logger.log(`Found ${response.results.length} messages matching search for user: ${userId}`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.MESSAGES_SEARCHED_SUCCESSFULLY,
|
||||
...response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to search messages for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################
|
||||
async listMessages(userId: string, mailboxId: string, query: MessageListQueryDto) {
|
||||
this.logger.log(`Listing messages in mailbox ${mailboxId} for user: ${userId}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(userId, mailboxId, query));
|
||||
|
||||
this.logger.log(`Found ${response.results.length} messages in mailbox ${mailboxId} for user: ${userId}`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.MESSAGES_LISTED_SUCCESSFULLY,
|
||||
...response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to list messages for mailbox ${mailboxId} and user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################
|
||||
async getMessage(userId: string, mailboxId: string, messageId: number) {
|
||||
this.logger.log(`Getting message ${messageId} from mailbox ${mailboxId} for user: ${userId}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.mailServerService.messages.getMessage(userId, mailboxId, messageId));
|
||||
|
||||
this.logger.log(`Retrieved message ${messageId} for user: ${userId}`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.MESSAGE_RETRIEVED_SUCCESSFULLY,
|
||||
...response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
//########################################################
|
||||
async deleteMessage(userId: string, mailboxId: string, messageId: number) {
|
||||
this.logger.log(`Deleting message ${messageId} from mailbox ${mailboxId} for user: ${userId}`);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(this.mailServerService.messages.deleteMessage(userId, mailboxId, messageId));
|
||||
|
||||
this.logger.log(`Deleted message ${messageId} for user: ${userId}`);
|
||||
|
||||
return {
|
||||
message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY,
|
||||
...response,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to delete message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user