215 lines
8.0 KiB
TypeScript
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"] },
|
|
);
|
|
}
|
|
}
|