chore: add assign template to user for email sending and template
This commit is contained in:
@@ -100,6 +100,8 @@ export const enum UserMessage {
|
|||||||
EMAIL_USER_CREATED_SUCCESSFULLY = "ایمیل با موفقیت ایجاد شد",
|
EMAIL_USER_CREATED_SUCCESSFULLY = "ایمیل با موفقیت ایجاد شد",
|
||||||
EMAIL_USER_UPDATED_SUCCESSFULLY = "ایمیل کاربر با موفقیت بهروزرسانی شد",
|
EMAIL_USER_UPDATED_SUCCESSFULLY = "ایمیل کاربر با موفقیت بهروزرسانی شد",
|
||||||
STATUS_UPDATED = "وضعیت کاربر با موفقیت تغییر کرد",
|
STATUS_UPDATED = "وضعیت کاربر با موفقیت تغییر کرد",
|
||||||
|
TEMPLATE_UUID = "شناسه قالب باید یک یو یو آی دی باشد",
|
||||||
|
TEMPLATE_NOT_EMPTY = "شناسه قالب نمیتواند خالی باشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum CommonMessage {
|
export const enum CommonMessage {
|
||||||
@@ -212,6 +214,7 @@ export const enum EmailMessage {
|
|||||||
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد",
|
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد",
|
||||||
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد",
|
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد",
|
||||||
USER_NOT_FOUND = "کاربر یافت نشد",
|
USER_NOT_FOUND = "کاربر یافت نشد",
|
||||||
|
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "Priority must be one of: high, normal, low",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WebSocketMessage {
|
export const enum WebSocketMessage {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Type } from "class-transformer";
|
|||||||
import { IsArray, IsBoolean, IsDateString, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
import { IsArray, IsBoolean, IsDateString, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||||
|
|
||||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||||
|
// import { Priority } from "../enums/email-header.enum";
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
export enum EmailType {
|
export enum EmailType {
|
||||||
@@ -107,62 +108,6 @@ export class EmailEnvelopeDto {
|
|||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
export class SendEmailDto {
|
export class SendEmailDto {
|
||||||
@IsOptional()
|
|
||||||
@IsEnum(EmailType, { message: "Email type must be one of: general, transactional, marketing, anti-threading" })
|
|
||||||
@ApiPropertyOptional({
|
|
||||||
description: "Email type for appropriate header configuration",
|
|
||||||
enum: EmailType,
|
|
||||||
default: EmailType.GENERAL,
|
|
||||||
example: EmailType.TRANSACTIONAL,
|
|
||||||
})
|
|
||||||
emailType?: EmailType;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "Transaction ID for transactional emails (optional)", example: "ORDER-123456" })
|
|
||||||
transactionId?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "Category for transactional emails (optional)", example: "order-confirmation" })
|
|
||||||
category?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "List ID for marketing emails (optional)", example: "newsletter-subscribers" })
|
|
||||||
listId?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "Unsubscribe URL for marketing emails (optional)", example: "https://yourdomain.com/unsubscribe?token=xyz" })
|
|
||||||
unsubscribeUrl?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "Campaign ID for marketing emails (optional)", example: "CAMPAIGN-2024-001" })
|
|
||||||
campaignId?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsEnum(["high", "normal", "low"], { message: "Priority must be one of: high, normal, low" })
|
|
||||||
@ApiPropertyOptional({ description: "Email priority (optional)", enum: ["high", "normal", "low"], default: "normal" })
|
|
||||||
priority?: "high" | "normal" | "low";
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN })
|
|
||||||
@ApiPropertyOptional({ description: "Force Gmail thread break for anti-threading emails (optional)", default: false })
|
|
||||||
forceGmailBreak?: boolean;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "Conversation breaker for anti-threading emails (optional)" })
|
|
||||||
conversationBreaker?: string;
|
|
||||||
|
|
||||||
@IsOptional()
|
|
||||||
@IsNotEmpty()
|
|
||||||
@IsString({ message: EmailMessage.MAILBOX_MUST_BE_STRING })
|
|
||||||
@ApiPropertyOptional({ description: "ID of the Mailbox (optional)", example: "507f1f77bcf86cd799439012" })
|
|
||||||
mailbox?: string;
|
|
||||||
|
|
||||||
@IsNotEmpty({ message: EmailMessage.FROM_ADDRESS_REQUIRED })
|
@IsNotEmpty({ message: EmailMessage.FROM_ADDRESS_REQUIRED })
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => EmailRecipientDto)
|
@Type(() => EmailRecipientDto)
|
||||||
@@ -272,6 +217,62 @@ export class SendEmailDto {
|
|||||||
@Type(() => EmailEnvelopeDto)
|
@Type(() => EmailEnvelopeDto)
|
||||||
@ApiPropertyOptional({ description: "Optional envelope (optional)" })
|
@ApiPropertyOptional({ description: "Optional envelope (optional)" })
|
||||||
envelope?: EmailEnvelopeDto;
|
envelope?: EmailEnvelopeDto;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsEnum(EmailType, { message: "Email type must be one of: general, transactional, marketing, anti-threading" })
|
||||||
|
// @ApiPropertyOptional({
|
||||||
|
// description: "Email type for appropriate header configuration",
|
||||||
|
// enum: EmailType,
|
||||||
|
// default: EmailType.GENERAL,
|
||||||
|
// example: EmailType.TRANSACTIONAL,
|
||||||
|
// })
|
||||||
|
// emailType?: EmailType;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "Transaction ID for transactional emails (optional)", example: "ORDER-123456" })
|
||||||
|
// transactionId?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "Category for transactional emails (optional)", example: "order-confirmation" })
|
||||||
|
// category?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "List ID for marketing emails (optional)", example: "newsletter-subscribers" })
|
||||||
|
// listId?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "Unsubscribe URL for marketing emails (optional)", example: "https://yourdomain.com/unsubscribe?token=xyz" })
|
||||||
|
// unsubscribeUrl?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "Campaign ID for marketing emails (optional)", example: "CAMPAIGN-2024-001" })
|
||||||
|
// campaignId?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsEnum(Priority, { message: EmailMessage.PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW })
|
||||||
|
// @ApiPropertyOptional({ description: "Email priority (optional)", enum: Priority, default: Priority.Normal })
|
||||||
|
// priority?: Priority;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN })
|
||||||
|
// @ApiPropertyOptional({ description: "Force Gmail thread break for anti-threading emails (optional)", default: false })
|
||||||
|
// forceGmailBreak?: boolean;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsString({ message: EmailMessage.HEADER_VALUE_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "Conversation breaker for anti-threading emails (optional)" })
|
||||||
|
// conversationBreaker?: string;
|
||||||
|
|
||||||
|
// @IsOptional()
|
||||||
|
// @IsNotEmpty()
|
||||||
|
// @IsString({ message: EmailMessage.MAILBOX_MUST_BE_STRING })
|
||||||
|
// @ApiPropertyOptional({ description: "ID of the Mailbox (optional)", example: "507f1f77bcf86cd799439012" })
|
||||||
|
// mailbox?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export const enum EmailHeaderKey {
|
||||||
|
MessageId = "Message-ID",
|
||||||
|
References = "References",
|
||||||
|
ThreadIndex = "Thread-Index",
|
||||||
|
XMailer = "X-Mailer",
|
||||||
|
XPriority = "X-Priority",
|
||||||
|
XMSMailPriority = "X-MSMail-Priority",
|
||||||
|
MIMEVersion = "MIME-Version",
|
||||||
|
AuthenticationResults = "Authentication-Results",
|
||||||
|
XAutoResponseSuppress = "X-Auto-Response-Suppress",
|
||||||
|
XEntityID = "X-Entity-ID",
|
||||||
|
Precedence = "Precedence",
|
||||||
|
ListUnsubscribePost = "List-Unsubscribe-Post",
|
||||||
|
ListUnsubscribe = "List-Unsubscribe",
|
||||||
|
ListId = "List-Id",
|
||||||
|
XSpamStatus = "X-Spam-Status",
|
||||||
|
XSpamScore = "X-Spam-Score",
|
||||||
|
XFeedbackID = "X-Feedback-ID",
|
||||||
|
XEntityRefID = "X-Entity-Ref-ID",
|
||||||
|
InReplyTo = "In-Reply-To",
|
||||||
|
XGmailThreadBreak = "X-Gmail-Thread-Break",
|
||||||
|
ThreadTopic = "Thread-Topic",
|
||||||
|
XTransactionID = "X-Transaction-ID",
|
||||||
|
XCategory = "X-Category",
|
||||||
|
Importance = "Importance",
|
||||||
|
XCampaignID = "X-Campaign-ID",
|
||||||
|
XMarketingEmail = "X-Marketing-Email",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum Priority {
|
||||||
|
High = "high",
|
||||||
|
Normal = "normal",
|
||||||
|
Low = "low",
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { Priority } from "../enums/email-header.enum";
|
||||||
|
|
||||||
|
export interface IGenerateAntiThreadingHeadersOptions {
|
||||||
|
messageId?: string;
|
||||||
|
listId?: string;
|
||||||
|
listUnsubscribe?: string;
|
||||||
|
businessName?: string;
|
||||||
|
businessDomain: string;
|
||||||
|
isTransactional?: boolean;
|
||||||
|
isMarketing?: boolean;
|
||||||
|
priority?: Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGenerateGmailAntiThreadingHeadersOptions extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain"> {
|
||||||
|
conversationBreaker?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGenerateTransactionalHeadersOptions extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain" | "businessName"> {
|
||||||
|
transactionId?: string;
|
||||||
|
category?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IGenerateMarketingHeadersOptions
|
||||||
|
extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain" | "businessName" | "listId" | "listUnsubscribe"> {
|
||||||
|
campaignId?: string;
|
||||||
|
unsubscribeUrl: string;
|
||||||
|
}
|
||||||
@@ -2,88 +2,65 @@ import { Injectable } from "@nestjs/common";
|
|||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import { EmailHeaderDto } from "../DTO/send-email.dto";
|
import { EmailHeaderDto } from "../DTO/send-email.dto";
|
||||||
|
import { EmailHeaderKey } from "../enums/email-header.enum";
|
||||||
export const EMAIL_HEADERS_SERVICE = Symbol("EMAIL_HEADERS_SERVICE");
|
import {
|
||||||
|
IGenerateAntiThreadingHeadersOptions,
|
||||||
export interface IGenerateAntiThreadingHeadersOptions {
|
IGenerateGmailAntiThreadingHeadersOptions,
|
||||||
messageId?: string;
|
IGenerateMarketingHeadersOptions,
|
||||||
listId?: string;
|
IGenerateTransactionalHeadersOptions,
|
||||||
listUnsubscribe?: string;
|
} from "../interfaces/email-header.interface";
|
||||||
businessName?: string;
|
|
||||||
businessDomain?: string;
|
|
||||||
isTransactional?: boolean;
|
|
||||||
isMarketing?: boolean;
|
|
||||||
priority?: "high" | "normal" | "low";
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGenerateGmailAntiThreadingHeadersOptions extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain"> {
|
|
||||||
conversationBreaker?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGenerateTransactionalHeadersOptions extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain" | "businessName"> {
|
|
||||||
transactionId?: string;
|
|
||||||
category?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IGenerateMarketingHeadersOptions
|
|
||||||
extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain" | "businessName" | "listId" | "listUnsubscribe"> {
|
|
||||||
campaignId?: string;
|
|
||||||
unsubscribeUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailHeadersService {
|
export class EmailHeadersService {
|
||||||
/**
|
//****************************************** */
|
||||||
* Generate headers to prevent Gmail threading and reduce spam likelihood
|
|
||||||
*/
|
|
||||||
generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
|
generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
|
||||||
const headers: EmailHeaderDto[] = [];
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
// Generate unique Message-ID to prevent threading
|
// Generate unique Message-ID to prevent threading
|
||||||
const messageId = options.messageId || this.generateMessageId(options.businessDomain);
|
const messageId = options.messageId || this.generateMessageId(options.businessDomain);
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Message-ID",
|
key: EmailHeaderKey.MessageId,
|
||||||
value: messageId,
|
value: messageId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add References header with unique value to break threading
|
// Add References header with unique value to break threading
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "References",
|
key: EmailHeaderKey.References,
|
||||||
value: `<${uuidv4()}@${options.businessDomain || "localhost"}>`,
|
value: `<${uuidv4()}@${options.businessDomain || "localhost"}>`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add unique Thread-Index to prevent Outlook threading
|
// Add unique Thread-Index to prevent Outlook threading
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Thread-Index",
|
key: EmailHeaderKey.ThreadIndex,
|
||||||
value: this.generateThreadIndex(),
|
value: this.generateThreadIndex(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Anti-spam headers
|
// Anti-spam headers
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Mailer",
|
key: EmailHeaderKey.XMailer,
|
||||||
value: `${options.businessName || "Email Service"} v1.0`,
|
value: `${options.businessName || "Email Service"} v1.0`,
|
||||||
});
|
});
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Priority",
|
key: EmailHeaderKey.XPriority,
|
||||||
value: this.getPriorityValue(options.priority || "normal"),
|
value: this.getPriorityValue(options.priority || "normal"),
|
||||||
});
|
});
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-MSMail-Priority",
|
key: EmailHeaderKey.XMSMailPriority,
|
||||||
value: options.priority || "Normal",
|
value: options.priority || "Normal",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add MIME version
|
// Add MIME version
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "MIME-Version",
|
key: EmailHeaderKey.MIMEVersion,
|
||||||
value: "1.0",
|
value: "1.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add authentication headers
|
// Add authentication headers
|
||||||
if (options.businessDomain) {
|
if (options.businessDomain) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Authentication-Results",
|
key: EmailHeaderKey.AuthenticationResults,
|
||||||
value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`,
|
value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -91,32 +68,32 @@ export class EmailHeadersService {
|
|||||||
// Content classification headers
|
// Content classification headers
|
||||||
if (options.isTransactional) {
|
if (options.isTransactional) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Auto-Response-Suppress",
|
key: EmailHeaderKey.XAutoResponseSuppress,
|
||||||
value: "All",
|
value: "All",
|
||||||
});
|
});
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Entity-ID",
|
key: EmailHeaderKey.XEntityID,
|
||||||
value: "transactional",
|
value: "transactional",
|
||||||
});
|
});
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Precedence",
|
key: EmailHeaderKey.Precedence,
|
||||||
value: "bulk",
|
value: "bulk",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.isMarketing) {
|
if (options.isMarketing) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Entity-ID",
|
key: EmailHeaderKey.XEntityID,
|
||||||
value: "marketing",
|
value: "marketing",
|
||||||
});
|
});
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "List-Unsubscribe-Post",
|
key: EmailHeaderKey.ListUnsubscribePost,
|
||||||
value: "List-Unsubscribe=One-Click",
|
value: "List-Unsubscribe=One-Click",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.listUnsubscribe) {
|
if (options.listUnsubscribe) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "List-Unsubscribe",
|
key: EmailHeaderKey.ListUnsubscribe,
|
||||||
value: options.listUnsubscribe,
|
value: options.listUnsubscribe,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -125,7 +102,7 @@ export class EmailHeadersService {
|
|||||||
// Add List-ID if provided
|
// Add List-ID if provided
|
||||||
if (options.listId) {
|
if (options.listId) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "List-Id",
|
key: EmailHeaderKey.ListId,
|
||||||
value: options.listId,
|
value: options.listId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,66 +114,68 @@ export class EmailHeadersService {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Spam-Status",
|
key: EmailHeaderKey.XSpamStatus,
|
||||||
value: "No",
|
value: "No",
|
||||||
});
|
});
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Spam-Score",
|
key: EmailHeaderKey.XSpamScore,
|
||||||
value: "0.0",
|
value: "0.0",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add feedback loop headers
|
// Add feedback loop headers
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Feedback-ID",
|
key: EmailHeaderKey.XFeedbackID,
|
||||||
value: `${uuidv4()}:${options.businessDomain || "localhost"}`,
|
value: `${uuidv4()}:${options.businessDomain || "localhost"}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
return headers;
|
// Add entity reference ID
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate headers specifically for preventing Gmail conversation threading
|
|
||||||
*/
|
|
||||||
generateGmailAntiThreadingHeaders(options: IGenerateGmailAntiThreadingHeadersOptions): EmailHeaderDto[] {
|
|
||||||
const headers: EmailHeaderDto[] = [];
|
|
||||||
|
|
||||||
// Generate completely unique Message-ID for each email
|
|
||||||
const uniqueMessageId = this.generateMessageId(options.businessDomain, true);
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Message-ID",
|
key: EmailHeaderKey.XEntityRefID,
|
||||||
value: uniqueMessageId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear any potential threading references
|
|
||||||
headers.push({
|
|
||||||
key: "References",
|
|
||||||
value: `<${uuidv4()}@${options.businessDomain || "localhost"}>`,
|
|
||||||
});
|
|
||||||
|
|
||||||
headers.push({
|
|
||||||
key: "In-Reply-To",
|
|
||||||
value: `<${uuidv4()}@${options.businessDomain || "localhost"}>`,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Gmail-specific thread breaking
|
|
||||||
headers.push({
|
|
||||||
key: "X-Gmail-Thread-Break",
|
|
||||||
value: options.conversationBreaker || uuidv4(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Additional thread prevention
|
|
||||||
headers.push({
|
|
||||||
key: "Thread-Topic",
|
|
||||||
value: uuidv4(),
|
value: uuidv4(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//****************************************** */
|
||||||
* Generate headers for transactional emails (receipts, notifications, etc.)
|
generateGmailAntiThreadingHeaders(options: IGenerateGmailAntiThreadingHeadersOptions): EmailHeaderDto[] {
|
||||||
*/
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
|
// Generate completely unique Message-ID for each email
|
||||||
|
const uniqueMessageId = this.generateMessageId(options.businessDomain, true);
|
||||||
|
headers.push({
|
||||||
|
key: EmailHeaderKey.MessageId,
|
||||||
|
value: uniqueMessageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear any potential threading references
|
||||||
|
headers.push({
|
||||||
|
key: EmailHeaderKey.References,
|
||||||
|
value: `<${uuidv4()}@${options.businessDomain}>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: EmailHeaderKey.InReplyTo,
|
||||||
|
value: `<${uuidv4()}@${options.businessDomain}>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gmail-specific thread breaking
|
||||||
|
headers.push({
|
||||||
|
key: EmailHeaderKey.XGmailThreadBreak,
|
||||||
|
value: options.conversationBreaker || uuidv4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Additional thread prevention
|
||||||
|
headers.push({
|
||||||
|
key: EmailHeaderKey.ThreadTopic,
|
||||||
|
value: uuidv4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
//****************************************** */
|
||||||
generateTransactionalHeaders(options: IGenerateTransactionalHeadersOptions): EmailHeaderDto[] {
|
generateTransactionalHeaders(options: IGenerateTransactionalHeadersOptions): EmailHeaderDto[] {
|
||||||
const headers: EmailHeaderDto[] = [];
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
@@ -211,34 +190,32 @@ export class EmailHeadersService {
|
|||||||
|
|
||||||
// Transactional-specific headers
|
// Transactional-specific headers
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Transaction-ID",
|
key: EmailHeaderKey.XTransactionID,
|
||||||
value: options.transactionId || uuidv4(),
|
value: options.transactionId || uuidv4(),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.category) {
|
if (options.category) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Category",
|
key: EmailHeaderKey.XCategory,
|
||||||
value: options.category,
|
value: options.category,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as important transactional email
|
// Mark as important transactional email
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Importance",
|
key: EmailHeaderKey.Importance,
|
||||||
value: "high",
|
value: "high",
|
||||||
});
|
});
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Auto-Response-Suppress",
|
key: EmailHeaderKey.XAutoResponseSuppress,
|
||||||
value: "All",
|
value: "All",
|
||||||
});
|
});
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
//****************************************** */
|
||||||
* Generate headers for marketing emails
|
|
||||||
*/
|
|
||||||
generateMarketingHeaders(options: IGenerateMarketingHeadersOptions): EmailHeaderDto[] {
|
generateMarketingHeaders(options: IGenerateMarketingHeadersOptions): EmailHeaderDto[] {
|
||||||
const headers: EmailHeaderDto[] = [];
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
@@ -256,38 +233,40 @@ export class EmailHeadersService {
|
|||||||
// Marketing-specific headers
|
// Marketing-specific headers
|
||||||
if (options.campaignId) {
|
if (options.campaignId) {
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Campaign-ID",
|
key: EmailHeaderKey.XCampaignID,
|
||||||
value: options.campaignId,
|
value: options.campaignId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "X-Marketing-Email",
|
key: EmailHeaderKey.XMarketingEmail,
|
||||||
value: "true",
|
value: "true",
|
||||||
});
|
});
|
||||||
|
|
||||||
// Required for bulk marketing emails
|
// Required for bulk marketing emails
|
||||||
headers.push({
|
headers.push({
|
||||||
key: "Precedence",
|
key: EmailHeaderKey.Precedence,
|
||||||
value: "bulk",
|
value: "bulk",
|
||||||
});
|
});
|
||||||
|
|
||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
//****************************************** */
|
||||||
|
|
||||||
private generateMessageId(domain?: string, extraUnique = false): string {
|
private generateMessageId(domain: string, extraUnique = false): string {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const random = Math.random().toString(36).substring(2, 15);
|
const random = Math.random().toString(36).substring(2, 15);
|
||||||
const uuid = extraUnique ? uuidv4() : "";
|
const uuid = extraUnique ? uuidv4() : "";
|
||||||
return `<${timestamp}.${random}${uuid}@${domain || "localhost"}>`;
|
return `<${timestamp}.${random}${uuid}@${domain}>`;
|
||||||
}
|
}
|
||||||
|
//****************************************** */
|
||||||
private generateThreadIndex(): string {
|
private generateThreadIndex(): string {
|
||||||
// Generate a unique thread index for Outlook
|
// Generate a unique thread index for Outlook
|
||||||
const timestamp = Date.now().toString(16);
|
const timestamp = Date.now().toString(16);
|
||||||
const random = Math.random().toString(16).substring(2, 10);
|
const random = Math.random().toString(16).substring(2, 10);
|
||||||
return `${timestamp}${random}`.toUpperCase();
|
return `${timestamp}${random}`.toUpperCase();
|
||||||
}
|
}
|
||||||
|
//****************************************** */
|
||||||
|
|
||||||
private getPriorityValue(priority: "high" | "normal" | "low"): string {
|
private getPriorityValue(priority: "high" | "normal" | "low"): string {
|
||||||
switch (priority) {
|
switch (priority) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
|||||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto";
|
||||||
import { EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
import { EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto";
|
||||||
import { EmailGateway } from "../email.gateway";
|
import { EmailGateway } from "../email.gateway";
|
||||||
|
import { Priority } from "../enums/email-header.enum";
|
||||||
import { EmailDeletePayload, EmailMovePayload, EmailStatusUpdatePayload } from "../interfaces/email-events.interface";
|
import { EmailDeletePayload, EmailMovePayload, EmailStatusUpdatePayload } from "../interfaces/email-events.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -30,9 +31,7 @@ export class EmailService {
|
|||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async sendEmail(userEmailId: string, sendEmailDto: SendEmailDto) {
|
async sendEmail(userEmailId: string, sendEmailDto: SendEmailDto) {
|
||||||
this.logger.log(
|
this.logger.log(`Sending ${EmailType.GENERAL} email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`);
|
||||||
`Sending ${sendEmailDto.emailType || EmailType.GENERAL} email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] });
|
const user = await this.userRepository.findOne({ wildduckUserId: userEmailId, deletedAt: null }, { populate: ["business"] });
|
||||||
@@ -42,7 +41,7 @@ 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.processEmailContent(user.business.id, {
|
const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, user.id, {
|
||||||
text: sendEmailDto.text,
|
text: sendEmailDto.text,
|
||||||
html: sendEmailDto.html,
|
html: sendEmailDto.html,
|
||||||
subject: sendEmailDto.subject,
|
subject: sendEmailDto.subject,
|
||||||
@@ -67,7 +66,7 @@ export class EmailService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Generate headers based on email type
|
// Generate headers based on email type
|
||||||
const headers = this.generateHeadersForEmailType(sendEmailDto, user);
|
const headers = this.generateHeadersForEmailType(user);
|
||||||
|
|
||||||
// Merge with existing headers if any
|
// Merge with existing headers if any
|
||||||
sendEmailDto.headers = [...(sendEmailDto.headers || []), ...headers];
|
sendEmailDto.headers = [...(sendEmailDto.headers || []), ...headers];
|
||||||
@@ -91,64 +90,6 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
|
||||||
private generateHeadersForEmailType(sendEmailDto: SendEmailDto, user: User) {
|
|
||||||
const emailType = sendEmailDto.emailType || EmailType.GENERAL;
|
|
||||||
const businessName = user.business.name;
|
|
||||||
const businessDomain = user.emailAddress.split("@")[1];
|
|
||||||
|
|
||||||
switch (emailType) {
|
|
||||||
case EmailType.TRANSACTIONAL:
|
|
||||||
return this.emailHeadersService.generateTransactionalHeaders({
|
|
||||||
businessName,
|
|
||||||
businessDomain,
|
|
||||||
transactionId: sendEmailDto.transactionId,
|
|
||||||
category: sendEmailDto.category,
|
|
||||||
});
|
|
||||||
|
|
||||||
case EmailType.MARKETING:
|
|
||||||
if (!sendEmailDto.listId || !sendEmailDto.unsubscribeUrl) {
|
|
||||||
throw new BadRequestException("Marketing emails require listId and unsubscribeUrl");
|
|
||||||
}
|
|
||||||
return this.emailHeadersService.generateMarketingHeaders({
|
|
||||||
businessName,
|
|
||||||
businessDomain,
|
|
||||||
listId: sendEmailDto.listId,
|
|
||||||
unsubscribeUrl: sendEmailDto.unsubscribeUrl,
|
|
||||||
campaignId: sendEmailDto.campaignId,
|
|
||||||
});
|
|
||||||
|
|
||||||
case EmailType.ANTI_THREADING: {
|
|
||||||
const antiThreadingHeaders = this.emailHeadersService.generateAntiThreadingHeaders({
|
|
||||||
businessName,
|
|
||||||
businessDomain,
|
|
||||||
isTransactional: true,
|
|
||||||
priority: sendEmailDto.priority,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add Gmail-specific headers if requested
|
|
||||||
if (sendEmailDto.forceGmailBreak) {
|
|
||||||
const gmailHeaders = this.emailHeadersService.generateGmailAntiThreadingHeaders({
|
|
||||||
businessDomain,
|
|
||||||
conversationBreaker: sendEmailDto.conversationBreaker,
|
|
||||||
});
|
|
||||||
antiThreadingHeaders.push(...gmailHeaders);
|
|
||||||
}
|
|
||||||
|
|
||||||
return antiThreadingHeaders;
|
|
||||||
}
|
|
||||||
|
|
||||||
case EmailType.GENERAL:
|
|
||||||
default:
|
|
||||||
return this.emailHeadersService.generateAntiThreadingHeaders({
|
|
||||||
businessName,
|
|
||||||
businessDomain,
|
|
||||||
isTransactional: true,
|
|
||||||
priority: sendEmailDto.priority,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async getEmailStatus(queueId: string) {
|
async getEmailStatus(queueId: string) {
|
||||||
this.logger.log(`Getting email status for queue: ${queueId}`);
|
this.logger.log(`Getting email status for queue: ${queueId}`);
|
||||||
@@ -234,42 +175,6 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
|
||||||
|
|
||||||
private async findMessageMailbox(userId: string, messageId: number): Promise<{ mailboxId: string; mailboxName: string } | null> {
|
|
||||||
try {
|
|
||||||
const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId);
|
|
||||||
|
|
||||||
const mailboxesToSearch = [
|
|
||||||
{ id: mailboxIds.inbox, name: MailboxEnum.INBOX },
|
|
||||||
{ id: mailboxIds.sent, name: MailboxEnum.SENT },
|
|
||||||
{ id: mailboxIds.drafts, name: MailboxEnum.DRAFTS },
|
|
||||||
{ id: mailboxIds.trash, name: MailboxEnum.TRASH },
|
|
||||||
{ id: mailboxIds.archive, name: MailboxEnum.ARCHIVE },
|
|
||||||
{ id: mailboxIds.favorite, name: MailboxEnum.FAVORITE },
|
|
||||||
{ id: mailboxIds.junk, name: MailboxEnum.Junk },
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const mailbox of mailboxesToSearch) {
|
|
||||||
if (!mailbox.id) continue;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await firstValueFrom(this.mailServerService.messages.getMessage(userId, mailbox.id, messageId));
|
|
||||||
this.logger.log(`Message ${messageId} found in ${mailbox.name} mailbox`);
|
|
||||||
return { mailboxId: mailbox.id, mailboxName: mailbox.name };
|
|
||||||
} catch (_error) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.logger.warn(`Message ${messageId} not found in any mailbox for user ${userId}`);
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to find message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async deleteMessage(userId: string, messageId: number) {
|
async deleteMessage(userId: string, messageId: number) {
|
||||||
try {
|
try {
|
||||||
@@ -459,7 +364,7 @@ export class EmailService {
|
|||||||
const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(userEmailId, draftsMailboxId, messageId));
|
const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(userEmailId, draftsMailboxId, messageId));
|
||||||
|
|
||||||
const draftData: SendEmailDto = {
|
const draftData: SendEmailDto = {
|
||||||
mailbox: updateDraftDto.mailbox,
|
// mailbox: updateDraftDto.mailbox,
|
||||||
from: updateDraftDto.from || { address: existingDraft.from?.address || "" },
|
from: updateDraftDto.from || { address: existingDraft.from?.address || "" },
|
||||||
to: updateDraftDto.to || [],
|
to: updateDraftDto.to || [],
|
||||||
cc: updateDraftDto.cc,
|
cc: updateDraftDto.cc,
|
||||||
@@ -486,7 +391,7 @@ 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.processEmailContent(user.business.id, {
|
const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, user.id, {
|
||||||
text: updateDraftDto.text,
|
text: updateDraftDto.text,
|
||||||
html: updateDraftDto.html,
|
html: updateDraftDto.html,
|
||||||
subject: updateDraftDto.subject,
|
subject: updateDraftDto.subject,
|
||||||
@@ -955,4 +860,105 @@ export class EmailService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//########################################################
|
||||||
|
|
||||||
|
private async findMessageMailbox(userId: string, messageId: number): Promise<{ mailboxId: string; mailboxName: string } | null> {
|
||||||
|
try {
|
||||||
|
const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId);
|
||||||
|
|
||||||
|
const mailboxesToSearch = [
|
||||||
|
{ id: mailboxIds.inbox, name: MailboxEnum.INBOX },
|
||||||
|
{ id: mailboxIds.sent, name: MailboxEnum.SENT },
|
||||||
|
{ id: mailboxIds.drafts, name: MailboxEnum.DRAFTS },
|
||||||
|
{ id: mailboxIds.trash, name: MailboxEnum.TRASH },
|
||||||
|
{ id: mailboxIds.archive, name: MailboxEnum.ARCHIVE },
|
||||||
|
{ id: mailboxIds.favorite, name: MailboxEnum.FAVORITE },
|
||||||
|
{ id: mailboxIds.junk, name: MailboxEnum.Junk },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const mailbox of mailboxesToSearch) {
|
||||||
|
if (!mailbox.id) continue;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await firstValueFrom(this.mailServerService.messages.getMessage(userId, mailbox.id, messageId));
|
||||||
|
this.logger.log(`Message ${messageId} found in ${mailbox.name} mailbox`);
|
||||||
|
return { mailboxId: mailbox.id, mailboxName: mailbox.name };
|
||||||
|
} catch (_error) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.warn(`Message ${messageId} not found in any mailbox for user ${userId}`);
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to find message ${messageId} for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//########################################################
|
||||||
|
private generateHeadersForEmailType(user: User) {
|
||||||
|
// const emailType = sendEmailDto.emailType || EmailType.GENERAL;
|
||||||
|
const businessName = user.business.name;
|
||||||
|
const businessDomain = user.emailAddress.split("@")[1];
|
||||||
|
|
||||||
|
return this.emailHeadersService.generateAntiThreadingHeaders({
|
||||||
|
businessName,
|
||||||
|
businessDomain,
|
||||||
|
isTransactional: true,
|
||||||
|
priority: Priority.Normal,
|
||||||
|
});
|
||||||
|
|
||||||
|
// switch (emailType) {
|
||||||
|
// case EmailType.TRANSACTIONAL:
|
||||||
|
// return this.emailHeadersService.generateTransactionalHeaders({
|
||||||
|
// businessName,
|
||||||
|
// businessDomain,
|
||||||
|
// transactionId: sendEmailDto.transactionId,
|
||||||
|
// category: sendEmailDto.category,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// case EmailType.MARKETING:
|
||||||
|
// if (!sendEmailDto.listId || !sendEmailDto.unsubscribeUrl) {
|
||||||
|
// throw new BadRequestException("Marketing emails require listId and unsubscribeUrl");
|
||||||
|
// }
|
||||||
|
// return this.emailHeadersService.generateMarketingHeaders({
|
||||||
|
// businessName,
|
||||||
|
// businessDomain,
|
||||||
|
// listId: sendEmailDto.listId,
|
||||||
|
// unsubscribeUrl: sendEmailDto.unsubscribeUrl,
|
||||||
|
// campaignId: sendEmailDto.campaignId,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// case EmailType.ANTI_THREADING: {
|
||||||
|
// const antiThreadingHeaders = this.emailHeadersService.generateAntiThreadingHeaders({
|
||||||
|
// businessName,
|
||||||
|
// businessDomain,
|
||||||
|
// isTransactional: true,
|
||||||
|
// priority: sendEmailDto.priority as Priority,
|
||||||
|
// });
|
||||||
|
|
||||||
|
// Add Gmail-specific headers if requested
|
||||||
|
// if (sendEmailDto.forceGmailBreak) {
|
||||||
|
// const gmailHeaders = this.emailHeadersService.generateGmailAntiThreadingHeaders({
|
||||||
|
// businessDomain,
|
||||||
|
// conversationBreaker: sendEmailDto.conversationBreaker,
|
||||||
|
// });
|
||||||
|
// antiThreadingHeaders.push(...gmailHeaders);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return antiThreadingHeaders;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// case EmailType.GENERAL:
|
||||||
|
// default:
|
||||||
|
// return this.emailHeadersService.generateAntiThreadingHeaders({
|
||||||
|
// businessName,
|
||||||
|
// businessDomain,
|
||||||
|
// isTransactional: true,
|
||||||
|
// priority: Priority.Normal,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,24 @@
|
|||||||
import { Injectable, Logger } from "@nestjs/common";
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
|
|
||||||
import { TemplatesService } from "./templates.service";
|
import { TemplatesService } from "./templates.service";
|
||||||
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { EmailContent, ProcessedEmailResult } from "../interfaces/structure.interface";
|
import { EmailContent, ProcessedEmailResult } from "../interfaces/structure.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TemplateProcessorService {
|
export class TemplateProcessorService {
|
||||||
private readonly logger = new Logger(TemplateProcessorService.name);
|
private readonly logger = new Logger(TemplateProcessorService.name);
|
||||||
|
|
||||||
constructor(private readonly templatesService: TemplatesService) {}
|
constructor(
|
||||||
|
private readonly templatesService: TemplatesService,
|
||||||
|
private readonly userRepository: UserRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
async processEmailContent(businessId: string, emailContent: EmailContent): Promise<ProcessedEmailResult> {
|
async processEmailContent(businessId: string, userId: string, emailContent: EmailContent): Promise<ProcessedEmailResult> {
|
||||||
try {
|
try {
|
||||||
// Get selected template for the business
|
// Get selected template for the business
|
||||||
const selectedTemplate = await this.getSelectedTemplate(businessId);
|
const selectedTemplate = await this.getSelectedTemplate(businessId, userId);
|
||||||
|
|
||||||
if (!selectedTemplate) {
|
if (!selectedTemplate) {
|
||||||
// No template - return original text content
|
// No template - return original text content
|
||||||
@@ -45,7 +50,12 @@ export class TemplateProcessorService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//**************************************************** */
|
//**************************************************** */
|
||||||
private async getSelectedTemplate(businessId: string) {
|
private async getSelectedTemplate(businessId: string, userId: string) {
|
||||||
|
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId } });
|
||||||
|
if (!user) throw new BadRequestException(UserMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (user.template) return user.template;
|
||||||
|
|
||||||
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
const templates = await this.templatesService.getTemplatesByBusinessId(businessId);
|
||||||
return templates.find((template) => template.selected) || null;
|
return templates.find((template) => template.selected) || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import { TemplatesService } from "./services/templates.service";
|
|||||||
import { TemplatesController } from "./templates.controller";
|
import { TemplatesController } from "./templates.controller";
|
||||||
import { BusinessesModule } from "../businesses/businesses.module";
|
import { BusinessesModule } from "../businesses/businesses.module";
|
||||||
import { Business } from "../businesses/entities/business.entity";
|
import { Business } from "../businesses/entities/business.entity";
|
||||||
|
import { User } from "../users/entities/user.entity";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MikroOrmModule.forFeature([Template, Business]), BusinessesModule],
|
imports: [MikroOrmModule.forFeature([Template, Business, User]), BusinessesModule],
|
||||||
providers: [TemplatesService, TemplateProcessorService],
|
providers: [TemplatesService, TemplateProcessorService],
|
||||||
controllers: [TemplatesController],
|
controllers: [TemplatesController],
|
||||||
exports: [TemplatesService, TemplateProcessorService],
|
exports: [TemplatesService, TemplateProcessorService],
|
||||||
|
|||||||
@@ -47,4 +47,10 @@ export class CreateEmailUserDto {
|
|||||||
@Transform(({ value }) => value?.trim())
|
@Transform(({ value }) => value?.trim())
|
||||||
@ApiProperty({ description: "User role/title", example: "Marketing Manager" })
|
@ApiProperty({ description: "User role/title", example: "Marketing Manager" })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: UserMessage.TEMPLATE_NOT_EMPTY })
|
||||||
|
@IsUUID("7", { message: UserMessage.TEMPLATE_UUID })
|
||||||
|
@ApiPropertyOptional({ description: "Template ID", example: "713e5000-0000-0000-0000-000000000000" })
|
||||||
|
templateId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
import { Transform } from "class-transformer";
|
import { Transform } from "class-transformer";
|
||||||
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, Matches, MinLength } from "class-validator";
|
import { IsArray, IsNotEmpty, IsNumber, IsOptional, IsString, IsUUID, Matches, MinLength } from "class-validator";
|
||||||
|
|
||||||
import { UserMessage } from "../../../common/enums/message.enum";
|
import { UserMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
@@ -38,4 +38,10 @@ export class UpdateEmailUserDto {
|
|||||||
@Transform(({ value }) => value?.trim())
|
@Transform(({ value }) => value?.trim())
|
||||||
@ApiPropertyOptional({ description: "User role/title", example: "Senior Marketing Manager" })
|
@ApiPropertyOptional({ description: "User role/title", example: "Senior Marketing Manager" })
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNotEmpty({ message: UserMessage.TEMPLATE_NOT_EMPTY })
|
||||||
|
@IsUUID("7", { message: UserMessage.TEMPLATE_UUID })
|
||||||
|
@ApiPropertyOptional({ description: "Template ID", example: "713e5000-0000-0000-0000-000000000000" })
|
||||||
|
templateId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { EntityManager } from "@mikro-orm/postgresql";
|
import { EntityManager } from "@mikro-orm/postgresql";
|
||||||
import { BadRequestException, Injectable, Logger, NotFoundException } from "@nestjs/common";
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, UserMessage } from "../../../common/enums/message.enum";
|
import { BusinessMessage, DomainMessage, MailServerMessage, RoleMessage, TemplateMessage, UserMessage } from "../../../common/enums/message.enum";
|
||||||
import { QUOTA_CONSTANTS } from "../../businesses/constant";
|
import { QUOTA_CONSTANTS } from "../../businesses/constant";
|
||||||
import { Business } from "../../businesses/entities/business.entity";
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
import { DomainStatus } from "../../domains/enums/domain-status.enum";
|
||||||
@@ -10,6 +10,7 @@ import { DomainAutomationService } from "../../domains/services/domain-automatio
|
|||||||
import { DomainsService } from "../../domains/services/domains.service";
|
import { DomainsService } from "../../domains/services/domains.service";
|
||||||
import { UpdateUserDto } from "../../mail-server/DTO";
|
import { UpdateUserDto } from "../../mail-server/DTO";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
|
import { Template } from "../../templates/entities/template.entity";
|
||||||
import { PasswordService } from "../../utils/services/password.service";
|
import { PasswordService } from "../../utils/services/password.service";
|
||||||
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
|
import { CreateEmailUserDto } from "../DTO/create-email-user.dto";
|
||||||
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
|
import { UpdateEmailUserDto } from "../DTO/update-email-user.dto";
|
||||||
@@ -80,7 +81,7 @@ export class UsersService {
|
|||||||
|
|
||||||
/*******************************/
|
/*******************************/
|
||||||
async createEmailUser(createEmailUserDto: CreateEmailUserDto, businessId: string) {
|
async createEmailUser(createEmailUserDto: CreateEmailUserDto, businessId: string) {
|
||||||
const { username, password, domainId, displayName, quota, aliases, title } = createEmailUserDto;
|
const { username, password, domainId, displayName, quota, aliases, title, templateId } = createEmailUserDto;
|
||||||
|
|
||||||
const business = await this.em.findOne(Business, { id: businessId });
|
const business = await this.em.findOne(Business, { id: businessId });
|
||||||
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
if (!business) throw new BadRequestException(BusinessMessage.NOT_FOUND);
|
||||||
@@ -145,6 +146,12 @@ export class UsersService {
|
|||||||
|
|
||||||
await this.em.persistAndFlush([user, business]);
|
await this.em.persistAndFlush([user, business]);
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
const template = await this.em.findOne(Template, { id: templateId, business: { id: businessId } });
|
||||||
|
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
||||||
|
user.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
if (aliases && aliases.length > 0) {
|
if (aliases && aliases.length > 0) {
|
||||||
for (const alias of aliases) {
|
for (const alias of aliases) {
|
||||||
try {
|
try {
|
||||||
@@ -160,6 +167,8 @@ export class UsersService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
this.logger.log(`Email user created: ${emailAddress} for business ${businessId}`);
|
this.logger.log(`Email user created: ${emailAddress} for business ${businessId}`);
|
||||||
|
|
||||||
await this.domainAutomationService.createDefaultMailboxes(mailServerUser.id);
|
await this.domainAutomationService.createDefaultMailboxes(mailServerUser.id);
|
||||||
@@ -199,7 +208,7 @@ export class UsersService {
|
|||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (user.wildduckUserId) {
|
if (user.wildduckUserId) {
|
||||||
@@ -224,7 +233,7 @@ export class UsersService {
|
|||||||
/*******************************/
|
/*******************************/
|
||||||
|
|
||||||
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
|
async updateEmailUser(userId: string, businessId: string, updateEmailUserDto: UpdateEmailUserDto) {
|
||||||
const { password, displayName, quota, aliases, title } = updateEmailUserDto;
|
const { password, displayName, quota, aliases, title, templateId } = updateEmailUserDto;
|
||||||
|
|
||||||
const user = await this.userRepository.findOne(
|
const user = await this.userRepository.findOne(
|
||||||
{
|
{
|
||||||
@@ -235,7 +244,13 @@ export class UsersService {
|
|||||||
{ populate: ["business"] },
|
{ populate: ["business"] },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
|
if (templateId) {
|
||||||
|
const template = await this.em.findOne(Template, { id: templateId, business: { id: businessId } });
|
||||||
|
if (!template) throw new BadRequestException(TemplateMessage.TEMPLATE_NOT_FOUND);
|
||||||
|
user.template = template;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate business quota if quota is being updated
|
// Validate business quota if quota is being updated
|
||||||
if (quota) {
|
if (quota) {
|
||||||
@@ -335,7 +350,7 @@ export class UsersService {
|
|||||||
{ populate: ["business"] },
|
{ populate: ["business"] },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
// Validate business quota before updating
|
// Validate business quota before updating
|
||||||
const currentQuota = user.emailQuota || 0;
|
const currentQuota = user.emailQuota || 0;
|
||||||
@@ -370,7 +385,7 @@ export class UsersService {
|
|||||||
|
|
||||||
async updateEmailUserStatus(userId: string, businessId: string) {
|
async updateEmailUserStatus(userId: string, businessId: string) {
|
||||||
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true });
|
const user = await this.userRepository.findOne({ id: userId, business: { id: businessId }, emailEnabled: true });
|
||||||
if (!user) throw new NotFoundException(UserMessage.EMAIL_USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(UserMessage.EMAIL_USER_NOT_FOUND);
|
||||||
|
|
||||||
user.isActive = !user.isActive;
|
user.isActive = !user.isActive;
|
||||||
await this.em.flush();
|
await this.em.flush();
|
||||||
|
|||||||
Reference in New Issue
Block a user