chore: first commit

This commit is contained in:
mahyargdz
2025-06-24 11:26:06 +03:30
commit ccbf743ecb
186 changed files with 22258 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
import { AuthMessage } from "../../../common/enums/message.enum";
export class LoginDto {
@IsNotEmpty({ message: AuthMessage.EMAIL_NOT_EMPTY })
@IsEmail({}, { message: AuthMessage.INVALID_EMAIL_FORMAT })
@ApiProperty({ description: "User's email address", example: "john.doe@example.com" })
email: string;
@IsNotEmpty({ message: AuthMessage.PASSWORD_NOT_EMPTY })
@IsString({ message: AuthMessage.INVALID_PASS_FORMAT })
@ApiProperty({ description: "User's password", example: "SecurePassword123!" })
password: string;
}
+9
View File
@@ -0,0 +1,9 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
export class RefreshTokenDto {
@ApiProperty({ description: "Refresh token" })
@IsString({ message: "Refresh token must be a string" })
@IsNotEmpty({ message: "Refresh token is required" })
refreshToken: string;
}
+41
View File
@@ -0,0 +1,41 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from "@nestjs/common";
import { ApiBadRequestResponse, ApiOperation, ApiResponse, ApiTags, ApiUnauthorizedResponse } from "@nestjs/swagger";
import { LoginDto } from "./DTO/login.dto";
import { RefreshTokenDto } from "./DTO/refresh-token.dto";
import { AuthService } from "./services/auth.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { RefreshTokenRateLimit } from "../../common/decorators/rate-limit.decorator";
import { UserDec } from "../../common/decorators/user.decorator";
@ApiTags("Authentication")
@Controller("auth")
export class AuthController {
constructor(private readonly authService: AuthService) {}
@Post("login")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Login user" })
@ApiResponse({ status: HttpStatus.OK, description: "User successfully logged in" })
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
@ApiBadRequestResponse({ description: "Invalid input data" })
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@RefreshTokenRateLimit()
@ApiOperation({ summary: "refresh the user access token / refresh token" })
@HttpCode(HttpStatus.OK)
@Post("refresh")
refreshToken(@Body() refreshTokenDto: RefreshTokenDto) {
return this.authService.refreshToken(refreshTokenDto.refreshToken);
}
@AuthGuards()
@ApiOperation({ summary: "logout the user" })
@HttpCode(HttpStatus.OK)
@Post("logout")
logout(@UserDec("id") userId: string) {
return this.authService.logout(userId);
}
}
+23
View File
@@ -0,0 +1,23 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { PassportModule } from "@nestjs/passport";
import { AuthController } from "./auth.controller";
import { jwtConfig } from "../../configs/jwt.config";
import { UtilsModule } from "../utils/utils.module";
import { AuthService } from "./services/auth.service";
import { TokensService } from "./services/tokens.service";
import { UsersModule } from "../users/users.module";
import { ConsoleJwtStrategy } from "./strategies/console-jwt.strategy";
import { LocalJwtStrategy } from "./strategies/local-jwt.strategy";
import { BusinessesModule } from "../businesses/businesses.module";
import { User } from "../users/entities/user.entity";
@Module({
imports: [MikroOrmModule.forFeature([User]), UtilsModule, UsersModule, PassportModule, JwtModule.registerAsync(jwtConfig()), BusinessesModule],
controllers: [AuthController],
providers: [AuthService, TokensService, LocalJwtStrategy, ConsoleJwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
+25
View File
@@ -0,0 +1,25 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { FastifyRequest } from "fastify";
import { ADMIN_ROUTE } from "../../../common/decorators/admin.decorator";
import { AuthMessage } from "../../../common/enums/message.enum";
@Injectable()
export class AdminRouteGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredAdmin = this.reflector.getAllAndOverride<boolean>(ADMIN_ROUTE, [context.getHandler(), context.getClass()]);
if (!requiredAdmin) return true;
const req = context.switchToHttp().getRequest<FastifyRequest>();
const user = req.user;
if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
if (!user.role) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
return true;
}
}
+28
View File
@@ -0,0 +1,28 @@
import { ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
import { Observable } from "rxjs";
import { CONSOLE_JWT_STRATEGY_NAME, LOCAL_JWT_STRATEGY_NAME } from "../../../common/constants";
import { SKIP_AUTH_KEY } from "../../../common/decorators/skip-auth.decorator";
import { AuthMessage } from "../../../common/enums/message.enum";
@Injectable()
export class JwtAuthGuard extends AuthGuard([CONSOLE_JWT_STRATEGY_NAME, LOCAL_JWT_STRATEGY_NAME]) {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const skipAuth = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH_KEY, [context.getHandler(), context.getClass()]);
if (skipAuth) return true;
return super.canActivate(context);
}
handleRequest(err: unknown, user: any, _info: unknown) {
if (err || !user) throw new UnauthorizedException(AuthMessage.TOKEN_EXPIRED_OR_INVALID);
return user;
}
}
+27
View File
@@ -0,0 +1,27 @@
// import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
// import { Reflector } from "@nestjs/core";
// import { FastifyRequest } from "fastify";
// import { ROLES_KEY } from "../../../common/decorators/roles.decorator";
// import { AuthMessage } from "../../../common/enums/message.enum";
// import { RoleEnum } from "../../users/enums/role.enum";
// @Injectable()
// export class RoleGuard implements CanActivate {
// constructor(private reflector: Reflector) {}
// canActivate(context: ExecutionContext): boolean {
// const requiredRole = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY, [context.getHandler(), context.getClass()]);
// if (!requiredRole) return true;
// const req = context.switchToHttp().getRequest<FastifyRequest>();
// const user = req.user;
// if (!user) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
// const hasRequiredRole = requiredRole.some((role) => user.roles.includes(role));
// if (!hasRequiredRole) throw new ForbiddenException(AuthMessage.UNAUTHORIZED_ACCESS);
// return true;
// }
// }
+14
View File
@@ -0,0 +1,14 @@
import { RoleEnum } from "../../users/enums/role.enum";
export interface ILocalTokenPayload {
id: string;
role: RoleEnum;
}
export interface IConsoleTokenPayload {
id: string;
permissions?: string[];
isAdmin?: boolean;
}
export interface ITokenPayload extends ILocalTokenPayload, IConsoleTokenPayload {}
+58
View File
@@ -0,0 +1,58 @@
import { EntityRepository } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { BadRequestException, Injectable } from "@nestjs/common";
import { TokensService } from "./tokens.service";
import { AuthMessage } from "../../../common/enums/message.enum";
import { User } from "../../users/entities/user.entity";
import { PasswordService } from "../../utils/services/password.service";
import { LoginDto } from "../DTO/login.dto";
@Injectable()
export class AuthService {
constructor(
@InjectRepository(User)
private readonly userRepository: EntityRepository<User>,
private readonly tokensService: TokensService,
private readonly passwordService: PasswordService,
) {}
//==============================================
//==============================================
async login(loginDto: LoginDto) {
const { email, password } = loginDto;
const user = await this.checkUserLoginCredentialWithEmail(email, password);
const tokens = await this.tokensService.generateTokens(user);
return {
message: AuthMessage.PASSWORD_LOGIN_SUCCESS,
...tokens,
};
}
//==============================================
//==============================================
async refreshToken(oldRefreshToken: string) {
return this.tokensService.refreshToken(oldRefreshToken);
}
//==============================================
//==============================================
async logout(userId: string) {
await this.tokensService.invalidateRefreshToken(userId);
return {
message: AuthMessage.LOGOUT_SUCCESS,
};
}
//==============================================
private async checkUserLoginCredentialWithEmail(email: string, password: string) {
const user = await this.userRepository.findOne({ emailAddress: email, isActive: true });
if (!user) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
const passCompare = await this.passwordService.comparePasswords(password, user.password);
if (!passCompare) throw new BadRequestException(AuthMessage.INVALID_PASSWORD);
return user;
}
}
+141
View File
@@ -0,0 +1,141 @@
import { randomBytes } from "node:crypto";
import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable, Logger, UnauthorizedException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { JwtService } from "@nestjs/jwt";
import dayjs from "dayjs";
import { AuthMessage, UserMessage } from "../../../common/enums/message.enum";
import { RefreshToken } from "../../users/entities/refresh-token.entity";
import { User } from "../../users/entities/user.entity";
import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable()
export class TokensService {
private readonly logger = new Logger(TokensService.name);
constructor(
private readonly configService: ConfigService,
private readonly jwtService: JwtService,
private readonly em: EntityManager,
) {}
async generateTokens(user: User, em?: EntityManager) {
return this.generateAccessAndRefreshToken(
{
id: user.id,
role: user.role.name,
},
em,
);
}
private async generateAccessAndRefreshToken(payload: ITokenPayload, em?: EntityManager) {
const accessExpire = this.configService.getOrThrow<number>("ACCESS_TOKEN_EXPIRE");
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const accessToken = await this.jwtService.signAsync(payload, { expiresIn: `${accessExpire}m` });
const refreshToken = await this.generateRefreshToken();
await this.storeRefreshToken(payload.id, refreshToken, em);
return {
accessToken: { token: accessToken, expire: dayjs().add(accessExpire, "minute").valueOf() },
refreshToken: { token: refreshToken, expire: dayjs().add(refreshExpire, "day").valueOf() },
};
}
async storeRefreshToken(userId: string, refreshToken: string, em?: EntityManager) {
const entityManager = em || this.em.fork();
let transactionStarted = false;
try {
if (!entityManager.isInTransaction()) {
await entityManager.begin();
transactionStarted = true;
}
const refreshExpire = this.configService.getOrThrow<number>("REFRESH_TOKEN_EXPIRE");
const expiresAt = dayjs().add(refreshExpire, "day").toDate();
const user = await entityManager.findOne(User, { id: userId });
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
const token = entityManager.create(RefreshToken, {
token: refreshToken,
user,
expiresAt,
});
await entityManager.persistAndFlush(token);
if (transactionStarted) await entityManager.commit();
} catch (error) {
this.logger.error(error);
if (transactionStarted) await entityManager.rollback();
throw error;
}
}
async refreshToken(oldRefreshToken: string) {
const entityManager = this.em.fork();
let transactionStarted = false;
try {
await entityManager.begin();
transactionStarted = true;
const token = await entityManager.findOne(
RefreshToken,
{ token: oldRefreshToken },
{
populate: ["user", "user.role"],
},
);
if (!token) throw new UnauthorizedException(AuthMessage.INVALID_REFRESH_TOKEN);
if (dayjs(token.expiresAt).isBefore(dayjs())) {
await entityManager.removeAndFlush(token);
throw new UnauthorizedException(AuthMessage.REFRESH_TOKEN_EXPIRED);
}
await entityManager.removeAndFlush(token);
const tokens = await this.generateTokens(token.user, entityManager);
await entityManager.commit();
return tokens;
} catch (error) {
this.logger.error(error);
if (transactionStarted) {
await entityManager.rollback();
}
throw error;
}
}
async invalidateRefreshToken(userId: string) {
const entityManager = this.em.fork();
let transactionStarted = false;
try {
await entityManager.begin();
transactionStarted = true;
await entityManager.nativeDelete(RefreshToken, { user: { id: userId } });
this.logger.log(`Successfully deleted all refresh tokens for user ${userId}`);
await entityManager.commit();
} catch (error) {
this.logger.error(error);
if (transactionStarted) {
await entityManager.rollback();
}
throw error;
}
}
private async generateRefreshToken() {
return randomBytes(32).toString("hex");
}
}
+24
View File
@@ -0,0 +1,24 @@
import { readFileSync } from "node:fs";
import { Injectable } from "@nestjs/common";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { CONSOLE_JWT_STRATEGY_NAME } from "../../../common/constants";
import { IConsoleTokenPayload } from "../interfaces/IToken-payload";
@Injectable()
export class ConsoleJwtStrategy extends PassportStrategy(Strategy, CONSOLE_JWT_STRATEGY_NAME) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: readFileSync(`${process.cwd()}/keys/public.pem`, "utf8"),
algorithms: ["RS256"],
});
}
async validate(payload: IConsoleTokenPayload) {
return { id: payload.id, permissions: payload.permissions, isAdmin: payload.isAdmin };
}
}
@@ -0,0 +1,23 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { PassportStrategy } from "@nestjs/passport";
import { ExtractJwt, Strategy } from "passport-jwt";
import { LOCAL_JWT_STRATEGY_NAME } from "../../../common/constants";
import { ITokenPayload } from "../interfaces/IToken-payload";
@Injectable()
export class LocalJwtStrategy extends PassportStrategy(Strategy, LOCAL_JWT_STRATEGY_NAME) {
constructor(configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.getOrThrow<string>("JWT_SECRET"),
issuer: configService.getOrThrow<string>("JWT_ISSUER"),
});
}
async validate(payload: ITokenPayload) {
return { id: payload.id, role: payload.role };
}
}
@@ -0,0 +1,11 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString } from "class-validator";
import { BusinessMessage } from "../../../common/enums/message.enum";
export class BusinessSlugParamDto {
@IsNotEmpty({ message: BusinessMessage.SLUG_REQUIRED })
@IsString({ message: BusinessMessage.SLUG_MUST_BE_STRING })
@ApiProperty({ description: "The slug of the business", example: "example-business" })
slug: string;
}
@@ -0,0 +1,25 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, IsUrl } from "class-validator";
import { BusinessMessage } from "../../../common/enums/message.enum";
export class UpdateBusinessSettingsDto {
@IsOptional()
@IsNotEmpty({ message: BusinessMessage.ZARINPAL_MERCHANT_ID_REQUIRED })
@IsString({ message: BusinessMessage.ZARINPAL_MERCHANT_ID_STRING })
@ApiProperty({ description: "Zarinpal merchant id", example: "1234567890" })
zarinpalMerchantId?: string;
@IsOptional()
@IsNotEmpty({ message: BusinessMessage.LOGO_URL_REQUIRED })
@IsString({ message: BusinessMessage.LOGO_URL_STRING })
@IsUrl({ protocols: ["https"] }, { message: BusinessMessage.LOGO_URL_INVALID })
@ApiProperty({ description: "Logo url", example: "https://example.com/logo.png" })
logoUrl?: string;
// @IsOptional()
// @IsNotEmpty({ message: BusinessMessage.NAME_REQUIRED })
// @IsString({ message: BusinessMessage.NAME_STRING })
// @ApiProperty({ description: "Name", example: "Example" })
// name?: string;
}
@@ -0,0 +1,37 @@
import { Body, Controller, Get, Param, Patch, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation } from "@nestjs/swagger";
import { BusinessSlugParamDto } from "./DTO/business-slug-param.dto";
import { UpdateBusinessSettingsDto } from "./DTO/update-business-settings.dto";
import { BusinessesService } from "./services/businesses.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { BusinessDec } from "../../common/decorators/business.decorator";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("business")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
export class BusinessesController {
constructor(private readonly businessesService: BusinessesService) {}
@Get("slug/:slug")
@ApiOperation({ summary: "Get business by slug" })
getBusinessBySlug(@Param() params: BusinessSlugParamDto) {
return this.businessesService.getBusinessBySlug(params.slug);
}
@Patch("settings")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update business settings" })
@AuthGuards()
updateBusinessSettings(@BusinessDec("id") businessId: string, @Body() settingsDto: UpdateBusinessSettingsDto) {
return this.businessesService.updateBusinessSettings(businessId, settingsDto);
}
@Get("settings")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Get business settings" })
@AuthGuards()
getBusinessSettings(@BusinessDec("id") businessId: string) {
return this.businessesService.getBusinessSettings(businessId);
}
}
@@ -0,0 +1,23 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { BullModule } from "@nestjs/bullmq";
import { Module } from "@nestjs/common";
import { BusinessesController } from "./businesses.controller";
import { SUBSCRIPTIONS } from "./constant";
import { Business } from "./entities/business.entity";
import { BusinessProvisioningProcessor } from "./queue/business-provisioning.processor";
import { BusinessesService } from "./services/businesses.service";
@Module({
imports: [
MikroOrmModule.forFeature([Business]),
BullModule.registerQueue({
name: SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME,
prefix: SUBSCRIPTIONS.PROVISIONING_QUEUE_PREFIX,
}),
],
providers: [BusinessesService, BusinessProvisioningProcessor],
exports: [BusinessesService],
controllers: [BusinessesController],
})
export class BusinessesModule {}
+9
View File
@@ -0,0 +1,9 @@
export const SUBSCRIPTIONS = Object.freeze({
PROVISIONING_QUEUE_PREFIX: "provisioning",
PROVISIONING_QUEUE_NAME: "provisioning",
PROVISIONING_JOB_NAME: "business.created",
//
SERVICE_SLUG: "Dmail",
SERVICE_ID: "e51afdc3-ea0b-49cf-8f49-2a6f131b024e",
});
@@ -0,0 +1,25 @@
import { Entity, EntityRepositoryType, ManyToOne, Property } from "@mikro-orm/core";
import { Business } from "./business.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { BusinessStaffRepository } from "../repositories/business-staff.repository";
@Entity({ repository: () => BusinessStaffRepository })
export class BusinessStaff extends BaseEntity {
@Property({ type: "uuid", nullable: false })
danakUserId: string;
@Property({ type: "varchar", length: 255, nullable: false })
fullName: string;
@Property({ type: "varchar", length: 255, nullable: true })
email?: string;
@Property({ type: "varchar", length: 255, nullable: false })
phone: string;
@ManyToOne(() => Business, { deleteRule: "cascade", nullable: false })
business: Business;
[EntityRepositoryType]?: BusinessStaffRepository;
}
@@ -0,0 +1,37 @@
import { Cascade, Collection, Entity, EntityRepositoryType, OneToMany, OneToOne, Property, Unique } from "@mikro-orm/core";
import { BusinessStaff } from "./business-staff.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Domain } from "../../domains/entities/domain.entity";
import { BusinessRepository } from "../repositories/business.repository";
@Entity({ repository: () => BusinessRepository })
export class Business extends BaseEntity {
@Unique()
@Property({ type: "uuid", nullable: false })
danakSubscriptionId!: string;
@Property({ type: "varchar", length: 255, nullable: false })
name!: string;
@Property({ type: "varchar", length: 255, nullable: false })
slug!: string;
@Property({ type: "varchar", length: 255, nullable: true })
logoUrl?: string;
//=========================
@OneToOne(() => Domain, (domain) => domain.business, { nullable: true, cascade: [Cascade.ALL] })
domain?: Domain;
//=========================
// @OneToMany(() => User, (user) => user.business)
// users = new Collection<User>(this);
@OneToMany(() => BusinessStaff, (businessStaff) => businessStaff.business)
businessStaffs = new Collection<BusinessStaff>(this);
[EntityRepositoryType]?: BusinessRepository;
}
@@ -0,0 +1,11 @@
export interface IBusinessProvisioningJob {
subscriptionId: string;
serviceId: string;
serviceName: string;
businessName: string;
userId: string;
slug: string;
phone: string;
email: string;
fullName: string;
}
@@ -0,0 +1,70 @@
// src/business/queue/business-provisioning.processor.ts
import { EntityManager } from "@mikro-orm/postgresql";
import { Processor } from "@nestjs/bullmq";
import { Logger } from "@nestjs/common";
import { Job } from "bullmq";
import { WorkerProcessor } from "../../../common/queues/worker.processor";
import { SUBSCRIPTIONS } from "../constant";
import { BusinessStaff } from "../entities/business-staff.entity";
import { Business } from "../entities/business.entity";
import { IBusinessProvisioningJob } from "../interfaces/IBusiness";
@Processor(SUBSCRIPTIONS.PROVISIONING_QUEUE_NAME)
export class BusinessProvisioningProcessor extends WorkerProcessor {
protected readonly logger = new Logger(BusinessProvisioningProcessor.name);
constructor(private readonly em: EntityManager) {
super();
}
async process(job: Job) {
switch (job.name) {
case SUBSCRIPTIONS.PROVISIONING_JOB_NAME:
await this.handleBusinessCreated(job);
break;
default:
this.logger.warn(`Unknown job name: ${job.name}`);
}
}
private async handleBusinessCreated(job: Job<IBusinessProvisioningJob>) {
const em = this.em.fork();
const { subscriptionId, serviceId, serviceName, businessName, slug, userId, fullName, phone, email } = job.data;
// Only act if the event is for this service
if (serviceId !== SUBSCRIPTIONS.SERVICE_ID) {
this.logger.debug(`Skipping job for service: ${serviceName}`);
return;
}
// Check if already exists
let business = await em.findOne(Business, { danakSubscriptionId: subscriptionId });
if (!business) {
business = em.create(Business, {
danakSubscriptionId: subscriptionId,
name: businessName,
slug: slug,
});
await em.persistAndFlush(business);
this.logger.log(`Created business for subscription ${subscriptionId}`);
} else {
this.logger.debug(`Business already exists for subscription ${subscriptionId}`);
}
const user = await em.findOne(BusinessStaff, { danakUserId: userId });
if (!user) {
const user = em.create(BusinessStaff, {
danakUserId: userId,
fullName,
email,
phone,
business,
});
await em.persistAndFlush(user);
this.logger.log(`Created business staff for subscription ${subscriptionId}`);
} else {
this.logger.debug(`Business staff already exists for subscription ${subscriptionId}`);
}
}
}
@@ -0,0 +1,5 @@
import { EntityRepository } from "@mikro-orm/core";
import { BusinessStaff } from "../entities/business-staff.entity";
export class BusinessStaffRepository extends EntityRepository<BusinessStaff> {}
@@ -0,0 +1,5 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { Business } from "../entities/business.entity";
export class BusinessRepository extends EntityRepository<Business> {}
@@ -0,0 +1,88 @@
import { EntityManager } from "@mikro-orm/postgresql";
import { BadRequestException, Injectable } from "@nestjs/common";
import { BusinessMessage } from "../../../common/enums/message.enum";
import { UpdateBusinessSettingsDto } from "../DTO/update-business-settings.dto";
import { BusinessStaff } from "../entities/business-staff.entity";
import { BusinessRepository } from "../repositories/business.repository";
@Injectable()
export class BusinessesService {
constructor(
private readonly businessRepository: BusinessRepository,
private readonly em: EntityManager,
) {}
async getBusinessByDanakSubscriptionId(danakSubscriptionId: string) {
const business = await this.businessRepository.findOne({ danakSubscriptionId, deletedAt: null });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
return business;
}
//************************************************** */
async getBusinessBySlug(slug: string) {
const business = await this.businessRepository.findOne({ slug, deletedAt: null });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
return { business };
}
//************************************************** */
async updateBusinessSettings(businessId: string, settingsDto: UpdateBusinessSettingsDto) {
const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
this.businessRepository.assign(business, settingsDto);
await this.em.flush();
return {
message: BusinessMessage.BUSINESS_SETTINGS_UPDATED,
business,
};
}
//************************************************** */
async getBusinessSettings(businessId: string) {
const business = await this.businessRepository.findOne({ id: businessId, deletedAt: null });
console.log("business", business);
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
return { business };
}
//************************************************** */
async getBusinessById(id: string) {
const business = await this.businessRepository.findOne({ id, deletedAt: null });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
return business;
}
//************************************************** */
async findAdminWithLeastTickets() {
// First query to get the ID of the business staff with the least tickets
const result = await this.em.getConnection().execute(
`
SELECT bs.id
FROM business_staff bs
WHERE bs.deleted_at IS NULL
ORDER BY (
SELECT COUNT(*) FROM ticket t WHERE t.assigned_to_id = bs.id
) ASC
LIMIT 1
`,
);
if (result.length === 0) return null;
return this.em.findOne(BusinessStaff, { id: result[0].id });
}
/*******************************/
async findOneByIdWithEntityManager(staffId: string, em: EntityManager) {
const staff = await em.findOne(BusinessStaff, { id: staffId });
if (!staff) throw new BadRequestException(BusinessMessage.STAFF_NOT_FOUND);
return staff;
}
}
@@ -0,0 +1,50 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from "class-validator";
import { DNSRecordType } from "../enums/domain-status.enum";
export class CreateDnsRecordDto {
@IsNotEmpty()
@IsString()
@MaxLength(255)
@ApiProperty({ description: "The name of the DNS record", example: "mail" })
name!: string;
@IsEnum(DNSRecordType)
@ApiProperty({ description: "The type of the DNS record", example: "MX" })
type!: DNSRecordType;
@IsNotEmpty()
@IsString()
@ApiProperty({ description: "The value of the DNS record", example: "10 mail.example.com" })
value!: string;
@IsNumber()
@IsOptional()
@Min(60)
@Max(86400)
@ApiProperty({ description: "The TTL of the DNS record", example: 300 })
ttl?: number = 300;
@IsNumber()
@IsOptional()
@Min(0)
@Max(65535)
@ApiProperty({ description: "The priority of the DNS record", example: 10 })
priority?: number;
@IsUUID()
@ApiProperty({ description: "The ID of the domain", example: "123e4567-e89b-12d3-a456-426614174000" })
domainId!: string;
@IsBoolean()
@IsOptional()
@ApiProperty({ description: "Whether the DNS record is required", example: true })
isRequired?: boolean = true;
@IsString()
@IsOptional()
@MaxLength(500)
@ApiProperty({ description: "The description of the DNS record", example: "This is a test DNS record" })
description?: string;
}
@@ -0,0 +1,38 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID, MaxLength } from "class-validator";
import { VerificationType } from "../enums/domain-status.enum";
export class CreateDomainVerificationDto {
@IsEnum(VerificationType)
@ApiProperty({ description: "The type of the domain verification", example: "DNS_TXT" })
type!: VerificationType;
@IsUUID()
@ApiProperty({ description: "The ID of the domain", example: "123e4567-e89b-12d3-a456-426614174000" })
domainId!: string;
@IsString()
@IsOptional()
@MaxLength(255)
@ApiProperty({ description: "The name of the record", example: "mail" })
recordName?: string;
@IsString()
@IsOptional()
@ApiProperty({ description: "The value of the record", example: "10 mail.example.com" })
recordValue?: string;
@IsString()
@IsOptional()
@MaxLength(1000)
@ApiProperty({ description: "The instructions for the domain verification", example: "This is a test domain verification" })
instructions?: string;
}
export class VerifyDomainDto {
@IsNotEmpty()
@IsString()
@ApiProperty({ description: "The token for the domain verification", example: "123e4567-e89b-12d3-a456-426614174000" })
token!: string;
}
@@ -0,0 +1,17 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsOptional, IsString, Matches, MaxLength } from "class-validator";
export class CreateDomainDto {
@IsNotEmpty()
@IsString()
@MaxLength(255)
@Matches(/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/, { message: "Domain name must be a valid domain format" })
@ApiProperty({ description: "The name of the domain", example: "example.com" })
name!: string;
@IsString()
@IsOptional()
@MaxLength(1000)
@ApiProperty({ description: "The notes of the domain", example: "This is a test domain" })
notes?: string;
}
@@ -0,0 +1,5 @@
import { PartialType } from "@nestjs/swagger";
import { CreateDomainDto } from "./create-domain.dto";
export class UpdateDomainDto extends PartialType(CreateDomainDto) {}
+78
View File
@@ -0,0 +1,78 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseInterceptors } from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiResponse } from "@nestjs/swagger";
import { CreateDomainDto } from "./DTO/create-domain.dto";
import { UpdateDomainDto } from "./DTO/update-domain.dto";
import { DomainsService } from "./services/domains.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { BusinessDec } from "../../common/decorators/business.decorator";
import { PaginationDto } from "../../common/DTO/pagination.dto";
import { ParamDto } from "../../common/DTO/param.dto";
import { BusinessInterceptor } from "../../core/interceptors/business.interceptor";
@Controller("domains")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class DomainsController {
constructor(private readonly domainsService: DomainsService) {}
@Post()
@ApiOperation({ summary: "Create a new domain and get setup instructions" })
@ApiResponse({ status: 201, description: "Domain created with setup instructions" })
createDomain(@Body() createDomainDto: CreateDomainDto, @BusinessDec("id") businessId: string) {
return this.domainsService.createDomain(createDomainDto, businessId);
}
@Get()
@ApiOperation({ summary: "Get domain for business" })
@ApiResponse({ status: 200, description: "Domains retrieved successfully" })
getBusinessDomain(@BusinessDec("id") businessId: string, @Query() _query: PaginationDto) {
return this.domainsService.getBusinessDomain(businessId);
}
@Patch(":id")
@ApiOperation({ summary: "Update domain" })
@ApiResponse({ status: 200, description: "Domain updated successfully" })
updateDomain(@Param() paramDto: ParamDto, @Body() updateDomainDto: UpdateDomainDto) {
return this.domainsService.updateDomain(paramDto.id, updateDomainDto);
}
@Delete(":id")
@ApiOperation({ summary: "Delete domain" })
@ApiResponse({ status: 200, description: "Domain deleted successfully" })
deleteDomain(@Param() paramDto: ParamDto) {
return this.domainsService.deleteDomain(paramDto.id);
}
@Get(":id/setup")
@ApiOperation({ summary: "Get domain setup instructions" })
@ApiResponse({ status: 200, description: "Domain setup instructions retrieved" })
getDomainSetupInstructions(@Param() paramDto: ParamDto) {
return this.domainsService.getDomainSetupInstructions(paramDto.id);
}
@Get(":id/dns-records")
@ApiOperation({ summary: "Get DNS records for domain" })
@ApiResponse({ status: 200, description: "DNS records retrieved successfully" })
getDomainDnsRecords(@Param() paramDto: ParamDto) {
return this.domainsService.getDomainDnsRecords(paramDto.id);
}
@Get(":id/verification-status")
@ApiOperation({ summary: "Check domain verification status" })
@ApiResponse({ status: 200, description: "Domain verification status checked" })
checkDomainVerificationStatus(@Param() paramDto: ParamDto) {
return this.domainsService.checkDomainVerificationStatus(paramDto.id);
}
@Post(":id/check-dns")
@ApiOperation({ summary: "Check DNS records and update verification status" })
@ApiResponse({ status: 200, description: "DNS records checked and updated" })
async checkDnsRecords(@Param() paramDto: ParamDto) {
// Trigger DNS verification
await this.domainsService.verifyDomain(paramDto.id);
// Return updated status
return this.domainsService.checkDomainVerificationStatus(paramDto.id);
}
}
+18
View File
@@ -0,0 +1,18 @@
import { MikroOrmModule } from "@mikro-orm/nestjs";
import { Module } from "@nestjs/common";
import { DomainsController } from "./domains.controller";
import { MailServerModule } from "../mail-server/mail-server.module";
import { DnsRecord } from "./entities/dns-record.entity";
import { Domain } from "./entities/domain.entity";
import { DnsService } from "./services/dns.service";
import { DomainsService } from "./services/domains.service";
import { BusinessesModule } from "../businesses/businesses.module";
@Module({
imports: [MikroOrmModule.forFeature([Domain, DnsRecord]), MailServerModule, BusinessesModule],
controllers: [DomainsController],
providers: [DomainsService, DnsService],
exports: [DomainsService, DnsService],
})
export class DomainsModule {}
@@ -0,0 +1,60 @@
import { Entity, EntityRepositoryType, Enum, ManyToOne, Opt, Property } from "@mikro-orm/core";
import { Domain } from "./domain.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { DNSRecordType, VerificationStatus } from "../enums/domain-status.enum";
import { DnsRecordRepository } from "../repositories/dns-record.repository";
@Entity({ repository: () => DnsRecordRepository })
export class DnsRecord extends BaseEntity {
@Property({ type: "varchar", length: 255, nullable: true })
wildduckId?: string;
@Property({ type: "varchar", length: 255, nullable: false })
name!: string;
@Enum({ items: () => DNSRecordType, nativeEnumName: "dns_record_type_enum" })
type!: DNSRecordType;
@Property({ type: "text", nullable: false })
value!: string;
@Property({ type: "integer", default: 300 })
ttl: number & Opt = 300;
@Property({ type: "integer", nullable: true })
priority?: number;
@Enum({ items: () => VerificationStatus, nativeEnumName: "verification_status_enum", default: VerificationStatus.PENDING })
status: VerificationStatus & Opt;
@Property({ type: "boolean", default: true })
isRequired: boolean & Opt;
@Property({ type: "boolean", default: true })
isActive: boolean & Opt;
@Property({ type: "datetime", nullable: true })
lastVerifiedAt?: Date;
@Property({ type: "datetime", nullable: true })
lastCheckedAt?: Date;
@Property({ type: "text", nullable: true })
description?: string;
@Property({ type: "text", nullable: true })
errorMessage?: string;
@Property({ type: "integer", default: 0 })
verificationAttempts: number & Opt;
@Property({ type: "datetime", nullable: true })
nextVerificationAt?: Date;
//==================================
@ManyToOne(() => Domain, { nullable: false })
domain!: Domain;
[EntityRepositoryType]?: DnsRecordRepository;
}
@@ -0,0 +1,70 @@
import { Collection, Entity, EntityRepositoryType, Enum, OneToMany, OneToOne, Opt, Property, Unique } from "@mikro-orm/core";
import { DnsRecord } from "./dns-record.entity";
import { BaseEntity } from "../../../common/entities/base.entity";
import { Business } from "../../businesses/entities/business.entity";
import { DomainStatus } from "../enums/domain-status.enum";
import { DomainRepository } from "../repositories/domain.repository";
@Entity({ repository: () => DomainRepository })
export class Domain extends BaseEntity {
@Unique()
@Property({ type: "varchar", length: 255, nullable: false })
name!: string;
@Enum({ items: () => DomainStatus, nativeEnumName: "domain_status_enum", default: DomainStatus.PENDING })
status: DomainStatus & Opt;
@Property({ type: "boolean", default: false })
isVerified: boolean & Opt;
@Property({ type: "boolean", default: true })
isActive: boolean & Opt;
@Property({ type: "datetime", nullable: true })
verifiedAt?: Date;
@Property({ type: "datetime", nullable: true })
lastCheckedAt?: Date;
@Property({ type: "text", nullable: true })
notes?: string;
// DKIM Configuration
@Property({ type: "boolean", default: false })
dkimEnabled: boolean & Opt;
@Property({ type: "varchar", length: 255, nullable: true })
dkimSelector?: string;
@Property({ type: "text", nullable: true })
dkimPrivateKey?: string;
@Property({ type: "text", nullable: true })
dkimPublicKey?: string;
// SPF Configuration
@Property({ type: "boolean", default: false })
spfEnabled: boolean & Opt;
@Property({ type: "text", nullable: true })
spfRecord?: string;
// DMARC Configuration
@Property({ type: "boolean", default: false })
dmarcEnabled: boolean & Opt;
@Property({ type: "text", nullable: true })
dmarcPolicy?: string;
//==================================
@OneToOne(() => Business, (business) => business.domain, { owner: true, orphanRemoval: true })
business!: Business;
//==================================
@OneToMany(() => DnsRecord, (dnsRecord) => dnsRecord.domain)
dnsRecords = new Collection<DnsRecord>(this);
[EntityRepositoryType]?: DomainRepository;
}
@@ -0,0 +1,29 @@
export enum DomainStatus {
PENDING = "PENDING",
VERIFIED = "VERIFIED",
FAILED = "FAILED",
SUSPENDED = "SUSPENDED",
}
export enum DNSRecordType {
A = "A",
AAAA = "AAAA",
CNAME = "CNAME",
MX = "MX",
TXT = "TXT",
SPF = "SPF",
DKIM = "DKIM",
DMARC = "DMARC",
SRV = "SRV",
}
export enum VerificationStatus {
PENDING = "PENDING",
VERIFIED = "VERIFIED",
FAILED = "FAILED",
EXPIRED = "EXPIRED",
}
export enum VerificationType {
DNS_TXT = "DNS_TXT",
DNS_CNAME = "DNS_CNAME",
}
@@ -0,0 +1,112 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { DnsRecord } from "../entities/dns-record.entity";
import { DNSRecordType, VerificationStatus } from "../enums/domain-status.enum";
export class DnsRecordRepository extends EntityRepository<DnsRecord> {
/**
* Find DNS records by domain ID
*/
async findByDomainId(domainId: string): Promise<DnsRecord[]> {
return this.find({ domain: domainId, deletedAt: null });
}
/**
* Find DNS records by type
*/
async findByType(domainId: string, type: DNSRecordType): Promise<DnsRecord[]> {
return this.find({ domain: domainId, type });
}
/**
* Find required DNS records that are not verified
*/
async findUnverifiedRequired(domainId: string): Promise<DnsRecord[]> {
return this.find({
domain: domainId,
isRequired: true,
status: { $ne: VerificationStatus.VERIFIED },
isActive: true,
});
}
/**
* Find DNS records that need verification
*/
async findNeedingVerification(): Promise<DnsRecord[]> {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return this.find(
{
$and: [
{
$or: [{ lastCheckedAt: null }, { lastCheckedAt: { $lt: oneHourAgo } }],
},
{
$or: [{ status: VerificationStatus.PENDING }, { status: VerificationStatus.FAILED }],
},
{ isActive: true },
{ verificationAttempts: { $lt: 5 } },
],
},
{ populate: ["domain"] },
);
}
/**
* Get DNS record statistics for a domain
*/
async getDomainDnsStats(domainId: string): Promise<{
total: number;
verified: number;
pending: number;
failed: number;
required: number;
}> {
const records = await this.find({ domain: domainId, isActive: true });
return {
total: records.length,
verified: records.filter((r) => r.status === VerificationStatus.VERIFIED).length,
pending: records.filter((r) => r.status === VerificationStatus.PENDING).length,
failed: records.filter((r) => r.status === VerificationStatus.FAILED).length,
required: records.filter((r) => r.isRequired).length,
};
}
/**
* Find MX records for a domain
*/
async findMXRecords(domainId: string): Promise<DnsRecord[]> {
return this.find(
{
domain: domainId,
type: DNSRecordType.MX,
isActive: true,
},
{ orderBy: { priority: "ASC" } },
);
}
/**
* Find SPF record for a domain
*/
async findSPFRecord(domainId: string): Promise<DnsRecord | null> {
return this.findOne({
domain: domainId,
type: DNSRecordType.SPF,
isActive: true,
});
}
/**
* Find DKIM records for a domain
*/
async findDKIMRecords(domainId: string): Promise<DnsRecord[]> {
return this.find({
domain: domainId,
type: DNSRecordType.DKIM,
isActive: true,
});
}
}
@@ -0,0 +1,66 @@
import { EntityRepository } from "@mikro-orm/postgresql";
import { Domain } from "../entities/domain.entity";
import { DomainStatus } from "../enums/domain-status.enum";
export class DomainRepository extends EntityRepository<Domain> {
/**
* Find domain by name
*/
async findByName(name: string): Promise<Domain | null> {
return this.findOne({ name, deletedAt: null }, { populate: ["business", "dnsRecords"] });
}
/**
* Find domains by business ID
*/
async findByBusinessId(businessId: string): Promise<Domain | null> {
return this.findOne({ business: businessId, deletedAt: null }, { populate: ["dnsRecords"] });
}
/**
* Find verified domains
*/
async findVerified(): Promise<Domain | null> {
return this.findOne({ status: DomainStatus.VERIFIED, isActive: true, deletedAt: null });
}
/**
* Find domains pending verification
*/
async findPendingVerification(): Promise<Domain | null> {
return this.findOne({ status: DomainStatus.PENDING, isActive: true, deletedAt: null });
}
/**
* Find domains that need checking
*/
async findNeedingCheck(): Promise<Domain | null> {
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
return this.findOne({
deletedAt: null,
$or: [{ lastCheckedAt: null }, { lastCheckedAt: { $lt: oneHourAgo } }],
isActive: true,
});
}
/**
* Get domain statistics for a business
*/
async getBusinessDomainStats(businessId: string): Promise<{
total: number;
verified: number;
pending: number;
failed: number;
}> {
const domains = await this.find({ business: businessId, deletedAt: null });
return {
total: domains.length,
verified: domains.filter((d) => d.status === DomainStatus.VERIFIED).length,
pending: domains.filter((d) => d.status === DomainStatus.PENDING).length,
failed: domains.filter((d) => d.status === DomainStatus.FAILED).length,
};
}
}
+334
View File
@@ -0,0 +1,334 @@
import * as crypto from "crypto";
import * as dns from "dns";
import { promisify } from "util";
import { EntityManager } from "@mikro-orm/core";
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { DnsRecordMessage, DomainMessage } from "../../../common/enums/message.enum";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { CreateDnsRecordDto } from "../DTO/create-dns-record.dto";
import { DnsRecord } from "../entities/dns-record.entity";
import { Domain } from "../entities/domain.entity";
import { DNSRecordType, VerificationStatus } from "../enums/domain-status.enum";
import { DnsRecordRepository } from "../repositories/dns-record.repository";
import { DomainRepository } from "../repositories/domain.repository";
@Injectable()
export class DnsService {
private readonly logger = new Logger(DnsService.name);
private readonly mailServerDomain: string = "mail.danakcorp.com";
private readonly spfRecord: string = `v=spf1 include:${this.mailServerDomain} ~all`;
private readonly dmarcRecord: string = `v=DMARC1; p=quarantine; rua=mailto:dmarc@${this.mailServerDomain}; ruf=mailto:dmarc@${this.mailServerDomain}`;
private readonly resolveTxt = promisify(dns.resolveTxt);
private readonly resolveCname = promisify(dns.resolveCname);
private readonly resolveMx = promisify(dns.resolveMx);
constructor(
private readonly dnsRecordRepository: DnsRecordRepository,
private readonly domainRepository: DomainRepository,
private readonly mailServerService: MailServerService,
private readonly em: EntityManager,
) {}
/*******************************/
async createDnsRecord(createDnsRecordDto: CreateDnsRecordDto) {
const domain = await this.domainRepository.findOne({ id: createDnsRecordDto.domainId });
if (!domain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
const dnsRecord = this.dnsRecordRepository.create({
...createDnsRecordDto,
domain,
});
await this.em.persistAndFlush(dnsRecord);
this.logger.log(`DNS record created: ${dnsRecord.name} ${dnsRecord.type}`);
return { dnsRecord };
}
/**
* Get DNS records for domain
*/
async getDomainDnsRecords(domainId: string) {
const dnsRecords = await this.dnsRecordRepository.findByDomainId(domainId);
return { dnsRecords };
}
/**
* Update DNS record
*/
async updateDnsRecord(id: string, updates: Partial<DnsRecord>) {
const dnsRecord = await this.dnsRecordRepository.findOne({ id });
if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND);
this.em.assign(dnsRecord, updates);
await this.em.persistAndFlush(dnsRecord);
return { dnsRecord };
}
/*******************************/
async deleteDnsRecord(id: string) {
const dnsRecord = await this.dnsRecordRepository.findOne({ id });
if (!dnsRecord) throw new BadRequestException(DnsRecordMessage.DNS_RECORD_NOT_FOUND);
dnsRecord.isActive = false;
dnsRecord.deletedAt = new Date();
await this.em.persistAndFlush(dnsRecord);
return { message: DnsRecordMessage.DNS_RECORD_DELETED };
}
/*******************************/
async generateRequiredDnsRecords(domain: Domain) {
const requiredRecords: Partial<DnsRecord>[] = [];
// MX Record
requiredRecords.push({
name: domain.name,
type: DNSRecordType.MX,
value: this.mailServerDomain,
priority: 10,
isRequired: true,
description: "Mail exchange record for email delivery",
});
// SPF Record
requiredRecords.push({
name: domain.name,
type: DNSRecordType.SPF,
value: this.spfRecord,
isRequired: true,
description: "Sender Policy Framework record",
});
// DMARC Record
requiredRecords.push({
name: `_dmarc.${domain.name}`,
type: DNSRecordType.DMARC,
value: this.dmarcRecord,
isRequired: true,
description: "DMARC policy record",
});
const dnsRecords: DnsRecord[] = [];
for (const recordData of requiredRecords) {
const dnsRecord = this.dnsRecordRepository.create({
name: recordData.name!,
type: recordData.type!,
value: recordData.value!,
domain,
isRequired: recordData.isRequired ?? true,
description: recordData.description,
priority: recordData.priority,
ttl: recordData.ttl,
});
dnsRecords.push(dnsRecord);
}
await this.em.persistAndFlush(dnsRecords);
this.logger.log(`Generated ${dnsRecords.length} required DNS records for ${domain.name}`);
return { dnsRecords };
}
/**
* Create DKIM record
*/
async createDKIMRecord(domain: Domain, selector: string, dkimName: string, dkimValue: string, wildduckId: string) {
const dkimRecord = this.dnsRecordRepository.create({
name: dkimName,
type: DNSRecordType.DKIM,
value: dkimValue,
domain,
isRequired: true,
description: `DKIM public key for selector ${selector}`,
wildduckId,
});
await this.em.persistAndFlush(dkimRecord);
this.logger.log(`DKIM record created for ${domain.name} with selector ${selector}`);
return { dkimRecord };
}
/**
* Generate DKIM keys
*/
async generateDKIMKeysByMailServer(domainName: string, selector: string) {
const data = await firstValueFrom(this.mailServerService.dkim.createDKIMKey({ domain: domainName, selector }));
return { publicKey: data.publicKey, privateKey: data.dnsTxt.value, dnsTxt: data.dnsTxt, wildduckId: data.id };
}
/**
* Generate DKIM keys native
*/
async generateDKIMKeysByNative() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: {
type: "spki",
format: "pem",
},
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
});
// Extract the public key without headers and newlines for DNS
const publicKeyForDNS = publicKey
.replace(/-----BEGIN PUBLIC KEY-----/g, "")
.replace(/-----END PUBLIC KEY-----/g, "")
.replace(/\n/g, "");
return {
privateKey,
publicKey: publicKeyForDNS,
};
}
/**
* Verify domain DNS configuration
*/
async verifyDomainDNS(domain: Domain) {
const dnsRecords = await this.dnsRecordRepository.findByDomainId(domain.id);
const requiredRecords = dnsRecords.filter((record) => record.isRequired && record.isActive);
let allVerified = true;
for (const record of requiredRecords) {
const isVerified = await this.verifyDnsRecord(record);
if (!isVerified) {
allVerified = false;
}
}
return allVerified;
}
/**
* Verify individual DNS record
*/
async verifyDnsRecord(dnsRecord: DnsRecord) {
try {
dnsRecord.verificationAttempts += 1;
dnsRecord.lastCheckedAt = new Date();
let isVerified = false;
switch (dnsRecord.type) {
case DNSRecordType.TXT:
case DNSRecordType.SPF:
case DNSRecordType.DMARC:
case DNSRecordType.DKIM:
isVerified = await this.verifyTxtRecord(dnsRecord);
break;
case DNSRecordType.MX:
isVerified = await this.verifyMxRecord(dnsRecord);
break;
case DNSRecordType.CNAME:
isVerified = await this.verifyCnameRecord(dnsRecord);
break;
default:
this.logger.warn(`DNS record type ${dnsRecord.type} verification not implemented`);
isVerified = false;
}
if (isVerified) {
dnsRecord.status = VerificationStatus.VERIFIED;
dnsRecord.lastVerifiedAt = new Date();
dnsRecord.errorMessage = undefined;
} else {
dnsRecord.status = VerificationStatus.FAILED;
dnsRecord.errorMessage = "DNS record not found or value mismatch";
}
await this.em.persistAndFlush(dnsRecord);
return isVerified;
} catch (error) {
dnsRecord.status = VerificationStatus.FAILED;
dnsRecord.errorMessage = error instanceof Error ? error.message : "Unknown error";
await this.em.persistAndFlush(dnsRecord);
this.logger.error(`DNS verification failed for ${dnsRecord.name}:`, error);
return false;
}
}
/**
* Verify TXT record
*/
private async verifyTxtRecord(dnsRecord: DnsRecord) {
try {
const txtRecords = await this.resolveTxt(dnsRecord.name);
const flatRecords = txtRecords.flat();
return flatRecords.some((record) => record.includes(dnsRecord.value) || record === dnsRecord.value);
} catch (error) {
this.logger.debug(`TXT record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
return false;
}
}
/**
* Verify MX record
*/
private async verifyMxRecord(dnsRecord: DnsRecord) {
try {
const mxRecords = await this.resolveMx(dnsRecord.name);
return mxRecords.some((mx) => mx.exchange === dnsRecord.value && mx.priority === (dnsRecord.priority || 10));
} catch (error) {
this.logger.debug(`MX record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
return false;
}
}
/**
* Verify CNAME record
*/
private async verifyCnameRecord(dnsRecord: DnsRecord) {
try {
const cnameRecords = await this.resolveCname(dnsRecord.name);
return cnameRecords.includes(dnsRecord.value);
} catch (error) {
this.logger.debug(`CNAME record verification failed for ${dnsRecord.name}:`, error instanceof Error ? error.message : "Unknown error");
return false;
}
}
/**
* Get DNS record statistics
*/
async getDnsRecordStats(domainId: string): Promise<{
total: number;
verified: number;
pending: number;
failed: number;
required: number;
}> {
return this.dnsRecordRepository.getDomainDnsStats(domainId);
}
/**
* Verify all DNS records that need checking
*/
async verifyPendingDnsRecords(): Promise<void> {
const records = await this.dnsRecordRepository.findNeedingVerification();
for (const record of records) {
try {
await this.verifyDnsRecord(record);
} catch (error) {
this.logger.error(`Failed to verify DNS record ${record.name}:`, error);
}
}
}
}
@@ -0,0 +1,450 @@
import { EntityManager } from "@mikro-orm/core";
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { DnsService } from "./dns.service";
import { BusinessMessage, DomainMessage, MailServerMessage } from "../../../common/enums/message.enum";
import { Business } from "../../businesses/entities/business.entity";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { formatDomainMessage } from "../../utils/services/message.utils";
import { CreateDomainDto } from "../DTO/create-domain.dto";
import { UpdateDomainDto } from "../DTO/update-domain.dto";
import { DnsRecord } from "../entities/dns-record.entity";
import { Domain } from "../entities/domain.entity";
import { DomainStatus, VerificationStatus } from "../enums/domain-status.enum";
import { DomainRepository } from "../repositories/domain.repository";
@Injectable()
export class DomainsService {
private readonly logger = new Logger(DomainsService.name);
constructor(
private readonly domainRepository: DomainRepository,
private readonly em: EntityManager,
private readonly dnsService: DnsService,
private readonly mailServerService: MailServerService,
) {}
/**
* Create a new domain
*/
async createDomain(createDomainDto: CreateDomainDto, businessId: string) {
const { name, notes } = createDomainDto;
const existBusinessDomain = await this.domainRepository.findOne({ business: { id: businessId }, deletedAt: null });
if (existBusinessDomain) throw new BadRequestException(DomainMessage.BUSINESS_ALREADY_HAS_DOMAIN);
// Check if domain already exists
const existingDomain = await this.domainRepository.findByName(name);
if (existingDomain) throw new BadRequestException(formatDomainMessage(DomainMessage.DOMAIN_ALREADY_EXISTS, { name }));
// Verify business exists
const business = await this.em.findOne(Business, { id: businessId });
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
const domain = this.domainRepository.create({
name,
business,
notes,
dkimEnabled: true,
dkimSelector: "default",
});
await this.em.persistAndFlush(domain);
// Generate required DNS records (MX, SPF, DMARC)
await this.dnsService.generateRequiredDnsRecords(domain);
// Generate and create DKIM record
try {
const dkimKeys = await this.dnsService.generateDKIMKeysByMailServer(domain.name, "default");
// Update domain with DKIM keys
domain.dkimPrivateKey = dkimKeys.privateKey;
domain.dkimPublicKey = dkimKeys.publicKey;
// Create DKIM DNS record
await this.dnsService.createDKIMRecord(domain, "default", dkimKeys.dnsTxt.name, dkimKeys.dnsTxt.value, dkimKeys.wildduckId);
await this.em.persistAndFlush(domain);
this.logger.log(`DKIM automatically enabled for domain ${name}`);
} catch (error) {
this.logger.warn(`Failed to create DKIM for domain ${name}:`, error);
// Continue with domain creation even if DKIM fails
domain.dkimEnabled = false;
await this.em.persistAndFlush(domain);
}
// Create initial verification
this.logger.log(`Domain ${name} created for business ${businessId} with all required records`);
// Get updated DNS records including DKIM
const { dnsRecords: allDnsRecords } = await this.dnsService.getDomainDnsRecords(domain.id);
// Return domain setup response with complete instructions
return this.formatDomainSetupResponse(domain, allDnsRecords);
}
/**
* Get domain by ID
*/
async getDomainById(id: string): Promise<Domain> {
const domain = await this.domainRepository.findOne({ id, deletedAt: null }, { populate: ["business", "dnsRecords"] });
if (!domain) throw new NotFoundException(DomainMessage.DOMAIN_NOT_FOUND);
return domain;
}
/**
* Get domain by name
*/
async getDomainByName(name: string) {
const domain = await this.domainRepository.findByName(name);
if (!domain) throw new BadRequestException(DomainMessage.DOMAIN_NOT_FOUND);
return { domain };
}
/**
* Get domains for a business
*/
async getBusinessDomain(businessId: string) {
const domain = await this.domainRepository.findByBusinessId(businessId);
return { domain };
}
/**
* Update domain
*/
async updateDomain(id: string, updateDomainDto: UpdateDomainDto) {
const domain = await this.getDomainById(id);
this.em.assign(domain, updateDomainDto);
await this.em.flush();
this.logger.log(`Domain ${domain.name} updated`);
return { domain, message: DomainMessage.DOMAIN_UPDATED_SUCCESSFULLY };
}
/**
* Delete domain
*/
async deleteDomain(id: string) {
const domain = await this.getDomainById(id);
// Soft delete
domain.deletedAt = new Date();
domain.isActive = false;
await this.em.persistAndFlush(domain);
this.logger.log(`Domain ${domain.name} deleted`);
return { message: DomainMessage.DOMAIN_DELETED_SUCCESSFULLY };
}
/**
* Verify domain ownership
*/
async verifyDomain(id: string) {
const domain = await this.getDomainById(id);
// 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 domain in mail server
*/
private async setupDomainInMailServer(domain: Domain) {
try {
// Create DKIM keys if enabled
if (domain.dkimEnabled && domain.dkimSelector) {
const dkimKey = await firstValueFrom(
this.mailServerService.dkim.createDKIMKey({
domain: domain.name,
selector: domain.dkimSelector,
description: `DKIM for ${domain.name}`,
}),
);
if (!dkimKey.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_DKIM_KEY);
}
this.logger.log(`Mail server setup completed for domain ${domain.name}`);
} catch (error) {
this.logger.error(`Failed to setup domain ${domain.name} in mail server:`, error);
throw error;
}
}
/**
* Enable DKIM for domain
*/
async enableDKIM(id: string, selector?: string) {
const domain = await this.getDomainById(id);
const dkimSelector = selector || "default";
// Generate DKIM keys
const dkimKeys = await this.dnsService.generateDKIMKeysByMailServer(domain.name, dkimSelector);
domain.dkimEnabled = true;
domain.dkimSelector = dkimSelector;
domain.dkimPrivateKey = dkimKeys.privateKey;
domain.dkimPublicKey = dkimKeys.publicKey;
// Create DKIM DNS record
await this.dnsService.createDKIMRecord(domain, dkimSelector, dkimKeys.dnsTxt.name, dkimKeys.dnsTxt.value, dkimKeys.wildduckId);
await this.em.persistAndFlush(domain);
this.logger.log(`DKIM enabled for domain ${domain.name}`);
return { domain };
}
/**
* Get domain statistics
*/
async getDomainStats(businessId: string) {
return this.domainRepository.getBusinessDomainStats(businessId);
}
/**
* Check all domains that need verification
*/
async checkDomainsNeedingVerification() {
const domain = await this.domainRepository.findNeedingCheck();
if (!domain) return { message: DomainMessage.NO_DOMAIN_FOUND };
await this.verifyDomain(domain.id);
return { message: DomainMessage.DOMAIN_CHECKED, domain: domain };
}
/**
* Get domain setup instructions
*/
async getDomainSetupInstructions(domainId: string) {
const domain = await this.getDomainById(domainId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domainId);
return this.formatDomainSetupResponse(domain, dnsRecords);
}
/**
* Get DNS records for domain
*/
async getDomainDnsRecords(domainId: string) {
// Verify domain exists
await this.getDomainById(domainId);
return this.dnsService.getDomainDnsRecords(domainId);
}
/**
* Check domain verification status
*/
async checkDomainVerificationStatus(domainId: string) {
await this.getDomainById(domainId);
const { dnsRecords } = await this.dnsService.getDomainDnsRecords(domainId);
const verificationStatuses = [];
let verified = 0;
let pending = 0;
let failed = 0;
const recommendations: string[] = [];
for (const record of dnsRecords) {
// Verify each DNS record
await this.dnsService.verifyDnsRecord(record);
const status = {
recordId: record.id,
name: record.name,
type: record.type,
expectedValue: record.value,
currentValue: undefined,
status: record.status,
lastChecked: record.lastCheckedAt || new Date(),
errorMessage: record.errorMessage,
isCorrect: record.status === VerificationStatus.VERIFIED,
};
verificationStatuses.push(status);
if (record.status === VerificationStatus.VERIFIED) {
verified++;
} else if (record.status === VerificationStatus.FAILED) {
failed++;
if (record.errorMessage) {
recommendations.push(
formatDomainMessage(DomainMessage.RECORD_NEEDS_FIXING, {
type: record.type,
name: record.name,
error: record.errorMessage,
}),
);
}
} else {
pending++;
recommendations.push(
formatDomainMessage(DomainMessage.RECORD_PENDING_VERIFICATION_DETAIL, {
type: record.type,
name: record.name,
}),
);
}
}
const isVerified = failed === 0 && pending === 0 && verified > 0;
return {
dnsRecords: verificationStatuses,
overallStatus: {
isVerified,
verified,
pending,
failed,
total: dnsRecords.length,
lastChecked: new Date(),
},
recommendations,
};
}
/**
* Format domain setup response with instructions
*/
private formatDomainSetupResponse(domain: Domain, dnsRecords: DnsRecord[]) {
const dnsInstructions = dnsRecords.map((record) => ({
id: record.id,
name: record.name,
type: record.type,
value: record.value,
ttl: record.ttl,
priority: record.priority,
description:
record.description ||
formatDomainMessage(DomainMessage.RECORD_DESCRIPTION_EMAIL_FUNCTIONALITY, {
type: record.type,
}),
isRequired: record.isRequired,
status: record.status,
instructions: this.generateRecordInstructions(record),
}));
const verified = dnsRecords.filter((r) => r.status === VerificationStatus.VERIFIED).length;
const pending = dnsRecords.filter((r) => r.status === VerificationStatus.PENDING).length;
const failed = dnsRecords.filter((r) => r.status === VerificationStatus.FAILED).length;
const nextSteps = this.generateNextSteps(domain, dnsRecords);
return {
domain,
dnsRecords: dnsInstructions,
setupStatus: {
isComplete: failed === 0 && pending === 0 && verified > 0,
verified,
pending,
failed,
total: dnsRecords.length,
},
nextSteps,
estimatedPropagationTime: DomainMessage.ESTIMATED_PROPAGATION_TIME,
};
}
/**
* Generate instructions for a specific DNS record
*/
private generateRecordInstructions(record: DnsRecord): string {
switch (record.type) {
case "MX":
return formatDomainMessage(DomainMessage.ADD_MX_RECORD, {
name: record.name,
value: record.value,
priority: record.priority,
});
case "TXT":
case "SPF":
return formatDomainMessage(DomainMessage.ADD_TXT_RECORD, {
name: record.name,
value: record.value,
});
case "DKIM":
return formatDomainMessage(DomainMessage.ADD_DKIM_RECORD, {
name: record.name,
value: record.value,
});
case "DMARC":
return formatDomainMessage(DomainMessage.ADD_DMARC_RECORD, {
name: record.name,
value: record.value,
});
case "CNAME":
return formatDomainMessage(DomainMessage.ADD_CNAME_RECORD, {
name: record.name,
value: record.value,
});
default:
return formatDomainMessage(DomainMessage.ADD_GENERIC_RECORD, {
type: record.type,
name: record.name,
value: record.value,
});
}
}
/**
* Generate next steps for domain setup
*/
private generateNextSteps(domain: Domain, dnsRecords: DnsRecord[]): string[] {
const steps: string[] = [];
if (domain.status === DomainStatus.PENDING) {
steps.push(`1. ${DomainMessage.STEP_LOGIN_DNS_PANEL}`);
steps.push(`2. ${DomainMessage.STEP_ADD_ALL_RECORDS}`);
steps.push(`3. ${DomainMessage.STEP_INCLUDE_ALL_TYPES}`);
steps.push(`4. ${DomainMessage.STEP_WAIT_PROPAGATION}`);
steps.push(`5. ${DomainMessage.STEP_USE_CHECK_ENDPOINT}`);
steps.push(`6. ${DomainMessage.STEP_DOMAIN_READY}`);
}
const pendingRecords = dnsRecords.filter((r) => r.status === VerificationStatus.PENDING);
if (pendingRecords.length > 0) {
steps.push(`📋 ${formatDomainMessage(DomainMessage.RECORDS_PENDING_VERIFICATION, { count: pendingRecords.length })}`);
}
const requiredRecords = dnsRecords.filter((r) => r.isRequired);
if (requiredRecords.length > 0) {
steps.push(`⚠️ ${formatDomainMessage(DomainMessage.ALL_REQUIRED_RECORDS_MUST_BE_CONFIGURED, { count: requiredRecords.length })}`);
}
if (domain.status === DomainStatus.VERIFIED) {
steps.push(`${DomainMessage.DOMAIN_FULLY_VERIFIED}`);
steps.push(`🚀 ${DomainMessage.EMAIL_ACCOUNTS_READY}`);
}
return steps;
}
}
@@ -0,0 +1,28 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsEmail, IsOptional } from "class-validator";
export class CreateAddressDto {
@ApiProperty({
description: "Email address to use as an alias",
example: "alias@example.com",
})
@IsEmail()
address: string;
@ApiPropertyOptional({
description: "Whether this is the default address for the user",
default: false,
})
@IsOptional()
@IsBoolean()
main?: boolean;
}
export class UpdateAddressDto {
@ApiProperty({
description: "Set as the default address for the user",
example: true,
})
@IsBoolean()
main: boolean;
}
@@ -0,0 +1,57 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsEnum, IsOptional, IsString } from "class-validator";
export enum AuthScope {
MASTER = "master",
IMAP = "imap",
POP3 = "pop3",
SMTP = "smtp",
}
export class AuthenticateDto {
@ApiProperty({
description: "Username or email address of the user",
example: "testuser",
})
@IsString()
username: string;
@ApiProperty({
description: "Password for the user (master or application specific)",
example: "secretpassword123",
})
@IsString()
password: string;
@ApiPropertyOptional({
description: "Scope to request for",
enum: AuthScope,
default: AuthScope.MASTER,
})
@IsOptional()
@IsEnum(AuthScope)
scope?: AuthScope;
@ApiPropertyOptional({
description: "Application type this authentication is made from",
example: "API",
})
@IsOptional()
@IsString()
protocol?: string;
@ApiPropertyOptional({
description: "Session identifier for logging",
})
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({
description: "IP address the request was made from",
example: "192.168.1.1",
})
@IsOptional()
@IsString()
ip?: string;
}
@@ -0,0 +1,97 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsEmail, IsNumber, IsOptional, IsString, IsUrl } from "class-validator";
export class CreateUserDto {
@ApiProperty({ description: "Username for the user (letters and numbers only)", example: "testuser" })
@IsString()
username: string;
@ApiProperty({ description: "Password for the user", example: "secretpassword123" })
@IsString()
password: string;
@ApiPropertyOptional({ description: "Main email address for the user", example: "testuser@example.com" })
@IsOptional()
@IsEmail()
address?: string;
@ApiPropertyOptional({ description: "If true, do not set up an address for the user", default: false })
@IsOptional()
@IsBoolean()
emptyAddress?: boolean;
@ApiPropertyOptional({ description: "Display name for the user", example: "Test User" })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: "Array of email addresses to forward all messages to",
example: ["forward1@example.com", "forward2@example.com"],
})
@IsOptional()
@IsArray()
@IsEmail({}, { each: true })
forward?: string[];
@ApiPropertyOptional({ description: "URL to upload all messages to", example: "https://example.com/webhook" })
@IsOptional()
@IsUrl()
targetUrl?: string;
@ApiPropertyOptional({ description: "Maximum storage in bytes allowed for this user", example: 1073741824 })
@IsOptional()
@IsNumber()
quota?: number;
@ApiPropertyOptional({ description: "Default retention time in milliseconds for mailboxes", example: 2592000000 })
@IsOptional()
@IsNumber()
retention?: number;
@ApiPropertyOptional({ description: "Language code for the user", example: "en" })
@IsOptional()
@IsString()
language?: string;
@ApiPropertyOptional({ description: "Maximum number of recipients allowed to send mail to in 24h", example: 2000 })
@IsOptional()
@IsNumber()
recipients?: number;
@ApiPropertyOptional({ description: "Maximum number of forwarded emails in 24h", example: 2000 })
@IsOptional()
@IsNumber()
forwards?: number;
@ApiPropertyOptional({ description: "Array of tags to associate with the user", example: ["vip", "customer"] })
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiPropertyOptional({ description: "PGP public key for encryption" })
@IsOptional()
@IsString()
pubKey?: string;
@ApiPropertyOptional({ description: "Whether to encrypt stored messages", default: false })
@IsOptional()
@IsBoolean()
encryptMessages?: boolean;
@ApiPropertyOptional({ description: "Whether to encrypt forwarded messages", default: false })
@IsOptional()
@IsBoolean()
encryptForwarded?: boolean;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address the request was made from", example: "192.168.1.1" })
@IsOptional()
@IsString()
ip?: string;
}
+27
View File
@@ -0,0 +1,27 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsOptional, IsString } from "class-validator";
export class CreateDKIMDto {
@ApiProperty({ description: "Domain name", example: "example.com" })
@IsString()
domain: string;
@ApiProperty({ description: "DKIM selector", example: "default" })
@IsString()
selector: string;
@ApiPropertyOptional({ description: "Description for the DKIM key" })
@IsOptional()
@IsString()
description?: string;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
+112
View File
@@ -0,0 +1,112 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsEmail, IsNumber, IsOptional, IsString, IsUrl } from "class-validator";
export class CreateFilterDto {
@ApiPropertyOptional({
description: "Name of the filter",
example: "Important emails",
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: "String to match against the From: header",
example: "boss@company.com",
})
@IsOptional()
@IsString()
query_from?: string;
@ApiPropertyOptional({
description: "String to match against the To:/Cc: headers",
example: "team@company.com",
})
@IsOptional()
@IsString()
query_to?: string;
@ApiPropertyOptional({
description: "String to match against the Subject: header",
example: "urgent",
})
@IsOptional()
@IsString()
query_subject?: string;
@ApiPropertyOptional({
description: "String to match against the message text",
example: "important project",
})
@IsOptional()
@IsString()
query_text?: string;
@ApiPropertyOptional({
description: "Require message to have attachments (true) or not (false)",
})
@IsOptional()
@IsBoolean()
query_ha?: boolean;
@ApiPropertyOptional({
description: "Message size requirement (positive for larger, negative for smaller)",
example: 1000000,
})
@IsOptional()
@IsNumber()
query_size?: number;
@ApiPropertyOptional({
description: "Mark message as seen (true) or unseen (false)",
})
@IsOptional()
@IsBoolean()
action_seen?: boolean;
@ApiPropertyOptional({
description: "Mark message as flagged (true) or not (false)",
})
@IsOptional()
@IsBoolean()
action_flag?: boolean;
@ApiPropertyOptional({
description: "Delete message immediately (true)",
})
@IsOptional()
@IsBoolean()
action_delete?: boolean;
@ApiPropertyOptional({
description: "Mark message as spam (true) or not spam (false)",
})
@IsOptional()
@IsBoolean()
action_spam?: boolean;
@ApiPropertyOptional({
description: "Mailbox ID to move the message to",
})
@IsOptional()
@IsString()
action_mailbox?: string;
@ApiPropertyOptional({
description: "Email address to forward the message to",
example: "forward@example.com",
})
@IsOptional()
@IsEmail()
action_forward?: string;
@ApiPropertyOptional({
description: "Web URL to upload the message to",
example: "https://webhook.example.com/messages",
})
@IsOptional()
@IsUrl()
action_targetUrl?: string;
}
export class UpdateFilterDto extends CreateFilterDto {}
+30
View File
@@ -0,0 +1,30 @@
// User DTOs
export * from "./create-user.dto";
export * from "./update-user.dto";
// Message DTOs
export * from "./message.dto";
// Mailbox DTOs
export * from "./mailbox.dto";
// Address DTOs
export * from "./address.dto";
// Authentication DTOs
export * from "./authenticate.dto";
// Filter DTOs
export * from "./filter.dto";
// Submission DTOs
export * from "./submission.dto";
// DKIM DTOs
export * from "./dkim.dto";
// Webhook DTOs
export * from "./webhook.dto";
// Two-Factor Authentication DTOs
export * from "./two-factor.dto";
@@ -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 ListMailboxesQueryDto {
@ApiPropertyOptional({
description: "Include message counters in results",
default: false,
})
@IsOptional()
@IsBoolean()
counters?: boolean;
}
+121
View File
@@ -0,0 +1,121 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsBoolean, IsDateString, IsNumber, IsOptional, IsString, Max, Min } from "class-validator";
export class ListMessagesQueryDto {
@ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" })
@IsOptional()
@IsString()
order?: "asc" | "desc";
@ApiPropertyOptional({ description: "Number of messages to return", minimum: 1, maximum: 250, default: 20 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(250)
limit?: number;
@ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number;
@ApiPropertyOptional({ description: "Cursor for next page" })
@IsOptional()
@IsString()
next?: string;
@ApiPropertyOptional({ description: "Cursor for previous page" })
@IsOptional()
@IsString()
previous?: string;
}
export class UpdateMessageDto {
@ApiPropertyOptional({ description: "ID of the destination mailbox if moving the message" })
@IsOptional()
@IsString()
moveTo?: string;
@ApiPropertyOptional({ description: "Mark message as seen or unseen" })
@IsOptional()
@IsBoolean()
seen?: boolean;
@ApiPropertyOptional({ description: "Mark message as flagged or not" })
@IsOptional()
@IsBoolean()
flagged?: boolean;
@ApiPropertyOptional({ description: "Mark message as draft or not" })
@IsOptional()
@IsBoolean()
draft?: boolean;
@ApiPropertyOptional({ description: "Mark message as deleted or not" })
@IsOptional()
@IsBoolean()
deleted?: boolean;
@ApiPropertyOptional({ description: "Expiration date for auto-deletion or false to clear", example: "2024-12-31T23:59:59.000Z" })
@IsOptional()
expires?: string | false;
}
export class SearchMessagesQueryDto {
@ApiProperty({ description: "Query string to search for", example: "important meeting" })
@IsString()
query: string;
@ApiPropertyOptional({ description: "Message ordering", enum: ["asc", "desc"], default: "desc" })
@IsOptional()
@IsString()
order?: "asc" | "desc";
@ApiPropertyOptional({ description: "Number of results to return", minimum: 1, maximum: 250, default: 20 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
@Max(250)
limit?: number;
@ApiPropertyOptional({ description: "Page number for pagination", minimum: 1, default: 1 })
@IsOptional()
@Type(() => Number)
@IsNumber()
@Min(1)
page?: number;
}
export class UploadMessageDto {
@ApiProperty({
description: "Raw message content in RFC822 format",
example: "From: sender@example.com\nTo: recipient@example.com\nSubject: Test\n\nMessage body",
})
@IsString()
raw: string;
@ApiPropertyOptional({ description: "Mark message as seen", default: false })
@IsOptional()
@IsBoolean()
seen?: boolean;
@ApiPropertyOptional({ description: "Mark message as flagged", default: false })
@IsOptional()
@IsBoolean()
flagged?: boolean;
@ApiPropertyOptional({ description: "Mark message as draft", default: false })
@IsOptional()
@IsBoolean()
draft?: boolean;
@ApiPropertyOptional({ description: "Message date", example: "2024-01-01T12:00:00.000Z" })
@IsOptional()
@IsDateString()
date?: string;
}
@@ -0,0 +1,92 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { Type } from "class-transformer";
import { IsArray, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
export class EmailAddress {
@ApiPropertyOptional({ description: "Display name", example: "John Doe" })
@IsOptional()
@IsString()
name?: string;
@ApiProperty({ description: "Email address", example: "john@example.com" })
@IsEmail()
address: string;
}
export class MessageAttachment {
@ApiPropertyOptional({ description: "Filename", example: "document.pdf" })
@IsOptional()
@IsString()
filename?: string;
@ApiPropertyOptional({ description: "Content type", example: "application/pdf" })
@IsOptional()
@IsString()
contentType?: string;
@ApiProperty({ description: "Base64 encoded content" })
@IsString()
content: string;
}
export class SubmitMessageDto {
@ApiProperty({ description: "From address" })
@ValidateNested()
@Type(() => EmailAddress)
from: EmailAddress;
@ApiProperty({ description: "To addresses", type: [EmailAddress] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => EmailAddress)
to: EmailAddress[];
@ApiPropertyOptional({ description: "CC addresses", type: [EmailAddress] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => EmailAddress)
cc?: EmailAddress[];
@ApiPropertyOptional({ description: "BCC addresses", type: [EmailAddress] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => EmailAddress)
bcc?: EmailAddress[];
@ApiProperty({ description: "Subject line", example: "Test email" })
@IsString()
subject: string;
@ApiPropertyOptional({ description: "Plain text content" })
@IsOptional()
@IsString()
text?: string;
@ApiPropertyOptional({ description: "HTML content" })
@IsOptional()
@IsString()
html?: string;
@ApiPropertyOptional({ description: "Attachments", type: [MessageAttachment] })
@IsOptional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => MessageAttachment)
attachments?: MessageAttachment[];
@ApiPropertyOptional({ description: "Custom headers" })
@IsOptional()
headers?: Record<string, string>;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
@@ -0,0 +1,56 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsBoolean, IsOptional, IsString } from "class-validator";
export class SetupTOTPDto {
@ApiPropertyOptional({ description: "TOTP issuer name", example: "My Email Service" })
@IsOptional()
@IsString()
issuer?: string;
@ApiPropertyOptional({ description: "Generate fresh secret", default: false })
@IsOptional()
@IsBoolean()
fresh?: boolean;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
export class VerifyTOTPDto {
@ApiProperty({ description: "TOTP token", example: "123456" })
@IsString()
token: string;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
export class UseBackupCodeDto {
@ApiProperty({ description: "Backup code", example: "abcd1234" })
@IsString()
code: string;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
@@ -0,0 +1,129 @@
import { ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsBoolean, IsNumber, IsOptional, IsString, IsUrl } from "class-validator";
export class UpdateUserDto {
@ApiPropertyOptional({
description: "Updated display name for the user",
example: "Updated User Name",
})
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({
description: "Updated password for the user",
example: "newsecretpassword123",
})
@IsOptional()
@IsString()
password?: string;
@ApiPropertyOptional({
description: "Array of email addresses to forward all messages to",
example: ["forward1@example.com", "forward2@example.com"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
forward?: string[];
@ApiPropertyOptional({
description: "URL to upload all messages to",
example: "https://example.com/webhook",
})
@IsOptional()
@IsUrl()
targetUrl?: string;
@ApiPropertyOptional({
description: "Maximum storage in bytes allowed for this user",
example: 2147483648,
})
@IsOptional()
@IsNumber()
quota?: number;
@ApiPropertyOptional({
description: "Default retention time in milliseconds for mailboxes",
example: 2592000000,
})
@IsOptional()
@IsNumber()
retention?: number;
@ApiPropertyOptional({
description: "Language code for the user",
example: "en",
})
@IsOptional()
@IsString()
language?: string;
@ApiPropertyOptional({
description: "Maximum number of recipients allowed to send mail to in 24h",
example: 3000,
})
@IsOptional()
@IsNumber()
recipients?: number;
@ApiPropertyOptional({
description: "Maximum number of forwarded emails in 24h",
example: 3000,
})
@IsOptional()
@IsNumber()
forwards?: number;
@ApiPropertyOptional({
description: "Array of tags to associate with the user",
example: ["premium", "customer"],
})
@IsOptional()
@IsArray()
@IsString({ each: true })
tags?: string[];
@ApiPropertyOptional({
description: "PGP public key for encryption",
})
@IsOptional()
@IsString()
pubKey?: string;
@ApiPropertyOptional({
description: "Whether to encrypt stored messages",
})
@IsOptional()
@IsBoolean()
encryptMessages?: boolean;
@ApiPropertyOptional({
description: "Whether to encrypt forwarded messages",
})
@IsOptional()
@IsBoolean()
encryptForwarded?: boolean;
@ApiPropertyOptional({
description: "Current password for verification",
})
@IsOptional()
@IsString()
existingPassword?: string;
@ApiPropertyOptional({
description: "Session identifier for logging",
})
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({
description: "IP address the request was made from",
example: "192.168.1.1",
})
@IsOptional()
@IsString()
ip?: string;
}
@@ -0,0 +1,32 @@
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
import { IsArray, IsOptional, IsString, IsUrl } from "class-validator";
export class CreateWebhookDto {
@ApiProperty({
description: "Event types to trigger webhook",
example: ["messageNew", "messageDeleted"],
type: [String],
})
@IsArray()
@IsString({ each: true })
type: string[];
@ApiPropertyOptional({ description: "User ID to limit webhook to specific user" })
@IsOptional()
@IsString()
user?: string;
@ApiProperty({ description: "Webhook URL", example: "https://example.com/webhook" })
@IsUrl()
url: string;
@ApiPropertyOptional({ description: "Session identifier for logging" })
@IsOptional()
@IsString()
sess?: string;
@ApiPropertyOptional({ description: "IP address" })
@IsOptional()
@IsString()
ip?: string;
}
@@ -0,0 +1,40 @@
export interface AddressInfo {
id: string;
address: string;
main: boolean;
user?: string;
created: string;
tags?: string[];
metaData?: Record<string, any>;
internalData?: Record<string, any>;
}
export interface AddressListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: AddressInfo[];
}
export interface AddressCreateResponse {
success: true;
id: string;
}
export interface AddressDetailsResponse extends AddressInfo {
success: true;
}
export interface AddressResolveResponse {
success: true;
id: string;
address: string;
user?: string;
targets?: Array<{
id: string;
type: "relay" | "webhook" | "http";
value: string;
}>;
}
@@ -0,0 +1,18 @@
export interface ApplicationPassword {
id: string;
description: string;
scopes: string[];
lastUse?: {
time: string;
event: string;
};
created: string;
}
export interface CreateApplicationPasswordData {
description: string;
scopes?: string[] | string;
generateMobileconfig?: boolean;
sess?: string;
ip?: string;
}
@@ -0,0 +1,57 @@
export interface ArchivedMessage {
id: string;
user: string;
from: {
address: string;
name?: string;
};
to: Array<{
address: string;
name?: string;
}>;
subject: string;
messageId: string;
date: string;
size: number;
archived: string;
expires?: string;
}
export interface ArchiveListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: ArchivedMessage[];
}
export interface ArchiveDetailsResponse extends ArchivedMessage {
success: true;
headers?: Record<string, string>;
html?: string;
text?: string;
attachments?: Array<{
id: string;
filename: string;
contentType: string;
size: number;
}>;
}
export interface ArchiveRestoreResponse {
success: true;
id: string;
message: string;
restored: boolean;
}
export interface ArchiveDeleteResponse {
success: true;
deleted: number;
}
export interface ArchiveCreateResponse {
success: true;
id: string;
}
@@ -0,0 +1,14 @@
export interface ArchiveMessage {
id: string;
user: string;
mailbox: string;
message: number;
archived: string;
expires?: string;
}
export interface ArchiveMessageData {
expires?: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,42 @@
export interface AuditLogEntry {
id: string;
user?: string;
action: string;
result: "success" | "fail";
sess?: string;
ip: string;
created: string;
expires?: string;
meta?: {
protocol?: string;
method?: string;
source?: string;
userAgent?: string;
[key: string]: any;
};
}
export interface AuditListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: AuditLogEntry[];
}
export interface AuditDetailsResponse extends AuditLogEntry {
success: true;
}
export interface AuditExportResponse {
success: true;
id: string;
url: string;
expires: string;
}
export interface AuditCreateResponse {
success: true;
id: string;
}
@@ -0,0 +1,20 @@
export interface AuditEntry {
id: string;
user: string;
action: string;
result: string;
sess: string;
ip: string;
created: string;
expires: string;
meta?: any;
}
export interface CreateAuditEntryData {
user: string;
action: string;
result: "success" | "fail";
sess?: string;
ip?: string;
meta?: any;
}
@@ -0,0 +1,43 @@
export interface AuthenticateResponse {
success: boolean;
id: string;
username: string;
scope: string;
require2fa: string[] | false;
requirePasswordChange: boolean;
u2fAuthRequest?: any;
}
export interface Setup2FAResponse {
success: true;
qrcode: string;
seed?: string;
secret?: string;
}
export interface ApplicationPasswordInfo {
id: string;
description: string;
scopes: string[];
created: string;
lastUse?: {
time: string;
event: string;
};
}
export interface ApplicationPasswordListResponse {
success: true;
results: ApplicationPasswordInfo[];
}
export interface ApplicationPasswordCreateResponse {
success: true;
id: string;
password: string;
mobileconfig?: string;
}
export interface ApplicationPasswordDetailsResponse extends ApplicationPasswordInfo {
success: true;
}
@@ -0,0 +1,36 @@
export interface AutoreplyInfo {
id: string;
user: string;
status: boolean;
subject?: string;
message?: string;
text?: string;
html?: string;
start?: string;
end?: string;
created: string;
updated?: string;
}
export interface AutoreplyListResponse {
success: true;
results: AutoreplyInfo[];
}
export interface AutoreplyCreateResponse {
success: true;
id: string;
}
export interface AutoreplyDetailsResponse extends AutoreplyInfo {
success: true;
}
export interface AutoreplyStatusResponse {
success: true;
status: boolean;
subject?: string;
message?: string;
start?: string;
end?: string;
}
@@ -0,0 +1,10 @@
export interface AutoreplyData {
status?: boolean;
subject?: string;
text?: string;
html?: string;
start?: string;
end?: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,50 @@
export interface DKIMKeyInfo {
id: string;
domain: string;
selector: string;
description?: string;
fingerprint: string;
created: string;
privateKey?: string;
publicKey: string;
dnsTxt: {
name: string;
value: string;
};
}
export interface DKIMListResponse {
success: true;
results: DKIMKeyInfo[];
}
export interface DKIMCreateResponse {
success: true;
id: string;
domain: string;
selector: string;
fingerprint: string;
publicKey: string;
dnsTxt: {
name: string;
value: string;
};
}
export interface DKIMDetailsResponse extends DKIMKeyInfo {
success: true;
}
export interface DKIMResolveResponse {
success: true;
id: string;
domain: string;
selector: string;
verified: boolean;
dnsTxt?: {
name: string;
value: string;
expected: string;
actual?: string;
};
}
@@ -0,0 +1,21 @@
export interface DKIMKey {
id: string;
domain: string;
selector: string;
description: string;
fingerprint: string;
publicKey: string;
dnsTxt: {
name: string;
value: string;
};
created: string;
}
export interface CreateDKIMData {
domain: string;
selector: string;
description?: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,41 @@
export interface ExportInfo {
id: string;
user: string;
type: "mbox" | "eml" | "pst";
status: "queued" | "processing" | "completed" | "failed";
created: string;
updated?: string;
completed?: string;
downloadUrl?: string;
expires?: string;
size?: number;
messages?: number;
progress?: {
processed: number;
total: number;
percentage: number;
};
error?: string;
}
export interface ExportListResponse {
success: true;
results: ExportInfo[];
}
export interface ExportCreateResponse {
success: true;
id: string;
}
export interface ExportDetailsResponse extends ExportInfo {
success: true;
}
export interface ExportDownloadResponse {
success: true;
id: string;
downloadUrl: string;
expires: string;
size: number;
}
@@ -0,0 +1,23 @@
export interface ExportJob {
id: string;
user: string;
type: string;
status: "queued" | "processing" | "completed" | "failed";
created: string;
started?: string;
completed?: string;
progress?: {
processed: number;
total: number;
};
downloadUrl?: string;
expires?: string;
}
export interface CreateExportData {
mailbox?: string;
start?: string;
end?: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,56 @@
export interface FilterQuery {
from?: string;
to?: string;
subject?: string;
text?: string;
ha?: boolean;
size?: number;
}
export interface FilterAction {
seen?: boolean;
flag?: boolean;
delete?: boolean;
spam?: boolean;
mailbox?: string;
forward?: string;
targetUrl?: string;
}
export interface FilterInfo {
id: string;
name: string;
query: Array<[string, string]>;
action: Array<[string]>;
created: string;
disabled?: boolean;
// Query conditions
query_from?: string;
query_to?: string;
query_subject?: string;
query_text?: string;
query_ha?: boolean;
query_size?: number;
// Actions
action_seen?: boolean;
action_flag?: boolean;
action_delete?: boolean;
action_spam?: boolean;
action_mailbox?: string;
action_forward?: string;
action_targetUrl?: string;
}
export interface FilterListResponse {
success: true;
results: FilterInfo[];
}
export interface FilterCreateResponse {
success: true;
id: string;
}
export interface FilterDetailsResponse extends FilterInfo {
success: true;
}
@@ -0,0 +1,49 @@
export interface SystemHealthStatus {
success: true;
version: string;
commit: string;
node: string;
hostname: string;
processes: {
api: number;
sender: number;
imap: number;
pop3: number;
lmtp: number;
};
connections: {
imap: number;
pop3: number;
lmtp: number;
};
storage: {
database: {
type: string;
status: "ok" | "error";
latency?: number;
};
redis: {
status: "ok" | "error";
latency?: number;
};
gridfs: {
status: "ok" | "error";
latency?: number;
};
};
memory: {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
};
uptime: number;
}
export interface ServiceHealthResponse {
success: true;
service: string;
status: "ok" | "error" | "degraded";
message?: string;
details?: Record<string, any>;
}
@@ -0,0 +1,8 @@
export interface HealthStatus {
status: "ok" | "error";
version: string;
node: string;
redis: boolean;
mongo: boolean;
database: string;
}
@@ -0,0 +1,27 @@
export interface MailboxInfo {
id: string;
name: string;
path: string;
specialUse: string | null | false;
modifyIndex: number;
subscribed: boolean;
total?: number;
unseen?: number;
size?: number;
retention?: number;
hidden?: boolean;
}
export interface MailboxListResponse {
success: true;
results: MailboxInfo[];
}
export interface MailboxCreateResponse {
success: true;
id: string;
}
export interface MailboxDetailsResponse extends MailboxInfo {
success: true;
}
@@ -0,0 +1,128 @@
export interface MessageAttachment {
id: string;
filename: string;
contentType: string;
disposition: string;
transferEncoding: string;
related: boolean;
sizeKb: number;
cid?: string;
contentId?: string;
}
export interface MessageAddress {
address: string;
name?: string;
}
export interface MessageHeaders {
[key: string]: string | string[];
}
export interface MessageDetails {
id: number;
mailbox: string;
user: string;
thread: string;
from: MessageAddress;
to?: MessageAddress[];
cc?: MessageAddress[];
bcc?: MessageAddress[];
subject: string;
messageId?: string;
inReplyTo?: string;
references?: string[];
date: string;
idate?: string;
intro?: string;
attachments: boolean | MessageAttachment[];
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
html?: string[];
text?: string;
textAsHtml?: string;
size: number;
headers?: MessageHeaders;
contentType?: {
value: string;
params?: Record<string, string>;
};
envelope?: {
from: string;
to: string[];
};
bodystructure?: any;
source?: string;
}
export interface MessageListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: MessageDetails[];
}
export interface MessageUploadResponse {
success: true;
id: number;
mailbox: string;
size: number;
}
export interface MessageEventsEntry {
id: string;
action: string;
result: string;
created: string;
ip?: string;
sess?: string;
data?: Record<string, any>;
}
export interface MessageEventsResponse {
success: true;
id: string;
events: MessageEventsEntry[];
}
export interface MessageDeleteAllResponse {
success: true;
deleted: number;
}
export interface MessageForwardResponse {
success: true;
id: string;
queueId: string;
}
export interface MessageSubmitResponse {
success: true;
id: string;
queueId: string;
}
export interface MessageSearchResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: MessageDetails[];
query: string;
}
export interface MessageFlaggedResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: MessageDetails[];
}
@@ -0,0 +1,38 @@
export interface ServerConfiguration {
[key: string]: any;
// Common settings
name?: string;
title?: string;
copy?: number;
retention?: number;
uploadSentMessages?: boolean;
maxStorage?: number;
maxRecipients?: number;
maxForwards?: number;
maxReceived?: number;
maxImapConnections?: number;
imapMaxDownload?: number;
imapMaxUpload?: number;
pop3MaxDownload?: number;
smtpMaxConnections?: number;
smtpHostname?: string;
smtpPort?: number;
smtpAuth?: boolean;
smtpTLS?: boolean;
defaultQuota?: number;
indexPolicy?: "all" | "partial" | "none";
disableWelcomeMessage?: boolean;
enableSpecialUse?: boolean;
enableLogging?: boolean;
logLevel?: "silly" | "debug" | "verbose" | "info" | "warn" | "error";
}
export interface SettingsResponse extends ServerConfiguration {
success: true;
}
export interface SettingResponse {
success: true;
key: string;
value: any;
}
@@ -0,0 +1,6 @@
export interface ServerSettings {
name: string;
value: any;
description?: string;
type?: string;
}
@@ -0,0 +1,30 @@
export interface StorageInfoResponse {
success: true;
storageUsed: number;
storageAvailable: number;
attachmentStorage: number;
}
export interface UserStorageStatsResponse {
success: true;
total: number;
page: number;
results: Array<{
user: string;
username: string;
storageUsed: number;
quota: number;
}>;
}
export interface AttachmentStorageResponse {
success: true;
attachmentStorage: number;
gridfsSize: number;
}
export interface AttachmentCleanupResponse {
success: true;
deleted: number;
freed: number;
}
@@ -0,0 +1,22 @@
export interface StorageInfo {
storageUsed: number;
storageAvailable: number;
attachmentStorage: number;
}
export interface UserStorageStats {
user: string;
username: string;
storageUsed: number;
quota: number;
}
export interface AttachmentStorageInfo {
attachmentStorage: number;
gridfsSize: number;
}
export interface CleanupResult {
deleted: number;
freed: number;
}
@@ -0,0 +1,49 @@
export interface SubmissionResponse {
success: true;
id: string;
queueId: string;
from: string;
to: string[];
subject: string;
messageId: string;
created: string;
}
export interface DeliveryStatus {
id: string;
seq: string;
messageId: string;
queueId: string;
from: string;
to: string[];
subject: string;
status: "queued" | "sending" | "sent" | "deferred" | "failed";
created: string;
updated?: string;
sendTime?: string;
delivered?: number;
bounced?: number;
complaints?: number;
response?: {
code: number;
message: string;
};
}
export interface DeliveryStatusResponse extends DeliveryStatus {
success: true;
}
export interface QueueListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: DeliveryStatus[];
}
export interface QueueDeleteResponse {
success: true;
deleted: number;
}
@@ -0,0 +1,47 @@
export interface SubmissionData {
from: {
name?: string;
address: string;
};
to: Array<{
name?: string;
address: string;
}>;
cc?: Array<{
name?: string;
address: string;
}>;
bcc?: Array<{
name?: string;
address: string;
}>;
subject: string;
text?: string;
html?: string;
attachments?: Array<{
filename?: string;
contentType?: string;
content: string; // base64 encoded
}>;
headers?: Record<string, string>;
sess?: string;
ip?: string;
}
export interface QueueStatus {
id: string;
user: string;
status: string;
created: string;
updated: string;
sendingZone: string;
origin: string;
messageId: string;
envelope: {
from: string;
to: string[];
};
messageSize: number;
attempts: number;
response?: string;
}
@@ -0,0 +1,48 @@
export interface TwoFactorStatusResponse {
success: true;
enabled2fa: string[];
totp?: {
enabled: boolean;
url?: string;
label?: string;
};
u2f?: {
enabled: boolean;
keyHandle?: string;
};
}
export interface TOTPSetupResponse {
success: true;
qrcode: string;
seed: string;
secret?: string;
}
export interface U2FRegistrationStartResponse {
success: true;
requestId: string;
request: any;
}
export interface U2FAuthenticationStartResponse {
success: true;
requestId: string;
request: any;
}
export interface BackupCodesGenerateResponse {
success: true;
codes: string[];
}
export interface BackupCodesListResponse {
success: true;
codes: Array<{
id: string;
code: string;
used: boolean;
created: string;
used_date?: string;
}>;
}
@@ -0,0 +1,52 @@
export interface TwoFactorTOTP {
enabled: boolean;
issuer?: string;
seed?: string;
qrcode?: string;
}
export interface TwoFactorU2F {
enabled: boolean;
keyHandle?: string;
publicKey?: string;
}
export interface BackupCode {
code: string;
used: boolean;
created: string;
used_time?: string;
}
export interface TOTPSetupData {
issuer?: string;
fresh?: boolean;
sess?: string;
ip?: string;
}
export interface TOTPVerificationData {
token: string;
sess?: string;
ip?: string;
}
export interface U2FRegistrationData {
requestId: string;
response: any;
sess?: string;
ip?: string;
}
export interface U2FAuthenticationData {
requestId: string;
response: any;
sess?: string;
ip?: string;
}
export interface BackupCodeData {
code: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,133 @@
export interface UserCreateResponse {
success: true;
id: string;
}
export interface UserQuotaResetResponse {
success: true;
storageUsed: number;
}
export interface UserPasswordResetResponse {
success: true;
password: string;
}
export interface UserAuthLogEntry {
id: string;
action: string;
result: "success" | "fail";
sess?: string;
ip: string;
created: string;
meta?: {
source?: string;
protocol?: string;
method?: string;
};
}
export interface UserStorageInfo {
used: number;
available: number;
quota: number;
}
export interface UserLimits {
quota: {
allowed: number;
used: number;
};
recipients: {
allowed: number;
used: number;
ttl: number | false;
};
forwards: {
allowed: number;
used: number;
ttl: number | false;
};
received: {
allowed: number;
used: number;
ttl: number | false;
};
imapUpload: {
allowed: number;
used: number;
ttl: number | false;
};
imapDownload: {
allowed: number;
used: number;
ttl: number | false;
};
pop3Download: {
allowed: number;
used: number;
ttl: number | false;
};
imapMaxConnections: {
allowed: number;
used: number;
};
}
export interface UserProfile {
id: string;
username: string;
name?: string;
address: string;
language?: string;
retention?: number;
enabled2fa?: string[];
autoreply?: boolean;
encryptMessages?: boolean;
encryptForwarded?: boolean;
pubKey?: string;
metaData?: Record<string, any>;
internalData?: Record<string, any>;
hasPasswordSet: boolean;
activated: boolean;
disabled: boolean;
suspended: boolean;
tags: string[];
fromWhitelist?: string[];
disabledScopes?: string[];
lastLogin?: {
time: string;
event: string;
};
previousPassword?: {
date: string;
hash: string;
};
tempPassword?: {
created: string;
validUntil: string;
};
quota: {
allowed: number;
used: number;
};
limits: UserLimits;
targets?: Array<{
id: string;
type: "relay" | "webhook" | "http";
value: string;
}>;
}
export interface UserListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: UserProfile[];
}
export interface UserDetailsResponse extends UserProfile {
success: true;
}
@@ -0,0 +1,15 @@
export interface Webhook {
id: string;
type: string[];
user?: string;
url: string;
created: string;
}
export interface CreateWebhookData {
type: string[];
user?: string;
url: string;
sess?: string;
ip?: string;
}
@@ -0,0 +1,51 @@
export interface WebhookInfo {
id: string;
type: string[];
user?: string;
url: string;
created: string;
}
export interface WebhookListResponse {
success: true;
results: WebhookInfo[];
}
export interface WebhookCreateResponse {
success: true;
id: string;
}
export interface WebhookDetailsResponse extends WebhookInfo {
success: true;
}
export interface WebhookEventData {
[key: string]: any;
}
export interface WebhookEvent {
id: string;
seq: string;
webhook: string;
type: string;
created: string;
data: WebhookEventData;
attempts: number;
lastAttempt?: string;
nextAttempt?: string;
status: "pending" | "success" | "failed";
response?: {
status: number;
message?: string;
};
}
export interface WebhookEventListResponse {
success: true;
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: WebhookEvent[];
}
@@ -0,0 +1,153 @@
export interface WildDuckSuccessResponse<T = any> {
success: true;
data?: T;
[key: string]: any;
}
export interface WildDuckErrorResponse {
success: false;
error: string;
code?: string;
}
export interface WildDuckPaginatedResponse<T = any> extends WildDuckSuccessResponse<T> {
total: number;
page: number;
previousCursor: string | false;
nextCursor: string | false;
results: T[];
}
// export interface WildDuckUser {
// id: string;
// username: string;
// name?: string;
// address: string;
// tags: string[];
// forward: string[];
// targetUrl: string;
// encryptMessages: boolean;
// encryptForwarded: boolean;
// quota: {
// allowed: number;
// used: number;
// };
// hasPasswordSet: boolean;
// activated: boolean;
// disabled: boolean;
// retention?: number;
// enabled2fa?: string[];
// pubKey?: string;
// keyInfo?: any;
// limits?: {
// quota: {
// allowed: number;
// used: number;
// };
// recipients: {
// allowed: number;
// used: number;
// ttl: number | false;
// };
// forwards: {
// allowed: number;
// used: number;
// ttl: number | false;
// };
// };
// }
// export interface WildDuckMessage {
// id: number;
// mailbox: string;
// thread: string;
// from: {
// address: string;
// name: string;
// };
// to?: Array<{
// address: string;
// name: string;
// }>;
// cc?: Array<{
// address: string;
// name: string;
// }>;
// bcc?: Array<{
// address: string;
// name: string;
// }>;
// subject: string;
// messageId?: string;
// date: string;
// intro?: string;
// attachments: boolean | WildDuckAttachment[];
// seen: boolean;
// deleted: boolean;
// flagged: boolean;
// draft: boolean;
// html?: string[];
// text?: string;
// size?: number;
// }
// export interface WildDuckAttachment {
// id: string;
// filename: string;
// contentType: string;
// disposition: string;
// transferEncoding: string;
// related: boolean;
// sizeKb: number;
// }
// export interface WildDuckMailbox {
// id: string;
// name: string;
// path: string;
// specialUse: string | null;
// modifyIndex: number;
// subscribed: boolean;
// total?: number;
// unseen?: number;
// retention?: number;
// }
// export interface WildDuckAddress {
// id: string;
// address: string;
// main: boolean;
// user?: string;
// created: string;
// }
// export interface WildDuckFilter {
// id: string;
// name: string;
// query: Array<[string, string]>;
// action: Array<[string]>;
// created: string;
// query_from?: string;
// query_to?: string;
// query_subject?: string;
// query_text?: string;
// query_ha?: boolean;
// query_size?: number;
// action_seen?: boolean;
// action_flag?: boolean;
// action_delete?: boolean;
// action_spam?: boolean;
// action_mailbox?: string;
// action_forward?: string;
// action_targetUrl?: string;
// }
// export interface WildDuckAuthResult {
// success: boolean;
// id: string;
// username: string;
// scope: string;
// require2fa: string[] | false;
// requirePasswordChange: boolean;
// u2fAuthRequest?: any;
// }
@@ -0,0 +1,199 @@
import { Injectable, Logger } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { MailServerService } from "./services/mail-server.service";
/**
* Example service showing how to use the Mail Server Gateway
* in other modules of your NestJS application
*/
@Injectable()
export class MailServerGatewayExample {
private readonly logger = new Logger(MailServerGatewayExample.name);
constructor(private readonly mailServerService: MailServerService) {}
/**
* Example: Create a complete email account
*/
async createUserAccount(username: string, password: string, email?: string) {
try {
const result = await firstValueFrom(
this.mailServerService.createEmailAccount({
username,
password,
email,
name: username,
quota: 1024 * 1024 * 1024, // 1GB
}),
);
this.logger.log(`Created user account: ${result.user.id}`);
return result;
} catch (error) {
this.logger.error(`Failed to create user account: ${(error as Error).message}`);
throw error;
}
}
/**
* Example: Send an email through the gateway
*/
async sendWelcomeEmail(userId: string, recipientEmail: string, recipientName: string) {
try {
const result = await firstValueFrom(
this.mailServerService.sendMessage(userId, {
from: {
name: "Support Team",
address: "support@yourcompany.com",
},
to: [
{
name: recipientName,
address: recipientEmail,
},
],
subject: "Welcome to Our Service!",
text: `Hello ${recipientName},\n\nWelcome to our email service!`,
html: `<h1>Hello ${recipientName}!</h1><p>Welcome to our email service!</p>`,
}),
);
this.logger.log(`Welcome email sent: ${result.id}`);
return result;
} catch (error) {
this.logger.error(`Failed to send welcome email: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Setup 2FA for a user
*/
async enable2FAForUser(userId: string) {
try {
const result = await firstValueFrom(this.mailServerService.setup2FAForUser(userId, "Your Company"));
this.logger.log(`2FA setup completed for user: ${userId}`);
return {
qrCode: result.qrcode,
backupCodes: result.backupCodes,
};
} catch (error) {
this.logger.error(`Failed to setup 2FA: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Get user's recent emails
*/
async getUserRecentEmails(userId: string, mailboxId: string) {
try {
const messages = await firstValueFrom(this.mailServerService.getRecentMessages(userId, mailboxId, 10));
this.logger.log(`Retrieved ${messages.length} recent messages for user: ${userId}`);
return messages;
} catch (error) {
this.logger.error(`Failed to get recent emails: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Search user's emails
*/
async searchUserEmails(userId: string, searchQuery: string) {
try {
const results = await firstValueFrom(this.mailServerService.searchAllMessages(userId, searchQuery, 50));
this.logger.log(`Found ${results.length} matching messages for query: ${searchQuery}`);
return results;
} catch (error) {
this.logger.error(`Failed to search emails: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Get system health status
*/
async getSystemHealth() {
try {
const status = await firstValueFrom(this.mailServerService.getSystemStatus());
this.logger.log("Retrieved system status");
return status;
} catch (error) {
this.logger.error(`Failed to get system status: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Direct access to specific services
*/
async directServiceAccess() {
try {
// Direct access to users service
const users = await firstValueFrom(this.mailServerService.users.listUsers({ limit: 10 }));
// Direct access to health service
const health = await firstValueFrom(this.mailServerService.health.getHealthStatus());
// Direct access to storage service
const storageInfo = await firstValueFrom(this.mailServerService.storage.getStorageInfo());
// Direct access to DKIM service
const dkimKeys = await firstValueFrom(this.mailServerService.dkim.listDKIMKeys());
return {
users: users.results,
health,
storage: storageInfo,
dkimKeys: dkimKeys.results,
};
} catch (error) {
this.logger.error(`Failed to access services directly: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Setup DKIM for a domain
*/
async setupDKIMForDomain(domain: string) {
try {
const result = await firstValueFrom(this.mailServerService.createDKIMForDomain(domain));
this.logger.log(`DKIM key created for domain: ${domain}`);
this.logger.log(`Add this DNS TXT record: ${result.dnsTxt.name} = ${result.dnsTxt.value}`);
return result;
} catch (error) {
this.logger.error(`Failed to setup DKIM: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
/**
* Example: Create webhook for message events
*/
async setupMessageWebhook(webhookUrl: string, userId?: string) {
try {
const result = await firstValueFrom(
this.mailServerService.webhooks.createWebhook({
type: ["messageNew", "messageDeleted"],
url: webhookUrl,
user: userId,
}),
);
this.logger.log(`Webhook created: ${result.id}`);
return result;
} catch (error) {
this.logger.error(`Failed to create webhook: ${error instanceof Error ? error.message : String(error)}`);
throw error;
}
}
}
@@ -0,0 +1,72 @@
import { HttpModule } from "@nestjs/axios";
import { Module } from "@nestjs/common";
import { MailServerService } from "./services/mail-server.service";
import { WildDuckAddressesService } from "./services/wildduck-addresses.service";
import { WildDuckApplicationPasswordsService } from "./services/wildduck-application-passwords.service";
import { WildDuckArchiveService } from "./services/wildduck-archive.service";
import { WildDuckAuditService } from "./services/wildduck-audit.service";
import { WildDuckAuthService } from "./services/wildduck-auth.service";
import { WildDuckAutorepliesService } from "./services/wildduck-autoreplies.service";
import { WildDuckBaseService } from "./services/wildduck-base.service";
import { WildDuckDKIMService } from "./services/wildduck-dkim.service";
import { WildDuckExportService } from "./services/wildduck-export.service";
import { WildDuckFiltersService } from "./services/wildduck-filters.service";
import { WildDuckHealthService } from "./services/wildduck-health.service";
import { WildDuckMailboxesService } from "./services/wildduck-mailboxes.service";
import { WildDuckMessagesService } from "./services/wildduck-messages.service";
import { WildDuckSettingsService } from "./services/wildduck-settings.service";
import { WildDuckStorageService } from "./services/wildduck-storage.service";
import { WildDuckSubmissionService } from "./services/wildduck-submission.service";
import { WildDuckTwoFactorService } from "./services/wildduck-two-factor.service";
import { WildDuckUsersService } from "./services/wildduck-users.service";
import { WildDuckWebhooksService } from "./services/wildduck-webhooks.service";
import { wildduckConfig } from "../../configs/wildduck.config";
@Module({
imports: [HttpModule.registerAsync(wildduckConfig())],
providers: [
WildDuckBaseService,
MailServerService,
WildDuckUsersService,
WildDuckMessagesService,
WildDuckMailboxesService,
WildDuckAddressesService,
WildDuckAuthService,
WildDuckFiltersService,
WildDuckStorageService,
WildDuckSubmissionService,
WildDuckSettingsService,
WildDuckDKIMService,
WildDuckWebhooksService,
WildDuckHealthService,
WildDuckAuditService,
WildDuckArchiveService,
WildDuckApplicationPasswordsService,
WildDuckAutorepliesService,
WildDuckExportService,
WildDuckTwoFactorService,
],
exports: [
MailServerService,
WildDuckUsersService,
WildDuckMessagesService,
WildDuckMailboxesService,
WildDuckAddressesService,
WildDuckAuthService,
WildDuckFiltersService,
WildDuckStorageService,
WildDuckSubmissionService,
WildDuckSettingsService,
WildDuckDKIMService,
WildDuckWebhooksService,
WildDuckHealthService,
WildDuckAuditService,
WildDuckArchiveService,
WildDuckApplicationPasswordsService,
WildDuckAutorepliesService,
WildDuckExportService,
WildDuckTwoFactorService,
],
})
export class MailServerModule {}
@@ -0,0 +1,345 @@
import { Injectable, Logger } from "@nestjs/common";
import { Observable, catchError, map, switchMap, throwError } from "rxjs";
import { WildDuckAddressesService } from "./wildduck-addresses.service";
import { WildDuckApplicationPasswordsService } from "./wildduck-application-passwords.service";
import { WildDuckArchiveService } from "./wildduck-archive.service";
import { WildDuckAuditService } from "./wildduck-audit.service";
import { WildDuckAuthService } from "./wildduck-auth.service";
import { WildDuckAutorepliesService } from "./wildduck-autoreplies.service";
import { WildDuckDKIMService } from "./wildduck-dkim.service";
import { WildDuckExportService } from "./wildduck-export.service";
import { WildDuckFiltersService } from "./wildduck-filters.service";
import { WildDuckHealthService } from "./wildduck-health.service";
import { WildDuckMailboxesService } from "./wildduck-mailboxes.service";
import { WildDuckMessagesService } from "./wildduck-messages.service";
import { WildDuckSettingsService } from "./wildduck-settings.service";
import { WildDuckStorageService } from "./wildduck-storage.service";
import { WildDuckSubmissionService } from "./wildduck-submission.service";
import { WildDuckTwoFactorService } from "./wildduck-two-factor.service";
import { WildDuckUsersService } from "./wildduck-users.service";
import { WildDuckWebhooksService } from "./wildduck-webhooks.service";
@Injectable()
export class MailServerService {
private readonly logger = new Logger(MailServerService.name);
constructor(
private readonly usersService: WildDuckUsersService,
private readonly messagesService: WildDuckMessagesService,
private readonly mailboxesService: WildDuckMailboxesService,
private readonly addressesService: WildDuckAddressesService,
private readonly authService: WildDuckAuthService,
private readonly filtersService: WildDuckFiltersService,
private readonly storageService: WildDuckStorageService,
private readonly submissionService: WildDuckSubmissionService,
private readonly settingsService: WildDuckSettingsService,
private readonly dkimService: WildDuckDKIMService,
private readonly webhooksService: WildDuckWebhooksService,
private readonly healthService: WildDuckHealthService,
private readonly auditService: WildDuckAuditService,
private readonly archiveService: WildDuckArchiveService,
private readonly applicationPasswordsService: WildDuckApplicationPasswordsService,
private readonly autorepliesService: WildDuckAutorepliesService,
private readonly exportService: WildDuckExportService,
private readonly twoFactorService: WildDuckTwoFactorService,
) {}
// Expose individual services for direct access
get users() {
return this.usersService;
}
get messages() {
return this.messagesService;
}
get mailboxes() {
return this.mailboxesService;
}
get addresses() {
return this.addressesService;
}
get auth() {
return this.authService;
}
get filters() {
return this.filtersService;
}
get storage() {
return this.storageService;
}
get submission() {
return this.submissionService;
}
get settings() {
return this.settingsService;
}
get dkim() {
return this.dkimService;
}
get webhooks() {
return this.webhooksService;
}
get health() {
return this.healthService;
}
get audit() {
return this.auditService;
}
get archive() {
return this.archiveService;
}
get applicationPasswords() {
return this.applicationPasswordsService;
}
get autoreplies() {
return this.autorepliesService;
}
get export() {
return this.exportService;
}
get twoFactor() {
return this.twoFactorService;
}
/**
* High-level method to create a complete email account
*/
createEmailAccount(data: { username: string; password: string; email?: string; name?: string; quota?: number }): Observable<{
user: { id: string };
address?: { id: string };
}> {
this.logger.log(`Creating email account for user: ${data.username}`);
return this.usersService
.createUser({
username: data.username,
password: data.password,
address: data.email,
name: data.name,
quota: data.quota,
})
.pipe(
map((userResult) => {
this.logger.log(`User created with ID: ${userResult.id}`);
return { user: { id: userResult.id } };
}),
catchError((error) => {
this.logger.error(`Failed to create email account: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to get user's full mailbox structure with message counts
*/
getUserMailboxes(userId: string): Observable<any> {
this.logger.log(`Getting mailboxes for user: ${userId}`);
return this.mailboxesService.listMailboxes(userId, { counters: true }).pipe(
map((result) => {
this.logger.log(`Found ${result.results.length} mailboxes for user: ${userId}`);
return result.results;
}),
catchError((error) => {
this.logger.error(`Failed to get mailboxes: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to authenticate user and get account info
*/
authenticateAndGetUser(
username: string,
password: string,
): Observable<{
auth: any;
user: any;
}> {
this.logger.log(`Authenticating user: ${username}`);
return this.authService.authenticate({ username, password }).pipe(
switchMap((authResult) => {
if (!authResult.success) {
throw new Error("Authentication failed");
}
this.logger.log(`User authenticated: ${authResult.id}`);
// Get user details after successful authentication
return this.usersService.getUser(authResult.id).pipe(
map((userDetails) => ({
auth: authResult,
user: userDetails,
})),
);
}),
catchError((error) => {
this.logger.error(`Authentication failed: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to send a message
*/
sendMessage(
userId: string,
messageData: {
from: { name?: string; address: string };
to: Array<{ name?: string; address: string }>;
subject: string;
text?: string;
html?: string;
},
): Observable<{ id: string; queueId: string }> {
this.logger.log(`Sending message for user: ${userId}`);
return this.submissionService.submitMessage(userId, messageData).pipe(
map((result) => {
this.logger.log(`Message submitted with ID: ${result.messageId}`);
return { id: result.messageId, queueId: result.queueId };
}),
catchError((error) => {
this.logger.error(`Failed to send message: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to get user's recent messages
*/
getRecentMessages(userId: string, mailboxId: string, limit: number = 20): Observable<any[]> {
this.logger.log(`Getting recent messages for user: ${userId}`);
return this.messagesService.listMessages(userId, mailboxId, { limit, order: "desc" }).pipe(
map((result) => {
this.logger.log(`Found ${result.results.length} messages`);
return result.results;
}),
catchError((error) => {
this.logger.error(`Failed to get messages: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to search across all user's messages
*/
searchAllMessages(userId: string, searchQuery: string, limit: number = 50): Observable<any[]> {
this.logger.log(`Searching messages for user: ${userId}, query: ${searchQuery}`);
return this.messagesService.searchMessages(userId, { query: searchQuery, limit }).pipe(
map((result) => {
this.logger.log(`Found ${result.results.length} matching messages`);
return result.results;
}),
catchError((error) => {
this.logger.error(`Search failed: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to check server health and get system status
*/
getSystemStatus(): Observable<{
health: any;
storage: any;
}> {
this.logger.log("Getting system status");
return this.healthService.getHealthStatus().pipe(
switchMap((healthStatus) => {
return this.storageService.getStorageInfo().pipe(
map((storageInfo) => ({
health: healthStatus,
storage: storageInfo,
})),
);
}),
catchError((error) => {
this.logger.error(`Failed to get system status: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to setup complete 2FA for user
*/
setup2FAForUser(
userId: string,
issuer?: string,
): Observable<{
qrcode: string;
backupCodes: string[];
}> {
this.logger.log(`Setting up 2FA for user: ${userId}`);
return this.twoFactorService.setupTOTP(userId, { issuer }).pipe(
switchMap((totpResult) => {
return this.twoFactorService.generateBackupCodes(userId).pipe(
map((backupResult) => ({
qrcode: totpResult.qrcode,
backupCodes: backupResult.codes,
})),
);
}),
catchError((error) => {
this.logger.error(`Failed to setup 2FA: ${error.message}`);
return throwError(() => error);
}),
);
}
/**
* High-level method to create DKIM key and get DNS settings
*/
createDKIMForDomain(
domain: string,
selector: string = "default",
): Observable<{
id: string;
dnsTxt: { name: string; value: string };
}> {
this.logger.log(`Creating DKIM key for domain: ${domain}`);
return this.dkimService.createDKIMKey({ domain, selector, description: `DKIM key for ${domain}` }).pipe(
map((result) => {
this.logger.log(`DKIM key created for domain: ${domain}`);
return {
id: result.id,
dnsTxt: result.dnsTxt,
};
}),
catchError((error) => {
this.logger.error(`Failed to create DKIM key: ${error.message}`);
return throwError(() => error);
}),
);
}
}
@@ -0,0 +1,52 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { CreateAddressDto, UpdateAddressDto } from "../DTO/address.dto";
import { AddressCreateResponse, AddressDetailsResponse, AddressListResponse } from "../interfaces/addresses-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckAddressesService extends WildDuckBaseService {
/**
* Search and list addresses across all users
*/
searchAddresses(params?: { query?: string; limit?: number }): Observable<AddressListResponse> {
return this.get<AddressListResponse>("/addresses", params);
}
/**
* List user addresses
*/
listUserAddresses(userId: string): Observable<AddressListResponse> {
return this.get<AddressListResponse>(`/users/${userId}/addresses`);
}
/**
* Get one address details
*/
getUserAddress(userId: string, addressId: string): Observable<AddressDetailsResponse> {
return this.get<AddressDetailsResponse>(`/users/${userId}/addresses/${addressId}`);
}
/**
* Add a new address/alias
*/
createUserAddress(userId: string, data: CreateAddressDto): Observable<AddressCreateResponse> {
return this.post<AddressCreateResponse>(`/users/${userId}/addresses`, data);
}
/**
* Update address details (mainly for setting as main address)
*/
updateUserAddress(userId: string, addressId: string, data: UpdateAddressDto): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/addresses/${addressId}`, data);
}
/**
* Delete an alias address
*/
deleteUserAddress(userId: string, addressId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/addresses/${addressId}`);
}
}
@@ -0,0 +1,42 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { CreateApplicationPasswordData } from "../interfaces/application-passwords.interface";
import {
ApplicationPasswordCreateResponse,
ApplicationPasswordDetailsResponse,
ApplicationPasswordListResponse,
} from "../interfaces/auth-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckApplicationPasswordsService extends WildDuckBaseService {
/**
* List application specific passwords for a user
*/
listApplicationPasswords(userId: string): Observable<ApplicationPasswordListResponse> {
return this.get<ApplicationPasswordListResponse>(`/users/${userId}/asps`);
}
/**
* Get application specific password details
*/
getApplicationPassword(userId: string, aspId: string): Observable<ApplicationPasswordDetailsResponse> {
return this.get<ApplicationPasswordDetailsResponse>(`/users/${userId}/asps/${aspId}`);
}
/**
* Create application specific password
*/
createApplicationPassword(userId: string, data: CreateApplicationPasswordData): Observable<ApplicationPasswordCreateResponse> {
return this.post<ApplicationPasswordCreateResponse>(`/users/${userId}/asps`, data);
}
/**
* Delete application specific password
*/
deleteApplicationPassword(userId: string, aspId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/asps/${aspId}`, params);
}
}
@@ -0,0 +1,30 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { ArchiveCreateResponse, ArchiveListResponse, ArchiveRestoreResponse } from "../interfaces/archive-response.interface";
import { ArchiveMessageData } from "../interfaces/archive.interface";
@Injectable()
export class WildDuckArchiveService extends WildDuckBaseService {
/**
* List archived messages
*/
listArchivedMessages(params?: { user?: string; limit?: number; page?: number }): Observable<ArchiveListResponse> {
return this.get<ArchiveListResponse>("/archive", params);
}
/**
* Archive a message
*/
archiveMessage(userId: string, mailboxId: string, messageId: number, data?: ArchiveMessageData): Observable<ArchiveCreateResponse> {
return this.post<ArchiveCreateResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/archive`, data);
}
/**
* Restore archived message
*/
restoreMessage(archiveId: string): Observable<ArchiveRestoreResponse> {
return this.post<ArchiveRestoreResponse>(`/archive/${archiveId}/restore`);
}
}
@@ -0,0 +1,37 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { AuditCreateResponse, AuditDetailsResponse, AuditListResponse } from "../interfaces/audit-response.interface";
import { CreateAuditEntryData } from "../interfaces/audit.interface";
@Injectable()
export class WildDuckAuditService extends WildDuckBaseService {
/**
* List audit entries
*/
listAuditEntries(params?: {
user?: string;
action?: string;
limit?: number;
page?: number;
sess?: string;
ip?: string;
}): Observable<AuditListResponse> {
return this.get<AuditListResponse>("/audit", params);
}
/**
* Get audit entry details
*/
getAuditEntry(auditId: string): Observable<AuditDetailsResponse> {
return this.get<AuditDetailsResponse>(`/audit/${auditId}`);
}
/**
* Create audit entry
*/
createAuditEntry(data: CreateAuditEntryData): Observable<AuditCreateResponse> {
return this.post<AuditCreateResponse>("/audit", data);
}
}
@@ -0,0 +1,109 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { AuthenticateDto } from "../DTO/authenticate.dto";
import {
ApplicationPasswordCreateResponse,
ApplicationPasswordListResponse,
AuthenticateResponse,
Setup2FAResponse,
} from "../interfaces/auth-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckAuthService extends WildDuckBaseService {
/**
* Authenticate a user
*/
authenticate(data: AuthenticateDto): Observable<AuthenticateResponse> {
return this.post<AuthenticateResponse>("/authenticate", data);
}
/**
* Setup 2FA (TOTP)
*/
setup2FA(
userId: string,
data: {
issuer?: string;
fresh?: boolean;
sess?: string;
ip?: string;
},
): Observable<Setup2FAResponse> {
return this.post<Setup2FAResponse>(`/users/${userId}/2fa/totp/setup`, data);
}
/**
* Enable 2FA (verify TOTP token)
*/
enable2FA(
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/enable`, data);
}
/**
* Check/verify 2FA token
*/
check2FA(
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/check`, data);
}
/**
* Disable TOTP 2FA
*/
disableTOTP(userId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp`, params);
}
/**
* Disable all 2FA
*/
disable2FA(userId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa`, params);
}
/**
* List application specific passwords
*/
listASPs(userId: string): Observable<ApplicationPasswordListResponse> {
return this.get<ApplicationPasswordListResponse>(`/users/${userId}/asps`);
}
/**
* Create application specific password
*/
createASP(
userId: string,
data: {
description: string;
scopes?: string[] | string;
generateMobileconfig?: boolean;
sess?: string;
ip?: string;
},
): Observable<ApplicationPasswordCreateResponse> {
return this.post<ApplicationPasswordCreateResponse>(`/users/${userId}/asps`, data);
}
/**
* Delete application specific password
*/
deleteASP(userId: string, aspId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/asps/${aspId}`, params);
}
}
@@ -0,0 +1,31 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { AutoreplyStatusResponse } from "../interfaces/autoreplies-response.interface";
import { AutoreplyData } from "../interfaces/autoreplies.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckAutorepliesService extends WildDuckBaseService {
/**
* Get autoreply settings for a user
*/
getAutoreply(userId: string): Observable<AutoreplyStatusResponse> {
return this.get<AutoreplyStatusResponse>(`/users/${userId}/autoreply`);
}
/**
* Set up or update autoreply for a user
*/
updateAutoreply(userId: string, data: AutoreplyData): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/autoreply`, data);
}
/**
* Delete/disable autoreply for a user
*/
deleteAutoreply(userId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/autoreply`, params);
}
}
@@ -0,0 +1,93 @@
import { HttpService } from "@nestjs/axios";
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { AxiosResponse } from "axios";
import { Observable, catchError, map, throwError } from "rxjs";
import { WildDuckErrorResponse, WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckBaseService {
protected readonly logger = new Logger(this.constructor.name);
protected readonly baseUrl: string;
protected readonly accessToken?: string;
constructor(
protected readonly httpService: HttpService,
protected readonly configService: ConfigService,
) {
this.baseUrl = this.configService.get<string>("WILDDUCK_BASE_URL", "http://localhost:8080");
this.accessToken = this.configService.get<string>("WILDDUCK_ACCESS_TOKEN");
}
protected buildUrl(path: string, params?: Record<string, any>): string {
const url = new URL(path, this.baseUrl);
// Add access token if configured
if (this.accessToken) {
url.searchParams.set("accessToken", this.accessToken);
}
// Add other parameters
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
});
}
return url.toString();
}
protected handleResponse<T>(observable: Observable<AxiosResponse<WildDuckSuccessResponse<T> | WildDuckErrorResponse>>): Observable<T> {
return observable.pipe(
map((response) => {
this.logger.debug(`HTTP ${response.status}: ${response.config?.url}`);
const data = response.data;
if ("success" in data && data.success === false) {
throw new Error(data.error || "WildDuck API error");
}
if ("success" in data && data.success === true) {
// Return the entire response data for paginated responses
// or extract specific data field for simple responses
return data as T;
}
return response.data as T;
}),
catchError((error) => {
this.logger.error(`WildDuck API error: ${error.message}`, error.stack);
if (error.response?.data?.error) {
return throwError(() => new Error(error.response.data.error));
}
return throwError(() => error);
}),
);
}
protected get<T>(path: string, params?: Record<string, any>): Observable<T> {
const url = this.buildUrl(path, params);
return this.handleResponse<T>(this.httpService.get(url));
}
protected post<T>(path: string, data?: any, params?: Record<string, any>): Observable<T> {
const url = this.buildUrl(path, params);
return this.handleResponse<T>(this.httpService.post(url, data));
}
protected put<T>(path: string, data?: any, params?: Record<string, any>): Observable<T> {
const url = this.buildUrl(path, params);
return this.handleResponse<T>(this.httpService.put(url, data));
}
protected delete<T>(path: string, params?: Record<string, any>): Observable<T> {
const url = this.buildUrl(path, params);
return this.handleResponse<T>(this.httpService.delete(url));
}
}
@@ -0,0 +1,45 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { DKIMCreateResponse, DKIMDetailsResponse, DKIMListResponse, DKIMResolveResponse } from "../interfaces/dkim-response.interface";
import { CreateDKIMData } from "../interfaces/dkim.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckDKIMService extends WildDuckBaseService {
/**
* List DKIM keys
*/
listDKIMKeys(params?: { domain?: string; limit?: number; page?: number }): Observable<DKIMListResponse> {
return this.get<DKIMListResponse>("/dkim", params);
}
/**
* Get DKIM key details
*/
getDKIMKey(keyId: string): Observable<DKIMDetailsResponse> {
return this.get<DKIMDetailsResponse>(`/dkim/${keyId}`);
}
/**
* Create new DKIM key
*/
createDKIMKey(data: CreateDKIMData): Observable<DKIMCreateResponse> {
return this.post<DKIMCreateResponse>("/dkim", data);
}
/**
* Delete DKIM key
*/
deleteDKIMKey(keyId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/dkim/${keyId}`);
}
/**
* Get DKIM key by domain and selector
*/
getDKIMKeyByDomain(domain: string, selector: string): Observable<DKIMResolveResponse> {
return this.get<DKIMResolveResponse>(`/dkim/resolve/${domain}/${selector}`);
}
}
@@ -0,0 +1,38 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { ExportCreateResponse, ExportDetailsResponse, ExportDownloadResponse } from "../interfaces/export-response.interface";
import { CreateExportData } from "../interfaces/export.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckExportService extends WildDuckBaseService {
/**
* Create export job for user data
*/
createExport(userId: string, data?: CreateExportData): Observable<ExportCreateResponse> {
return this.post<ExportCreateResponse>(`/users/${userId}/export`, data);
}
/**
* Get export job status
*/
getExportStatus(userId: string, exportId: string): Observable<ExportDetailsResponse> {
return this.get<ExportDetailsResponse>(`/users/${userId}/export/${exportId}`);
}
/**
* Download export file
*/
downloadExport(userId: string, exportId: string): Observable<ExportDownloadResponse> {
return this.get<ExportDownloadResponse>(`/users/${userId}/export/${exportId}/download`);
}
/**
* Delete export job
*/
deleteExport(userId: string, exportId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/export/${exportId}`);
}
}
@@ -0,0 +1,45 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { CreateFilterDto, UpdateFilterDto } from "../DTO/filter.dto";
import { FilterCreateResponse, FilterDetailsResponse, FilterListResponse } from "../interfaces/filters-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckFiltersService extends WildDuckBaseService {
/**
* List existing filters for a user
*/
listFilters(userId: string): Observable<FilterListResponse> {
return this.get<FilterListResponse>(`/users/${userId}/filters`);
}
/**
* Get filter details
*/
getFilter(userId: string, filterId: string): Observable<FilterDetailsResponse> {
return this.get<FilterDetailsResponse>(`/users/${userId}/filters/${filterId}`);
}
/**
* Create a new filter
*/
createFilter(userId: string, data: CreateFilterDto): Observable<FilterCreateResponse> {
return this.post<FilterCreateResponse>(`/users/${userId}/filters`, data);
}
/**
* Update filter details
*/
updateFilter(userId: string, filterId: string, data: UpdateFilterDto): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/filters/${filterId}`, data);
}
/**
* Delete a filter
*/
deleteFilter(userId: string, filterId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/filters/${filterId}`);
}
}
@@ -0,0 +1,15 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { SystemHealthStatus } from "../interfaces/health-response.interface";
@Injectable()
export class WildDuckHealthService extends WildDuckBaseService {
/**
* Check server health status
*/
getHealthStatus(): Observable<SystemHealthStatus> {
return this.get<SystemHealthStatus>("/health");
}
}
@@ -0,0 +1,45 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { CreateMailboxDto, ListMailboxesQueryDto, UpdateMailboxDto } from "../DTO/mailbox.dto";
import { MailboxCreateResponse, MailboxDetailsResponse, MailboxListResponse } from "../interfaces/mailboxes-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckMailboxesService extends WildDuckBaseService {
/**
* List existing mailboxes for a user
*/
listMailboxes(userId: string, query?: ListMailboxesQueryDto): Observable<MailboxListResponse> {
return this.get<MailboxListResponse>(`/users/${userId}/mailboxes`, query);
}
/**
* Get one mailbox details
*/
getMailbox(userId: string, mailboxId: string): Observable<MailboxDetailsResponse> {
return this.get<MailboxDetailsResponse>(`/users/${userId}/mailboxes/${mailboxId}`);
}
/**
* Create a new mailbox
*/
createMailbox(userId: string, data: CreateMailboxDto): Observable<MailboxCreateResponse> {
return this.post<MailboxCreateResponse>(`/users/${userId}/mailboxes`, data);
}
/**
* Update mailbox details
*/
updateMailbox(userId: string, mailboxId: string, data: UpdateMailboxDto): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/mailboxes/${mailboxId}`, data);
}
/**
* Delete a mailbox
*/
deleteMailbox(userId: string, mailboxId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/mailboxes/${mailboxId}`);
}
}
@@ -0,0 +1,132 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { ListMessagesQueryDto, SearchMessagesQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto";
import {
MessageDeleteAllResponse,
MessageDetails,
MessageEventsResponse,
MessageFlaggedResponse,
MessageForwardResponse,
MessageListResponse,
MessageSearchResponse,
MessageSubmitResponse,
MessageUploadResponse,
} from "../interfaces/messages-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckMessagesService extends WildDuckBaseService {
/**
* List messages in a mailbox
*/
listMessages(userId: string, mailboxId: string, query?: ListMessagesQueryDto): Observable<MessageListResponse> {
return this.get<MessageListResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages`, query);
}
/**
* Get message details
*/
getMessage(userId: string, mailboxId: string, messageId: number): Observable<MessageDetails> {
return this.get<MessageDetails>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}`);
}
/**
* Update message details
*/
updateMessage(userId: string, mailboxId: string, messageId: number | string, data: UpdateMessageDto): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}`, data);
}
/**
* Delete a message
*/
deleteMessage(userId: string, mailboxId: string, messageId: number): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}`);
}
/**
* Upload message to mailbox
*/
uploadMessage(userId: string, mailboxId: string, data: UploadMessageDto): Observable<MessageUploadResponse> {
return this.post<MessageUploadResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages`, data);
}
/**
* Get message source (raw email)
*/
getMessageSource(userId: string, mailboxId: string, messageId: number): Observable<string> {
// This returns raw email content, not JSON
return this.get<string>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/message.eml`);
}
/**
* Get message attachment
*/
getMessageAttachment(userId: string, mailboxId: string, messageId: number, attachmentId: string): Observable<any> {
// This returns binary content
return this.get<any>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/attachments/${attachmentId}`);
}
/**
* Get message events/timeline
*/
getMessageEvents(userId: string, mailboxId: string, messageId: number): Observable<MessageEventsResponse> {
return this.get<MessageEventsResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/events`);
}
/**
* List flagged messages across all mailboxes
*/
listFlaggedMessages(userId: string, query?: { order?: "asc" | "desc"; limit?: number; page?: number }): Observable<MessageFlaggedResponse> {
return this.get<MessageFlaggedResponse>(`/users/${userId}/flagged`, query);
}
/**
* Search for messages across all mailboxes
*/
searchMessages(userId: string, query: SearchMessagesQueryDto): Observable<MessageSearchResponse> {
return this.get<MessageSearchResponse>(`/users/${userId}/search`, query);
}
/**
* Delete all messages from a mailbox
*/
deleteAllMessages(userId: string, mailboxId: string): Observable<MessageDeleteAllResponse> {
return this.delete<MessageDeleteAllResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages`);
}
/**
* Forward stored message
*/
forwardMessage(
userId: string,
mailboxId: string,
messageId: number,
data: {
target: string[];
from?: string;
sessId?: string;
ip?: string;
},
): Observable<MessageForwardResponse> {
return this.post<MessageForwardResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/forward`, data);
}
/**
* Submit draft for delivery
*/
submitDraft(
userId: string,
mailboxId: string,
messageId: number,
data?: {
deleteFiles?: boolean;
sess?: string;
ip?: string;
},
): Observable<MessageSubmitResponse> {
return this.post<MessageSubmitResponse>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/submit`, data);
}
}
@@ -0,0 +1,30 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { SettingResponse, SettingsResponse } from "../interfaces/settings-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckSettingsService extends WildDuckBaseService {
/**
* Get all server settings
*/
getAllSettings(): Observable<SettingsResponse> {
return this.get<SettingsResponse>("/settings");
}
/**
* Get specific setting by key
*/
getSetting(key: string): Observable<SettingResponse> {
return this.get<SettingResponse>(`/settings/${key}`);
}
/**
* Set server setting value
*/
setSetting(key: string, value: any): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/settings/${key}`, { value });
}
}
@@ -0,0 +1,41 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import {
AttachmentCleanupResponse,
AttachmentStorageResponse,
StorageInfoResponse,
UserStorageStatsResponse,
} from "../interfaces/storage-response.interface";
@Injectable()
export class WildDuckStorageService extends WildDuckBaseService {
/**
* Get storage information
*/
getStorageInfo(): Observable<StorageInfoResponse> {
return this.get<StorageInfoResponse>("/storage");
}
/**
* Get storage statistics by user
*/
getUserStorageStats(params?: { limit?: number; page?: number; query?: string }): Observable<UserStorageStatsResponse> {
return this.get<UserStorageStatsResponse>("/storage/users", params);
}
/**
* Get attachment storage information
*/
getAttachmentStorage(): Observable<AttachmentStorageResponse> {
return this.get<AttachmentStorageResponse>("/storage/attachments");
}
/**
* Clean up orphaned attachments
*/
cleanupAttachments(): Observable<AttachmentCleanupResponse> {
return this.post<AttachmentCleanupResponse>("/storage/attachments/cleanup");
}
}
@@ -0,0 +1,23 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { DeliveryStatusResponse, SubmissionResponse } from "../interfaces/submission-response.interface";
import { SubmissionData } from "../interfaces/submission.interface";
@Injectable()
export class WildDuckSubmissionService extends WildDuckBaseService {
/**
* Submit a message for delivery
*/
submitMessage(userId: string, data: SubmissionData): Observable<SubmissionResponse> {
return this.post<SubmissionResponse>(`/users/${userId}/submit`, data);
}
/**
* Get submission queue status
*/
getQueueStatus(queueId: string): Observable<DeliveryStatusResponse> {
return this.get<DeliveryStatusResponse>(`/submission/${queueId}`);
}
}
@@ -0,0 +1,216 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import {
BackupCodesGenerateResponse,
BackupCodesListResponse,
TOTPSetupResponse,
TwoFactorStatusResponse,
} from "../interfaces/two-factor-response.interface";
import { WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckTwoFactorService extends WildDuckBaseService {
/**
* Get 2FA status for user
*/
get2FAStatus(userId: string): Observable<TwoFactorStatusResponse> {
return this.get<TwoFactorStatusResponse>(`/users/${userId}/2fa`);
}
// TOTP Methods
/**
* Setup TOTP 2FA
*/
setupTOTP(
userId: string,
data?: {
issuer?: string;
fresh?: boolean;
sess?: string;
ip?: string;
},
): Observable<TOTPSetupResponse> {
return this.post<TOTPSetupResponse>(`/users/${userId}/2fa/totp/setup`, data);
}
/**
* Enable TOTP 2FA
*/
enableTOTP(
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/enable`, data);
}
/**
* Check TOTP token
*/
checkTOTP(
userId: string,
data: {
token: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp/check`, data);
}
/**
* Disable TOTP 2FA
*/
disableTOTP(
userId: string,
params?: {
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa/totp`, params);
}
// U2F Methods
/**
* Start U2F registration
*/
startU2FRegistration(
userId: string,
data?: {
sess?: string;
ip?: string;
},
): Observable<
WildDuckSuccessResponse<{
requestId: string;
request: any;
}>
> {
return this.post<
WildDuckSuccessResponse<{
requestId: string;
request: any;
}>
>(`/users/${userId}/2fa/u2f/start`, data);
}
/**
* Complete U2F registration
*/
completeU2FRegistration(
userId: string,
data: {
requestId: string;
response: any;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/enable`, data);
}
/**
* Start U2F authentication
*/
startU2FAuthentication(
userId: string,
data?: {
sess?: string;
ip?: string;
},
): Observable<
WildDuckSuccessResponse<{
requestId: string;
request: any;
}>
> {
return this.post<
WildDuckSuccessResponse<{
requestId: string;
request: any;
}>
>(`/users/${userId}/2fa/u2f/start-authenticate`, data);
}
/**
* Complete U2F authentication
*/
completeU2FAuthentication(
userId: string,
data: {
requestId: string;
response: any;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f/check`, data);
}
/**
* Disable U2F 2FA
*/
disableU2F(
userId: string,
params?: {
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa/u2f`, params);
}
// Backup Codes
/**
* Generate backup codes
*/
generateBackupCodes(
userId: string,
data?: {
sess?: string;
ip?: string;
},
): Observable<BackupCodesGenerateResponse> {
return this.post<BackupCodesGenerateResponse>(`/users/${userId}/2fa/backup/generate`, data);
}
/**
* List backup codes
*/
listBackupCodes(userId: string): Observable<BackupCodesListResponse> {
return this.get<BackupCodesListResponse>(`/users/${userId}/2fa/backup`);
}
/**
* Use backup code
*/
useBackupCode(
userId: string,
data: {
code: string;
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.post<WildDuckSuccessResponse>(`/users/${userId}/2fa/backup/use`, data);
}
/**
* Disable all 2FA methods
*/
disableAll2FA(
userId: string,
params?: {
sess?: string;
ip?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/2fa`, params);
}
}
@@ -0,0 +1,122 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { WildDuckBaseService } from "./wildduck-base.service";
import { CreateUserDto } from "../DTO/create-user.dto";
import { UpdateUserDto } from "../DTO/update-user.dto";
import { AutoreplyStatusResponse } from "../interfaces/autoreplies-response.interface";
import {
UserAuthLogEntry,
UserCreateResponse,
UserDetailsResponse,
UserListResponse,
UserPasswordResetResponse,
UserQuotaResetResponse,
} from "../interfaces/users-response.interface";
import { WildDuckPaginatedResponse, WildDuckSuccessResponse } from "../interfaces/wildduck-response.interface";
@Injectable()
export class WildDuckUsersService extends WildDuckBaseService {
/**
* Search and list users
*/
listUsers(params?: { query?: string; limit?: number; tags?: string; requiredTags?: string; page?: number }): Observable<UserListResponse> {
return this.get<UserListResponse>("/users", params);
}
/**
* Get one user by ID
*/
getUser(userId: string): Observable<UserDetailsResponse> {
return this.get<UserDetailsResponse>(`/users/${userId}`);
}
/**
* Create a new user
*/
createUser(userData: CreateUserDto): Observable<UserCreateResponse> {
return this.post<UserCreateResponse>("/users", userData);
}
/**
* Update user details
*/
updateUser(userId: string, userData: UpdateUserDto): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}`, userData);
}
/**
* Delete user
*/
deleteUser(userId: string, params?: { sess?: string; ip?: string }): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}`, params);
}
/**
* Log out user from all IMAP sessions
*/
logoutUser(userId: string, data?: { reason?: string }): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/logout`, data);
}
/**
* Reset user password
*/
resetUserPassword(userId: string, params?: { sess?: string; ip?: string }): Observable<UserPasswordResetResponse> {
return this.post<UserPasswordResetResponse>(`/users/${userId}/password/reset`, {}, params);
}
/**
* Get user authentication log
*/
getUserAuthLog(
userId: string,
params?: {
limit?: number;
action?: string;
sess?: string;
ip?: string;
page?: number;
},
): Observable<WildDuckPaginatedResponse<UserAuthLogEntry>> {
return this.get<WildDuckPaginatedResponse<UserAuthLogEntry>>(`/users/${userId}/authlog`, params);
}
/**
* Recalculate user quota
*/
resetUserQuota(userId: string): Observable<UserQuotaResetResponse> {
return this.post<UserQuotaResetResponse>(`/users/${userId}/quota/reset`, {});
}
/**
* Setup autoreply
*/
setupAutoreply(
userId: string,
data: {
status?: boolean;
subject?: string;
text?: string;
html?: string;
start?: string;
end?: string;
},
): Observable<WildDuckSuccessResponse> {
return this.put<WildDuckSuccessResponse>(`/users/${userId}/autoreply`, data);
}
/**
* Disable autoreply
*/
disableAutoreply(userId: string): Observable<WildDuckSuccessResponse> {
return this.delete<WildDuckSuccessResponse>(`/users/${userId}/autoreply`);
}
/**
* Check autoreply status
*/
getAutoreplyStatus(userId: string): Observable<AutoreplyStatusResponse> {
return this.get<AutoreplyStatusResponse>(`/users/${userId}/autoreply`);
}
}

Some files were not shown because too many files have changed in this diff Show More