update: the email notif service

This commit is contained in:
mahyargdz
2025-07-30 11:18:49 +03:30
parent 136db8fef4
commit 028d0d202e
7 changed files with 208 additions and 394 deletions
@@ -5,10 +5,10 @@ import { firstValueFrom } from "rxjs";
import { WorkerProcessor } from "../../../common/queues/worker.processor"; import { WorkerProcessor } from "../../../common/queues/worker.processor";
import { EMAIL_QUEUE_CONSTANTS } from "../../email/constants/email-events.constant"; import { EMAIL_QUEUE_CONSTANTS } from "../../email/constants/email-events.constant";
import { UserMailboxIds } from "../../email/interfaces/user-mailbox.interface";
import { EmailGateway } from "../../email-gateway/email.gateway"; import { EmailGateway } from "../../email-gateway/email.gateway";
import { MailServerService } from "../../mail-server/services/mail-server.service"; import { MailServerService } from "../../mail-server/services/mail-server.service";
import { EmailPollingJobData } from "../interfaces/email-polling-job.interface"; import { EmailPollingJobData } from "../interfaces/email-polling-job.interface";
import { EmailNotificationService } from "../services/email-notification.service";
import { MailboxResolverService } from "../services/mailbox-resolver.service"; import { MailboxResolverService } from "../services/mailbox-resolver.service";
@Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE) @Processor(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE)
@@ -16,6 +16,7 @@ export class EmailPollingProcessor extends WorkerProcessor {
protected readonly logger = new Logger(EmailPollingProcessor.name); protected readonly logger = new Logger(EmailPollingProcessor.name);
constructor( constructor(
private readonly emailGateway: EmailGateway, private readonly emailGateway: EmailGateway,
private readonly emailNotificationService: EmailNotificationService,
private readonly mailboxResolverService: MailboxResolverService, private readonly mailboxResolverService: MailboxResolverService,
private readonly mailServerService: MailServerService, private readonly mailServerService: MailServerService,
) { ) {
@@ -50,9 +51,9 @@ export class EmailPollingProcessor extends WorkerProcessor {
this.logger.log(`Found ${newEmails.length} new emails for user ${wildduckUserId}`); this.logger.log(`Found ${newEmails.length} new emails for user ${wildduckUserId}`);
const lastEmail = newEmails[newEmails.length - 1]; const lastEmail = newEmails[newEmails.length - 1];
await this.emailGateway.notifyNewEmail(userId, { await this.emailNotificationService.notifyNewEmail({
wildduckUserId,
userId, userId,
wildduckUserId,
messageId: lastEmail.id, messageId: lastEmail.id,
date: lastEmail.date || new Date().toISOString(), date: lastEmail.date || new Date().toISOString(),
from: lastEmail.from, from: lastEmail.from,
@@ -65,16 +66,6 @@ export class EmailPollingProcessor extends WorkerProcessor {
hasAttachments: !!(lastEmail.attachments && Array.isArray(lastEmail.attachments) && lastEmail.attachments.length > 0), hasAttachments: !!(lastEmail.attachments && Array.isArray(lastEmail.attachments) && lastEmail.attachments.length > 0),
}); });
// Update unread count
const unreadStats = await this.getUnreadCounts(wildduckUserId, mailboxIds);
await this.emailGateway.notifyUnreadCountUpdate(userId, {
userId,
totalUnread: unreadStats.total,
mailboxCounts: unreadStats.mailboxCounts,
count: unreadStats.total,
timestamp: now.toISOString(),
});
this.logger.log(`Notified user ${userId} of ${newEmails.length} new emails`); this.logger.log(`Notified user ${userId} of ${newEmails.length} new emails`);
} else { } else {
this.logger.debug(`No new emails found for user ${userId}`); this.logger.debug(`No new emails found for user ${userId}`);
@@ -115,38 +106,4 @@ export class EmailPollingProcessor extends WorkerProcessor {
return []; return [];
} }
} }
//=======================================================
private async getUnreadCounts(wildduckUserId: string, mailboxIds: UserMailboxIds) {
try {
const mailboxCounts: Record<string, number> = {};
let total = 0;
for (const [mailboxName, mailboxId] of Object.entries(mailboxIds)) {
if (mailboxId) {
try {
const response = await firstValueFrom(this.mailServerService.mailboxes.getMailbox(wildduckUserId, mailboxId));
if (response.success && response.unseen !== undefined) {
mailboxCounts[mailboxName] = response.unseen;
total += response.unseen;
}
} catch (error) {
this.logger.warn(`Error getting unread count for ${mailboxName} (${mailboxId}):`, error);
mailboxCounts[mailboxName] = 0;
}
}
}
return {
total,
mailboxCounts,
};
} catch (error) {
this.logger.error(`Error calculating unread counts for user ${wildduckUserId}:`, error);
return {
total: 0,
mailboxCounts: {},
};
}
}
} }
@@ -1,25 +1,19 @@
import { EntityManager } from "@mikro-orm/core";
import { Injectable, Logger } from "@nestjs/common"; import { Injectable, Logger } from "@nestjs/common";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { MailboxResolverService } from "./mailbox-resolver.service"; import { MailboxResolverService } from "./mailbox-resolver.service";
import { WebSocketEvent } from "../../email/constants/email-events.constant"; import { WebSocketEvent } from "../../email/constants/email-events.constant";
import { import {
IEmailDeletePayload,
IEmailPayload, IEmailPayload,
IEmailSentNotificationParams,
IEmailSentPayload, IEmailSentPayload,
IEmailStatusNotificationParams, IEmailStatusPayload,
IMailboxUpdateNotificationParams,
IMailboxUpdatePayload, IMailboxUpdatePayload,
IUnreadCountPayload, IUnreadCountPayload,
} from "../../email/interfaces/email-events.interface"; } from "../../email/interfaces/email-events.interface";
import { IEmailStatusPayload } from "../../email/interfaces/email-events.interface";
import { IEmailDeletePayload } from "../../email/interfaces/email-events.interface";
import { EmailGateway } from "../../email-gateway/email.gateway"; import { EmailGateway } from "../../email-gateway/email.gateway";
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 { NotificationQueue } from "../../notifications/queue/notification.queue"; import { NotificationQueue } from "../../notifications/queue/notification.queue";
import { User } from "../../users/entities/user.entity";
@Injectable() @Injectable()
export class EmailNotificationService { export class EmailNotificationService {
@@ -30,298 +24,109 @@ export class EmailNotificationService {
private readonly mailboxResolver: MailboxResolverService, private readonly mailboxResolver: MailboxResolverService,
private readonly mailServerService: MailServerService, private readonly mailServerService: MailServerService,
private readonly notificationQueue: NotificationQueue, private readonly notificationQueue: NotificationQueue,
private readonly em: EntityManager,
) {} ) {}
//************************************************************************ */ //************************************************************************ */
async notifyNewEmail(params: IEmailPayload) { async notifyNewEmail(newEmailData: IEmailPayload) {
try { try {
const em = this.em.fork(); const success = await this.emailGateway.notifyNewEmail(newEmailData.userId, newEmailData);
const user = await em.findOne(User, {
wildduckUserId: params.wildduckUserId, await this.notificationQueue.addNewEmailNotification(newEmailData.userId, {
emailEnabled: true, fromEmail: newEmailData.from.address,
deletedAt: null, fromName: newEmailData.from.name || "Unknown Sender",
subject: newEmailData.subject,
}); });
if (!user) { await this.updateUnreadCount(newEmailData.wildduckUserId, newEmailData.userId);
this.logger.warn(`User not found with wildduckUserId: ${params.wildduckUserId}`);
return;
}
// Create WebSocket payload
const emailPayload: IEmailPayload = {
wildduckUserId: params.wildduckUserId,
messageId: params.messageId,
userId: user.id,
date: params.date,
from: params.from,
to: params.to,
subject: params.subject,
hasAttachments: params.hasAttachments,
mailboxName: params.mailboxName,
isRead: false,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
// Send WebSocket notification
const success = await this.emailGateway.notifyNewEmail(user.id, emailPayload);
// Send push notification
await this.notificationQueue.addNewEmailNotification(user.id, {
fromEmail: params.from.address,
fromName: params.from.name || "Unknown Sender",
subject: params.subject,
});
// Update unread count
await this.updateUnreadCount(user);
if (success) { if (success) {
this.logger.log(`New email notification dispatched for user ${user.id}: ${params.subject}`); this.logger.log(`New email notification dispatched for user ${newEmailData.userId}: ${newEmailData.subject}`);
} else { } else {
this.logger.warn(`Failed to dispatch new email notification for user ${user.id}: user not connected`); this.logger.warn(`Failed to dispatch new email notification for user ${newEmailData.userId}: user not connected`);
} }
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify new email for user ${params.wildduckUserId}:`, error); this.logger.error(`Failed to notify new email for user ${newEmailData.wildduckUserId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailSent(params: IEmailSentNotificationParams) { async notifyEmailSent(emailSentData: IEmailSentPayload) {
try { try {
const payload: IEmailSentPayload = { await this.emailGateway.notifyEmailSent(emailSentData.userId, emailSentData);
userId: params.userId, await this.updateUnreadCount(emailSentData.wildduckUserId, emailSentData.userId);
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
from: params.from,
to: params.recipients,
subject: params.subject,
hasAttachments: false,
mailboxName: MailboxEnum.SENT,
isRead: true,
};
const success = await this.emailGateway.notifyEmailSent(params.userId, payload);
if (success) {
this.logger.log(`Email sent notification dispatched for user ${params.userId}: ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email sent notification for user ${params.userId}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email sent for user ${params.userId}:`, error); this.logger.error(`Failed to notify email sent for user ${emailSentData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailUnread(params: IEmailStatusNotificationParams) { async notifyEmailUnread(emailUnreadData: IEmailStatusPayload) {
try { try {
const em = this.em.fork(); await this.emailGateway.notifyEmailUnread(emailUnreadData.userId, emailUnreadData);
const user = await em.findOne(User, { await this.updateUnreadCount(emailUnreadData.wildduckUserId, emailUnreadData.userId);
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: IEmailStatusPayload = {
userId: user.id,
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailUnread(user.id, payload);
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email unread notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email unread notification for user ${user.id}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email unread for user ${params.userId}:`, error); this.logger.error(`Failed to notify email unread for user ${emailUnreadData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailRead(params: IEmailStatusNotificationParams) { async notifyEmailRead(emailReadData: IEmailStatusPayload) {
try { try {
const em = this.em.fork(); await this.emailGateway.notifyEmailRead(emailReadData.userId, emailReadData);
const user = await em.findOne(User, { await this.updateUnreadCount(emailReadData.wildduckUserId, emailReadData.userId);
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: IEmailStatusPayload = {
userId: user.id,
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailRead(user.id, payload);
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email read notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email read notification for user ${user.id}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email read for user ${params.userId}:`, error); this.logger.error(`Failed to notify email read for user ${emailReadData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailDeleted(params: IEmailStatusNotificationParams) { async notifyEmailDeleted(emailDeletedData: IEmailDeletePayload) {
try { try {
const em = this.em.fork(); await this.emailGateway.notifyEmailDeleted(emailDeletedData.userId, emailDeletedData);
const user = await em.findOne(User, { await this.updateUnreadCount(emailDeletedData.wildduckUserId, emailDeletedData.userId);
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: IEmailDeletePayload = {
userId: user.id,
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailDeleted(user.id, payload);
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email deleted notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email deleted notification for user ${user.id}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email deleted for user ${params.userId}:`, error); this.logger.error(`Failed to notify email deleted for user ${emailDeletedData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailFlagged(params: IEmailStatusNotificationParams) { async notifyEmailFlagged(emailFlaggedData: IEmailStatusPayload) {
try { try {
// Find the user to get the correct User entity ID for websocket notifications await this.emailGateway.notifyEmailFlagged(emailFlaggedData.userId, emailFlaggedData);
const em = this.em.fork(); await this.updateUnreadCount(emailFlaggedData.wildduckUserId, emailFlaggedData.userId);
const user = await em.findOne(User, {
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: IEmailStatusPayload = {
userId: user.id, // Use User entity ID for websocket
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailFlagged(user.id, payload);
if (success) {
this.logger.log(`Email flagged notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email flagged notification for user ${user.id}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email flagged for user ${params.userId}:`, error); this.logger.error(`Failed to notify email flagged for user ${emailFlaggedData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyEmailUnflagged(params: IEmailStatusNotificationParams) { async notifyEmailUnflagged(emailUnflaggedData: IEmailStatusPayload) {
try { try {
// Find the user to get the correct User entity ID for websocket notifications await this.emailGateway.notifyEmailUnflagged(emailUnflaggedData.userId, emailUnflaggedData);
const em = this.em.fork(); await this.updateUnreadCount(emailUnflaggedData.wildduckUserId, emailUnflaggedData.userId);
const user = await em.findOne(User, {
wildduckUserId: params.userId,
emailEnabled: true,
deletedAt: null,
});
if (!user) {
this.logger.warn(`User not found with wildduckUserId: ${params.userId}`);
return;
}
const payload: IEmailStatusPayload = {
userId: user.id, // Use User entity ID for websocket
messageId: params.messageId,
mailboxId: params.mailboxId,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyEmailUnflagged(user.id, payload);
if (success) {
this.logger.log(`Email unflagged notification dispatched for user ${user.id}: message ${params.messageId}`);
} else {
this.logger.warn(`Failed to dispatch email unflagged notification for user ${user.id}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify email unflagged for user ${params.userId}:`, error); this.logger.error(`Failed to notify email unflagged for user ${emailUnflaggedData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
async notifyMailboxUpdate(params: IMailboxUpdateNotificationParams) { async notifyMailboxUpdate(emailMailboxUpdateData: IMailboxUpdatePayload) {
try { try {
const payload: IMailboxUpdatePayload = { await this.emailGateway.notifyMailboxUpdate(emailMailboxUpdateData.userId, emailMailboxUpdateData);
userId: params.userId, await this.updateUnreadCount(emailMailboxUpdateData.wildduckUserId, emailMailboxUpdateData.userId);
mailboxId: params.mailboxId,
mailboxName: params.mailboxName,
unreadCount: params.unreadCount,
totalCount: params.totalCount,
timestamp: new Date().toISOString(),
};
const success = await this.emailGateway.notifyMailboxUpdate(params.userId, payload);
if (success) {
this.logger.log(`Mailbox update notification dispatched for user ${params.userId}: ${params.mailboxName}`);
} else {
this.logger.warn(`Failed to dispatch mailbox update notification for user ${params.userId}: user not connected`);
}
} catch (error) { } catch (error) {
this.logger.error(`Failed to notify mailbox update for user ${params.userId}:`, error); this.logger.error(`Failed to notify mailbox update for user ${emailMailboxUpdateData.userId}:`, error);
} }
} }
//************************************************************************ */ //************************************************************************ */
private async updateUnreadCount(user: User) { private async updateUnreadCount(wildduckUserId: string, userId: string) {
try { try {
const inboxMailboxId = await this.mailboxResolver.getInboxMailboxId(user.wildduckUserId); const inboxMailboxId = await this.mailboxResolver.getInboxMailboxId(wildduckUserId);
const { results } = await firstValueFrom( const { results } = await firstValueFrom(
this.mailServerService.messages.listMessages(user.wildduckUserId, inboxMailboxId, { this.mailServerService.messages.listMessages(wildduckUserId, inboxMailboxId, {
limit: 50, limit: 50,
unseen: true, unseen: true,
}), }),
@@ -330,7 +135,7 @@ export class EmailNotificationService {
const unreadCount = results.length; const unreadCount = results.length;
const payload: IUnreadCountPayload = { const payload: IUnreadCountPayload = {
userId: user.id, // Use the actual User entity ID for the notification payload userId,
count: unreadCount, count: unreadCount,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
totalUnread: unreadCount, totalUnread: unreadCount,
@@ -339,13 +144,13 @@ export class EmailNotificationService {
}, },
}; };
const success = await this.emailGateway.notifyUnreadCountUpdate(user.id, payload); // Use User entity ID here too const success = await this.emailGateway.notifyUnreadCountUpdate(userId, payload);
if (!success) { if (!success) {
this.logger.warn(`Failed to update unread count for user ${user.id}: user not connected`); this.logger.warn(`Failed to update unread count for user ${userId}: user not connected`);
} }
} catch (error) { } catch (error) {
this.logger.error(`Failed to update unread count for user ${user.wildduckUserId}:`, error); this.logger.error(`Failed to update unread count for user ${wildduckUserId}:`, error);
} }
} }
+21 -20
View File
@@ -10,6 +10,7 @@ import { SendEmailDto, UpdateDraftDto } from "./DTO/send-email.dto";
import { EmailService } from "./services/email.service"; import { EmailService } from "./services/email.service";
import { AuthGuards } from "../../common/decorators/auth-guard.decorator"; import { AuthGuards } from "../../common/decorators/auth-guard.decorator";
import { UserDec } from "../../common/decorators/user.decorator"; import { UserDec } from "../../common/decorators/user.decorator";
import { User } from "../users/entities/user.entity";
@AuthGuards() @AuthGuards()
@Controller("email") @Controller("email")
@@ -41,8 +42,8 @@ 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, @Query() query: MailboxIdQueryDto) { getMessage(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.getMessage(userId, params.messageId, query.mailbox); return this.emailService.getMessage(user.wildduckUserId, user.id, params.messageId, query.mailbox);
} }
@Delete("messages/:messageId") @Delete("messages/:messageId")
@@ -97,16 +98,16 @@ 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, @Query() query: MailboxIdQueryDto) { markMessageAsSeen(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.markMessageAsSeen(userId, Number(params.messageId), query.mailbox); return this.emailService.markMessageAsSeen(user.wildduckUserId, user.id, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/unseen") @Patch("messages/:messageId/unseen")
@ApiOperation({ summary: "Mark a message as unseen" }) @ApiOperation({ summary: "Mark a message as unseen" })
@ApiResponse({ status: 200, description: "Message marked as unseen successfully" }) @ApiResponse({ status: 200, description: "Message marked as unseen successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
markMessageAsUnseen(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { markMessageAsUnseen(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.markMessageAsUnseen(userId, Number(params.messageId), query.mailbox); return this.emailService.markMessageAsUnseen(user.wildduckUserId, user.id, Number(params.messageId), query.mailbox);
} }
@Patch("messages/mark-all-read") @Patch("messages/mark-all-read")
@@ -115,24 +116,24 @@ export class EmailController {
@ApiResponse({ status: 206, description: "Some messages marked as read, some failed" }) @ApiResponse({ status: 206, description: "Some messages marked as read, some failed" })
@ApiResponse({ status: 404, description: "Mailbox not found" }) @ApiResponse({ status: 404, description: "Mailbox not found" })
@ApiResponse({ status: 403, description: "Not authorized to mark messages as read" }) @ApiResponse({ status: 403, description: "Not authorized to mark messages as read" })
markAllMessagesAsRead(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) { markAllMessagesAsRead(@UserDec() user: User, @Query() query: MailboxIdQueryDto) {
return this.emailService.markAllMessagesAsRead(wildduckUserId, userId, query.mailbox); return this.emailService.markAllMessagesAsRead(user.wildduckUserId, user.id, query.mailbox);
} }
@Patch("messages/empty-trash") @Patch("messages/empty-trash")
@ApiOperation({ summary: "Empty trash" }) @ApiOperation({ summary: "Empty trash" })
@ApiResponse({ status: 200, description: "Trash emptied successfully" }) @ApiResponse({ status: 200, description: "Trash emptied successfully" })
@ApiResponse({ status: 404, description: "Trash not found" }) @ApiResponse({ status: 404, description: "Trash not found" })
emptyTrash(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) { emptyTrash(@UserDec() user: User, @Query() query: MailboxIdQueryDto) {
return this.emailService.emptyTrash(wildduckUserId, userId, query.mailbox); return this.emailService.emptyTrash(user.wildduckUserId, user.id, query.mailbox);
} }
@Patch("messages/empty-spam") @Patch("messages/empty-spam")
@ApiOperation({ summary: "Empty spam" }) @ApiOperation({ summary: "Empty spam" })
@ApiResponse({ status: 200, description: "Spam emptied successfully" }) @ApiResponse({ status: 200, description: "Spam emptied successfully" })
@ApiResponse({ status: 404, description: "Spam not found" }) @ApiResponse({ status: 404, description: "Spam not found" })
emptySpam(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) { emptySpam(@UserDec() user: User, @Query() query: MailboxIdQueryDto) {
return this.emailService.emptySpam(wildduckUserId, userId, query.mailbox); return this.emailService.emptySpam(user.wildduckUserId, user.id, query.mailbox);
} }
@Get("messages/inbox") @Get("messages/inbox")
@@ -251,8 +252,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message starred successfully" }) @ApiResponse({ status: 200, description: "Message starred successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to star message" }) @ApiResponse({ status: 403, description: "Not authorized to star message" })
starMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { starMessage(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.starMessage(userId, Number(params.messageId), query.mailbox); return this.emailService.starMessage(user.wildduckUserId, user.id, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/unfavorite") @Patch("messages/:messageId/unfavorite")
@@ -260,8 +261,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message unstarred successfully" }) @ApiResponse({ status: 200, description: "Message unstarred successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to unstar message" }) @ApiResponse({ status: 403, description: "Not authorized to unstar message" })
unstarMessage(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { unstarMessage(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.unstarMessage(userId, Number(params.messageId), query.mailbox); return this.emailService.unstarMessage(user.wildduckUserId, user.id, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/toggle-favorite") @Patch("messages/:messageId/toggle-favorite")
@@ -269,8 +270,8 @@ export class EmailController {
@ApiResponse({ status: 200, description: "Message star status toggled successfully" }) @ApiResponse({ status: 200, description: "Message star status toggled successfully" })
@ApiResponse({ status: 404, description: "Message not found" }) @ApiResponse({ status: 404, description: "Message not found" })
@ApiResponse({ status: 403, description: "Not authorized to modify message" }) @ApiResponse({ status: 403, description: "Not authorized to modify message" })
toggleMessageStar(@UserDec("wildduckUserId") userId: string, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) { toggleMessageStar(@UserDec() user: User, @Param() params: MessageParamDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.toggleMessageStar(userId, Number(params.messageId), query.mailbox); return this.emailService.toggleMessageStar(user.wildduckUserId, user.id, Number(params.messageId), query.mailbox);
} }
@Patch("messages/:messageId/restore") @Patch("messages/:messageId/restore")
@@ -309,7 +310,7 @@ 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, @Query() query: MailboxIdQueryDto) { performBulkAction(@UserDec() user: User, @Body() bulkActionDto: BulkActionDto, @Query() query: MailboxIdQueryDto) {
return this.emailService.performBulkAction(userId, bulkActionDto, query.mailbox); return this.emailService.performBulkAction(user.wildduckUserId, user.id, bulkActionDto, query.mailbox);
} }
} }
@@ -1,46 +1,66 @@
// Base interface with common properties // ====================
// BASE INTERFACES
// ====================
export interface IBaseEventPayload { export interface IBaseEventPayload {
userId: string; userId: string;
timestamp: string; timestamp: string;
} }
// Base interface for message-related events
export interface IBaseMessageEventPayload extends IBaseEventPayload { export interface IBaseMessageEventPayload extends IBaseEventPayload {
messageId: number; messageId: number;
} }
// Base interface for mailbox-related events
export interface IBaseMailboxEventPayload extends IBaseEventPayload { export interface IBaseMailboxEventPayload extends IBaseEventPayload {
mailboxId: string; mailboxId: string;
} }
// Base interface for message events that also involve mailboxes
export interface IBaseMessageMailboxEventPayload extends IBaseMessageEventPayload { export interface IBaseMessageMailboxEventPayload extends IBaseMessageEventPayload {
mailboxId: string; mailboxId: string;
} }
// Specific email event interfaces // ====================
export interface IEmailPayload extends IBaseMessageMailboxEventPayload { // COMMON EMAIL INTERFACES
wildduckUserId: string; // ====================
userId: string;
messageId: number; export interface IEmailContact {
date: string;
from: {
name?: string; name?: string;
address: string; address: string;
}; }
to: Array<{
name?: string; export interface IEmailMetadata {
address: string;
}>;
subject: string; subject: string;
preview?: string;
hasAttachments: boolean; hasAttachments: boolean;
mailboxName: string; mailboxName: string;
isRead: boolean; isRead: boolean;
} }
export type IEmailStatusPayload = IBaseMessageMailboxEventPayload; export interface IEmailContent extends IEmailMetadata {
from: IEmailContact;
to: IEmailContact[];
preview?: string;
}
// ====================
// SPECIFIC EVENT PAYLOADS
// ====================
export interface IEmailPayload extends IBaseMessageMailboxEventPayload, IEmailContent {
wildduckUserId: string;
date: string;
}
export interface IEmailSentPayload extends IBaseMessageMailboxEventPayload, IEmailContent {
wildduckUserId: string;
}
export interface IEmailStatusPayload extends IBaseMessageMailboxEventPayload {
wildduckUserId: string;
}
export interface IEmailDeletePayload extends IBaseMessageMailboxEventPayload {
wildduckUserId: string;
}
export interface IEmailMovePayload extends IBaseMessageEventPayload { export interface IEmailMovePayload extends IBaseMessageEventPayload {
fromMailboxId: string; fromMailboxId: string;
@@ -49,27 +69,11 @@ export interface IEmailMovePayload extends IBaseMessageEventPayload {
toMailboxName: string; toMailboxName: string;
} }
export type IEmailDeletePayload = IBaseMessageMailboxEventPayload;
export interface IEmailSentPayload extends IBaseMessageMailboxEventPayload {
from: {
name?: string;
address: string;
};
to: Array<{
name?: string;
address: string;
}>;
subject: string;
hasAttachments: boolean;
mailboxName: string;
isRead: boolean;
}
export interface IMailboxUpdatePayload extends IBaseMailboxEventPayload { export interface IMailboxUpdatePayload extends IBaseMailboxEventPayload {
mailboxName: string; mailboxName: string;
unreadCount: number; unreadCount: number;
totalCount: number; totalCount: number;
wildduckUserId: string;
} }
export interface IUnreadCountPayload extends IBaseEventPayload { export interface IUnreadCountPayload extends IBaseEventPayload {
@@ -78,6 +82,21 @@ export interface IUnreadCountPayload extends IBaseEventPayload {
count: number; count: number;
} }
// ====================
// NOTIFICATION PARAMETERS
// ====================
export interface INewEmailData {
userId: string;
messageId: number;
wildduckUserId: string;
mailboxId: string;
}
// ====================
// WEBSOCKET EVENTS
// ====================
export interface IEmailWebSocketEvents { export interface IEmailWebSocketEvents {
// Server to Client events // Server to Client events
new_email: IEmailPayload; new_email: IEmailPayload;
@@ -100,33 +119,12 @@ export interface IEmailWebSocketEvents {
pong: { timestamp: string }; pong: { timestamp: string };
} }
export interface INewEmailData { // ====================
userId: string; // TYPE UNIONS FOR CONVENIENCE
messageId: number; // ====================
wildduckUserId: string;
mailboxId: string;
}
// Helper interfaces for notification service parameters export type EmailEventPayload = IEmailPayload | IEmailSentPayload | IEmailStatusPayload | IEmailDeletePayload | IEmailMovePayload;
export interface IEmailSentNotificationParams {
userId: string;
messageId: number;
mailboxId: string;
recipients: Array<{ name?: string; address: string }>;
subject: string;
from: { name?: string; address: string };
}
export interface IEmailStatusNotificationParams { export type MailboxEventPayload = IMailboxUpdatePayload | IUnreadCountPayload;
userId: string;
messageId: number;
mailboxId: string;
}
export interface IMailboxUpdateNotificationParams { export type AllEventPayloads = EmailEventPayload | MailboxEventPayload;
userId: string;
mailboxId: string;
mailboxName: string;
unreadCount: number;
totalCount: number;
}
@@ -90,7 +90,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
for (const message of response.results) { for (const message of response.results) {
try { try {
await this.emailService.markMessageAsSeen(wildduckUserId, message.id, mailboxId); await this.emailService.markMessageAsSeen(wildduckUserId, userId, message.id, mailboxId);
results.successful.push(message.id); results.successful.push(message.id);
totalMarked++; totalMarked++;
} catch (error) { } catch (error) {
@@ -109,7 +109,9 @@ export class EmailBulkActionProcessor extends WorkerHost {
results.totalMarked = totalMarked; results.totalMarked = totalMarked;
await this.emailNotificationService.notifyEmailRead({ await this.emailNotificationService.notifyEmailRead({
timestamp: new Date().toISOString(),
userId, userId,
wildduckUserId,
messageId: results.successful[0], messageId: results.successful[0],
mailboxId, mailboxId,
}); });
@@ -143,7 +145,9 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
await this.emailNotificationService.notifyEmailDeleted({ await this.emailNotificationService.notifyEmailDeleted({
timestamp: new Date().toISOString(),
userId, userId,
wildduckUserId,
messageId: randomInt(1, 500), messageId: randomInt(1, 500),
mailboxId, mailboxId,
}); });
@@ -172,7 +176,9 @@ export class EmailBulkActionProcessor extends WorkerHost {
} }
await this.emailNotificationService.notifyEmailDeleted({ await this.emailNotificationService.notifyEmailDeleted({
timestamp: new Date().toISOString(),
userId, userId,
wildduckUserId,
messageId: randomInt(1, 500), messageId: randomInt(1, 500),
mailboxId, mailboxId,
}); });
+32 -19
View File
@@ -79,12 +79,17 @@ export class EmailService {
const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId);
await this.emailNotificationService.notifyEmailSent({ await this.emailNotificationService.notifyEmailSent({
userId: wildduckUserId, wildduckUserId,
userId,
messageId: parseInt(response.message.id), messageId: parseInt(response.message.id),
mailboxId: sentMailboxId, mailboxId: sentMailboxId,
recipients: sendEmailDto.to, to: sendEmailDto.to,
subject: sendEmailDto.subject, subject: sendEmailDto.subject,
from: sendEmailDto.from, from: sendEmailDto.from,
hasAttachments: false,
mailboxName: "Sent",
isRead: true,
timestamp: new Date().toISOString(),
}); });
return { return {
@@ -217,8 +222,8 @@ export class EmailService {
} }
//######################################################## //########################################################
async getMessage(wildduckUserId: string, messageId: number, mailboxId: string) { async getMessage(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); await this.markMessageAsSeen(wildduckUserId, userId, messageId, mailboxId);
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId)); const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId); const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId);
@@ -253,13 +258,15 @@ export class EmailService {
} }
//============================================== //==============================================
async markMessageAsSeen(wildduckUserId: string, messageId: number, mailboxId: string) { async markMessageAsSeen(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: true })); const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: true }));
await this.emailNotificationService.notifyEmailRead({ await this.emailNotificationService.notifyEmailRead({
userId: wildduckUserId, wildduckUserId,
userId,
messageId, messageId,
mailboxId, mailboxId,
timestamp: new Date().toISOString(),
}); });
return { return {
@@ -270,13 +277,15 @@ export class EmailService {
} }
//============================================== //==============================================
async markMessageAsUnseen(wildduckUserId: string, messageId: number, mailboxId: string) { async markMessageAsUnseen(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: false })); const { updated } = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { seen: false }));
await this.emailNotificationService.notifyEmailUnread({ await this.emailNotificationService.notifyEmailUnread({
userId: wildduckUserId, wildduckUserId,
userId,
messageId, messageId,
mailboxId, mailboxId,
timestamp: new Date().toISOString(),
}); });
return { return {
success: true, success: true,
@@ -608,13 +617,15 @@ export class EmailService {
} }
//============================================== //==============================================
async starMessage(wildduckUserId: string, messageId: number, mailboxId: string) { async starMessage(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
const result = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { flagged: true })); const result = await firstValueFrom(this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { flagged: true }));
await this.emailNotificationService.notifyEmailFlagged({ await this.emailNotificationService.notifyEmailFlagged({
userId: wildduckUserId, userId,
wildduckUserId,
messageId, messageId,
mailboxId, mailboxId,
timestamp: new Date().toISOString(),
}); });
return { return {
@@ -625,7 +636,7 @@ export class EmailService {
} }
//============================================== //==============================================
async unstarMessage(wildduckUserId: string, messageId: number, mailboxId: string) { async unstarMessage(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
const result = await firstValueFrom( const result = await firstValueFrom(
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
flagged: false, flagged: false,
@@ -633,9 +644,11 @@ export class EmailService {
); );
await this.emailNotificationService.notifyEmailUnflagged({ await this.emailNotificationService.notifyEmailUnflagged({
userId: wildduckUserId, userId,
wildduckUserId,
messageId, messageId,
mailboxId, mailboxId,
timestamp: new Date().toISOString(),
}); });
return { return {
@@ -646,13 +659,13 @@ export class EmailService {
} }
//============================================== //==============================================
async toggleMessageStar(wildduckUserId: string, messageId: number, mailboxId: string) { async toggleMessageStar(wildduckUserId: string, userId: string, messageId: number, mailboxId: string) {
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId)); const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
if (message.flagged) { if (message.flagged) {
return this.unstarMessage(wildduckUserId, messageId, mailboxId); return this.unstarMessage(wildduckUserId, userId, messageId, mailboxId);
} else { } else {
return this.starMessage(wildduckUserId, messageId, mailboxId); return this.starMessage(wildduckUserId, userId, messageId, mailboxId);
} }
} }
@@ -726,7 +739,7 @@ export class EmailService {
} }
//============================================== //==============================================
async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) { async performBulkAction(wildduckUserId: string, userId: string, bulkActionDto: BulkActionDto, mailboxId: string) {
const results = { const results = {
successful: [] as number[], successful: [] as number[],
failed: [] as { messageId: number; error: string }[], failed: [] as { messageId: number; error: string }[],
@@ -736,7 +749,7 @@ export class EmailService {
try { try {
switch (bulkActionDto.action) { switch (bulkActionDto.action) {
case BulkActionType.SEEN: case BulkActionType.SEEN:
await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); await this.markMessageAsSeen(wildduckUserId, userId, messageId, mailboxId);
break; break;
case BulkActionType.DELETE: case BulkActionType.DELETE:
await this.deleteMessage(wildduckUserId, messageId, mailboxId); await this.deleteMessage(wildduckUserId, messageId, mailboxId);
@@ -748,10 +761,10 @@ export class EmailService {
await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId); await this.moveMessageToInboxFromArchive(wildduckUserId, messageId, mailboxId);
break; break;
case BulkActionType.FAVORITE: case BulkActionType.FAVORITE:
await this.starMessage(wildduckUserId, messageId, mailboxId); await this.starMessage(wildduckUserId, userId, messageId, mailboxId);
break; break;
case BulkActionType.UNFAVORITE: case BulkActionType.UNFAVORITE:
await this.unstarMessage(wildduckUserId, messageId, mailboxId); await this.unstarMessage(wildduckUserId, userId, messageId, mailboxId);
break; break;
case BulkActionType.JUNK: case BulkActionType.JUNK:
await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId); await this.moveMessageToJunk(wildduckUserId, messageId, mailboxId);
@@ -88,6 +88,24 @@ export class NajvaPushService {
return response.data; return response.data;
} catch (error) { } catch (error) {
// Handle gateway/proxy timeout errors - notification likely sent successfully
if (error instanceof AxiosError && [502, 503, 504].includes(error.response?.status || 0)) {
this.logger.warn(
`Gateway/proxy error (${error.response?.status}) occurred, but notification likely sent successfully to ${validTokens.length} tokens`,
);
// Return a mock successful response for gateway errors since notification is likely delivered
return {
Entries: {
request_id: `gateway_error_${Date.now()}`,
tokens: validTokens.map((token) => ({
token,
status: "Sent" as const,
})),
},
};
}
this.logger.error("Failed to send push notification:", error); this.logger.error("Failed to send push notification:", error);
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
@@ -111,6 +129,14 @@ export class NajvaPushService {
const tokenResult = result.Entries.tokens.find((t) => t.token === userToken); const tokenResult = result.Entries.tokens.find((t) => t.token === userToken);
return tokenResult?.status === "Sent"; return tokenResult?.status === "Sent";
} catch (error) { } catch (error) {
// Handle gateway/proxy timeout errors - notification likely sent successfully
if (error instanceof AxiosError && [502, 503, 504].includes(error.response?.status || 0)) {
this.logger.warn(
`Gateway/proxy error (${error.response?.status}) occurred for user token ${userToken}, but notification likely sent successfully`,
);
return true;
}
this.logger.error(`Failed to send notification to user token ${userToken}:`, error); this.logger.error(`Failed to send notification to user token ${userToken}:`, error);
return false; return false;
} }
@@ -131,6 +157,14 @@ export class NajvaPushService {
return { success: successCount, failed: failedCount, invalidTokens }; return { success: successCount, failed: failedCount, invalidTokens };
} catch (error) { } catch (error) {
// Handle gateway/proxy timeout errors - notification likely sent successfully
if (error instanceof AxiosError && [502, 503, 504].includes(error.response?.status || 0)) {
this.logger.warn(
`Gateway/proxy error (${error.response?.status}) occurred for ${userTokens.length} tokens, but notifications likely sent successfully`,
);
return { success: userTokens.length, failed: 0, invalidTokens: [] };
}
this.logger.error(`Failed to send notifications to multiple users:`, error); this.logger.error(`Failed to send notifications to multiple users:`, error);
return { success: 0, failed: userTokens.length }; return { success: 0, failed: userTokens.length };
} }