Files
dmail-api/src/modules/email/services/email-headers.service.ts
T
2025-07-21 09:25:05 +03:30

351 lines
9.5 KiB
TypeScript

import { Injectable } from "@nestjs/common";
import { v4 as uuidv4 } from "uuid";
import { EmailHeaderDto } from "../DTO/send-email.dto";
import { EmailHeaderKey, Priority } from "../enums/email-header.enum";
import {
IGenerateAntiThreadingHeadersOptions,
IGenerateGmailAntiThreadingHeadersOptions,
IGenerateMarketingHeadersOptions,
IGenerateTransactionalHeadersOptions,
IImageDetectionResult,
} from "../interfaces/email-header.interface";
@Injectable()
export class EmailHeadersService {
private readonly hostDomain = "danakcorp.com";
private readonly hostDomain2 = "storage.danakcorp.com";
//****************************************** */
generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
const headers: EmailHeaderDto[] = [];
headers.push({
key: EmailHeaderKey.References,
value: `<${uuidv4()}@${options.businessDomain}>`,
});
headers.push({
key: EmailHeaderKey.ThreadIndex,
value: this.generateThreadIndex(),
});
headers.push({
key: EmailHeaderKey.XMailer,
value: `Danak Mail Service v1.0`,
});
headers.push({
key: EmailHeaderKey.XPriority,
value: this.getPriorityValue(options.priority || Priority.Normal),
});
headers.push({
key: EmailHeaderKey.XMSMailPriority,
value: options.priority || Priority.Normal,
});
headers.push({
key: EmailHeaderKey.MIMEVersion,
value: "1.0",
});
headers.push({
key: EmailHeaderKey.XSpamStatus,
value: "No",
});
headers.push({
key: EmailHeaderKey.XSpamScore,
value: "0.0",
});
headers.push({
key: EmailHeaderKey.XFeedbackID,
value: `${uuidv4()}:${options.businessDomain}`,
});
headers.push({
key: EmailHeaderKey.XEntityRefID,
value: uuidv4(),
});
if (options.hasHostedImages) {
headers.push(...this.generateImageAntiSpamHeaders(options));
}
if (options.isTransactional) {
headers.push({
key: EmailHeaderKey.XAutoResponseSuppress,
value: "All",
});
headers.push({
key: EmailHeaderKey.XEntityID,
value: "transactional",
});
headers.push({
key: EmailHeaderKey.Precedence,
value: "bulk",
});
}
if (options.isMarketing) {
headers.push({
key: EmailHeaderKey.XEntityID,
value: "marketing",
});
headers.push({
key: EmailHeaderKey.ListUnsubscribePost,
value: "List-Unsubscribe=One-Click",
});
if (options.listUnsubscribe) {
headers.push({
key: EmailHeaderKey.ListUnsubscribe,
value: options.listUnsubscribe,
});
}
}
if (options.listId) {
headers.push({
key: EmailHeaderKey.ListId,
value: options.listId,
});
}
return headers;
}
//****************************************** */
generateImageAntiSpamHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
const headers: EmailHeaderDto[] = [];
if (!options.hasHostedImages) {
return headers;
}
headers.push({
key: EmailHeaderKey.XImageSource,
value: this.hostDomain2,
});
headers.push({
key: EmailHeaderKey.XContentSecurity,
value: "verified",
});
headers.push({
key: EmailHeaderKey.XImageValidation,
value: "trusted-source",
});
headers.push({
key: EmailHeaderKey.XOriginVerified,
value: `${this.hostDomain}-${Date.now()}`,
});
if (options.imageCount && options.imageCount > 0) {
headers.push({
key: EmailHeaderKey.XImageCount,
value: options.imageCount.toString(),
});
}
if (options.trustedImageSources && options.trustedImageSources.length > 0) {
headers.push({
key: EmailHeaderKey.XTrustedSources,
value: options.trustedImageSources.join(", "),
});
}
return headers;
}
//****************************************** */
detectImagesInContent(htmlContent: string): IImageDetectionResult {
if (!htmlContent) {
return {
hasHostedImages: false,
imageCount: 0,
trustedImageSources: [],
danakCorpImages: [],
};
}
// Regex to find img tags with src attributes
const imgRegex = /<img[^>]+src\s*=\s*["']([^"']+)["'][^>]*>/gi;
const matches = [...htmlContent.matchAll(imgRegex)];
const allImages = matches.map((match) => match[1]);
const danakCorpImages = allImages.filter((src) => src.includes(this.hostDomain) || src.includes(this.hostDomain2));
// Also check for CSS background images
const cssImageRegex = /background-image\s*:\s*url\(["']?([^"')]+)["']?\)/gi;
const cssMatches = [...htmlContent.matchAll(cssImageRegex)];
const cssImages = cssMatches.map((match) => match[1]);
const danakCorpCssImages = cssImages.filter((src) => src.includes(this.hostDomain) || src.includes(this.hostDomain2));
const allDanakCorpImages = [...danakCorpImages, ...danakCorpCssImages];
const hasHostedImages = allDanakCorpImages.length > 0;
// Get unique domains from danakcorp images
const trustedImageSources = [
...new Set(
allDanakCorpImages
.map((src) => {
try {
const url = new URL(src.startsWith("//") ? "https:" + src : src);
return url.hostname;
} catch {
// If URL parsing fails, extract domain manually
const match = src.match(/(?:https?:\/\/)?(?:www\.)?([^/]+)/);
return match ? match[1] : src;
}
})
.filter((domain) => domain.includes(this.hostDomain)),
),
];
return {
hasHostedImages,
imageCount: allDanakCorpImages.length,
trustedImageSources,
danakCorpImages: allDanakCorpImages,
};
}
//****************************************** */
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[] {
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: EmailHeaderKey.XTransactionID,
value: options.transactionId || uuidv4(),
});
if (options.category) {
headers.push({
key: EmailHeaderKey.XCategory,
value: options.category,
});
}
// Mark as important transactional email
headers.push({
key: EmailHeaderKey.Importance,
value: "high",
});
headers.push({
key: EmailHeaderKey.XAutoResponseSuppress,
value: "All",
});
return headers;
}
//****************************************** */
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: EmailHeaderKey.XCampaignID,
value: options.campaignId,
});
}
headers.push({
key: EmailHeaderKey.XMarketingEmail,
value: "true",
});
// Required for bulk marketing emails
headers.push({
key: EmailHeaderKey.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}>`;
}
//****************************************** */
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: Priority): string {
return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3";
}
}