chore: template phase 2
This commit is contained in:
@@ -10,7 +10,6 @@ import { MailboxResolverService } from "./services/mailbox-resolver.service";
|
|||||||
import { WebSocketAuthService } from "./services/websocket-auth.service";
|
import { WebSocketAuthService } from "./services/websocket-auth.service";
|
||||||
import { jwtConfig } from "../../configs/jwt.config";
|
import { jwtConfig } from "../../configs/jwt.config";
|
||||||
import { MailServerModule } from "../mail-server/mail-server.module";
|
import { MailServerModule } from "../mail-server/mail-server.module";
|
||||||
import { TemplateProcessorService } from "../templates/services/template-processor.service";
|
|
||||||
import { TemplatesModule } from "../templates/templates.module";
|
import { TemplatesModule } from "../templates/templates.module";
|
||||||
import { User } from "../users/entities/user.entity";
|
import { User } from "../users/entities/user.entity";
|
||||||
import { UsersModule } from "../users/users.module";
|
import { UsersModule } from "../users/users.module";
|
||||||
@@ -18,7 +17,7 @@ import { UsersModule } from "../users/users.module";
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [MailServerModule, UsersModule, TemplatesModule, JwtModule.registerAsync(jwtConfig()), MikroOrmModule.forFeature([User])],
|
imports: [MailServerModule, UsersModule, TemplatesModule, JwtModule.registerAsync(jwtConfig()), MikroOrmModule.forFeature([User])],
|
||||||
controllers: [EmailController],
|
controllers: [EmailController],
|
||||||
providers: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService, TemplateProcessorService],
|
providers: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService],
|
||||||
exports: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService, TemplateProcessorService],
|
exports: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService],
|
||||||
})
|
})
|
||||||
export class EmailModule {}
|
export class EmailModule {}
|
||||||
|
|||||||
@@ -37,15 +37,20 @@ export class EmailService {
|
|||||||
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
||||||
|
|
||||||
// Process email content through business template
|
// Process email content through business template
|
||||||
const processedTemplate = await this.templateProcessorService.processEmailWithTemplate(user.business.id, {
|
const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, {
|
||||||
text: sendEmailDto.text,
|
text: sendEmailDto.text,
|
||||||
html: sendEmailDto.html,
|
html: sendEmailDto.html,
|
||||||
subject: sendEmailDto.subject,
|
subject: sendEmailDto.subject,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the DTO with processed content
|
// Update the DTO with processed content
|
||||||
sendEmailDto.html = processedTemplate.html;
|
if (processedTemplate.isHtml) {
|
||||||
sendEmailDto.text = processedTemplate.text;
|
sendEmailDto.html = processedTemplate.content;
|
||||||
|
sendEmailDto.text = undefined; // Clear text when using HTML template
|
||||||
|
} else {
|
||||||
|
sendEmailDto.text = processedTemplate.content;
|
||||||
|
sendEmailDto.html = undefined; // Clear HTML when no template
|
||||||
|
}
|
||||||
|
|
||||||
if (processedTemplate.hasTemplate) {
|
if (processedTemplate.hasTemplate) {
|
||||||
this.logger.log(`Email content processed through template for business: ${user.business.id}`);
|
this.logger.log(`Email content processed through template for business: ${user.business.id}`);
|
||||||
@@ -410,14 +415,19 @@ export class EmailService {
|
|||||||
const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] });
|
const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] });
|
||||||
|
|
||||||
if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) {
|
if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) {
|
||||||
const processedTemplate = await this.templateProcessorService.processEmailWithTemplate(user.business.id, {
|
const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, {
|
||||||
text: updateDraftDto.text,
|
text: updateDraftDto.text,
|
||||||
html: updateDraftDto.html,
|
html: updateDraftDto.html,
|
||||||
subject: updateDraftDto.subject,
|
subject: updateDraftDto.subject,
|
||||||
});
|
});
|
||||||
|
|
||||||
draftData.html = processedTemplate.html;
|
if (processedTemplate.isHtml) {
|
||||||
draftData.text = processedTemplate.text;
|
draftData.html = processedTemplate.content;
|
||||||
|
draftData.text = undefined;
|
||||||
|
} else {
|
||||||
|
draftData.text = processedTemplate.content;
|
||||||
|
draftData.html = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
if (processedTemplate.hasTemplate) {
|
if (processedTemplate.hasTemplate) {
|
||||||
this.logger.log(`Draft content processed through template for business: ${user.business.id}`);
|
this.logger.log(`Draft content processed through template for business: ${user.business.id}`);
|
||||||
|
|||||||
@@ -102,3 +102,15 @@ export type ImageType = {
|
|||||||
alignment?: HorizontalAlignment;
|
alignment?: HorizontalAlignment;
|
||||||
verticalAlignment?: VerticalAlignment;
|
verticalAlignment?: VerticalAlignment;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface EmailContent {
|
||||||
|
text?: string;
|
||||||
|
html?: string;
|
||||||
|
subject?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProcessedEmailResult {
|
||||||
|
content: string;
|
||||||
|
isHtml: boolean;
|
||||||
|
hasTemplate: boolean;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { Injectable, Logger } from "@nestjs/common";
|
||||||
|
|
||||||
import { TemplatesService } from "./templates.service";
|
import { TemplatesService } from "./templates.service";
|
||||||
// import { PersonalityDataType, SectionItemType, TextType } from "../interfaces/structure.interface";
|
import { EmailContent, ProcessedEmailResult } from "../interfaces/structure.interface";
|
||||||
|
|
||||||
interface ProcessedTemplateResult {
|
|
||||||
html: string;
|
|
||||||
text: string;
|
|
||||||
hasTemplate: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TemplateProcessorService {
|
export class TemplateProcessorService {
|
||||||
@@ -15,101 +9,66 @@ export class TemplateProcessorService {
|
|||||||
|
|
||||||
constructor(private readonly templatesService: TemplatesService) {}
|
constructor(private readonly templatesService: TemplatesService) {}
|
||||||
|
|
||||||
/**
|
//**************************************************** */
|
||||||
* Process email content through business template
|
async processEmailContent(businessId: string, emailContent: EmailContent): Promise<ProcessedEmailResult> {
|
||||||
*/
|
|
||||||
async processEmailWithTemplate(
|
|
||||||
businessId: string,
|
|
||||||
emailContent: { text?: string; html?: string; subject?: string },
|
|
||||||
// userId?: string,
|
|
||||||
): Promise<ProcessedTemplateResult> {
|
|
||||||
try {
|
try {
|
||||||
this.logger.log(`Processing email with template for business: ${businessId}`);
|
// Get selected template for the business
|
||||||
|
const selectedTemplate = await this.getSelectedTemplate(businessId);
|
||||||
|
|
||||||
// Get business templates
|
if (!selectedTemplate) {
|
||||||
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
// No template - return original text content
|
||||||
|
|
||||||
if (!templates || templates.length === 0) {
|
|
||||||
this.logger.log(`No templates found for business: ${businessId}, using original content`);
|
|
||||||
return {
|
return {
|
||||||
html: emailContent.html || this.convertTextToHtml(emailContent.text || ""),
|
content: emailContent.text || emailContent.html || "",
|
||||||
text: emailContent.text || this.stripHtmlTags(emailContent.html || ""),
|
isHtml: false,
|
||||||
hasTemplate: false,
|
hasTemplate: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the first template
|
// Process with template
|
||||||
const template = templates[0];
|
const processedContent = this.injectContentIntoTemplate(selectedTemplate.rawHtml, emailContent);
|
||||||
|
|
||||||
this.logger.log(`Using template: ${template.name} for email processing`);
|
|
||||||
|
|
||||||
// Process the template with email content
|
|
||||||
const processedContent = await this.injectContentIntoTemplate(emailContent, template.rawHtml);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
html: processedContent.html,
|
content: processedContent,
|
||||||
text: processedContent.text,
|
isHtml: true,
|
||||||
hasTemplate: true,
|
hasTemplate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to process email with template for business ${businessId}:`, error);
|
this.logger.error(`Failed to process email for business ${businessId}:`, error);
|
||||||
|
|
||||||
// Fallback to original content if template processing fails
|
// Fallback to original content
|
||||||
return {
|
return {
|
||||||
html: emailContent.html || this.convertTextToHtml(emailContent.text || ""),
|
content: emailContent.text || emailContent.html || "",
|
||||||
text: emailContent.text || this.stripHtmlTags(emailContent.html || ""),
|
isHtml: false,
|
||||||
hasTemplate: false,
|
hasTemplate: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//**************************************************** */
|
||||||
* Inject email content into template structure
|
private async getSelectedTemplate(businessId: string) {
|
||||||
*/
|
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
||||||
private async injectContentIntoTemplate(
|
return templates.find((template) => template.selected) || null;
|
||||||
// templateStructure: Record<string, unknown>,
|
|
||||||
emailContent: { text?: string; html?: string; subject?: string },
|
|
||||||
rawHtml?: string,
|
|
||||||
): Promise<{ html: string; text: string }> {
|
|
||||||
try {
|
|
||||||
// If template has raw HTML, use it as base and inject content
|
|
||||||
if (rawHtml) {
|
|
||||||
const html = this.injectContentIntoRawHtml(rawHtml, emailContent);
|
|
||||||
return {
|
|
||||||
html,
|
|
||||||
text: this.stripHtmlTags(html),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, build simple HTML with email content
|
//**************************************************** */
|
||||||
const html = this.buildSimpleTemplate(emailContent);
|
private injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent): string {
|
||||||
return {
|
if (!templateHtml) {
|
||||||
html,
|
return emailContent.text || emailContent.html || "";
|
||||||
text: this.stripHtmlTags(html),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error("Failed to inject content into template:", error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
let processedHtml = templateHtml;
|
||||||
* Inject email content into raw HTML template
|
|
||||||
*/
|
|
||||||
private injectContentIntoRawHtml(rawHtml: string, emailContent: { text?: string; html?: string; subject?: string }): string {
|
|
||||||
let processedHtml = rawHtml;
|
|
||||||
|
|
||||||
// Replace placeholders with actual content
|
// Content to inject (prefer text, fallback to html)
|
||||||
const contentToInject = emailContent.html || this.convertTextToHtml(emailContent.text || "");
|
const contentToInject = emailContent.text || this.stripHtmlTags(emailContent.html || "");
|
||||||
|
|
||||||
// Common placeholder patterns
|
// Replace content placeholders
|
||||||
const placeholders = [/\{\{content\}\}/gi, /\{\{message\}\}/gi, /\{\{body\}\}/gi, /\{content\}/gi, /\{message\}/gi, /\{body\}/gi];
|
const contentPlaceholders = [/\{\{content\}\}/gi, /\{\{message\}\}/gi, /\{\{body\}\}/gi, /\{content\}/gi, /\{message\}/gi, /\{body\}/gi];
|
||||||
|
|
||||||
placeholders.forEach((placeholder) => {
|
contentPlaceholders.forEach((placeholder) => {
|
||||||
processedHtml = processedHtml.replace(placeholder, contentToInject);
|
processedHtml = processedHtml.replace(placeholder, contentToInject);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace subject placeholders
|
// Replace subject placeholders if subject exists
|
||||||
if (emailContent.subject) {
|
if (emailContent.subject) {
|
||||||
const subjectPlaceholders = [/\{\{subject\}\}/gi, /\{\{title\}\}/gi, /\{subject\}/gi, /\{title\}/gi];
|
const subjectPlaceholders = [/\{\{subject\}\}/gi, /\{\{title\}\}/gi, /\{subject\}/gi, /\{title\}/gi];
|
||||||
|
|
||||||
@@ -121,49 +80,7 @@ export class TemplateProcessorService {
|
|||||||
return processedHtml;
|
return processedHtml;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//**************************************************** */
|
||||||
* Build simple HTML template
|
|
||||||
*/
|
|
||||||
private buildSimpleTemplate(emailContent: { text?: string; html?: string; subject?: string }): string {
|
|
||||||
const content = emailContent.html || this.convertTextToHtml(emailContent.text || "");
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>${emailContent.subject || "Email"}</title>
|
|
||||||
<style>
|
|
||||||
body { font-family: Arial, sans-serif; margin: 0; padding: 20px; line-height: 1.6; }
|
|
||||||
.email-container { max-width: 600px; margin: 0 auto; }
|
|
||||||
.content { padding: 20px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="email-container">
|
|
||||||
<div class="content">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert plain text to HTML
|
|
||||||
*/
|
|
||||||
private convertTextToHtml(text: string): string {
|
|
||||||
return text
|
|
||||||
.replace(/\n\n/g, "</p><p>")
|
|
||||||
.replace(/\n/g, "<br>")
|
|
||||||
.replace(/^(.*)$/, "<p>$1</p>");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Strip HTML tags from content
|
|
||||||
*/
|
|
||||||
private stripHtmlTags(html: string): string {
|
private stripHtmlTags(html: string): string {
|
||||||
return html
|
return html
|
||||||
.replace(/<[^>]*>/g, "")
|
.replace(/<[^>]*>/g, "")
|
||||||
@@ -172,6 +89,7 @@ export class TemplateProcessorService {
|
|||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
.replace(/"/g, '"')
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, "'")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export class TemplatesController {
|
|||||||
return this.templatesService.deleteTemplate(params.id, businessId);
|
return this.templatesService.deleteTemplate(params.id, businessId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(":id/set-selected")
|
@Patch(":id/set-selected")
|
||||||
@ApiOperation({ summary: "Set template as selected (unselects all other templates for the business)" })
|
@ApiOperation({ summary: "Set template as selected (unselects all other templates for the business)" })
|
||||||
@ApiResponse({ status: 200, description: "Template set as selected successfully" })
|
@ApiResponse({ status: 200, description: "Template set as selected successfully" })
|
||||||
@ApiResponse({ status: 404, description: "Template not found" })
|
@ApiResponse({ status: 404, description: "Template not found" })
|
||||||
|
|||||||
Reference in New Issue
Block a user