351 lines
9.5 KiB
TypeScript
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";
|
|
}
|
|
}
|