chore: add email and mailbox module
This commit is contained in:
@@ -14,7 +14,9 @@ import { databaseConfig } from "./configs/mikro-orm.config";
|
|||||||
import { rateLimitConfig } from "./configs/rateLimit.config";
|
import { rateLimitConfig } from "./configs/rateLimit.config";
|
||||||
import { AuthModule } from "./modules/auth/auth.module";
|
import { AuthModule } from "./modules/auth/auth.module";
|
||||||
import { DomainsModule } from "./modules/domains/domains.module";
|
import { DomainsModule } from "./modules/domains/domains.module";
|
||||||
|
import { EmailModule } from "./modules/email/email.module";
|
||||||
import { MailServerModule } from "./modules/mail-server/mail-server.module";
|
import { MailServerModule } from "./modules/mail-server/mail-server.module";
|
||||||
|
import { MailboxModule } from "./modules/mailbox/mailbox.module";
|
||||||
import { QuotaSyncModule } from "./modules/quota-sync/quota-sync.module";
|
import { QuotaSyncModule } from "./modules/quota-sync/quota-sync.module";
|
||||||
import { UsersModule } from "./modules/users/users.module";
|
import { UsersModule } from "./modules/users/users.module";
|
||||||
import { UtilsModule } from "./modules/utils/utils.module";
|
import { UtilsModule } from "./modules/utils/utils.module";
|
||||||
@@ -32,6 +34,8 @@ import { UtilsModule } from "./modules/utils/utils.module";
|
|||||||
AuthModule,
|
AuthModule,
|
||||||
UsersModule,
|
UsersModule,
|
||||||
DomainsModule,
|
DomainsModule,
|
||||||
|
EmailModule,
|
||||||
|
MailboxModule,
|
||||||
MailServerModule,
|
MailServerModule,
|
||||||
QuotaSyncModule,
|
QuotaSyncModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { Type } from "class-transformer";
|
import { Transform, Type } from "class-transformer";
|
||||||
import { IsNotEmpty, IsNumber, IsOptional, Max, Min } from "class-validator";
|
import { IsInt, IsNotEmpty, IsNumber, IsOptional, Max, Min } from "class-validator";
|
||||||
|
|
||||||
export class PaginationDto {
|
export class PaginationDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -20,3 +20,19 @@ export class PaginationDto {
|
|||||||
@ApiPropertyOptional({ type: "number", required: false })
|
@ApiPropertyOptional({ type: "number", required: false })
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class PaginationQueryDto {
|
||||||
|
@ApiPropertyOptional({ description: "Number of items per page", example: 20, minimum: 1, maximum: 100 })
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
limit?: number;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Page number", example: 1, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Transform(({ value }) => parseInt(value))
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number;
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,6 +131,20 @@ export const enum EmailMessage {
|
|||||||
ANNOUNCEMENT = "اعلان",
|
ANNOUNCEMENT = "اعلان",
|
||||||
PAYMENT_REMINDER = "یادآوری پرداخت",
|
PAYMENT_REMINDER = "یادآوری پرداخت",
|
||||||
PAYMENT_CANCELLATION = "لغو پرداخت",
|
PAYMENT_CANCELLATION = "لغو پرداخت",
|
||||||
|
EMAIL_SENDING_SUCCESS = "ارسال ایمیل با موفقیت انجام شد",
|
||||||
|
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = "وضعیت ایمیل با موفقیت دریافت شد",
|
||||||
|
MESSAGES_SEARCHED_SUCCESSFULLY = "جستجوی ایمیل با موفقیت انجام شد",
|
||||||
|
MESSAGES_LISTED_SUCCESSFULLY = "لیست ایمیل ها با موفقیت دریافت شد",
|
||||||
|
MESSAGE_RETRIEVED_SUCCESSFULLY = "ایمیل با موفقیت دریافت شد",
|
||||||
|
MESSAGE_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد",
|
||||||
|
USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است",
|
||||||
|
USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد",
|
||||||
|
MAILBOX_ID_REQUIRED = "شناسه مستندک ایمیل مورد نیاز است",
|
||||||
|
MAILBOX_ID_MUST_BE_STRING = "شناسه مستندک ایمیل باید یک رشته باشد",
|
||||||
|
QUEUE_ID_REQUIRED = "شناسه پیشنویس ایمیل مورد نیاز است",
|
||||||
|
QUEUE_ID_MUST_BE_STRING = "شناسه پیشنویس ایمیل باید یک رشته باشد",
|
||||||
|
MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد",
|
||||||
|
FROM_ADDRESS_REQUIRED = "آدرس از مورد نیاز است",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum SmsMessage {
|
export const enum SmsMessage {
|
||||||
|
|||||||
@@ -6,13 +6,15 @@ import { MailServerModule } from "../mail-server/mail-server.module";
|
|||||||
import { DnsRecord } from "./entities/dns-record.entity";
|
import { DnsRecord } from "./entities/dns-record.entity";
|
||||||
import { Domain } from "./entities/domain.entity";
|
import { Domain } from "./entities/domain.entity";
|
||||||
import { DnsService } from "./services/dns.service";
|
import { DnsService } from "./services/dns.service";
|
||||||
|
import { DomainAutomationService } from "./services/domain-automation.service";
|
||||||
import { DomainsService } from "./services/domains.service";
|
import { DomainsService } from "./services/domains.service";
|
||||||
import { BusinessesModule } from "../businesses/businesses.module";
|
import { BusinessesModule } from "../businesses/businesses.module";
|
||||||
|
import { UtilsModule } from "../utils/utils.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MikroOrmModule.forFeature([Domain, DnsRecord]), MailServerModule, BusinessesModule],
|
imports: [MikroOrmModule.forFeature([Domain, DnsRecord]), MailServerModule, BusinessesModule, UtilsModule],
|
||||||
controllers: [DomainsController],
|
controllers: [DomainsController],
|
||||||
providers: [DomainsService, DnsService],
|
providers: [DomainsService, DnsService, DomainAutomationService],
|
||||||
exports: [DomainsService, DnsService],
|
exports: [DomainsService, DnsService, DomainAutomationService],
|
||||||
})
|
})
|
||||||
export class DomainsModule {}
|
export class DomainsModule {}
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { EntityManager } from "@mikro-orm/core";
|
||||||
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
|
import { Role } from "../../users/entities/role.entity";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import { RoleEnum } from "../../users/enums/role.enum";
|
||||||
|
import { PasswordService } from "../../utils/services/password.service";
|
||||||
|
import { Domain } from "../entities/domain.entity";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DomainAutomationService {
|
||||||
|
private readonly logger = new Logger(DomainAutomationService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly em: EntityManager,
|
||||||
|
private readonly mailServerService: MailServerService,
|
||||||
|
private readonly passwordService: PasswordService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create info@domain.com address when domain is verified
|
||||||
|
*/
|
||||||
|
async createInfoAddress(domain: Domain): Promise<void> {
|
||||||
|
const infoEmail = `info@${domain.name}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.log(`Creating info address for domain: ${domain.name}`);
|
||||||
|
|
||||||
|
// Check if info address already exists in local database
|
||||||
|
const existingUser = await this.em.findOne(User, {
|
||||||
|
emailAddress: infoEmail,
|
||||||
|
business: { id: domain.business.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingUser) {
|
||||||
|
this.logger.warn(`Info address ${infoEmail} already exists, skipping creation`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a secure password for the info account
|
||||||
|
const password = this.generateSecurePassword();
|
||||||
|
const hashedPassword = await this.passwordService.hashPassword(password);
|
||||||
|
|
||||||
|
// Get user role
|
||||||
|
const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
|
||||||
|
if (!userRole) {
|
||||||
|
throw new Error("User role not found in database");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user in mail server (WildDuck)
|
||||||
|
const mailServerUser = await firstValueFrom(
|
||||||
|
this.mailServerService.users.createUser({
|
||||||
|
username: "info",
|
||||||
|
password,
|
||||||
|
address: infoEmail,
|
||||||
|
name: `Info - ${domain.business.name}`,
|
||||||
|
quota: 1073741824, // 1GB default quota
|
||||||
|
language: "fa",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mailServerUser.success) {
|
||||||
|
throw new Error(`Failed to create mail server user`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create user in local database
|
||||||
|
const user = this.em.create(User, {
|
||||||
|
title: `Info - ${domain.business.name}`,
|
||||||
|
userName: "info",
|
||||||
|
password: hashedPassword,
|
||||||
|
emailAddress: infoEmail,
|
||||||
|
emailEnabled: true,
|
||||||
|
emailQuota: 1073741824, // 1GB default
|
||||||
|
wildduckUserId: mailServerUser.id,
|
||||||
|
displayName: `Info - ${domain.business.name}`,
|
||||||
|
isActive: true,
|
||||||
|
business: domain.business,
|
||||||
|
domain: domain,
|
||||||
|
role: userRole,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(user);
|
||||||
|
|
||||||
|
this.logger.log(`Successfully created info address: ${infoEmail} for business ${domain.business.name}`);
|
||||||
|
this.logger.log(`Info account credentials - Email: ${infoEmail}, Password: [GENERATED]`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to create info address for domain ${domain.name}:`, error);
|
||||||
|
// Don't throw the error to prevent domain verification from failing
|
||||||
|
// The domain should still be marked as verified even if info address creation fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a secure password for the info account
|
||||||
|
*/
|
||||||
|
private generateSecurePassword(): string {
|
||||||
|
const length = 16;
|
||||||
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
|
||||||
|
let password = "";
|
||||||
|
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
password += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if info address exists for a domain
|
||||||
|
*/
|
||||||
|
async infoAddressExists(domain: Domain): Promise<boolean> {
|
||||||
|
const infoEmail = `info@${domain.name}`;
|
||||||
|
const existingUser = await this.em.findOne(User, {
|
||||||
|
emailAddress: infoEmail,
|
||||||
|
business: { id: domain.business.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return !!existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get info address details for a domain
|
||||||
|
*/
|
||||||
|
async getInfoAddressDetails(domain: Domain): Promise<User | null> {
|
||||||
|
const infoEmail = `info@${domain.name}`;
|
||||||
|
return this.em.findOne(
|
||||||
|
User,
|
||||||
|
{
|
||||||
|
emailAddress: infoEmail,
|
||||||
|
business: { id: domain.business.id },
|
||||||
|
},
|
||||||
|
{ populate: ["role", "business", "domain"] },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { BadRequestException, Injectable, Logger, NotFoundException } from "@nes
|
|||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { DnsService } from "./dns.service";
|
import { DnsService } from "./dns.service";
|
||||||
|
import { DomainAutomationService } from "./domain-automation.service";
|
||||||
import { BusinessMessage, DomainMessage, MailServerMessage } from "../../../common/enums/message.enum";
|
import { BusinessMessage, DomainMessage, MailServerMessage } from "../../../common/enums/message.enum";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
@@ -23,6 +24,7 @@ export class DomainsService {
|
|||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
private readonly dnsService: DnsService,
|
private readonly dnsService: DnsService,
|
||||||
private readonly mailServerService: MailServerService,
|
private readonly mailServerService: MailServerService,
|
||||||
|
private readonly domainAutomationService: DomainAutomationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,34 +124,6 @@ export class DomainsService {
|
|||||||
return { message: DomainMessage.DOMAIN_DELETED_SUCCESSFULLY };
|
return { message: DomainMessage.DOMAIN_DELETED_SUCCESSFULLY };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify domain ownership
|
|
||||||
*/
|
|
||||||
async verifyDomain(id: string, businessId: string) {
|
|
||||||
const domain = await this.getDomainById(id, businessId);
|
|
||||||
|
|
||||||
// Check DNS records for verification
|
|
||||||
const isVerified = await this.dnsService.verifyDomainDNS(domain);
|
|
||||||
|
|
||||||
if (isVerified) {
|
|
||||||
domain.status = DomainStatus.VERIFIED;
|
|
||||||
domain.isVerified = true;
|
|
||||||
domain.verifiedAt = new Date();
|
|
||||||
|
|
||||||
// Setup domain in mail server
|
|
||||||
await this.setupDomainInMailServer(domain);
|
|
||||||
|
|
||||||
await this.em.persistAndFlush(domain);
|
|
||||||
this.logger.log(`Domain ${domain.name} verified successfully`);
|
|
||||||
} else {
|
|
||||||
domain.status = DomainStatus.FAILED;
|
|
||||||
await this.em.persistAndFlush(domain);
|
|
||||||
this.logger.warn(`Domain ${domain.name} verification failed`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { domain };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Setup DKIM for domain
|
* Setup DKIM for domain
|
||||||
*/
|
*/
|
||||||
@@ -246,8 +220,8 @@ export class DomainsService {
|
|||||||
|
|
||||||
const domain = await this.getDomainById(businessDomain.id, businessId);
|
const domain = await this.getDomainById(businessDomain.id, businessId);
|
||||||
|
|
||||||
// Trigger DNS verification
|
// // Check DNS records for verification
|
||||||
await this.verifyDomain(domain.id, businessId);
|
// await this.dnsService.verifyDomainDNS(domain);
|
||||||
|
|
||||||
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
|
||||||
|
|
||||||
@@ -256,10 +230,35 @@ export class DomainsService {
|
|||||||
|
|
||||||
const isVerified = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
|
const isVerified = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
|
||||||
|
|
||||||
|
const isComplete = statusCounts.failed === 0 && statusCounts.pending === 0 && statusCounts.verified > 0;
|
||||||
|
|
||||||
|
if (isVerified && isComplete && !domain.isVerified) {
|
||||||
|
// Update domain status
|
||||||
|
domain.status = DomainStatus.VERIFIED;
|
||||||
|
domain.isVerified = true;
|
||||||
|
domain.verifiedAt = new Date();
|
||||||
|
|
||||||
|
// Setup domain in mail server
|
||||||
|
await this.setupDomainInMailServer(domain);
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(domain);
|
||||||
|
|
||||||
|
// Automatically create info@domain.com address
|
||||||
|
await this.domainAutomationService.createInfoAddress(domain);
|
||||||
|
|
||||||
|
this.logger.log(`Domain ${domain.name} verified successfully`);
|
||||||
|
} else if (!isVerified && !isComplete && domain.status !== DomainStatus.FAILED) {
|
||||||
|
// Update domain status to failed if verification failed
|
||||||
|
domain.status = DomainStatus.FAILED;
|
||||||
|
await this.em.persistAndFlush(domain);
|
||||||
|
this.logger.warn(`Domain ${domain.name} verification failed`);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
dnsRecords: verificationStatuses,
|
dnsRecords: verificationStatuses,
|
||||||
overallStatus: {
|
overallStatus: {
|
||||||
isVerified,
|
isVerified,
|
||||||
|
isComplete,
|
||||||
...statusCounts,
|
...statusCounts,
|
||||||
total: dnsRecords.length,
|
total: dnsRecords.length,
|
||||||
lastChecked: new Date(),
|
lastChecked: new Date(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,54 @@
|
|||||||
|
export interface EmailAddress {
|
||||||
|
name?: string;
|
||||||
|
address: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailHeader {
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailAttachment {
|
||||||
|
filename?: string;
|
||||||
|
contentType?: string;
|
||||||
|
encoding?: string;
|
||||||
|
contentTransferEncoding?: string;
|
||||||
|
contentDisposition?: "inline" | "attachment";
|
||||||
|
content: string; // base64 encoded
|
||||||
|
cid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DraftReference {
|
||||||
|
mailbox: string;
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailEnvelope {
|
||||||
|
from: EmailAddress;
|
||||||
|
to: EmailAddress[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface SubmissionData {
|
export interface SubmissionData {
|
||||||
from: {
|
mailbox?: string;
|
||||||
name?: string;
|
from: EmailAddress;
|
||||||
address: string;
|
replyTo?: EmailAddress;
|
||||||
};
|
to: EmailAddress[];
|
||||||
to: Array<{
|
cc?: EmailAddress[];
|
||||||
name?: string;
|
bcc?: EmailAddress[];
|
||||||
address: string;
|
headers?: EmailHeader[];
|
||||||
}>;
|
subject?: string;
|
||||||
cc?: Array<{
|
|
||||||
name?: string;
|
|
||||||
address: string;
|
|
||||||
}>;
|
|
||||||
bcc?: Array<{
|
|
||||||
name?: string;
|
|
||||||
address: string;
|
|
||||||
}>;
|
|
||||||
subject: string;
|
|
||||||
text?: string;
|
text?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
attachments?: Array<{
|
attachments?: EmailAttachment[];
|
||||||
filename?: string;
|
meta?: any;
|
||||||
contentType?: string;
|
|
||||||
content: string; // base64 encoded
|
|
||||||
}>;
|
|
||||||
headers?: Record<string, string>;
|
|
||||||
sess?: string;
|
sess?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
reference?: any;
|
||||||
|
isDraft?: boolean;
|
||||||
|
draft?: DraftReference;
|
||||||
|
sendTime?: string; // ISO date string
|
||||||
|
uploadOnly?: boolean;
|
||||||
|
envelope?: EmailEnvelope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QueueStatus {
|
export interface QueueStatus {
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsBoolean, IsNumber, IsOptional, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateMailboxDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: "Mailbox path with slashes as folder separators",
|
||||||
|
example: "Important/Work",
|
||||||
|
})
|
||||||
|
@IsString()
|
||||||
|
path: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Retention time in milliseconds for messages in this mailbox",
|
||||||
|
example: 2592000000,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
retention?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateMailboxDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "New mailbox path if renaming",
|
||||||
|
example: "Renamed Folder",
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "New retention time in milliseconds",
|
||||||
|
example: 5184000000,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
retention?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MailboxQueryDto {
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: "Include message counters in results",
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
counters?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiParam, ApiQuery, ApiResponse } from "@nestjs/swagger";
|
||||||
|
import { Observable } from "rxjs";
|
||||||
|
|
||||||
|
import { MailboxQueryDto } from "./DTO/mailbox.dto";
|
||||||
|
import { MailboxService } from "./services/mailbox.service";
|
||||||
|
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
|
||||||
|
import { CreateMailboxDto, UpdateMailboxDto } from "../mail-server/DTO";
|
||||||
|
|
||||||
|
@Controller("mailbox")
|
||||||
|
@AuthGuards()
|
||||||
|
export class MailboxController {
|
||||||
|
constructor(private readonly mailboxService: MailboxService) {}
|
||||||
|
|
||||||
|
@Get(":userId/mailboxes")
|
||||||
|
@ApiOperation({ summary: "List mailboxes for a user" })
|
||||||
|
@ApiParam({ name: "userId", description: "User ID" })
|
||||||
|
@ApiQuery({ name: "counters", required: false, description: "Include message counters" })
|
||||||
|
@ApiResponse({ status: 200, description: "List of mailboxes" })
|
||||||
|
@ApiResponse({ status: 404, description: "User not found" })
|
||||||
|
listMailboxes(@Param("userId") userId: string, @Query() query: MailboxQueryDto): Observable<any[]> {
|
||||||
|
return this.mailboxService.listMailboxes(userId, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":userId/mailboxes/:mailboxId")
|
||||||
|
@ApiOperation({ summary: "Get mailbox details" })
|
||||||
|
@ApiParam({ name: "userId", description: "User ID" })
|
||||||
|
@ApiParam({ name: "mailboxId", description: "Mailbox ID" })
|
||||||
|
@ApiResponse({ status: 200, description: "Mailbox details" })
|
||||||
|
@ApiResponse({ status: 404, description: "Mailbox not found" })
|
||||||
|
getMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable<any> {
|
||||||
|
return this.mailboxService.getMailbox(userId, mailboxId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post(":userId/mailboxes")
|
||||||
|
@ApiOperation({ summary: "Create a new mailbox" })
|
||||||
|
@ApiParam({ name: "userId", description: "User ID" })
|
||||||
|
@ApiResponse({ status: 201, description: "Mailbox created successfully" })
|
||||||
|
@ApiResponse({ status: 400, description: "Invalid mailbox data" })
|
||||||
|
createMailbox(@Param("userId") userId: string, @Body() createMailboxDto: CreateMailboxDto): Observable<any> {
|
||||||
|
return this.mailboxService.createMailbox(userId, createMailboxDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":userId/mailboxes/:mailboxId")
|
||||||
|
@ApiOperation({ summary: "Update mailbox" })
|
||||||
|
@ApiParam({ name: "userId", description: "User ID" })
|
||||||
|
@ApiParam({ name: "mailboxId", description: "Mailbox ID" })
|
||||||
|
@ApiResponse({ status: 200, description: "Mailbox updated successfully" })
|
||||||
|
@ApiResponse({ status: 404, description: "Mailbox not found" })
|
||||||
|
updateMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string, @Body() updateMailboxDto: UpdateMailboxDto): Observable<any> {
|
||||||
|
return this.mailboxService.updateMailbox(userId, mailboxId, updateMailboxDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":userId/mailboxes/:mailboxId")
|
||||||
|
@ApiOperation({ summary: "Delete a mailbox" })
|
||||||
|
@ApiParam({ name: "userId", description: "User ID" })
|
||||||
|
@ApiParam({ name: "mailboxId", description: "Mailbox ID" })
|
||||||
|
@ApiResponse({ status: 200, description: "Mailbox deleted successfully" })
|
||||||
|
@ApiResponse({ status: 404, description: "Mailbox not found" })
|
||||||
|
deleteMailbox(@Param("userId") userId: string, @Param("mailboxId") mailboxId: string): Observable<any> {
|
||||||
|
return this.mailboxService.deleteMailbox(userId, mailboxId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
|
||||||
|
import { MailboxController } from "./mailbox.controller";
|
||||||
|
import { MailboxService } from "./services/mailbox.service";
|
||||||
|
import { MailServerModule } from "../mail-server/mail-server.module";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MailServerModule],
|
||||||
|
controllers: [MailboxController],
|
||||||
|
providers: [MailboxService],
|
||||||
|
exports: [MailboxService],
|
||||||
|
})
|
||||||
|
export class MailboxModule {}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
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";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MailboxService {
|
||||||
|
private readonly logger = new Logger(MailboxService.name);
|
||||||
|
|
||||||
|
constructor(private readonly mailServerService: MailServerService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List all mailboxes for a user
|
||||||
|
*/
|
||||||
|
listMailboxes(userId: string, query?: MailboxQueryDto): Observable<any[]> {
|
||||||
|
this.logger.log(`Listing mailboxes for user: ${userId}`);
|
||||||
|
|
||||||
|
return this.mailServerService.mailboxes.listMailboxes(userId, query).pipe(
|
||||||
|
map((response) => {
|
||||||
|
this.logger.log(`Found ${response.results.length} mailboxes for user: ${userId}`);
|
||||||
|
return response.results;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.logger.error(`Failed to list mailboxes for user ${userId}: ${error.message}`);
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific mailbox
|
||||||
|
*/
|
||||||
|
getMailbox(userId: string, mailboxId: string): Observable<any> {
|
||||||
|
this.logger.log(`Getting mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
|
||||||
|
return this.mailServerService.mailboxes.getMailbox(userId, mailboxId).pipe(
|
||||||
|
map((response) => {
|
||||||
|
this.logger.log(`Retrieved mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.logger.error(`Failed to get mailbox ${mailboxId} for user ${userId}: ${error.message}`);
|
||||||
|
if (error.status === 404) {
|
||||||
|
throw new NotFoundException(`Mailbox ${mailboxId} not found for user ${userId}`);
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new mailbox
|
||||||
|
*/
|
||||||
|
createMailbox(userId: string, createMailboxDto: CreateMailboxDto): Observable<any> {
|
||||||
|
this.logger.log(`Creating mailbox "${createMailboxDto.path}" for user: ${userId}`);
|
||||||
|
|
||||||
|
return this.mailServerService.mailboxes.createMailbox(userId, createMailboxDto).pipe(
|
||||||
|
map((response) => {
|
||||||
|
this.logger.log(`Created mailbox ${response.id} for user: ${userId}`);
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.logger.error(`Failed to create mailbox for user ${userId}: ${error.message}`);
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a mailbox
|
||||||
|
*/
|
||||||
|
updateMailbox(userId: string, mailboxId: string, updateMailboxDto: UpdateMailboxDto): Observable<any> {
|
||||||
|
this.logger.log(`Updating mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
|
||||||
|
return this.mailServerService.mailboxes.updateMailbox(userId, mailboxId, updateMailboxDto).pipe(
|
||||||
|
map((response) => {
|
||||||
|
this.logger.log(`Updated mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.logger.error(`Failed to update mailbox ${mailboxId} for user ${userId}: ${error.message}`);
|
||||||
|
if (error.status === 404) {
|
||||||
|
throw new NotFoundException(`Mailbox ${mailboxId} not found for user ${userId}`);
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a mailbox
|
||||||
|
*/
|
||||||
|
deleteMailbox(userId: string, mailboxId: string): Observable<any> {
|
||||||
|
this.logger.log(`Deleting mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
|
||||||
|
return this.mailServerService.mailboxes.deleteMailbox(userId, mailboxId).pipe(
|
||||||
|
map((response) => {
|
||||||
|
this.logger.log(`Deleted mailbox ${mailboxId} for user: ${userId}`);
|
||||||
|
return response;
|
||||||
|
}),
|
||||||
|
catchError((error) => {
|
||||||
|
this.logger.error(`Failed to delete mailbox ${mailboxId} for user ${userId}: ${error.message}`);
|
||||||
|
if (error.status === 404) {
|
||||||
|
throw new NotFoundException(`Mailbox ${mailboxId} not found for user ${userId}`);
|
||||||
|
}
|
||||||
|
return throwError(() => error);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import { Role } from "./role.entity";
|
|||||||
import { BaseEntity } from "../../../common/entities/base.entity";
|
import { BaseEntity } from "../../../common/entities/base.entity";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { Domain } from "../../domains/entities/domain.entity";
|
import { Domain } from "../../domains/entities/domain.entity";
|
||||||
import { EmailSignature } from "../../email-signatures/entities/email-signature.entity";
|
import { EmailSignature } from "../../signatures/entities/email-signature.entity";
|
||||||
import { UserRepository } from "../repositories/user.repository";
|
import { UserRepository } from "../repositories/user.repository";
|
||||||
|
|
||||||
@Entity({ repository: () => UserRepository })
|
@Entity({ repository: () => UserRepository })
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ export class UsersService {
|
|||||||
address: emailAddress,
|
address: emailAddress,
|
||||||
name: displayName || username,
|
name: displayName || username,
|
||||||
quota: quota || 1073741824,
|
quota: quota || 1073741824,
|
||||||
language: "fa",
|
// language: "fa",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user