chore: update the template processor

This commit is contained in:
mahyargdz
2025-08-04 12:14:30 +03:30
parent 6c1904cb60
commit c21037a683
10 changed files with 61 additions and 43 deletions
-1
View File
@@ -92,7 +92,6 @@
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/passport-jwt": "^4.0.1", "@types/passport-jwt": "^4.0.1",
"@types/supertest": "^6.0.3", "@types/supertest": "^6.0.3",
"@types/web-push": "^3.6.4",
"@typescript-eslint/eslint-plugin": "^8.35.1", "@typescript-eslint/eslint-plugin": "^8.35.1",
"@typescript-eslint/parser": "^8.35.1", "@typescript-eslint/parser": "^8.35.1",
"eslint": "^9.30.1", "eslint": "^9.30.1",
+2
View File
@@ -281,6 +281,8 @@ export const enum EmailMessage {
TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد", TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد",
SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند", SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند",
SAVE_DRAFT_SUCCESS = "پیش نویس با موفقیت ذخیره شد", SAVE_DRAFT_SUCCESS = "پیش نویس با موفقیت ذخیره شد",
HTML_CONTENT_REQUIRED = "محتوای HTML مورد نیاز است",
TEXT_CONTENT_REQUIRED = "TEXT_CONTENT_REQUIRED",
} }
export const enum WebSocketMessage { export const enum WebSocketMessage {
@@ -76,7 +76,6 @@ export class EmailPollingProcessor extends WorkerProcessor {
timestamp: email.date || new Date().toISOString(), timestamp: email.date || new Date().toISOString(),
mailboxId: email.mailbox, mailboxId: email.mailbox,
mailboxName: "Inbox", mailboxName: "Inbox",
isRead: !!email.seen,
hasAttachments: !!(email.attachments && Array.isArray(email.attachments) && email.attachments.length > 0), hasAttachments: !!(email.attachments && Array.isArray(email.attachments) && email.attachments.length > 0),
}); });
@@ -80,6 +80,16 @@ export class EmailNotificationService {
} }
} }
//************************************************************************ */
async notifyEmailSentFromDraft(emailSentFromDraftData: IEmailSentPayload) {
try {
await this.emailGateway.notifyEmailSent(emailSentFromDraftData.userId, emailSentFromDraftData);
await this.updateUnreadCount(emailSentFromDraftData.wildduckUserId, emailSentFromDraftData.userId);
} catch (error) {
this.logger.error(`Failed to notify email sent from draft for user ${emailSentFromDraftData.userId}:`, error);
}
}
//************************************************************************ */ //************************************************************************ */
async notifyEmailDeleted(emailDeletedData: IEmailDeletePayload) { async notifyEmailDeleted(emailDeletedData: IEmailDeletePayload) {
try { try {
+4 -4
View File
@@ -157,15 +157,15 @@ export class SendEmailDto {
@ApiPropertyOptional({ description: "Message subject. If not then resolved from Reference message (optional)", example: "Test email" }) @ApiPropertyOptional({ description: "Message subject. If not then resolved from Reference message (optional)", example: "Test email" })
subject: string; subject: string;
@IsOptional() @IsNotEmpty({ message: EmailMessage.TEXT_CONTENT_REQUIRED })
@IsString({ message: EmailMessage.TEXT_CONTENT_MUST_BE_STRING }) @IsString({ message: EmailMessage.TEXT_CONTENT_MUST_BE_STRING })
@ApiPropertyOptional({ description: "Plaintext message (optional)" }) @ApiPropertyOptional({ description: "Plaintext message (optional)" })
text?: string; text: string;
@IsOptional() @IsNotEmpty({ message: EmailMessage.HTML_CONTENT_REQUIRED })
@IsString({ message: EmailMessage.HTML_CONTENT_MUST_BE_STRING }) @IsString({ message: EmailMessage.HTML_CONTENT_MUST_BE_STRING })
@ApiPropertyOptional({ description: "HTML formatted message (optional)" }) @ApiPropertyOptional({ description: "HTML formatted message (optional)" })
html?: string; html: string;
@IsOptional() @IsOptional()
@IsArray({ message: EmailMessage.ATTACHMENTS_MUST_BE_ARRAY }) @IsArray({ message: EmailMessage.ATTACHMENTS_MUST_BE_ARRAY })
+2 -2
View File
@@ -207,8 +207,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Draft sent successfully" }) @ApiResponse({ status: 200, description: "Draft sent successfully" })
@ApiResponse({ status: 404, description: "Draft not found" }) @ApiResponse({ status: 404, description: "Draft not found" })
@ApiResponse({ status: 403, description: "Not authorized to send draft" }) @ApiResponse({ status: 403, description: "Not authorized to send draft" })
sendDraft(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { sendDraft(@UserDec() user: User, @Param() params: MessageParamDto) {
return this.emailService.sendDraft(userId, Number(params.messageId)); return this.emailService.sendDraft(user.wildduckUserId, user.id, Number(params.messageId));
} }
@Delete("messages/drafts/:messageId") @Delete("messages/drafts/:messageId")
@@ -2,6 +2,8 @@
// BASE INTERFACES // BASE INTERFACES
// ==================== // ====================
import { MessageAddress } from "../../mail-server/interfaces/messages-response.interface";
export interface IBaseEventPayload { export interface IBaseEventPayload {
userId: string; userId: string;
timestamp: string; timestamp: string;
@@ -23,21 +25,15 @@ export interface IBaseMessageMailboxEventPayload extends IBaseMessageEventPayloa
// COMMON EMAIL INTERFACES // COMMON EMAIL INTERFACES
// ==================== // ====================
export interface IEmailContact {
name?: string;
address: string;
}
export interface IEmailMetadata { export interface IEmailMetadata {
subject: string; subject: string;
hasAttachments: boolean; hasAttachments: boolean;
mailboxName: string; mailboxName: string;
isRead: boolean;
} }
export interface IEmailContent extends IEmailMetadata { export interface IEmailContent extends IEmailMetadata {
from: IEmailContact; from: MessageAddress;
to: IEmailContact[]; to: MessageAddress[];
preview?: string; preview?: string;
} }
+20 -6
View File
@@ -58,10 +58,10 @@ export class EmailService {
if (processedTemplate.isHtml) { if (processedTemplate.isHtml) {
sendEmailDto.html = processedTemplate.content; sendEmailDto.html = processedTemplate.content;
sendEmailDto.text = undefined; sendEmailDto.text = "";
} else { } else {
sendEmailDto.text = processedTemplate.content; sendEmailDto.text = processedTemplate.content;
sendEmailDto.html = undefined; sendEmailDto.html = "";
} }
} }
@@ -86,7 +86,6 @@ export class EmailService {
from: sendEmailDto.from, from: sendEmailDto.from,
hasAttachments: false, hasAttachments: false,
mailboxName: "Sent", mailboxName: "Sent",
isRead: true,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
}); });
@@ -514,10 +513,10 @@ export class EmailService {
if (processedTemplate.isHtml) { if (processedTemplate.isHtml) {
draftData.html = processedTemplate.content; draftData.html = processedTemplate.content;
draftData.text = undefined; draftData.text = "";
} else { } else {
draftData.text = processedTemplate.content; draftData.text = processedTemplate.content;
draftData.html = undefined; draftData.html = "";
} }
const headers = this.generateHeadersForEmailType(user, { const headers = this.generateHeadersForEmailType(user, {
@@ -538,12 +537,27 @@ export class EmailService {
} }
//============================================== //==============================================
async sendDraft(wildduckUserId: string, messageId: number) { async sendDraft(wildduckUserId: string, userId: string, messageId: number) {
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }), this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }),
); );
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId));
await this.emailNotificationService.notifyEmailSentFromDraft({
wildduckUserId,
userId,
from: message.from,
to: message.to!,
subject: message.subject,
hasAttachments: false,
mailboxName: message.mailbox,
messageId: Number(message.id),
mailboxId: message.mailbox,
timestamp: new Date().toISOString(),
});
return { return {
message: EmailMessage.DRAFT_SENT_SUCCESSFULLY, message: EmailMessage.DRAFT_SENT_SUCCESSFULLY,
messageId: result.message.id, messageId: result.message.id,
@@ -180,7 +180,7 @@ export type PersonalityDataType = {
}; };
export interface EmailContent { export interface EmailContent {
text?: string; text: string;
html?: string; html: string;
subject?: string; subject?: string;
} }
@@ -18,7 +18,7 @@ export class TemplateProcessorService {
if (!selectedTemplate) { if (!selectedTemplate) {
return { return {
content: emailContent.text || emailContent.html || "", content: emailContent.html || emailContent.text || "",
isHtml: false, isHtml: false,
hasTemplate: false, hasTemplate: false,
}; };
@@ -45,23 +45,21 @@ export class TemplateProcessorService {
//**************************************************** */ //**************************************************** */
private async injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent) { private async injectContentIntoTemplate(templateHtml: string | undefined, emailContent: EmailContent) {
if (!templateHtml) { const { html, text } = emailContent;
return emailContent.text || emailContent.html || "";
} if (!templateHtml) return html || text || "";
let processedHtml = templateHtml; let processedHtml = templateHtml;
// Content to inject (prefer text, fallback to html) // const contentToInject = this.stripHtmlTags(html || text);
const contentToInject = emailContent.text || this.stripHtmlTags(emailContent.html || ""); const contentToInject = html || text;
// Replace content placeholders
const contentPlaceholders = [/\{\{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];
contentPlaceholders.forEach((placeholder) => { contentPlaceholders.forEach((placeholder) => {
processedHtml = processedHtml.replace(placeholder, contentToInject); processedHtml = processedHtml.replace(placeholder, contentToInject);
}); });
// 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];
@@ -74,15 +72,15 @@ export class TemplateProcessorService {
} }
//**************************************************** */ //**************************************************** */
private stripHtmlTags(html: string): string { // private stripHtmlTags(html: string): string {
return html // return html
.replace(/<[^>]*>/g, "") // .replace(/<[^>]*>/g, "")
.replace(/&nbsp;/g, " ") // .replace(/&nbsp;/g, " ")
.replace(/&amp;/g, "&") // .replace(/&amp;/g, "&")
.replace(/&lt;/g, "<") // .replace(/&lt;/g, "<")
.replace(/&gt;/g, ">") // .replace(/&gt;/g, ">")
.replace(/&quot;/g, '"') // .replace(/&quot;/g, '"')
.replace(/&#39;/g, "'") // .replace(/&#39;/g, "'")
.trim(); // .trim();
} // }
} }