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 { 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 { 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 { 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 { 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 { const infoEmail = `info@${domain.name}`; return this.em.findOne( User, { emailAddress: infoEmail, business: { id: domain.business.id }, }, { populate: ["role", "business", "domain"] }, ); } }