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 = "ایمیل با موفقیت حذف شد",
USER_ID_REQUIRED = "شناسه کاربری مورد نیاز است",
USER_ID_MUST_BE_STRING = "شناسه کاربری باید یک رشته باشد",
MAILBOX_ID_REQUIRED = "شناسه مستندک ایمیل مورد نیاز است",
MAILBOX_ID_MUST_BE_STRING = "شناسه مستندک ایمیل باید یک رشته باشد",
MAILBOX_ID_REQUIRED = "شناسه میل باکس ایمیل مورد نیاز است",
MAILBOX_ID_MUST_BE_STRING = "شناسه میل باکس ایمیل باید یک رشته باشد",
QUEUE_ID_REQUIRED = "شناسه پیش‌نویس ایمیل مورد نیاز است",
QUEUE_ID_MUST_BE_STRING = "شناسه پیش‌نویس ایمیل باید یک رشته باشد",
MESSAGE_ID_MUST_BE_NUMBER = "شناسه ایمیل باید یک عدد باشد",
@@ -223,6 +223,7 @@ export const enum EmailMessage {
PRIORITY_MUST_BE_ONE_OF_HIGH_NORMAL_LOW = "اولویت باید یکی از مقادیر high, normal, low باشد",
SUBJECT_REQUIRED = "موضوع ایمیل مورد نیاز است",
ALL_MESSAGES_DELETED_SUCCESSFULLY = "همه پیام ها با موفقیت حذف شدند",
MAILBOX_ID_MUST_BE_MONGO_ID = "شناسه میل باکس ایمیل باید یک آی دی معتبر باشد",
}
export const enum WebSocketMessage {
+7 -6
View File
@@ -1,6 +1,6 @@
import { ApiProperty } from "@nestjs/swagger";
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";
@@ -11,11 +11,12 @@ export class UserIdParamDto {
userId: string;
}
export class MailboxIdParamDto {
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
@IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
export class MailboxIdQueryDto {
@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 {
@@ -32,7 +33,7 @@ export class QueueIdParamDto {
queueId: string;
}
export class UserMailboxParamDto {
export class MailboxParamDto {
@ApiProperty({ description: "Mailbox ID", example: "507f1f77bcf86cd799439012" })
@IsString({ message: EmailMessage.MAILBOX_ID_MUST_BE_STRING })
@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 { 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 { SearchMessagesQueryDto } from "./DTO/email-query.dto";
import { SendEmailDto, UpdateDraftDto } from "./DTO/send-email.dto";
@@ -40,16 +40,16 @@ export class EmailController {
@ApiOperation({ summary: "Get a specific message" })
@ApiResponse({ status: 200, description: "Message details" })
@ApiResponse({ status: 404, description: "Message not found" })
getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.getMessage(userId, params.messageId);
getMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.getMessage(userId, params.messageId, query.mailbox);
}
@Delete("messages/:messageId")
@ApiOperation({ summary: "Delete a message" })
@ApiResponse({ status: 200, description: "Message deleted successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.deleteMessage(userId, params.messageId);
deleteMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.deleteMessage(userId, params.messageId, query.mailbox);
}
@Get("search")
@@ -63,8 +63,8 @@ export class EmailController {
@ApiOperation({ summary: "Mark a message as seen" })
@ApiResponse({ status: 200, description: "Message marked as seen successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.markMessageAsSeen(userId, Number(params.messageId));
markMessageAsSeen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.markMessageAsSeen(userId, Number(params.messageId), query.mailbox);
}
@Get("messages/inbox")
@@ -156,8 +156,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to archive successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToArchive(userId, Number(params.messageId));
moveMessageToArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToArchive(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/favorite")
@@ -165,8 +165,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to favorite successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToFavorite(userId, Number(params.messageId));
moveMessageToFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToFavorite(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/trash")
@@ -174,8 +174,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to trash successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToTrash(userId, Number(params.messageId));
moveMessageToTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToTrash(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/unarchive")
@@ -183,8 +183,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved from archive to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in archive" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId));
moveMessageToInboxFromArchive(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromArchive(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/unfavorite")
@@ -192,8 +192,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved from favorite to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in favorite" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId));
moveMessageToInboxFromFavorite(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromFavorite(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/restore")
@@ -201,8 +201,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message restored from trash to inbox successfully" })
@ApiResponse({ status: 404, description: "Message not found in trash" })
@ApiResponse({ status: 403, description: "Not authorized to restore message" })
moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId));
moveMessageToInboxFromTrash(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromTrash(userId, Number(params.messageId), query.mailbox);
}
@Patch("messages/:messageId/junk")
@@ -210,8 +210,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message moved to junk successfully" })
@ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToJunk(userId, Number(params.messageId));
moveMessageToJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToJunk(userId, Number(params.messageId), query.mailbox);
}
@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: 404, description: "Message not found in junk" })
@ApiResponse({ status: 403, description: "Not authorized to move message" })
moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto) {
return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId));
moveMessageToInboxFromJunk(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.moveMessageToInboxFromJunk(userId, Number(params.messageId), query.mailbox);
}
@Post("messages/bulk-action")
@@ -232,8 +232,8 @@ export class EmailController {
@ApiResponse({ status: 206, description: "Bulk action partially completed" })
@ApiResponse({ status: 400, description: "Invalid bulk action data" })
@ApiResponse({ status: 403, description: "Not authorized to perform bulk action" })
performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto) {
return this.emailService.performBulkAction(userId, bulkActionDto);
performBulkAction(@UserDec("wildduckUserId") userId: string, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox);
}
// // Domain Access Management Endpoints
@@ -25,6 +25,13 @@ export const enum EmailHeaderKey {
Importance = "Importance",
XCampaignID = "X-Campaign-ID",
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 {
+2 -20
View File
@@ -104,7 +104,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
});
if (userInfo) {
// Leave user room
const userRoom = WEBSOCKET_ROOMS.USER(userInfo.userId);
client.leave(userRoom);
@@ -137,12 +136,8 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
}
//************************************************************************ */
async notifyEmailSent(userId: string, data: EmailSentPayload) {
return await this.sendUserNotification(
userId,
WEBSOCKET_EVENTS.EMAIL_SENT,
data,
`Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`,
);
const content = `Email ${data.messageId} sent to ${data.to.map((t) => t.address).join(", ")}`;
return await this.sendUserNotification(userId, WEBSOCKET_EVENTS.EMAIL_SENT, data, content);
}
//************************************************************************ */
async notifyUnreadCountUpdate(userId: string, data: UnreadCountPayload) {
@@ -193,19 +188,6 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
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
// ============================================================================
@@ -9,9 +9,14 @@ export interface IGenerateAntiThreadingHeadersOptions {
isTransactional?: boolean;
isMarketing?: boolean;
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;
}
@@ -25,3 +30,10 @@ export interface IGenerateMarketingHeadersOptions
campaignId?: 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 { EmailMonitoringService } from "../services/email-monitoring.service";
@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE, {
concurrency: 5,
})
@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_QUEUE)
export class EmailMonitoringProcessor extends WorkerHost {
private readonly logger = new Logger(EmailMonitoringProcessor.name);
@@ -8,27 +8,27 @@ import {
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[] = [];
// Add References header with unique value to break threading
headers.push({
key: EmailHeaderKey.References,
value: `<${uuidv4()}@${options.businessDomain}>`,
});
// Add unique Thread-Index to prevent Outlook threading
headers.push({
key: EmailHeaderKey.ThreadIndex,
value: this.generateThreadIndex(),
});
// Anti-spam headers
headers.push({
key: EmailHeaderKey.XMailer,
value: `Danak Mail Service v1.0`,
@@ -44,7 +44,6 @@ export class EmailHeadersService {
value: options.priority || Priority.Normal,
});
// Add MIME version
headers.push({
key: EmailHeaderKey.MIMEVersion,
value: "1.0",
@@ -60,19 +59,20 @@ export class EmailHeadersService {
value: "0.0",
});
// Add feedback loop headers
headers.push({
key: EmailHeaderKey.XFeedbackID,
value: `${uuidv4()}:${options.businessDomain}`,
});
// Add entity reference ID
headers.push({
key: EmailHeaderKey.XEntityRefID,
value: uuidv4(),
});
// Content classification headers
if (options.hasHostedImages) {
headers.push(...this.generateImageAntiSpamHeaders(options));
}
if (options.isTransactional) {
headers.push({
key: EmailHeaderKey.XAutoResponseSuppress,
@@ -106,7 +106,6 @@ export class EmailHeadersService {
}
}
// Add List-ID if provided
if (options.listId) {
headers.push({
key: EmailHeaderKey.ListId,
@@ -114,30 +113,107 @@ export class EmailHeadersService {
});
}
// Add authentication headers
// if (options.businessDomain) {
// headers.push({
// key: EmailHeaderKey.AuthenticationResults,
// value: `${options.businessDomain}; dkim=pass; spf=pass; dmarc=pass`,
// });
// }
return headers;
}
// Generate unique Message-ID to prevent threading
// const messageId = options.messageId || this.generateMessageId(options.businessDomain);
// headers.push({
// key: EmailHeaderKey.MessageId,
// value: messageId,
// });
//****************************************** */
generateImageAntiSpamHeaders(options: IGenerateAntiThreadingHeadersOptions): EmailHeaderDto[] {
const headers: EmailHeaderDto[] = [];
// Add custom headers to help with deliverability
// headers.push({
// key: "X-Originating-IP",
// value: "[" + this.getServerIP() + "]",
// });
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[] = [];
@@ -268,20 +344,7 @@ export class EmailHeadersService {
}
//****************************************** */
private getPriorityValue(priority: "high" | "normal" | "low"): string {
switch (priority) {
case "high":
return "1";
case "low":
return "5";
default:
return "3";
}
private getPriorityValue(priority: Priority): string {
return priority === Priority.High ? "1" : priority === Priority.Low ? "5" : "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,
};
const headers = this.generateHeadersForEmailType(user);
const headers = this.generateHeadersForEmailType(user, {
html: sendEmailDto.html,
text: sendEmailDto.text,
});
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 {
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}`);
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}`);
this.logger.log(`Retrieved message ${messageId} from ${mailboxId} for user: ${wildduckUserId}`);
const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId);
return {
message,
message: {
...message,
mailboxName,
},
};
} catch (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 {
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}`);
const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, messageLocation.mailboxId, messageId));
this.logger.log(`Message ${messageId} deleted successfully from ${messageLocation.mailboxName}`);
this.logger.log(`Message ${messageId} deleted successfully from ${mailboxId}`);
return {
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 {
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}`);
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}`);
this.logger.log(`Message ${messageId} marked as seen successfully in ${mailboxId}`);
await this.emailNotificationService.notifyEmailRead({
userId: wildduckUserId,
messageId,
mailboxId: messageLocation.mailboxId,
mailboxId,
});
return {
@@ -468,6 +461,12 @@ export class EmailService {
if (processedTemplate.hasTemplate) {
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));
@@ -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}`);
try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
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(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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 {
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}`);
try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
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(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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 {
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}`);
try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
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(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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 {
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}`);
try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, archiveMailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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}`);
try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, favoriteMailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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}`);
try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, trashMailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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}`);
try {
const messageLocation = await this.findMessageMailbox(wildduckUserId, messageId);
if (!messageLocation) throw new BadRequestException(EmailMessage.MESSAGE_NOT_FOUND);
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(
this.mailServerService.messages.updateMessage(wildduckUserId, messageLocation.mailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
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);
@@ -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}`);
try {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, junkMailboxId, messageId, {
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
moveTo: inboxMailboxId,
}),
);
@@ -811,7 +790,7 @@ export class EmailService {
//==============================================
// 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}`);
const results = {
@@ -824,34 +803,34 @@ export class EmailService {
try {
switch (bulkActionDto.action) {
case BulkActionType.SEEN:
await this.markMessageAsSeen(wildduckUserId, messageId);
await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.DELETE:
await this.deleteMessage(wildduckUserId, messageId);
await this.deleteMessage(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.ARCHIVE:
await this.moveMessageToArchive(wildduckUserId, messageId);
await this.moveMessageToArchive(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.UNARCHIVE:
await this.moveMessageToInboxFromArchive(wildduckUserId, messageId);
await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.FAVORITE:
await this.moveMessageToFavorite(wildduckUserId, messageId);
await this.moveMessageToFavorite(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.UNFAVORITE:
await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId);
await this.moveMessageToInboxFromFavorite(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.JUNK:
await this.moveMessageToJunk(wildduckUserId, messageId);
await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.NOTJUNK:
await this.moveMessageToInboxFromJunk(wildduckUserId, messageId);
await this.moveMessageToInboxFromJunk(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.TRASH:
await this.moveMessageToTrash(wildduckUserId, messageId);
await this.moveMessageToTrash(wildduckUserId, messageId, mailboxId);
break;
case BulkActionType.RESTORE:
await this.moveMessageToInboxFromTrash(wildduckUserId, messageId);
await this.moveMessageToInboxFromTrash(wildduckUserId, messageId, mailboxId);
break;
default:
throw new BadRequestException(`Unknown action: ${bulkActionDto.action}`);
@@ -920,42 +899,6 @@ export class EmailService {
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) {
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
@@ -964,68 +907,21 @@ export class EmailService {
//==============================================
//########################################################
private generateHeadersForEmailType(user: User) {
// const emailType = sendEmailDto.emailType || EmailType.GENERAL;
private generateHeadersForEmailType(user: User, emailContent?: { html?: string; text?: string }) {
const businessName = user.business.name;
const businessDomain = user.emailAddress.split("@")[1];
const htmlContent = emailContent?.html || "";
const imageDetection = this.emailHeadersService.detectImagesInContent(htmlContent);
return this.emailHeadersService.generateAntiThreadingHeaders({
businessName,
businessDomain,
isTransactional: true,
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 { MailServerService } from "../../mail-server/services/mail-server.service";
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
export interface UserMailboxIds {
inbox: string;
sent: string;
drafts: string;
trash: string;
junk: string;
archive: string;
favorite: string;
}
import { UserMailboxIds } from "../interfaces/user-mailbox.interface";
@Injectable()
export class MailboxResolverService {
@@ -70,6 +61,15 @@ export class MailboxResolverService {
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
*/
+6 -1
View File
@@ -13,7 +13,6 @@ import { BusinessInterceptor } from "../../core/interceptors/business.intercepto
@Controller("users")
@ApiHeader({ name: "x-business-id", description: "Business ID" })
@UseInterceptors(BusinessInterceptor)
@AuthGuards()
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@@ -26,6 +25,7 @@ export class UsersController {
}
@Post()
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Create a new email user" })
@ApiResponse({ status: 201, description: "Email user created successfully" })
createEmailUser(@Body() createEmailUserDto: CreateEmailUserDto, @BusinessDec("id") businessId: string) {
@@ -33,6 +33,7 @@ export class UsersController {
}
@Get()
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Get email users for business" })
@ApiResponse({ status: 200, description: "Email users retrieved successfully" })
getEmailUsers(@BusinessDec("id") businessId: string, @Query() queryDto: UserListQueryDto) {
@@ -40,6 +41,7 @@ export class UsersController {
}
@Delete(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Delete email user" })
@ApiResponse({ status: 200, description: "Email user deleted successfully" })
deleteEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {
@@ -47,6 +49,7 @@ export class UsersController {
}
@Patch(":id")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user" })
@ApiResponse({ status: 200, description: "Email user updated successfully" })
updateEmailUser(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: UpdateEmailUserDto) {
@@ -54,6 +57,7 @@ export class UsersController {
}
@Patch(":id/quota")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user quota" })
@ApiResponse({ status: 200, description: "Email user quota updated successfully" })
updateEmailUserQuota(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string, @Body() body: { quota: number }) {
@@ -61,6 +65,7 @@ export class UsersController {
}
@Patch(":id/toggle-status")
@UseInterceptors(BusinessInterceptor)
@ApiOperation({ summary: "Update email user status" })
@ApiResponse({ status: 200, description: "Email user status updated successfully" })
updateEmailUserStatus(@Param() paramDto: ParamDto, @BusinessDec("id") businessId: string) {