Files
dmail-api/src/modules/domains/services/domain-automation.service.ts
T
2025-07-12 10:08:06 +03:30

215 lines
8.0 KiB
TypeScript

import { EntityManager } from "@mikro-orm/core";
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { firstValueFrom } from "rxjs";
import { BusinessMessage, MailServerMessage, RoleMessage } from "../../../common/enums/message.enum";
import { QUOTA_CONSTANTS } from "../../businesses/constant";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
import { Role } from "../../users/entities/role.entity";
import { User } from "../../users/entities/user.entity";
import { RoleEnum } from "../../users/enums/role.enum";
import { PasswordService } from "../../utils/services/password.service";
import { Domain } from "../entities/domain.entity";
@Injectable()
export class DomainAutomationService {
private readonly logger = new Logger(DomainAutomationService.name);
constructor(
private readonly em: EntityManager,
private readonly mailServerService: MailServerService,
private readonly passwordService: PasswordService,
) {}
/**
* Check if a user exists in WildDuck by username or email address
*/
private async checkUserExistsInWildDuck(username: string, email: string): Promise<boolean> {
try {
// First try to search by username
const usersByUsername = await firstValueFrom(this.mailServerService.users.listUsers({ query: username, limit: 10 }));
// Check if any user has the exact username
const userWithUsername = usersByUsername.results.find((user) => user.username === username);
if (userWithUsername) {
this.logger.debug(`User with username "${username}" already exists in WildDuck`);
return true;
}
// Then search by email address
const addressSearch = await firstValueFrom(this.mailServerService.addresses.searchAddresses({ query: email, limit: 10 }));
// Check if the exact email address exists
const addressExists = addressSearch.results.find((addr) => addr.address === email);
if (addressExists) {
this.logger.debug(`Email address "${email}" already exists in WildDuck`);
return true;
}
return false;
} catch (error) {
this.logger.warn(`Failed to check user existence in WildDuck: ${(error as Error).message}`);
// If we can't check, assume user doesn't exist to avoid blocking domain verification
return false;
}
}
/**
* Create default mailboxes for a user (Archive and Favorite)
*/
async createDefaultMailboxes(wildduckUserId: string): Promise<void> {
try {
const mailboxesToCreate = [
{ path: MailboxEnum.ARCHIVE, name: "Archive" },
{ path: MailboxEnum.FAVORITE, name: "Favorite" },
];
for (const mailbox of mailboxesToCreate) {
try {
await firstValueFrom(
this.mailServerService.mailboxes.createMailbox(wildduckUserId, {
path: mailbox.path,
}),
);
this.logger.log(`Created ${mailbox.name} mailbox for user: ${wildduckUserId}`);
} catch (error) {
this.logger.warn(`Failed to create ${mailbox.name} mailbox for user ${wildduckUserId}: ${(error as Error).message}`);
}
}
} catch (error) {
this.logger.error(`Failed to create default mailboxes for user ${wildduckUserId}: ${(error as Error).message}`);
}
}
/**
* Create info@domain.com address when domain is verified
*/
async createInfoAddress(domain: Domain): Promise<void> {
const infoEmail = `info@${domain.name}`;
const uniqueUsername = `info-${domain.name}`;
try {
this.logger.log(`Creating info address for domain: ${domain.name}`);
// Check if info address already exists in local database
const existingUser = await this.em.findOne(User, {
emailAddress: infoEmail,
business: { id: domain.business.id },
});
if (existingUser) {
this.logger.warn(`Info address ${infoEmail} already exists in local database, skipping creation`);
return;
}
// Check if user exists in WildDuck
const userExistsInWildDuck = await this.checkUserExistsInWildDuck(uniqueUsername, infoEmail);
if (userExistsInWildDuck) {
this.logger.warn(`Info address ${infoEmail} or username "${uniqueUsername}" already exists in WildDuck, skipping creation`);
return;
}
// Generate a secure password for the info account
const password = this.generateSecurePassword();
const hashedPassword = await this.passwordService.hashPassword(password);
// Get user role
const userRole = await this.em.findOne(Role, { name: RoleEnum.USER });
if (!userRole) throw new BadRequestException(RoleMessage.NOT_FOUND);
// Create user in mail server (WildDuck)
const mailServerUser = await firstValueFrom(
this.mailServerService.users.createUser({
username: uniqueUsername,
password,
address: infoEmail,
name: `Info - ${domain.business.name}`,
quota: QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES,
}),
);
if (!mailServerUser.success) throw new BadRequestException(MailServerMessage.FAILED_TO_CREATE_ACCOUNT);
// Check if business has enough quota
const remainingQuota = domain.business.remainingQuota || 0;
if (remainingQuota < QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION) throw new BadRequestException(BusinessMessage.INSUFFICIENT_QUOTA);
// Create user in local database
const user = this.em.create(User, {
title: `Info - ${domain.business.name}`,
userName: uniqueUsername,
password: hashedPassword,
emailAddress: infoEmail,
emailEnabled: true,
emailQuota: QUOTA_CONSTANTS.DEFAULT_EMAIL_QUOTA_BYTES,
wildduckUserId: mailServerUser.id,
displayName: `Info - ${domain.business.name}`,
isActive: true,
business: domain.business,
domain: domain,
role: userRole,
});
// Update business quota after successful user creation
domain.business.usedQuota = (domain.business.usedQuota || 0) + QUOTA_CONSTANTS.USER_QUOTA_DEDUCTION;
domain.business.remainingQuota = (domain.business.quota || 0) - domain.business.usedQuota;
await this.em.persistAndFlush([user, domain.business]);
// Create default mailboxes (Archive and Favorite)
await this.createDefaultMailboxes(mailServerUser.id);
this.logger.log(`Successfully created info address: ${infoEmail} (username: ${uniqueUsername}) for business ${domain.business.name}`);
this.logger.log(`Info account credentials - Email: ${infoEmail}, Username: ${uniqueUsername}, Password: [GENERATED]`);
} catch (error) {
this.logger.error(`Failed to create info address for domain ${domain.name}:`, error);
// Don't throw the error to prevent domain verification from failing
// The domain should still be marked as verified even if info address creation fails
}
}
/**
* Generate a secure password for the info account
*/
private generateSecurePassword(): string {
const length = 16;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*";
let password = "";
for (let i = 0; i < length; i++) {
password += charset.charAt(Math.floor(Math.random() * charset.length));
}
return password;
}
/**
* Check if info address exists for a domain
*/
async infoAddressExists(domain: Domain): Promise<boolean> {
const infoEmail = `info@${domain.name}`;
const existingUser = await this.em.findOne(User, {
emailAddress: infoEmail,
business: { id: domain.business.id },
});
return !!existingUser;
}
/**
* Get info address details for a domain
*/
async getInfoAddressDetails(domain: Domain): Promise<User | null> {
const infoEmail = `info@${domain.name}`;
return this.em.findOne(
User,
{
emailAddress: infoEmail,
business: { id: domain.business.id },
},
{ populate: ["role", "business", "domain"] },
);
}
}