chore: add anti spamming and headers to email send
This commit is contained in:
@@ -211,6 +211,7 @@ export const enum EmailMessage {
|
|||||||
BULK_ACTION_COMPLETED_SUCCESSFULLY = "عملیات گروهی با موفقیت انجام شد",
|
BULK_ACTION_COMPLETED_SUCCESSFULLY = "عملیات گروهی با موفقیت انجام شد",
|
||||||
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد",
|
BULK_ACTION_PARTIALLY_COMPLETED = "عملیات گروهی تا حدی انجام شد",
|
||||||
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد",
|
BULK_ACTION_FAILED = "عملیات گروهی با خطا مواجه شد",
|
||||||
|
USER_NOT_FOUND = "کاربر یافت نشد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WebSocketMessage {
|
export const enum WebSocketMessage {
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ import { IsArray, IsBoolean, IsDateString, IsEmail, IsEnum, IsNotEmpty, IsNumber
|
|||||||
|
|
||||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||||
|
|
||||||
|
//########################################################
|
||||||
|
export enum EmailType {
|
||||||
|
GENERAL = "general",
|
||||||
|
TRANSACTIONAL = "transactional",
|
||||||
|
MARKETING = "marketing",
|
||||||
|
ANTI_THREADING = "anti-threading",
|
||||||
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
export class EmailRecipientDto {
|
export class EmailRecipientDto {
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@@ -99,6 +107,56 @@ 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()
|
@IsOptional()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString({ message: EmailMessage.MAILBOX_MUST_BE_STRING })
|
@IsString({ message: EmailMessage.MAILBOX_MUST_BE_STRING })
|
||||||
|
|||||||
@@ -16,9 +16,13 @@ export class EmailController {
|
|||||||
constructor(private readonly emailService: EmailService) {}
|
constructor(private readonly emailService: EmailService) {}
|
||||||
|
|
||||||
@Post("send")
|
@Post("send")
|
||||||
@ApiOperation({ summary: "Send an email" })
|
@ApiOperation({
|
||||||
|
summary: "Send an email with configurable headers based on type",
|
||||||
|
description:
|
||||||
|
"Send emails with automatic anti-threading and anti-spam headers. Supports different email types: general, transactional, marketing, anti-threading. Marketing emails require listId and unsubscribeUrl.",
|
||||||
|
})
|
||||||
@ApiResponse({ status: 201, description: "Email sent successfully" })
|
@ApiResponse({ status: 201, description: "Email sent successfully" })
|
||||||
@ApiResponse({ status: 400, description: "Invalid email data" })
|
@ApiResponse({ status: 400, description: "Invalid email data or missing required fields for email type" })
|
||||||
@ApiResponse({ status: 403, description: "Not authorized to send email" })
|
@ApiResponse({ status: 403, description: "Not authorized to send email" })
|
||||||
sendEmail(@UserDec("wildduckUserId") userEmailId: string, @Body() sendEmailDto: SendEmailDto) {
|
sendEmail(@UserDec("wildduckUserId") userEmailId: string, @Body() sendEmailDto: SendEmailDto) {
|
||||||
return this.emailService.sendEmail(userEmailId, sendEmailDto);
|
return this.emailService.sendEmail(userEmailId, sendEmailDto);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { JwtModule } from "@nestjs/jwt";
|
|||||||
|
|
||||||
import { EmailController } from "./email.controller";
|
import { EmailController } from "./email.controller";
|
||||||
import { EmailGateway } from "./email.gateway";
|
import { EmailGateway } from "./email.gateway";
|
||||||
|
import { EmailHeadersService } from "./services/email-headers.service";
|
||||||
import { EmailNotificationService } from "./services/email-notification.service";
|
import { EmailNotificationService } from "./services/email-notification.service";
|
||||||
import { EmailService } from "./services/email.service";
|
import { EmailService } from "./services/email.service";
|
||||||
import { MailboxResolverService } from "./services/mailbox-resolver.service";
|
import { MailboxResolverService } from "./services/mailbox-resolver.service";
|
||||||
@@ -17,7 +18,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],
|
providers: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService, EmailHeadersService],
|
||||||
exports: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService],
|
exports: [EmailService, MailboxResolverService, EmailGateway, EmailNotificationService, WebSocketAuthService, EmailHeadersService],
|
||||||
})
|
})
|
||||||
export class EmailModule {}
|
export class EmailModule {}
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { EmailHeaderDto } from "../DTO/send-email.dto";
|
||||||
|
|
||||||
|
export const EMAIL_HEADERS_SERVICE = Symbol("EMAIL_HEADERS_SERVICE");
|
||||||
|
|
||||||
|
export interface IGenerateAntiThreadingHeadersOptions {
|
||||||
|
messageId?: string;
|
||||||
|
listId?: string;
|
||||||
|
listUnsubscribe?: string;
|
||||||
|
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()
|
||||||
|
export class EmailHeadersService {
|
||||||
|
/**
|
||||||
|
* Generate headers to prevent Gmail threading and reduce spam likelihood
|
||||||
|
*/
|
||||||
|
generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
|
||||||
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
|
// Generate unique Message-ID to prevent threading
|
||||||
|
const messageId = options.messageId || this.generateMessageId(options.businessDomain);
|
||||||
|
headers.push({
|
||||||
|
key: "Message-ID",
|
||||||
|
value: messageId,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add References header with unique value to break threading
|
||||||
|
headers.push({
|
||||||
|
key: "References",
|
||||||
|
value: `<${uuidv4()}@${options.businessDomain || "localhost"}>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add unique Thread-Index to prevent Outlook threading
|
||||||
|
headers.push({
|
||||||
|
key: "Thread-Index",
|
||||||
|
value: this.generateThreadIndex(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Anti-spam headers
|
||||||
|
headers.push({
|
||||||
|
key: "X-Mailer",
|
||||||
|
value: `${options.businessName || "Email Service"} v1.0`,
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-Priority",
|
||||||
|
value: this.getPriorityValue(options.priority || "normal"),
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-MSMail-Priority",
|
||||||
|
value: options.priority || "Normal",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add MIME version
|
||||||
|
headers.push({
|
||||||
|
key: "MIME-Version",
|
||||||
|
value: "1.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add authentication headers
|
||||||
|
if (options.businessDomain) {
|
||||||
|
headers.push({
|
||||||
|
key: "Authentication-Results",
|
||||||
|
value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content classification headers
|
||||||
|
if (options.isTransactional) {
|
||||||
|
headers.push({
|
||||||
|
key: "X-Auto-Response-Suppress",
|
||||||
|
value: "All",
|
||||||
|
});
|
||||||
|
headers.push({
|
||||||
|
key: "X-Entity-ID",
|
||||||
|
value: "transactional",
|
||||||
|
});
|
||||||
|
headers.push({
|
||||||
|
key: "Precedence",
|
||||||
|
value: "bulk",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.isMarketing) {
|
||||||
|
headers.push({
|
||||||
|
key: "X-Entity-ID",
|
||||||
|
value: "marketing",
|
||||||
|
});
|
||||||
|
headers.push({
|
||||||
|
key: "List-Unsubscribe-Post",
|
||||||
|
value: "List-Unsubscribe=One-Click",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.listUnsubscribe) {
|
||||||
|
headers.push({
|
||||||
|
key: "List-Unsubscribe",
|
||||||
|
value: options.listUnsubscribe,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add List-ID if provided
|
||||||
|
if (options.listId) {
|
||||||
|
headers.push({
|
||||||
|
key: "List-Id",
|
||||||
|
value: options.listId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom headers to help with deliverability
|
||||||
|
// headers.push({
|
||||||
|
// key: "X-Originating-IP",
|
||||||
|
// value: "[" + this.getServerIP() + "]",
|
||||||
|
// });
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-Spam-Status",
|
||||||
|
value: "No",
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-Spam-Score",
|
||||||
|
value: "0.0",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add feedback loop headers
|
||||||
|
headers.push({
|
||||||
|
key: "X-Feedback-ID",
|
||||||
|
value: `${uuidv4()}:${options.businessDomain || "localhost"}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
||||||
|
key: "Message-ID",
|
||||||
|
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(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate headers for transactional emails (receipts, notifications, etc.)
|
||||||
|
*/
|
||||||
|
generateTransactionalHeaders(options: IGenerateTransactionalHeadersOptions): EmailHeaderDto[] {
|
||||||
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
|
// Base anti-threading headers
|
||||||
|
headers.push(
|
||||||
|
...this.generateAntiThreadingHeaders({
|
||||||
|
businessName: options.businessName,
|
||||||
|
businessDomain: options.businessDomain,
|
||||||
|
isTransactional: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Transactional-specific headers
|
||||||
|
headers.push({
|
||||||
|
key: "X-Transaction-ID",
|
||||||
|
value: options.transactionId || uuidv4(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.category) {
|
||||||
|
headers.push({
|
||||||
|
key: "X-Category",
|
||||||
|
value: options.category,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as important transactional email
|
||||||
|
headers.push({
|
||||||
|
key: "Importance",
|
||||||
|
value: "high",
|
||||||
|
});
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-Auto-Response-Suppress",
|
||||||
|
value: "All",
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate headers for marketing emails
|
||||||
|
*/
|
||||||
|
generateMarketingHeaders(options: IGenerateMarketingHeadersOptions): EmailHeaderDto[] {
|
||||||
|
const headers: EmailHeaderDto[] = [];
|
||||||
|
|
||||||
|
// Base anti-threading headers
|
||||||
|
headers.push(
|
||||||
|
...this.generateAntiThreadingHeaders({
|
||||||
|
businessName: options.businessName,
|
||||||
|
businessDomain: options.businessDomain,
|
||||||
|
isMarketing: true,
|
||||||
|
listId: options.listId,
|
||||||
|
listUnsubscribe: `<${options.unsubscribeUrl}>`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Marketing-specific headers
|
||||||
|
if (options.campaignId) {
|
||||||
|
headers.push({
|
||||||
|
key: "X-Campaign-ID",
|
||||||
|
value: options.campaignId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.push({
|
||||||
|
key: "X-Marketing-Email",
|
||||||
|
value: "true",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Required for bulk marketing emails
|
||||||
|
headers.push({
|
||||||
|
key: "Precedence",
|
||||||
|
value: "bulk",
|
||||||
|
});
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateMessageId(domain?: string, extraUnique = false): string {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.random().toString(36).substring(2, 15);
|
||||||
|
const uuid = extraUnique ? uuidv4() : "";
|
||||||
|
return `<${timestamp}.${random}${uuid}@${domain || "localhost"}>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateThreadIndex(): string {
|
||||||
|
// Generate a unique thread index for Outlook
|
||||||
|
const timestamp = Date.now().toString(16);
|
||||||
|
const random = Math.random().toString(16).substring(2, 10);
|
||||||
|
return `${timestamp}${random}`.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPriorityValue(priority: "high" | "normal" | "low"): string {
|
||||||
|
switch (priority) {
|
||||||
|
case "high":
|
||||||
|
return "1";
|
||||||
|
case "low":
|
||||||
|
return "5";
|
||||||
|
default:
|
||||||
|
return "3";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private getServerIP(): string {
|
||||||
|
// // In a real implementation, you might want to get the actual server IP
|
||||||
|
// // For now, returning a placeholder
|
||||||
|
// return "127.0.0.1";
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1,15 +1,17 @@
|
|||||||
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { EmailHeadersService } from "./email-headers.service";
|
||||||
import { MailboxResolverService } from "./mailbox-resolver.service";
|
import { MailboxResolverService } from "./mailbox-resolver.service";
|
||||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
||||||
import { TemplateProcessorService } from "../../templates/services/template-processor.service";
|
import { TemplateProcessorService } from "../../templates/services/template-processor.service";
|
||||||
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { UserRepository } from "../../users/repositories/user.repository";
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
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 { 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 { EmailDeletePayload, EmailMovePayload, EmailStatusUpdatePayload } from "../interfaces/email-events.interface";
|
import { EmailDeletePayload, EmailMovePayload, EmailStatusUpdatePayload } from "../interfaces/email-events.interface";
|
||||||
|
|
||||||
@@ -23,17 +25,20 @@ export class EmailService {
|
|||||||
private readonly emailGateway: EmailGateway,
|
private readonly emailGateway: EmailGateway,
|
||||||
private readonly templateProcessorService: TemplateProcessorService,
|
private readonly templateProcessorService: TemplateProcessorService,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly emailHeadersService: EmailHeadersService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async sendEmail(userEmailId: string, sendEmailDto: SendEmailDto) {
|
async sendEmail(userEmailId: string, sendEmailDto: SendEmailDto) {
|
||||||
this.logger.log(`Sending email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`);
|
this.logger.log(
|
||||||
|
`Sending ${sendEmailDto.emailType || EmailType.GENERAL} email from user ${userEmailId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get user's business information
|
|
||||||
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) {
|
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
||||||
|
|
||||||
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
|
||||||
@@ -55,9 +60,17 @@ export class EmailService {
|
|||||||
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}`);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.logger.log(`No business template found for user ${userEmailId}, sending original content`);
|
sendEmailDto.from = {
|
||||||
}
|
address: user.emailAddress,
|
||||||
|
name: user.displayName,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate headers based on email type
|
||||||
|
const headers = this.generateHeadersForEmailType(sendEmailDto, user);
|
||||||
|
|
||||||
|
// Merge with existing headers if any
|
||||||
|
sendEmailDto.headers = [...(sendEmailDto.headers || []), ...headers];
|
||||||
|
|
||||||
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userEmailId, sendEmailDto));
|
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(userEmailId, sendEmailDto));
|
||||||
|
|
||||||
@@ -78,6 +91,64 @@ 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}`);
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export class UsersService {
|
|||||||
|
|
||||||
const emailAddress = `${username}@${domain.name}`;
|
const emailAddress = `${username}@${domain.name}`;
|
||||||
const finalUserName = `${username}-${domain.name}`;
|
const finalUserName = `${username}-${domain.name}`;
|
||||||
|
const finalDisplayName = `${username}`;
|
||||||
|
|
||||||
const existingUser = await this.userRepository.findOne({ emailAddress });
|
const existingUser = await this.userRepository.findOne({ emailAddress });
|
||||||
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
|
if (existingUser) throw new BadRequestException(UserMessage.EMAIL_ADDRESS_ALREADY_EXISTS);
|
||||||
@@ -131,7 +132,7 @@ export class UsersService {
|
|||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
emailQuota: userQuota,
|
emailQuota: userQuota,
|
||||||
wildduckUserId: mailServerUser.id,
|
wildduckUserId: mailServerUser.id,
|
||||||
displayName: displayName || username,
|
displayName: finalDisplayName || displayName || username,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
business,
|
business,
|
||||||
domain,
|
domain,
|
||||||
|
|||||||
Reference in New Issue
Block a user