chore: add new route to download email

This commit is contained in:
mahyargdz
2025-07-28 16:05:25 +03:30
parent b683633e48
commit 1a056a330e
6 changed files with 122 additions and 28 deletions
@@ -81,7 +81,7 @@ export class EmailNotificationService {
};
const success = await this.emailGateway.notifyEmailUnread(user.id, payload);
await this.updateUnreadCount(params.userId);
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email unread notification dispatched for user ${user.id}: message ${params.messageId}`);
@@ -96,7 +96,6 @@ export class EmailNotificationService {
//************************************************************************ */
async notifyEmailRead(params: EmailStatusNotificationParams) {
try {
// Find the user to get the correct User entity ID for websocket notifications
const em = this.em.fork();
const user = await em.findOne(User, {
wildduckUserId: params.userId,
@@ -110,14 +109,14 @@ export class EmailNotificationService {
}
const payload: EmailStatusPayload = {
userId: user.id, // Use User entity ID for websocket
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(params.userId); // Pass wildduckUserId to updateUnreadCount
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email read notification dispatched for user ${user.id}: message ${params.messageId}`);
@@ -132,7 +131,6 @@ export class EmailNotificationService {
//************************************************************************ */
async notifyEmailDeleted(params: EmailStatusNotificationParams) {
try {
// Find the user to get the correct User entity ID for websocket notifications
const em = this.em.fork();
const user = await em.findOne(User, {
wildduckUserId: params.userId,
@@ -146,14 +144,14 @@ export class EmailNotificationService {
}
const payload: EmailDeletePayload = {
userId: user.id, // Use User entity ID for websocket
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(params.userId); // Pass wildduckUserId to updateUnreadCount
await this.updateUnreadCount(user);
if (success) {
this.logger.log(`Email deleted notification dispatched for user ${user.id}: message ${params.messageId}`);
@@ -260,20 +258,8 @@ export class EmailNotificationService {
}
//************************************************************************ */
private async updateUnreadCount(userId: string) {
private async updateUnreadCount(user: User) {
try {
const em = this.em.fork();
const user = await em.findOne(User, {
wildduckUserId: userId,
emailEnabled: true,
deletedAt: null,
});
if (!user || !user.wildduckUserId) {
return;
}
const inboxMailboxId = await this.mailboxResolver.getInboxMailboxId(user.wildduckUserId);
const { results } = await firstValueFrom(
@@ -301,7 +287,7 @@ export class EmailNotificationService {
this.logger.warn(`Failed to update unread count for user ${user.id}: user not connected`);
}
} catch (error) {
this.logger.error(`Failed to update unread count for user ${userId}:`, error);
this.logger.error(`Failed to update unread count for user ${user.wildduckUserId}:`, error);
}
}
+13
View File
@@ -66,6 +66,19 @@ export class EmailController {
this.emailService.downloadMessageAttachment(userId, params.messageId, query.mailbox, query.attachmentId, reply);
}
@Get("messages/:messageId/download")
@ApiOperation({ summary: "Download an email" })
@ApiResponse({ status: 200, description: "Email downloaded successfully" })
@ApiResponse({ status: 404, description: "Email not found" })
downloadEmail(
@UserDec("wildduckUserId") userId: string,
@Param() params: MessageParamDto,
@Query() query: MailboxIdQueryDto,
@Res({ passthrough: false }) reply: FastifyReply,
) {
return this.emailService.downloadEmail(userId, params.messageId, query.mailbox, reply);
}
@Get("search")
@ApiOperation({ summary: "Search messages across all mailboxes" })
@ApiResponse({ status: 200, description: "Search results" })
@@ -1,9 +1,12 @@
import { randomInt } from "node:crypto";
import { Processor, WorkerHost } from "@nestjs/bullmq";
import { Logger } from "@nestjs/common";
import { Job } from "bullmq";
import { firstValueFrom } from "rxjs";
import { EmailMessage } from "../../../common/enums/message.enum";
import { EmailNotificationService } from "../../email-utils/services/email-notification.service";
import { MailServerService } from "../../mail-server/services/mail-server.service";
import { EMAIL_QUEUE_CONSTANTS } from "../constants/email-events.constant";
import { IEmailBulkActionJob } from "../interfaces/email.interface";
@@ -16,6 +19,7 @@ export class EmailBulkActionProcessor extends WorkerHost {
constructor(
private readonly mailServerService: MailServerService,
private readonly emailService: EmailService,
private readonly emailNotificationService: EmailNotificationService,
) {
super();
}
@@ -108,6 +112,12 @@ export class EmailBulkActionProcessor extends WorkerHost {
results.totalMarked = totalMarked;
await this.emailNotificationService.notifyEmailRead({
userId: wildduckUserId,
messageId: results.successful[0],
mailboxId,
});
this.logger.log(`Marked ${totalMarked} messages as read for user ${wildduckUserId} in mailbox ${mailboxId}, ${results.failed.length} failed`);
return {
@@ -134,6 +144,12 @@ export class EmailBulkActionProcessor extends WorkerHost {
throw new Error(result.success);
}
await this.emailNotificationService.notifyEmailDeleted({
userId: wildduckUserId,
messageId: randomInt(1, 500),
mailboxId,
});
return {
success: true,
message: EmailMessage.TRASH_EMPTYED_SUCCESSFULLY,
@@ -157,6 +173,12 @@ export class EmailBulkActionProcessor extends WorkerHost {
throw new Error(result.success);
}
await this.emailNotificationService.notifyEmailDeleted({
userId: wildduckUserId,
messageId: randomInt(1, 500),
mailboxId,
});
return {
success: true,
message: EmailMessage.SPAM_EMPTYED_SUCCESSFULLY,
+32 -1
View File
@@ -138,7 +138,7 @@ export class EmailService {
);
const attachmentData = {
stream: attachmentResponse.success,
stream: attachmentResponse.stream,
contentType: attachmentResponse.contentType || "application/octet-stream",
contentDisposition: attachmentResponse.contentDisposition,
contentLength: attachmentResponse.contentLength,
@@ -164,6 +164,37 @@ export class EmailService {
fastifyReply.send(attachmentData.stream);
}
//########################################################
async downloadEmail(wildduckUserId: string, messageId: number, mailboxId: string, fastifyReply: FastifyReply) {
const messageResponse = await firstValueFrom(this.mailServerService.messages.getMessageSource(wildduckUserId, mailboxId, messageId));
const attachmentData = {
stream: messageResponse.stream,
contentType: messageResponse.contentType || "application/octet-stream",
contentDisposition: messageResponse.contentDisposition,
contentLength: messageResponse.contentLength,
headers: messageResponse.headers,
};
// Set appropriate headers for file download
fastifyReply.header("Content-Type", attachmentData.contentType || "application/octet-stream");
if (attachmentData.contentLength) {
fastifyReply.header("Content-Length", attachmentData.contentLength);
}
// Set content disposition if available
if (attachmentData.contentDisposition) {
fastifyReply.header("Content-Disposition", attachmentData.contentDisposition);
} else {
// Default to attachment if no disposition specified
fastifyReply.header("Content-Disposition", `attachment; filename="message_${messageId}.eml"`);
}
// Send the stream directly
fastifyReply.send(attachmentData.stream);
}
//########################################################
async listMessages(wildduckUserId: string, query: MessageListQueryDto) {
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
@@ -1,7 +1,15 @@
export interface AttachmentResponse {
success: any; // Stream data
headers?: any;
stream: unknown; // Stream data
headers?: unknown;
contentType?: string;
contentDisposition?: string;
contentLength?: string;
}
export interface FileResponse {
stream: unknown;
contentType?: string;
contentDisposition?: string;
contentLength?: string;
headers?: unknown;
}
@@ -6,7 +6,7 @@ import { WildDuckBaseService } from "./wildduck-base.service";
import * as MailServerExceptions from "../../../core/exceptions/mail-server.exceptions";
import { MailServerException } from "../../../core/exceptions/mail-server.exceptions";
import { ListMessagesQueryDto, SearchMessagesMailServerQueryDto, UpdateMessageDto, UploadMessageDto } from "../DTO/message.dto";
import { AttachmentResponse } from "../interfaces/attachment.interface";
import { AttachmentResponse, FileResponse } from "../interfaces/attachment.interface";
import {
MessageDeleteAllResponse,
MessageDetails,
@@ -67,9 +67,43 @@ export class WildDuckMessagesService extends WildDuckBaseService {
/**
* Get message source (raw email)
*/
getMessageSource(userId: string, mailboxId: string, messageId: number): Observable<string> {
getMessageSource(userId: string, mailboxId: string, messageId: number): Observable<FileResponse> {
// This returns raw email content, not JSON
return this.get<string>(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/message.eml`);
const url = this.buildUrl(`/users/${userId}/mailboxes/${mailboxId}/messages/${messageId}/message.eml`);
this.logger.debug(`WildDuck GET: ${url}`);
return this.httpService
.get(url, {
responseType: "stream",
headers: {
Accept: "*/*",
},
})
.pipe(
map((response) => {
this.logger.debug(`HTTP ${response.status}: ${response.config?.url}`);
return {
stream: response.data,
contentType: response.headers["content-type"],
contentDisposition: response.headers["content-disposition"],
contentLength: response.headers["content-length"],
headers: response.headers,
} as FileResponse;
}),
catchError((error) => {
this.logger.error(`WildDuck API error: ${error.message}`, error.stack);
if (error.response?.data?.error) {
return throwError(() => this.handleMailServerError(error.response.data.error, error.response.status));
}
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
return throwError(() => new MailServerExceptions.MailServerConnectionException(error.message));
}
return throwError(() => new MailServerException(error.message || "Unknown mail server error"));
}),
);
}
/**
@@ -91,7 +125,7 @@ export class WildDuckMessagesService extends WildDuckBaseService {
map((response) => {
this.logger.debug(`HTTP ${response.status}: ${response.config?.url}`);
return {
success: response.data,
stream: response.data,
headers: response.headers,
contentType: response.headers["content-type"],
contentDisposition: response.headers["content-disposition"],