chore: change all to have mailbox id in query

This commit is contained in:
mahyargdz
2025-07-20 16:41:57 +03:30
parent 97f0a20ab3
commit a5d28b7f26
12 changed files with 258 additions and 284 deletions
+3 -2
View File
@@ -147,8 +147,8 @@ export const enum EmailMessage {
MESSAGE_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد", MESSAGE_DELETED_SUCCESSFULLY = "ایمیل با موفقیت حذف شد",
USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است", USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است",
USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد", USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد",
MAILBOX_ID_REQUIRED = "شناسه مستندک ایمیل مورد نیاز است", MAILBOX_ID_REQUIRED = "شناسه میل باکس ایمیل مورد نیاز است",
MAILBOX_ID_MUST_BE_STRING = "شناسه مستندک ایمیل باید یک رشته باشد", MAILBOX_ID_MUST_BE_STRING = "شناسه میل باکس ایمیل باید یک رشته باشد",
QUEUE_ID_REQUIRED = "شناسه پیش‌نویس ایمیل مورد نیاز است", QUEUE_ID_REQUIRED = "شناسه پیش‌نویس ایمیل مورد نیاز است",
QUEUE_ID_MUST_BE_STRING = "شناسه پیش‌نویس ایمیل باید یک رشته باشد", QUEUE_ID_MUST_BE_STRING = "شناسه پیش‌نویس ایمیل باید یک رشته باشد",
MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد", MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد",
@@ -223,6 +223,7 @@ export const enum EmailMessage {
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد", PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد",
SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است", SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است",
ALL_MESSAGES_DELETED_SUCCESSFULLY = "همه پیام ها با موفقیت حذف شدند", ALL_MESSAGES_DELETED_SUCCESSFULLY = "همه پیام ها با موفقیت حذف شدند",
MAILBOX_ID_MUST_BE_MONGO_ID = "شناسه میل باکس ایمیل باید یک آی دی معتبر باشد",
} }
export const enum WebSocketMessage { export const enum WebSocketMessage {
+7 -6
View File
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from "@nestjs/swagger";
import { Transform } from "class-transformer"; import { Transform } from "class-transformer";
import { IsNotEmpty, IsNumber, IsNumberString, IsString } from "class-validator"; import { IsMongoId, IsNotEmpty, IsNumber, IsNumberString, IsString } from "class-validator";
import { EmailMessage } from "../../../common/enums/message.enum"; import { EmailMessage } from "../../../common/enums/message.enum";
@@ -11,11 +11,12 @@ export class UserIdParamDto {
userId: string; userId: string;
} }
export class MailboxIdParamDto { export class MailboxIdQueryDto {
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
@IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
@IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED }) @IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED })
mailboxId: string; @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
@IsMongoId({ message: EmailMessage.MAILBOX_ID_MUST_BE_MONGO_ID })
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
mailbox: string;
} }
export class MessageIdParamDto { export class MessageIdParamDto {
@@ -32,7 +33,7 @@ export class QueueIdParamDto {
queueId: string; queueId: string;
} }
export class UserMailboxParamDto { export class MailboxParamDto {
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" }) @ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
@IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING }) @IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
@IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED }) @IsNotEmpty({ message: EmailMessage.MAILBOX_ID_REQUIRED })
+25 -25
View File
@@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestj
import { ApiOperation, ApiResponse } from "@nestjs/swagger"; import { ApiOperation, ApiResponse } from "@nestjs/swagger";
import { BulkActionDto } from "./DTO/bulk-actions.dto"; import { BulkActionDto } from "./DTO/bulk-actions.dto";
import { MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto"; import { MailboxIdQueryDto, MessageParamDto, QueueIdParamDto } from "./DTO/email-params.dto";
import { MessageListQueryDto } from "./DTO/email-query.dto"; import { MessageListQueryDto } from "./DTO/email-query.dto";
import { SearchMessagesQueryDto } from "./DTO/email-query.dto"; import { SearchMessagesQueryDto } from "./DTO/email-query.dto";
import { SendEmailDto, UpdateDraftDto } from "./DTO/send-email.dto"; import { SendEmailDto, UpdateDraftDto } from "./DTO/send-email.dto";
@@ -40,16 +40,16 @@ export class EmailController {
@ApiOperation({ summary: "Get a specific message" }) @ApiOperation({ summary: "Get a specific message" })
@ApiResponse({ status: 200, description: "Message details" }) @ApiResponse({ status: 200, description: "Message details" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.getMessage(userId, params.messageId); return this.emailService.getMessage(userId, params.messageId, query.mailbox);
} }
@Delete("messages/:messageId") @Delete("messages/:messageId")
@ApiOperation({ summary: "Delete a message" }) @ApiOperation({ summary: "Delete a message" })
@ApiResponse({ status: 200, description: "Message deleted successfully" }) @ApiResponse({ status: 200, description: "Message deleted successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.deleteMessage(userId, params.messageId); return this.emailService.deleteMessage(userId, params.messageId, query.mailbox);
} }
@Get("search") @Get("search")
@@ -63,8 +63,8 @@ export class EmailController {
@ApiOperation({ summary: "Mark a message as seen" }) @ApiOperation({ summary: "Mark a message as seen" })
@ApiResponse({ status: 200, description: "Message marked as seen successfully" }) @ApiResponse({ status: 200, description: "Message marked as seen successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.markMessageAsSeen(userId, Number(params.messageId)); return this.emailService.markMessageAsSeen(userId, Number(params.messageId), query.mailbox);
} }
@Get("messages/inbox") @Get("messages/inbox")
@@ -156,8 +156,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to archive successfully" }) @ApiResponse({ status: 200, description: "Message moved to archive successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToArchive(userId, Number(params.messageId)); return this.emailService.moveMessageToArchive(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/favorite") @Patch("messages/:messageId/favorite")
@@ -165,8 +165,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to favorite successfully" }) @ApiResponse({ status: 200, description: "Message moved to favorite successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToFavorite(userId, Number(params.messageId)); return this.emailService.moveMessageToFavorite(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/trash") @Patch("messages/:messageId/trash")
@@ -174,8 +174,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to trash successfully" }) @ApiResponse({ status: 200, description: "Message moved to trash successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToTrash(userId, Number(params.messageId)); return this.emailService.moveMessageToTrash(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/unarchive") @Patch("messages/:messageId/unarchive")
@@ -183,8 +183,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved from archive to inbox successfully" }) @ApiResponse({ status: 200, description: "Message moved from archive to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in archive" }) @ApiResponse({ status: 404, description: "Message not found in archive" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId)); return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/unfavorite") @Patch("messages/:messageId/unfavorite")
@@ -192,8 +192,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" }) @ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in favorite" }) @ApiResponse({ status: 404, description: "Message not found in favorite" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId)); return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/restore") @Patch("messages/:messageId/restore")
@@ -201,8 +201,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message restored from trash to inbox successfully" }) @ApiResponse({ status: 200, description: "Message restored from trash to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in trash" }) @ApiResponse({ status: 404, description: "Message not found in trash" })
@ApiResponse({ status: 403, description: "Not authorized to restore message" }) @ApiResponse({ status: 403, description: "Not authorized to restore message" })
moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId)); return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/junk") @Patch("messages/:messageId/junk")
@@ -210,8 +210,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to junk successfully" }) @ApiResponse({ status: 200, description: "Message moved to junk successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToJunk(userId, Number(params.messageId)); return this.emailService.moveMessageToJunk(userId, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/not-junk") @Patch("messages/:messageId/not-junk")
@@ -219,8 +219,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved from junk to inbox successfully" }) @ApiResponse({ status: 200, description: "Message moved from junk to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in junk" }) @ApiResponse({ status: 404, description: "Message not found in junk" })
@ApiResponse({ status: 403, description: "Not authorized to move message" }) @ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) { moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId)); return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId), query.mailbox);
} }
@Post("messages/bulk-action") @Post("messages/bulk-action")
@@ -232,8 +232,8 @@ export class EmailController {
@ApiResponse({ status: 206, description: "Bulk action partially completed" }) @ApiResponse({ status: 206, description: "Bulk action partially completed" })
@ApiResponse({ status: 400, description: "Invalid bulk action data" }) @ApiResponse({ status: 400, description: "Invalid bulk action data" })
@ApiResponse({ status: 403, description: "Not authorized to perform bulk action" }) @ApiResponse({ status: 403, description: "Not authorized to perform bulk action" })
performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto) { performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.performBulkAction(userId, bulkActionDto); return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox);
} }
// // Domain Access Management Endpoints // // Domain Access Management Endpoints
@@ -25,6 +25,13 @@ export const enum EmailHeaderKey {
Importance = "Importance", Importance = "Importance",
XCampaignID = "X-Campaign-ID", XCampaignID = "X-Campaign-ID",
XMarketingEmail = "X-Marketing-Email", XMarketingEmail = "X-Marketing-Email",
// Image-related anti-spam headers
XImageSource = "X-Image-Source",
XContentSecurity = "X-Content-Security",
XImageValidation = "X-Image-Validation",
XOriginVerified = "X-Origin-Verified",
XImageCount = "X-Image-Count",
XTrustedSources = "X-Trusted-Sources",
} }
export enum Priority { export enum Priority {
+2 -20
View File
@@ -104,7 +104,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
}); });
if (userInfo) { if (userInfo) {
// Leave user room
const userRoom = WEBSOCKET_ROOMS.USER(userInfo.userId); const userRoom = WEBSOCKET_ROOMS.USER(userInfo.userId);
client.leave(userRoom); client.leave(userRoom);
@@ -137,12 +136,8 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailSent(userId: string, data: EmailSentPayload) { async notifyEmailSent(userId: string, data: EmailSentPayload) {
return await this.sendUserNotification( const content = `Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`;
userId, return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_SENT, data, content);
WEBSOCKET_EVENTS.EMAIL_SENT,
data,
`Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`,
);
} }
//************************************************************************ */ //************************************************************************ */
async notifyUnreadCountUpdate(userId: string, data: UnreadCountPayload) { async notifyUnreadCountUpdate(userId: string, data: UnreadCountPayload) {
@@ -193,19 +188,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
return userIds.filter((userId) => this.isUserConnected(userId)); return userIds.filter((userId) => this.isUserConnected(userId));
} }
//************************************************************************ */
async getBusinessConnectedUsers(businessId: string): Promise<string[]> {
try {
// Note: Currently returning all connected users since business filtering
// requires additional user data lookup. This can be enhanced later.
this.logger.warn(`getBusinessConnectedUsers called for business ${businessId} - returning all connected users for now`);
return this.getConnectedUserIds();
} catch (error) {
this.logger.error(`Failed to get business connected users for ${businessId}:`, error);
return [];
}
}
// ============================================================================ // ============================================================================
// PRIVATE HELPER METHODS // PRIVATE HELPER METHODS
// ============================================================================ // ============================================================================
@@ -9,9 +9,14 @@ export interface IGenerateAntiThreadingHeadersOptions {
isTransactional?: boolean; isTransactional?: boolean;
isMarketing?: boolean; isMarketing?: boolean;
priority?: Priority; priority?: Priority;
// Image-related options
hasHostedImages?: boolean;
imageCount?: number;
trustedImageSources?: string[];
} }
export interface IGenerateGmailAntiThreadingHeadersOptions extends Pick<IGenerateAntiThreadingHeadersOptions, "businessDomain"> { export interface IGenerateGmailAntiThreadingHeadersOptions {
businessDomain: string;
conversationBreaker?: string; conversationBreaker?: string;
} }
@@ -25,3 +30,10 @@ export interface IGenerateMarketingHeadersOptions
campaignId?: string; campaignId?: string;
unsubscribeUrl: string; unsubscribeUrl: string;
} }
export interface IImageDetectionResult {
hasHostedImages: boolean;
imageCount: number;
trustedImageSources: string[];
danakCorpImages: string[];
}
@@ -0,0 +1,9 @@
export interface UserMailboxIds {
inbox: string;
sent: string;
drafts: string;
trash: string;
junk: string;
archive: string;
favorite: string;
}
@@ -6,9 +6,7 @@ import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant";
import { EmailMonitoringJob, EmailSchedulerJob } from "../interfaces/email-events.interface"; import { EmailMonitoringJob, EmailSchedulerJob } from "../interfaces/email-events.interface";
import { EmailMonitoringService } from "../services/email-monitoring.service"; import { EmailMonitoringService } from "../services/email-monitoring.service";
@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE, { @Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE)
concurrency: 5,
})
export class EmailMonitoringProcessor extends WorkerHost { export class EmailMonitoringProcessor extends WorkerHost {
private readonly logger = new Logger(EmailMonitoringProcessor.name); private readonly logger = new Logger(EmailMonitoringProcessor.name);
@@ -8,27 +8,27 @@ import {
IGenerateGmailAntiThreadingHeadersOptions, IGenerateGmailAntiThreadingHeadersOptions,
IGenerateMarketingHeadersOptions, IGenerateMarketingHeadersOptions,
IGenerateTransactionalHeadersOptions, IGenerateTransactionalHeadersOptions,
IImageDetectionResult,
} from "../interfaces/email-header.interface"; } from "../interfaces/email-header.interface";
@Injectable() @Injectable()
export class EmailHeadersService { export class EmailHeadersService {
private readonly hostDomain = "danakcorp.com";
private readonly hostDomain2 = "storage.danakcorp.com";
//****************************************** */ //****************************************** */
generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] { generateAntiThreadingHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
const headers: EmailHeaderDto[] = []; const headers: EmailHeaderDto[] = [];
// Add References header with unique value to break threading
headers.push({ headers.push({
key: EmailHeaderKey.References, key: EmailHeaderKey.References,
value: `<${uuidv4()}@${options.businessDomain}>`, value: `<${uuidv4()}@${options.businessDomain}>`,
}); });
// Add unique Thread-Index to prevent Outlook threading
headers.push({ headers.push({
key: EmailHeaderKey.ThreadIndex, key: EmailHeaderKey.ThreadIndex,
value: this.generateThreadIndex(), value: this.generateThreadIndex(),
}); });
// Anti-spam headers
headers.push({ headers.push({
key: EmailHeaderKey.XMailer, key: EmailHeaderKey.XMailer,
value: `Danak Mail Service v1.0`, value: `Danak Mail Service v1.0`,
@@ -44,7 +44,6 @@ export class EmailHeadersService {
value: options.priority || Priority.Normal, value: options.priority || Priority.Normal,
}); });
// Add MIME version
headers.push({ headers.push({
key: EmailHeaderKey.MIMEVersion, key: EmailHeaderKey.MIMEVersion,
value: "1.0", value: "1.0",
@@ -60,19 +59,20 @@ export class EmailHeadersService {
value: "0.0", value: "0.0",
}); });
// Add feedback loop headers
headers.push({ headers.push({
key: EmailHeaderKey.XFeedbackID, key: EmailHeaderKey.XFeedbackID,
value: `${uuidv4()}:${options.businessDomain}`, value: `${uuidv4()}:${options.businessDomain}`,
}); });
// Add entity reference ID
headers.push({ headers.push({
key: EmailHeaderKey.XEntityRefID, key: EmailHeaderKey.XEntityRefID,
value: uuidv4(), value: uuidv4(),
}); });
// Content classification headers if (options.hasHostedImages) {
headers.push(...this.generateImageAntiSpamHeaders(options));
}
if (options.isTransactional) { if (options.isTransactional) {
headers.push({ headers.push({
key: EmailHeaderKey.XAutoResponseSuppress, key: EmailHeaderKey.XAutoResponseSuppress,
@@ -106,7 +106,6 @@ export class EmailHeadersService {
} }
} }
// Add List-ID if provided
if (options.listId) { if (options.listId) {
headers.push({ headers.push({
key: EmailHeaderKey.ListId, key: EmailHeaderKey.ListId,
@@ -114,30 +113,107 @@ export class EmailHeadersService {
}); });
} }
// Add authentication headers return headers;
// if (options.businessDomain) { }
// headers.push({
// key: EmailHeaderKey.AuthenticationResults,
// value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`,
// });
// }
// Generate unique Message-ID to prevent threading //****************************************** */
// const messageId = options.messageId || this.generateMessageId(options.businessDomain); generateImageAntiSpamHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
// headers.push({ const headers: EmailHeaderDto[] = [];
// key: EmailHeaderKey.MessageId,
// value: messageId,
// });
// Add custom headers to help with deliverability if (!options.hasHostedImages) {
// headers.push({ return headers;
// key: "X-Originating-IP", }
// value: "[" + this.getServerIP() + "]",
// }); 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; 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[] { generateGmailAntiThreadingHeaders(options: IGenerateGmailAntiThreadingHeadersOptions): EmailHeaderDto[] {
const headers: EmailHeaderDto[] = []; const headers: EmailHeaderDto[] = [];
@@ -268,20 +344,7 @@ export class EmailHeadersService {
} }
//****************************************** */ //****************************************** */
private getPriorityValue(priority: "high" | "normal" | "low"): string { private getPriorityValue(priority: Priority): string {
switch (priority) { return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "3";
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";
// }
} }
+71 -175
View File
@@ -62,7 +62,10 @@ export class EmailService {
name: user.displayName, name: user.displayName,
}; };
const headers = this.generateHeadersForEmailType(user); const headers = this.generateHeadersForEmailType(user, {
html: sendEmailDto.html,
text: sendEmailDto.text,
});
sendEmailDto.headers = [...headers]; sendEmailDto.headers = [...headers];
@@ -177,21 +180,21 @@ export class EmailService {
} }
//######################################################## //########################################################
async getMessage(wildduckUserId: string, messageId: number) { async getMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); this.logger.log(`Getting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId);
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
this.logger.log(`Getting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); this.logger.log(`Retrieved message ${messageId} from ${mailboxId} for user: ${wildduckUserId}`);
const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId);
await this.markMessageAsSeen(wildduckUserId, messageId);
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, messageLocation.mailboxId, messageId));
this.logger.log(`Retrieved message ${messageId} from ${messageLocation.mailboxName} for user: ${wildduckUserId}`);
return { return {
message, message: {
...message,
mailboxName,
},
}; };
} catch (error) { } catch (error) {
this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
@@ -200,17 +203,13 @@ export class EmailService {
} }
//######################################################## //########################################################
async deleteMessage(wildduckUserId: string, messageId: number) { async deleteMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); this.logger.log(`Deleting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, mailboxId, messageId));
this.logger.log(`Deleting message ${messageId} from ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); this.logger.log(`Message ${messageId} deleted successfully from ${mailboxId}`);
const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, messageLocation.mailboxId, messageId));
this.logger.log(`Message ${messageId} deleted successfully from ${messageLocation.mailboxName}`);
return { return {
message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY, message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY,
@@ -243,24 +242,18 @@ export class EmailService {
} }
//============================================== //==============================================
async markMessageAsSeen(wildduckUserId: string, messageId: number) { async markMessageAsSeen(wildduckUserId: string, messageId: number, mailboxId: string) {
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId); this.logger.log(`Marking message ${messageId} as seen in ${mailboxId} mailbox for user: ${wildduckUserId}`);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND); const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: true }));
this.logger.log(`Marking message ${messageId} as seen in ${messageLocation.mailboxName} mailbox for user: ${wildduckUserId}`); this.logger.log(`Message ${messageId} marked as seen successfully in ${mailboxId}`);
const { updated } = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { seen: true }),
);
this.logger.log(`Message ${messageId} marked as seen successfully in ${messageLocation.mailboxName}`);
await this.emailNotificationService.notifyEmailRead({ await this.emailNotificationService.notifyEmailRead({
userId: wildduckUserId, userId: wildduckUserId,
messageId, messageId,
mailboxId: messageLocation.mailboxId, mailboxId,
}); });
return { return {
@@ -468,6 +461,12 @@ export class EmailService {
if (processedTemplate.hasTemplate) { if (processedTemplate.hasTemplate) {
this.logger.log(`Draft content processed through template for business: ${user.business.id}`); this.logger.log(`Draft content processed through template for business: ${user.business.id}`);
} }
const headers = this.generateHeadersForEmailType(user, {
html: draftData.html,
text: draftData.text,
});
draftData.headers = [...(draftData.headers || []), ...headers];
} }
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData)); const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData));
@@ -535,25 +534,21 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToArchive(wildduckUserId: string, messageId: number) { async moveMessageToArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`);
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: archiveMailboxId, moveTo: archiveMailboxId,
}), }),
); );
this.logger.log(`Message ${messageId} moved to archive successfully from ${messageLocation.mailboxName}`); this.logger.log(`Message ${messageId} moved to archive successfully from ${mailboxId}`);
return { return {
success: true, success: true,
@@ -569,25 +564,21 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToFavorite(wildduckUserId: string, messageId: number) { async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`);
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: favoriteMailboxId, moveTo: favoriteMailboxId,
}), }),
); );
this.logger.log(`Message ${messageId} moved to favorite successfully from ${messageLocation.mailboxName}`); this.logger.log(`Message ${messageId} moved to favorite successfully from ${mailboxId}`);
return { return {
success: true, success: true,
@@ -603,25 +594,21 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToTrash(wildduckUserId: string, messageId: number) { async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`);
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: trashMailboxId, moveTo: trashMailboxId,
}), }),
); );
this.logger.log(`Message ${messageId} moved to trash successfully from ${messageLocation.mailboxName}`); this.logger.log(`Message ${messageId} moved to trash successfully from ${mailboxId}`);
return { return {
success: true, success: true,
@@ -637,15 +624,14 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number) { async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`);
try { try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, archiveMailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: inboxMailboxId, moveTo: inboxMailboxId,
}), }),
); );
@@ -666,15 +652,14 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number) { async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`);
try { try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, favoriteMailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: inboxMailboxId, moveTo: inboxMailboxId,
}), }),
); );
@@ -695,15 +680,14 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number) { async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`);
try { try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, trashMailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: inboxMailboxId, moveTo: inboxMailboxId,
}), }),
); );
@@ -724,25 +708,21 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToJunk(wildduckUserId: string, messageId: number) { async moveMessageToJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`);
try { try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
this.logger.log(`Moving message ${messageId} from ${messageLocation.mailboxName} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: junkMailboxId, moveTo: junkMailboxId,
}), }),
); );
this.logger.log(`Message ${messageId} moved to junk successfully from ${messageLocation.mailboxName}`); this.logger.log(`Message ${messageId} moved to junk successfully from ${mailboxId}`);
const domainTag = await this.getUserBusinessId(wildduckUserId); const domainTag = await this.getUserBusinessId(wildduckUserId);
@@ -769,15 +749,14 @@ export class EmailService {
} }
//============================================== //==============================================
async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number) { async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
this.logger.log(`Moving message ${messageId} from junk to inbox for user: ${wildduckUserId}`); this.logger.log(`Moving message ${messageId} from junk to inbox for user: ${wildduckUserId}`);
try { try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, junkMailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: inboxMailboxId, moveTo: inboxMailboxId,
}), }),
); );
@@ -811,7 +790,7 @@ export class EmailService {
//============================================== //==============================================
// BULK ACTIONS // BULK ACTIONS
//============================================== //==============================================
async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto) { async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) {
this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${wildduckUserId}`); this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${wildduckUserId}`);
const results = { const results = {
@@ -824,34 +803,34 @@ export class EmailService {
try { try {
switch (bulkActionDto.action) { switch (bulkActionDto.action) {
case BulkActionType.SEEN: case BulkActionType.SEEN:
await this.markMessageAsSeen(wildduckUserId, messageId); await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.DELETE: case BulkActionType.DELETE:
await this.deleteMessage(wildduckUserId, messageId); await this.deleteMessage(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.ARCHIVE: case BulkActionType.ARCHIVE:
await this.moveMessageToArchive(wildduckUserId, messageId); await this.moveMessageToArchive(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.UNARCHIVE: case BulkActionType.UNARCHIVE:
await this.moveMessageToInboxFromArchive(wildduckUserId, messageId); await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.FAVORITE: case BulkActionType.FAVORITE:
await this.moveMessageToFavorite(wildduckUserId, messageId); await this.moveMessageToFavorite(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.UNFAVORITE: case BulkActionType.UNFAVORITE:
await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId); await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.JUNK: case BulkActionType.JUNK:
await this.moveMessageToJunk(wildduckUserId, messageId); await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.NOTJUNK: case BulkActionType.NOTJUNK:
await this.moveMessageToInboxFromJunk(wildduckUserId, messageId); await this.moveMessageToInboxFromJunk(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.TRASH: case BulkActionType.TRASH:
await this.moveMessageToTrash(wildduckUserId, messageId); await this.moveMessageToTrash(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.RESTORE: case BulkActionType.RESTORE:
await this.moveMessageToInboxFromTrash(wildduckUserId, messageId); await this.moveMessageToInboxFromTrash(wildduckUserId, messageId, mailboxId);
break; break;
default: default:
throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`); throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`);
@@ -920,42 +899,6 @@ export class EmailService {
return transformedQuery; return transformedQuery;
} }
//########################################################
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 async getUserBusinessId(wildduckUserId: string) { private async getUserBusinessId(wildduckUserId: string) {
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
@@ -964,68 +907,21 @@ export class EmailService {
//============================================== //==============================================
//######################################################## private generateHeadersForEmailType(user: User, emailContent?: { html?: string; text?: string }) {
private generateHeadersForEmailType(user: User) {
// const emailType = sendEmailDto.emailType || EmailType.GENERAL;
const businessName = user.business.name; const businessName = user.business.name;
const businessDomain = user.emailAddress.split("@")[1]; const businessDomain = user.emailAddress.split("@")[1];
const htmlContent = emailContent?.html || "";
const imageDetection = this.emailHeadersService.detectImagesInContent(htmlContent);
return this.emailHeadersService.generateAntiThreadingHeaders({ return this.emailHeadersService.generateAntiThreadingHeaders({
businessName, businessName,
businessDomain, businessDomain,
isTransactional: true, isTransactional: true,
priority: Priority.Normal, priority: Priority.Normal,
hasHostedImages: imageDetection.hasHostedImages,
imageCount: imageDetection.imageCount,
trustedImageSources: imageDetection.trustedImageSources,
}); });
// 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,
// });
// }
} }
} }
@@ -4,16 +4,7 @@ import { firstValueFrom } from "rxjs";
import { MailboxInfo } from "../../mail-server/interfaces/mailboxes-response.interface"; import { MailboxInfo } from "../../mail-server/interfaces/mailboxes-response.interface";
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 { UserMailboxIds } from "../interfaces/user-mailbox.interface";
export interface UserMailboxIds {
inbox: string;
sent: string;
drafts: string;
trash: string;
junk: string;
archive: string;
favorite: string;
}
@Injectable() @Injectable()
export class MailboxResolverService { export class MailboxResolverService {
@@ -70,6 +61,15 @@ export class MailboxResolverService {
return mailboxId; return mailboxId;
} }
/**
* Get mailbox name by ID
*/
async getMailboxName(wildduckUserId: string, mailboxId: string): Promise<string> {
const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(wildduckUserId));
const mailbox = mailboxes.results.find((mb) => mb.id === mailboxId);
return mailbox?.name || "";
}
/** /**
* Get drafts mailbox ID * Get drafts mailbox ID
*/ */
+6 -1
View File
@@ -13,7 +13,6 @@ import { BusinessInterceptor } from "../../core/interceptors/business.intercepto
@Controller("users") @Controller("users")
@ApiHeader({ name: "x-business-id", description: "Business ID" }) @ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards() @AuthGuards()
export class UsersController { export class UsersController {
constructor(private readonly usersService: UsersService) {} constructor(private readonly usersService: UsersService) {}
@@ -26,6 +25,7 @@ export class UsersController {
} }
@Post() @Post()
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Create a new email user" }) @ApiOperation({ summary: "Create a new email user" })
@ApiResponse({ status: 201, description: "Email user created successfully" }) @ApiResponse({ status: 201, description: "Email user created successfully" })
createEmailUser(@Body() createEmailUserDto: CreateEmailUserDto, @BusinessDec("id") businessId: string) { createEmailUser(@Body() createEmailUserDto: CreateEmailUserDto, @BusinessDec("id") businessId: string) {
@@ -33,6 +33,7 @@ export class UsersController {
} }
@Get() @Get()
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Get email users for business" }) @ApiOperation({ summary: "Get email users for business" })
@ApiResponse({ status: 200, description: "Email users retrieved successfully" }) @ApiResponse({ status: 200, description: "Email users retrieved successfully" })
getEmailUsers(@BusinessDec("id") businessId: string, @Query() queryDto: UserListQueryDto) { getEmailUsers(@BusinessDec("id") businessId: string, @Query() queryDto: UserListQueryDto) {
@@ -40,6 +41,7 @@ export class UsersController {
} }
@Delete(":id") @Delete(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Delete email user" }) @ApiOperation({ summary: "Delete email user" })
@ApiResponse({ status: 200, description: "Email user deleted successfully" }) @ApiResponse({ status: 200, description: "Email user deleted successfully" })
deleteEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { deleteEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
@@ -47,6 +49,7 @@ export class UsersController {
} }
@Patch(":id") @Patch(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user" }) @ApiOperation({ summary: "Update email user" })
@ApiResponse({ status: 200, description: "Email user updated successfully" }) @ApiResponse({ status: 200, description: "Email user updated successfully" })
updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) { updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) {
@@ -54,6 +57,7 @@ export class UsersController {
} }
@Patch(":id/quota") @Patch(":id/quota")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user quota" }) @ApiOperation({ summary: "Update email user quota" })
@ApiResponse({ status: 200, description: "Email user quota updated successfully" }) @ApiResponse({ status: 200, description: "Email user quota updated successfully" })
updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) { updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) {
@@ -61,6 +65,7 @@ export class UsersController {
} }
@Patch(":id/toggle-status") @Patch(":id/toggle-status")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user status" }) @ApiOperation({ summary: "Update email user status" })
@ApiResponse({ status: 200, description: "Email user status updated successfully" }) @ApiResponse({ status: 200, description: "Email user status updated successfully" })
updateEmailUserStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) { updateEmailUserStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {