chore: add new route to download email
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user