chore: update webscoket connetion and polling service
This commit is contained in:
@@ -280,6 +280,7 @@ export const enum EmailMessage {
|
|||||||
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد",
|
MESSAGE_REMOVED_FROM_FAVORITE_SUCCESSFULLY = "پیام با موفقیت از مورد علاقه حذف شد",
|
||||||
TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد",
|
TRASH_EMPTYED_SUCCESSFULLY = "سطل زباله با موفقیت خالی شد",
|
||||||
SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند",
|
SPAM_EMPTYED_SUCCESSFULLY = "همه پیام های اسپم با موفقیت حذف شدند",
|
||||||
|
SAVE_DRAFT_SUCCESS = "پیش نویس با موفقیت ذخیره شد",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WebSocketMessage {
|
export const enum WebSocketMessage {
|
||||||
|
|||||||
@@ -28,11 +28,7 @@ import { EmailPollingService } from "../email-utils/services/email-polling.servi
|
|||||||
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
@UseFilters(WsExceptionFilter)
|
@UseFilters(WsExceptionFilter)
|
||||||
@WebSocketGateway({
|
@WebSocketGateway({ namespace: WEBSOCKET_NAMESPACES.EMAIL, cors: { origin: true, credentials: true }, transports: ["websocket", "polling"] })
|
||||||
namespace: WEBSOCKET_NAMESPACES.EMAIL,
|
|
||||||
cors: { origin: true, credentials: true },
|
|
||||||
transports: ["websocket", "polling"],
|
|
||||||
})
|
|
||||||
export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
|
||||||
@WebSocketServer()
|
@WebSocketServer()
|
||||||
server: Server;
|
server: Server;
|
||||||
@@ -86,7 +82,7 @@ export class EmailGateway implements OnGatewayInit, OnGatewayConnection, OnGatew
|
|||||||
|
|
||||||
// Start email polling for this user
|
// Start email polling for this user
|
||||||
try {
|
try {
|
||||||
await this.emailPollingService.startPollingForUser(user.id, client.id);
|
await this.emailPollingService.startPollingForUser(user.wildduckUserId, user.id, client.id);
|
||||||
this.logger.log(`Email polling started for user ${user.id}`);
|
this.logger.log(`Email polling started for user ${user.id}`);
|
||||||
} catch (pollingError) {
|
} catch (pollingError) {
|
||||||
this.logger.error(`Failed to start email polling for user ${user.id}:`, pollingError);
|
this.logger.error(`Failed to start email polling for user ${user.id}:`, pollingError);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface EmailPollingJobData {
|
export interface EmailPollingJobData {
|
||||||
|
wildduckUserId: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
lastCheckTimestamp?: string;
|
lastCheckTimestamp?: string;
|
||||||
connectionId: string;
|
connectionId: string;
|
||||||
|
|||||||
@@ -23,47 +23,41 @@ export class EmailPollingProcessor extends WorkerProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async process(job: Job<EmailPollingJobData>): Promise<void> {
|
async process(job: Job<EmailPollingJobData>): Promise<void> {
|
||||||
const { userId, lastCheckTimestamp, connectionId } = job.data;
|
const { wildduckUserId, userId, lastCheckTimestamp, connectionId } = job.data;
|
||||||
|
|
||||||
this.logger.debug(`Processing email polling job for user ${userId} (connection: ${connectionId})`);
|
this.logger.debug(`Processing email polling job for user ${userId} (connection: ${connectionId})`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if user is still connected before processing
|
|
||||||
if (!this.emailGateway.isUserConnected(userId)) {
|
if (!this.emailGateway.isUserConnected(userId)) {
|
||||||
this.logger.debug(`User ${userId} no longer connected, skipping email check`);
|
this.logger.debug(`User ${userId} no longer connected, skipping email check`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user's mailbox information
|
const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(wildduckUserId);
|
||||||
const mailboxIds = await this.mailboxResolverService.getUserMailboxIds(userId);
|
|
||||||
if (!mailboxIds.inbox) {
|
if (!mailboxIds.inbox) {
|
||||||
this.logger.warn(`No inbox found for user ${userId}`);
|
this.logger.warn(`No inbox found for user ${userId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate time range for checking new emails
|
|
||||||
const checkSince = lastCheckTimestamp ? new Date(lastCheckTimestamp) : new Date(Date.now() - 5 * 60 * 1000);
|
const checkSince = lastCheckTimestamp ? new Date(lastCheckTimestamp) : new Date(Date.now() - 5 * 60 * 1000);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
this.logger.debug(`Checking emails for user ${userId} since ${checkSince.toISOString()}`);
|
this.logger.debug(`Checking emails for user ${userId} since ${checkSince.toISOString()}`);
|
||||||
|
|
||||||
// Get new emails from inbox since last check
|
const newEmails = await this.getRecentEmails(wildduckUserId, mailboxIds.inbox, checkSince);
|
||||||
const newEmails = await this.getRecentEmails(userId, mailboxIds.inbox, checkSince);
|
|
||||||
|
|
||||||
if (newEmails.length > 0) {
|
if (newEmails.length > 0) {
|
||||||
this.logger.log(`Found ${newEmails.length} new emails for user ${userId}`);
|
this.logger.log(`Found ${newEmails.length} new emails for user ${wildduckUserId}`);
|
||||||
|
|
||||||
// Notify user of each new email via websocket
|
|
||||||
|
|
||||||
const lastEmail = newEmails[newEmails.length - 1];
|
const lastEmail = newEmails[newEmails.length - 1];
|
||||||
await this.emailGateway.notifyNewEmail(userId, {
|
await this.emailGateway.notifyNewEmail(userId, {
|
||||||
wildduckUserId: userId,
|
wildduckUserId,
|
||||||
|
userId,
|
||||||
messageId: lastEmail.id,
|
messageId: lastEmail.id,
|
||||||
date: lastEmail.date || new Date().toISOString(),
|
date: lastEmail.date || new Date().toISOString(),
|
||||||
from: lastEmail.from,
|
from: lastEmail.from,
|
||||||
to: lastEmail.to || [],
|
to: lastEmail.to || [],
|
||||||
subject: lastEmail.subject,
|
subject: lastEmail.subject,
|
||||||
userId,
|
|
||||||
timestamp: lastEmail.date || new Date().toISOString(),
|
timestamp: lastEmail.date || new Date().toISOString(),
|
||||||
mailboxId: lastEmail.mailbox,
|
mailboxId: lastEmail.mailbox,
|
||||||
mailboxName: "Inbox",
|
mailboxName: "Inbox",
|
||||||
@@ -72,7 +66,7 @@ export class EmailPollingProcessor extends WorkerProcessor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update unread count
|
// Update unread count
|
||||||
const unreadStats = await this.getUnreadCounts(userId, mailboxIds);
|
const unreadStats = await this.getUnreadCounts(wildduckUserId, mailboxIds);
|
||||||
await this.emailGateway.notifyUnreadCountUpdate(userId, {
|
await this.emailGateway.notifyUnreadCountUpdate(userId, {
|
||||||
userId,
|
userId,
|
||||||
totalUnread: unreadStats.total,
|
totalUnread: unreadStats.total,
|
||||||
@@ -96,15 +90,15 @@ export class EmailPollingProcessor extends WorkerProcessor {
|
|||||||
throw error; // Re-throw to trigger retry mechanism
|
throw error; // Re-throw to trigger retry mechanism
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//=======================================================
|
||||||
// Helper method to get recent emails from a mailbox
|
private async getRecentEmails(wildduckUserId: string, mailboxId: string, since: Date) {
|
||||||
private async getRecentEmails(userId: string, mailboxId: string, since: Date) {
|
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(
|
const response = await firstValueFrom(
|
||||||
this.mailServerService.messages.searchMessages(userId, {
|
this.mailServerService.messages.searchMessages(wildduckUserId, {
|
||||||
mailbox: mailboxId,
|
mailbox: mailboxId,
|
||||||
limit: 50, // Limit to avoid too many results
|
limit: 50,
|
||||||
order: "desc", // Most recent first
|
order: "desc",
|
||||||
|
unseen: true,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -117,22 +111,20 @@ export class EmailPollingProcessor extends WorkerProcessor {
|
|||||||
return emailDate >= since;
|
return emailDate >= since;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error fetching recent emails for user ${userId}:`, error);
|
this.logger.error(`Error fetching recent emails for user ${wildduckUserId}:`, error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//=======================================================
|
||||||
// Helper method to get unread counts from all mailboxes
|
private async getUnreadCounts(wildduckUserId: string, mailboxIds: UserMailboxIds) {
|
||||||
private async getUnreadCounts(userId: string, mailboxIds: UserMailboxIds) {
|
|
||||||
try {
|
try {
|
||||||
const mailboxCounts: Record<string, number> = {};
|
const mailboxCounts: Record<string, number> = {};
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
|
||||||
// Get unread count from each mailbox
|
|
||||||
for (const [mailboxName, mailboxId] of Object.entries(mailboxIds)) {
|
for (const [mailboxName, mailboxId] of Object.entries(mailboxIds)) {
|
||||||
if (mailboxId) {
|
if (mailboxId) {
|
||||||
try {
|
try {
|
||||||
const response = await firstValueFrom(this.mailServerService.mailboxes.getMailbox(userId, mailboxId));
|
const response = await firstValueFrom(this.mailServerService.mailboxes.getMailbox(wildduckUserId, mailboxId));
|
||||||
|
|
||||||
if (response.success && response.unseen !== undefined) {
|
if (response.success && response.unseen !== undefined) {
|
||||||
mailboxCounts[mailboxName] = response.unseen;
|
mailboxCounts[mailboxName] = response.unseen;
|
||||||
@@ -150,7 +142,7 @@ export class EmailPollingProcessor extends WorkerProcessor {
|
|||||||
mailboxCounts,
|
mailboxCounts,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Error calculating unread counts for user ${userId}:`, error);
|
this.logger.error(`Error calculating unread counts for user ${wildduckUserId}:`, error);
|
||||||
return {
|
return {
|
||||||
total: 0,
|
total: 0,
|
||||||
mailboxCounts: {},
|
mailboxCounts: {},
|
||||||
|
|||||||
@@ -14,11 +14,12 @@ export class EmailPollingService {
|
|||||||
constructor(@InjectQueue(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE) private readonly emailPollingQueue: Queue<EmailPollingJobData>) {}
|
constructor(@InjectQueue(EMAIL_QUEUE_CONSTANTS.EMAIL_POLLING_QUEUE) private readonly emailPollingQueue: Queue<EmailPollingJobData>) {}
|
||||||
|
|
||||||
//=======================================================
|
//=======================================================
|
||||||
async startPollingForUser(userId: string, connectionId: string): Promise<void> {
|
async startPollingForUser(wildduckUserId: string, userId: string, connectionId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.stopPollingForUser(userId);
|
await this.stopPollingForUser(userId);
|
||||||
|
|
||||||
const jobData: EmailPollingJobData = {
|
const jobData: EmailPollingJobData = {
|
||||||
|
wildduckUserId,
|
||||||
userId,
|
userId,
|
||||||
connectionId,
|
connectionId,
|
||||||
lastCheckTimestamp: new Date().toISOString(),
|
lastCheckTimestamp: new Date().toISOString(),
|
||||||
@@ -54,6 +55,7 @@ export class EmailPollingService {
|
|||||||
async stopPollingForUser(userId: string): Promise<void> {
|
async stopPollingForUser(userId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const activeJob = this.activeJobs.get(userId);
|
const activeJob = this.activeJobs.get(userId);
|
||||||
|
console.log(activeJob);
|
||||||
if (!activeJob) {
|
if (!activeJob) {
|
||||||
this.logger.debug(`No active polling job found for user ${userId}`);
|
this.logger.debug(`No active polling job found for user ${userId}`);
|
||||||
return;
|
return;
|
||||||
@@ -140,9 +142,9 @@ export class EmailPollingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//=======================================================
|
//=======================================================
|
||||||
async restartPollingForUser(userId: string, connectionId: string): Promise<void> {
|
async restartPollingForUser(wildduckUserId: string, userId: string, connectionId: string): Promise<void> {
|
||||||
this.logger.log(`Restarting email polling for user ${userId}`);
|
this.logger.log(`Restarting email polling for user ${userId}`);
|
||||||
await this.stopPollingForUser(userId);
|
await this.stopPollingForUser(userId);
|
||||||
await this.startPollingForUser(userId, connectionId);
|
await this.startPollingForUser(wildduckUserId, userId, connectionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Get user mailbox IDs with caching
|
* Get user mailbox IDs with caching
|
||||||
*/
|
*/
|
||||||
async getUserMailboxIds(userId: string): Promise<UserMailboxIds> {
|
async getUserMailboxIds(wildduckUserId: string): Promise<UserMailboxIds> {
|
||||||
const cached = this.getCachedMailboxIds(userId);
|
const cached = this.getCachedMailboxIds(wildduckUserId);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
||||||
this.logger.log(`Fetching mailbox IDs for user: ${userId}`);
|
this.logger.log(`Fetching mailbox IDs for user: ${wildduckUserId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(userId));
|
const mailboxes = await firstValueFrom(this.mailServerService.mailboxes.listMailboxes(wildduckUserId));
|
||||||
|
|
||||||
const mailboxIds: UserMailboxIds = {
|
const mailboxIds: UserMailboxIds = {
|
||||||
inbox: this.findMailboxId(mailboxes.results, MailboxEnum.INBOX),
|
inbox: this.findMailboxId(mailboxes.results, MailboxEnum.INBOX),
|
||||||
@@ -37,12 +37,12 @@ export class MailboxResolverService {
|
|||||||
// favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false),
|
// favorite: this.findMailboxId(mailboxes.results, MailboxEnum.FAVORITE, false),
|
||||||
};
|
};
|
||||||
|
|
||||||
this.cacheMailboxIds(userId, mailboxIds);
|
this.cacheMailboxIds(wildduckUserId, mailboxIds);
|
||||||
|
|
||||||
this.logger.log(`Mailbox IDs cached for user: ${userId}`);
|
this.logger.log(`Mailbox IDs cached for user: ${wildduckUserId}`);
|
||||||
return mailboxIds;
|
return mailboxIds;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`Failed to fetch mailbox IDs for user ${userId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
this.logger.error(`Failed to fetch mailbox IDs for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,12 +50,12 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Get specific mailbox ID
|
* Get specific mailbox ID
|
||||||
*/
|
*/
|
||||||
async getMailboxId(userId: string, mailboxType: keyof UserMailboxIds): Promise<string> {
|
async getMailboxId(wildduckUserId: string, mailboxType: keyof UserMailboxIds): Promise<string> {
|
||||||
const mailboxIds = await this.getUserMailboxIds(userId);
|
const mailboxIds = await this.getUserMailboxIds(wildduckUserId);
|
||||||
const mailboxId = mailboxIds[mailboxType];
|
const mailboxId = mailboxIds[mailboxType];
|
||||||
|
|
||||||
if (!mailboxId) {
|
if (!mailboxId) {
|
||||||
throw new NotFoundException(`${mailboxType} mailbox not found for user ${userId}`);
|
throw new NotFoundException(`${mailboxType} mailbox not found for user ${wildduckUserId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mailboxId;
|
return mailboxId;
|
||||||
@@ -73,37 +73,37 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Get drafts mailbox ID
|
* Get drafts mailbox ID
|
||||||
*/
|
*/
|
||||||
async getDraftsMailboxId(userId: string): Promise<string> {
|
async getDraftsMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "drafts");
|
return this.getMailboxId(wildduckUserId, "drafts");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get inbox mailbox ID
|
* Get inbox mailbox ID
|
||||||
*/
|
*/
|
||||||
async getInboxMailboxId(userId: string): Promise<string> {
|
async getInboxMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "inbox");
|
return this.getMailboxId(wildduckUserId, "inbox");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get sent mailbox ID
|
* Get sent mailbox ID
|
||||||
*/
|
*/
|
||||||
async getSentMailboxId(userId: string): Promise<string> {
|
async getSentMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "sent");
|
return this.getMailboxId(wildduckUserId, "sent");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get trash mailbox ID
|
* Get trash mailbox ID
|
||||||
*/
|
*/
|
||||||
async getTrashMailboxId(userId: string): Promise<string> {
|
async getTrashMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "trash");
|
return this.getMailboxId(wildduckUserId, "trash");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getJunkMailboxId(userId: string): Promise<string> {
|
async getJunkMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "junk");
|
return this.getMailboxId(wildduckUserId, "junk");
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArchiveMailboxId(userId: string): Promise<string> {
|
async getArchiveMailboxId(wildduckUserId: string): Promise<string> {
|
||||||
return this.getMailboxId(userId, "archive");
|
return this.getMailboxId(wildduckUserId, "archive");
|
||||||
}
|
}
|
||||||
|
|
||||||
// async getFavoriteMailboxId(userId: string): Promise<string> {
|
// async getFavoriteMailboxId(userId: string): Promise<string> {
|
||||||
@@ -113,10 +113,10 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Clear cache for a specific user
|
* Clear cache for a specific user
|
||||||
*/
|
*/
|
||||||
clearUserCache(userId: string): void {
|
clearUserCache(wildduckUserId: string): void {
|
||||||
this.mailboxCache.delete(userId);
|
this.mailboxCache.delete(wildduckUserId);
|
||||||
this.cacheExpiry.delete(userId);
|
this.cacheExpiry.delete(wildduckUserId);
|
||||||
this.logger.log(`Cache cleared for user: ${userId}`);
|
this.logger.log(`Cache cleared for user: ${wildduckUserId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -131,18 +131,18 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Get cached mailbox IDs if not expired
|
* Get cached mailbox IDs if not expired
|
||||||
*/
|
*/
|
||||||
private getCachedMailboxIds(userId: string): UserMailboxIds | null {
|
private getCachedMailboxIds(wildduckUserId: string): UserMailboxIds | null {
|
||||||
const cached = this.mailboxCache.get(userId);
|
const cached = this.mailboxCache.get(wildduckUserId);
|
||||||
const expiry = this.cacheExpiry.get(userId);
|
const expiry = this.cacheExpiry.get(wildduckUserId);
|
||||||
|
|
||||||
if (cached && expiry && Date.now() < expiry) {
|
if (cached && expiry && Date.now() < expiry) {
|
||||||
this.logger.debug(`Using cached mailbox IDs for user: ${userId}`);
|
this.logger.debug(`Using cached mailbox IDs for user: ${wildduckUserId}`);
|
||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cached) {
|
if (cached) {
|
||||||
this.logger.debug(`Cache expired for user: ${userId}`);
|
this.logger.debug(`Cache expired for user: ${wildduckUserId}`);
|
||||||
this.clearUserCache(userId);
|
this.clearUserCache(wildduckUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -151,9 +151,9 @@ export class MailboxResolverService {
|
|||||||
/**
|
/**
|
||||||
* Cache mailbox IDs with expiry
|
* Cache mailbox IDs with expiry
|
||||||
*/
|
*/
|
||||||
private cacheMailboxIds(userId: string, mailboxIds: UserMailboxIds): void {
|
private cacheMailboxIds(wildduckUserId: string, mailboxIds: UserMailboxIds): void {
|
||||||
this.mailboxCache.set(userId, mailboxIds);
|
this.mailboxCache.set(wildduckUserId, mailboxIds);
|
||||||
this.cacheExpiry.set(userId, Date.now() + this.CACHE_DURATION);
|
this.cacheExpiry.set(wildduckUserId, Date.now() + this.CACHE_DURATION);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -49,13 +49,13 @@ export const EMAIL_QUEUE_CONSTANTS = {
|
|||||||
EMAIL_BULK_ACTION_DELAY: 1000,
|
EMAIL_BULK_ACTION_DELAY: 1000,
|
||||||
EMAIL_BULK_ACTION_BATCH_SIZE: 250,
|
EMAIL_BULK_ACTION_BATCH_SIZE: 250,
|
||||||
EMAIL_BULK_ACTION_ATTEMPTS: 3,
|
EMAIL_BULK_ACTION_ATTEMPTS: 3,
|
||||||
EMAIL_BULK_ACTION_BACKOFF_DELAY: 2000, // 2 seconds
|
EMAIL_BULK_ACTION_BACKOFF_DELAY: 2000,
|
||||||
EMAIL_BULK_ACTION_PRIORITY: 1,
|
EMAIL_BULK_ACTION_PRIORITY: 1,
|
||||||
//
|
//
|
||||||
EMAIL_POLLING_INTERVAL: 30000, // 30 seconds
|
EMAIL_POLLING_INTERVAL: 3000,
|
||||||
EMAIL_POLLING_REMOVE_ON_COMPLETE: 5, // Keep last 5 completed jobs
|
EMAIL_POLLING_REMOVE_ON_COMPLETE: true,
|
||||||
EMAIL_POLLING_REMOVE_ON_FAIL: 10, // Keep last 10 failed jobs
|
EMAIL_POLLING_REMOVE_ON_FAIL: 10,
|
||||||
EMAIL_POLLING_ATTEMPTS: 3, // Retry failed jobs 3 times
|
EMAIL_POLLING_ATTEMPTS: 3,
|
||||||
EMAIL_POLLING_BACKOFF_DELAY: 2000, // 2 seconds
|
EMAIL_POLLING_BACKOFF_DELAY: 2000,
|
||||||
EMAIL_POLLING_PRIORITY: 1,
|
EMAIL_POLLING_PRIORITY: 1,
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -115,24 +115,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") userId: string, @Query() query: MailboxIdQueryDto) {
|
markAllMessagesAsRead(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
|
||||||
return this.emailService.markAllMessagesAsRead(userId, query.mailbox);
|
return this.emailService.markAllMessagesAsRead(wildduckUserId, userId, 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") userId: string, @Query() query: MailboxIdQueryDto) {
|
emptyTrash(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
|
||||||
return this.emailService.emptyTrash(userId, query.mailbox);
|
return this.emailService.emptyTrash(wildduckUserId, userId, 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") userId: string, @Query() query: MailboxIdQueryDto) {
|
emptySpam(@UserDec("wildduckUserId") wildduckUserId: string, @UserDec("id") userId: string, @Query() query: MailboxIdQueryDto) {
|
||||||
return this.emailService.emptySpam(userId, query.mailbox);
|
return this.emailService.emptySpam(wildduckUserId, userId, query.mailbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("messages/inbox")
|
@Get("messages/inbox")
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ import { User } from "../users/entities/user.entity";
|
|||||||
imports: [
|
imports: [
|
||||||
BullModule.registerQueue({
|
BullModule.registerQueue({
|
||||||
name: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_QUEUE,
|
name: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_QUEUE,
|
||||||
|
defaultJobOptions: {
|
||||||
|
removeOnComplete: true,
|
||||||
|
removeOnFail: false,
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
MailServerModule,
|
MailServerModule,
|
||||||
TemplatesModule,
|
TemplatesModule,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export interface IEmailBulkActionJob {
|
export interface IEmailBulkActionJob {
|
||||||
wildduckUserId: string;
|
wildduckUserId: string;
|
||||||
|
userId: string;
|
||||||
mailboxId: string;
|
mailboxId: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,17 +31,17 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
switch (name) {
|
switch (name) {
|
||||||
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL:
|
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL:
|
||||||
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
||||||
await this.markAllMessagesAsRead(data.wildduckUserId, data.mailboxId);
|
await this.markAllMessagesAsRead(data.wildduckUserId, data.userId, data.mailboxId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH:
|
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH:
|
||||||
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
||||||
await this.emptyTrash(data.wildduckUserId, data.mailboxId);
|
await this.emptyTrash(data.wildduckUserId, data.userId, data.mailboxId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM:
|
case EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM:
|
||||||
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
this.logger.debug(`Processing bulk action for: ${data.wildduckUserId}`);
|
||||||
await this.emptySpam(data.wildduckUserId, data.mailboxId);
|
await this.emptySpam(data.wildduckUserId, data.userId, data.mailboxId);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -55,12 +55,11 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
private async markAllMessagesAsRead(wildduckUserId: string, mailboxId: string) {
|
private async markAllMessagesAsRead(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
this.logger.log(`Marking all messages as read for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
this.logger.log(`Marking all messages as read for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get all unread messages from the mailbox with a reasonable limit
|
const limit = EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BATCH_SIZE;
|
||||||
const limit = EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BATCH_SIZE; // Process in batches to avoid memory issues
|
|
||||||
let totalMarked = 0;
|
let totalMarked = 0;
|
||||||
let hasMore = true;
|
let hasMore = true;
|
||||||
let next: string | undefined;
|
let next: string | undefined;
|
||||||
@@ -72,11 +71,10 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
};
|
};
|
||||||
|
|
||||||
while (hasMore) {
|
while (hasMore) {
|
||||||
// Fetch unread messages in batches
|
|
||||||
const query: Record<string, unknown> = {
|
const query: Record<string, unknown> = {
|
||||||
limit,
|
limit,
|
||||||
order: "desc",
|
order: "desc",
|
||||||
unseen: true, // Only get unread messages
|
unseen: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (next) {
|
if (next) {
|
||||||
@@ -90,7 +88,6 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark each message as seen
|
|
||||||
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, message.id, mailboxId);
|
||||||
@@ -105,7 +102,6 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if there are more messages
|
|
||||||
next = typeof response.nextCursor === "string" ? response.nextCursor : undefined;
|
next = typeof response.nextCursor === "string" ? response.nextCursor : undefined;
|
||||||
hasMore = !!next && response.results.length === limit;
|
hasMore = !!next && response.results.length === limit;
|
||||||
}
|
}
|
||||||
@@ -113,12 +109,14 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
results.totalMarked = totalMarked;
|
results.totalMarked = totalMarked;
|
||||||
|
|
||||||
await this.emailNotificationService.notifyEmailRead({
|
await this.emailNotificationService.notifyEmailRead({
|
||||||
userId: wildduckUserId,
|
userId,
|
||||||
messageId: results.successful[0],
|
messageId: results.successful[0],
|
||||||
mailboxId,
|
mailboxId,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Marked ${totalMarked} messages as read for user ${wildduckUserId} in mailbox ${mailboxId}, ${results.failed.length} failed`);
|
this.logger.verbose(
|
||||||
|
`Marked ${totalMarked} messages as read for user ${wildduckUserId} in mailbox ${mailboxId}, ${results.failed.length} failed`,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -133,7 +131,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|
||||||
private async emptyTrash(wildduckUserId: string, mailboxId: string) {
|
private async emptyTrash(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
this.logger.log(`Emptying trash for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
this.logger.log(`Emptying trash for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -145,7 +143,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.emailNotificationService.notifyEmailDeleted({
|
await this.emailNotificationService.notifyEmailDeleted({
|
||||||
userId: wildduckUserId,
|
userId,
|
||||||
messageId: randomInt(1, 500),
|
messageId: randomInt(1, 500),
|
||||||
mailboxId,
|
mailboxId,
|
||||||
});
|
});
|
||||||
@@ -162,7 +160,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
private async emptySpam(wildduckUserId: string, mailboxId: string) {
|
private async emptySpam(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
this.logger.log(`Emptying spam for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
this.logger.log(`Emptying spam for user ${wildduckUserId} in mailbox ${mailboxId}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -174,7 +172,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.emailNotificationService.notifyEmailDeleted({
|
await this.emailNotificationService.notifyEmailDeleted({
|
||||||
userId: wildduckUserId,
|
userId,
|
||||||
messageId: randomInt(1, 500),
|
messageId: randomInt(1, 500),
|
||||||
mailboxId,
|
mailboxId,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export class EmailService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: EmailMessage.EMAIL_SENDING_SUCCESS,
|
message: sendEmailDto.isDraft ? EmailMessage.SAVE_DRAFT_SUCCESS : EmailMessage.EMAIL_SENDING_SUCCESS,
|
||||||
messageId: response.message.id,
|
messageId: response.message.id,
|
||||||
queueId: response.message.queueId,
|
queueId: response.message.queueId,
|
||||||
};
|
};
|
||||||
@@ -109,15 +109,18 @@ export class EmailService {
|
|||||||
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
|
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
|
||||||
const wildDuckQuery: SearchMessagesMailServerQueryDto = {
|
const wildDuckQuery: SearchMessagesMailServerQueryDto = {
|
||||||
...query,
|
...query,
|
||||||
limit: query.limit || 20,
|
limit: 100,
|
||||||
page: query.page || 1,
|
page: query.page || 1,
|
||||||
order: query.order || "desc",
|
order: query.order || "desc",
|
||||||
|
searchable: true,
|
||||||
query: query.search,
|
query: query.search,
|
||||||
from: query.from || query.search,
|
// subject: query.subject || query.search,
|
||||||
// mailbox: query.mailbox,
|
// from: query.from || query.search,
|
||||||
|
// to: query.to || query.search,
|
||||||
};
|
};
|
||||||
|
|
||||||
delete wildDuckQuery?.mailbox;
|
delete wildDuckQuery?.mailbox;
|
||||||
|
delete wildDuckQuery.search;
|
||||||
|
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
|
const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery));
|
||||||
|
|
||||||
@@ -283,11 +286,12 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async markAllMessagesAsRead(wildduckUserId: string, mailboxId: string) {
|
async markAllMessagesAsRead(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
await this.emailBulkActionQueue.add(
|
await this.emailBulkActionQueue.add(
|
||||||
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL,
|
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_SEEN_ALL,
|
||||||
{
|
{
|
||||||
wildduckUserId,
|
wildduckUserId,
|
||||||
|
userId,
|
||||||
mailboxId,
|
mailboxId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -308,11 +312,24 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async emptyTrash(wildduckUserId: string, mailboxId: string) {
|
async emptyTrash(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
await this.emailBulkActionQueue.add(EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH, {
|
await this.emailBulkActionQueue.add(
|
||||||
wildduckUserId,
|
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_TRASH,
|
||||||
mailboxId,
|
{
|
||||||
});
|
wildduckUserId,
|
||||||
|
userId,
|
||||||
|
mailboxId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_DELAY,
|
||||||
|
priority: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_PRIORITY,
|
||||||
|
attempts: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_ATTEMPTS,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BACKOFF_DELAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -321,11 +338,24 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async emptySpam(wildduckUserId: string, mailboxId: string) {
|
async emptySpam(wildduckUserId: string, userId: string, mailboxId: string) {
|
||||||
await this.emailBulkActionQueue.add(EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM, {
|
await this.emailBulkActionQueue.add(
|
||||||
wildduckUserId,
|
EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_EMPTY_SPAM,
|
||||||
mailboxId,
|
{
|
||||||
});
|
wildduckUserId,
|
||||||
|
userId,
|
||||||
|
mailboxId,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_DELAY,
|
||||||
|
priority: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_PRIORITY,
|
||||||
|
attempts: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_ATTEMPTS,
|
||||||
|
backoff: {
|
||||||
|
type: "exponential",
|
||||||
|
delay: EMAIL_QUEUE_CONSTANTS.EMAIL_BULK_ACTION_BACKOFF_DELAY,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export class UpdateMessageDto {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class SearchMessagesMailServerQueryDto {
|
export class SearchMessagesMailServerQueryDto {
|
||||||
|
search?: string;
|
||||||
|
|
||||||
q?: string;
|
q?: string;
|
||||||
|
|
||||||
mailbox?: string;
|
mailbox?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user