chore: first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Executable
+9
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Executable
+23
@@ -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 {}
|
||||
Executable
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+28
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+27
@@ -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
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Executable
+141
@@ -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
@@ -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 {}
|
||||
@@ -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) {}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user