From 97e77d23a079040797fafda0884bf8a36d88fb04 Mon Sep 17 00:00:00 2001 From: mahyargdz Date: Tue, 22 Jul 2025 12:10:55 +0330 Subject: [PATCH] update: for the reply message --- FRONTEND_WEBSOCKET_INTEGRATION.md | 849 --------------- src/common/enums/message.enum.ts | 2 + src/modules/email/DTO/send-email.dto.ts | 13 +- .../email/interfaces/email-map.interface.ts | 6 + src/modules/email/services/email.service.ts | 992 +++++++----------- .../services/wildduck-messages.service.ts | 6 +- 6 files changed, 382 insertions(+), 1486 deletions(-) delete mode 100644 FRONTEND_WEBSOCKET_INTEGRATION.md create mode 100644 src/modules/email/interfaces/email-map.interface.ts diff --git a/FRONTEND_WEBSOCKET_INTEGRATION.md b/FRONTEND_WEBSOCKET_INTEGRATION.md deleted file mode 100644 index 5691791..0000000 --- a/FRONTEND_WEBSOCKET_INTEGRATION.md +++ /dev/null @@ -1,849 +0,0 @@ -# 📧 Email WebSocket Integration Guide for React - -This guide will help you integrate real-time email notifications into your React application using WebSocket connections. - -## 🚀 Quick Start - -### Installation - -```bash -npm install socket.io-client -# or -yarn add socket.io-client -``` - -### Basic Setup - -```typescript -import io from "socket.io-client"; - -const socket = io("ws://localhost:3000/email", { - auth: { - token: localStorage.getItem("jwt-token"), // Your JWT authentication token - }, -}); -``` - -## 🔌 Connection Setup - -### 1. Basic Connection Hook - -```typescript -// hooks/useEmailWebSocket.ts -import { useEffect, useState } from "react"; -import io, { Socket } from "socket.io-client"; - -interface UseEmailWebSocketOptions { - token: string; - serverUrl?: string; - onConnect?: () => void; - onDisconnect?: () => void; - onError?: (error: string) => void; -} - -export const useEmailWebSocket = ({ token, serverUrl = "ws://localhost:3000/email", onConnect, onDisconnect, onError }: UseEmailWebSocketOptions) => { - const [socket, setSocket] = useState(null); - const [isConnected, setIsConnected] = useState(false); - - useEffect(() => { - if (!token) return; - - const newSocket = io(serverUrl, { - auth: { token }, - transports: ["websocket", "polling"], - }); - - // Connection events - newSocket.on("connect", () => { - console.log("✅ Connected to email WebSocket"); - setIsConnected(true); - onConnect?.(); - }); - - newSocket.on("disconnect", () => { - console.log("❌ Disconnected from email WebSocket"); - setIsConnected(false); - onDisconnect?.(); - }); - - newSocket.on("connection_established", (data) => { - console.log("🎉 Email WebSocket connection established:", data); - }); - - newSocket.on("connection_error", (error) => { - console.error("💥 Email WebSocket connection error:", error); - onError?.(error.error); - }); - - // Health monitoring - newSocket.on("ping", () => { - newSocket.emit("pong", { timestamp: new Date().toISOString() }); - }); - - setSocket(newSocket); - - return () => { - newSocket.close(); - setSocket(null); - }; - }, [token, serverUrl]); - - return { socket, isConnected }; -}; -``` - -### 2. Email Events Hook - -```typescript -// hooks/useEmailEvents.ts -import { useEffect, useState } from "react"; -import { Socket } from "socket.io-client"; - -// TypeScript interfaces (copy from your backend) -export interface EmailPayload { - messageId: number; - userId: string; - from: { - name?: string; - address: string; - }; - to: Array<{ - name?: string; - address: string; - }>; - subject: string; - preview?: string; - hasAttachments: boolean; - timestamp: string; - mailboxId: string; - mailboxName: string; - isRead: boolean; - isFlagged: boolean; - size: number; -} - -export interface EmailStatusPayload { - messageId: number; - userId: string; - mailboxId: string; - timestamp: string; -} - -export interface UnreadCountPayload { - userId: string; - totalUnread: number; - mailboxCounts: Record; - timestamp: string; - count: number; -} - -interface EmailEventHandlers { - onNewEmail?: (email: EmailPayload) => void; - onEmailRead?: (data: EmailStatusPayload) => void; - onEmailDeleted?: (data: EmailStatusPayload) => void; - onEmailSent?: (data: any) => void; - onUnreadCountUpdated?: (data: UnreadCountPayload) => void; - onMailboxUpdated?: (data: any) => void; -} - -export const useEmailEvents = (socket: Socket | null, handlers: EmailEventHandlers) => { - useEffect(() => { - if (!socket) return; - - // Register event handlers - if (handlers.onNewEmail) { - socket.on("new_email", handlers.onNewEmail); - } - - if (handlers.onEmailRead) { - socket.on("email_read", handlers.onEmailRead); - } - - if (handlers.onEmailDeleted) { - socket.on("email_deleted", handlers.onEmailDeleted); - } - - if (handlers.onEmailSent) { - socket.on("email_sent", handlers.onEmailSent); - } - - if (handlers.onUnreadCountUpdated) { - socket.on("unread_count_updated", handlers.onUnreadCountUpdated); - } - - if (handlers.onMailboxUpdated) { - socket.on("mailbox_updated", handlers.onMailboxUpdated); - } - - // Cleanup on unmount - return () => { - socket.off("new_email"); - socket.off("email_read"); - socket.off("email_deleted"); - socket.off("email_sent"); - socket.off("unread_count_updated"); - socket.off("mailbox_updated"); - }; - }, [socket, handlers]); -}; -``` - -## 📱 Complete React Integration - -### 1. Email Notification Component - -```typescript -// components/EmailNotifications.tsx -import React, { useState, useCallback } from 'react'; -import { useEmailWebSocket, useEmailEvents, EmailPayload, UnreadCountPayload } from '../hooks'; - -interface EmailNotificationsProps { - userToken: string; - onEmailClick?: (email: EmailPayload) => void; -} - -export const EmailNotifications: React.FC = ({ - userToken, - onEmailClick -}) => { - const [notifications, setNotifications] = useState([]); - const [unreadCount, setUnreadCount] = useState(0); - const [isVisible, setIsVisible] = useState(false); - - const { socket, isConnected } = useEmailWebSocket({ - token: userToken, - onConnect: () => console.log('Email notifications connected'), - onError: (error) => console.error('WebSocket error:', error), - }); - - const handleNewEmail = useCallback((email: EmailPayload) => { - console.log('📧 New email received:', email); - - // Add to notifications list - setNotifications(prev => [email, ...prev.slice(0, 9)]); // Keep only 10 latest - - // Show browser notification if permission granted - if (Notification.permission === 'granted') { - new Notification(`📧 New Email: ${email.subject}`, { - body: `From: ${email.from.address}\n${email.preview}`, - icon: '/email-icon.png', - tag: `email-${email.messageId}`, // Prevent duplicates - }); - } - - // Play notification sound (optional) - const audio = new Audio('/notification.mp3'); - audio.play().catch(() => {}); // Ignore if audio fails - }, []); - - const handleUnreadCountUpdate = useCallback((data: UnreadCountPayload) => { - setUnreadCount(data.count); - }, []); - - const handleEmailRead = useCallback((data: any) => { - // Remove from notifications if it was there - setNotifications(prev => - prev.filter(notification => notification.messageId !== data.messageId) - ); - }, []); - - useEmailEvents(socket, { - onNewEmail: handleNewEmail, - onUnreadCountUpdated: handleUnreadCountUpdate, - onEmailRead: handleEmailRead, - }); - - // Request notification permission on mount - React.useEffect(() => { - if (Notification.permission === 'default') { - Notification.requestPermission(); - } - }, []); - - return ( -
- {/* Connection Status */} -
- - {isConnected ? 'Connected' : 'Disconnected'} -
- - {/* Notification Bell */} -
setIsVisible(!isVisible)} - > - 🔔 - {unreadCount > 0 && ( - {unreadCount > 99 ? '99+' : unreadCount} - )} -
- - {/* Notifications Dropdown */} - {isVisible && ( -
-
-

📧 Email Notifications

- {unreadCount} unread -
- -
- {notifications.length === 0 ? ( -
No new emails
- ) : ( - notifications.map((email) => ( -
onEmailClick?.(email)} - > -
- {email.from.name || email.from.address} - {email.hasAttachments && 📎} -
-
{email.subject}
-
{email.preview}
-
- {new Date(email.timestamp).toLocaleString()} -
-
- )) - )} -
-
- )} -
- ); -}; -``` - -### 2. Email Dashboard Component - -```typescript -// components/EmailDashboard.tsx -import React, { useState } from 'react'; -import { useEmailWebSocket, useEmailEvents } from '../hooks'; - -interface EmailDashboardProps { - userToken: string; -} - -export const EmailDashboard: React.FC = ({ userToken }) => { - const [emailStats, setEmailStats] = useState({ - totalUnread: 0, - newToday: 0, - sentToday: 0, - }); - - const { socket, isConnected } = useEmailWebSocket({ - token: userToken, - }); - - useEmailEvents(socket, { - onNewEmail: (email) => { - setEmailStats(prev => ({ - ...prev, - totalUnread: prev.totalUnread + 1, - newToday: prev.newToday + 1, - })); - }, - - onEmailSent: () => { - setEmailStats(prev => ({ - ...prev, - sentToday: prev.sentToday + 1, - })); - }, - - onUnreadCountUpdated: (data) => { - setEmailStats(prev => ({ - ...prev, - totalUnread: data.count, - })); - }, - }); - - return ( -
-

📧 Email Dashboard

- -
-
-

{emailStats.totalUnread}

-

Unread Emails

-
- -
-

{emailStats.newToday}

-

Received Today

-
- -
-

{emailStats.sentToday}

-

Sent Today

-
- -
-

- {isConnected ? '🟢' : '🔴'} -

-

{isConnected ? 'Connected' : 'Disconnected'}

-
-
-
- ); -}; -``` - -### 3. Context Provider (Advanced) - -```typescript -// context/EmailWebSocketContext.tsx -import React, { createContext, useContext, ReactNode } from 'react'; -import { useEmailWebSocket } from '../hooks/useEmailWebSocket'; -import { Socket } from 'socket.io-client'; - -interface EmailWebSocketContextValue { - socket: Socket | null; - isConnected: boolean; -} - -const EmailWebSocketContext = createContext(null); - -interface EmailWebSocketProviderProps { - children: ReactNode; - token: string; - serverUrl?: string; -} - -export const EmailWebSocketProvider: React.FC = ({ - children, - token, - serverUrl, -}) => { - const { socket, isConnected } = useEmailWebSocket({ token, serverUrl }); - - return ( - - {children} - - ); -}; - -export const useEmailWebSocketContext = () => { - const context = useContext(EmailWebSocketContext); - if (!context) { - throw new Error('useEmailWebSocketContext must be used within EmailWebSocketProvider'); - } - return context; -}; -``` - -## 🎨 CSS Styles - -```css -/* styles/email-notifications.css */ -.email-notifications { - position: relative; - display: inline-block; -} - -.connection-status { - position: absolute; - top: -8px; - right: -8px; - font-size: 10px; - padding: 2px 6px; - border-radius: 10px; - display: flex; - align-items: center; - gap: 4px; -} - -.connection-status.connected { - background: #d4edda; - color: #155724; -} - -.connection-status.disconnected { - background: #f8d7da; - color: #721c24; -} - -.status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: currentColor; -} - -.notification-bell { - position: relative; - cursor: pointer; - font-size: 24px; - padding: 8px; - border-radius: 50%; - transition: background-color 0.2s; -} - -.notification-bell:hover { - background-color: #f5f5f5; -} - -.badge { - position: absolute; - top: 0; - right: 0; - background: #dc3545; - color: white; - border-radius: 50%; - min-width: 18px; - height: 18px; - font-size: 11px; - display: flex; - align-items: center; - justify-content: center; - font-weight: bold; -} - -.notifications-dropdown { - position: absolute; - top: 100%; - right: 0; - width: 350px; - max-height: 400px; - background: white; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - z-index: 1000; - overflow: hidden; -} - -.notifications-header { - padding: 12px 16px; - background: #f8f9fa; - border-bottom: 1px solid #dee2e6; - display: flex; - justify-content: space-between; - align-items: center; -} - -.notifications-list { - max-height: 300px; - overflow-y: auto; -} - -.notification-item { - padding: 12px 16px; - border-bottom: 1px solid #eee; - cursor: pointer; - transition: background-color 0.2s; -} - -.notification-item:hover { - background-color: #f8f9fa; -} - -.notification-item:last-child { - border-bottom: none; -} - -.sender { - display: flex; - align-items: center; - gap: 8px; - margin-bottom: 4px; -} - -.attachment-icon { - font-size: 12px; -} - -.subject { - font-weight: 500; - margin-bottom: 4px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.preview { - color: #666; - font-size: 14px; - margin-bottom: 4px; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.timestamp { - font-size: 12px; - color: #999; -} - -.no-notifications { - padding: 20px; - text-align: center; - color: #666; -} - -/* Dashboard Styles */ -.email-dashboard { - padding: 20px; -} - -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - margin-top: 20px; -} - -.stat-card { - background: white; - padding: 20px; - border-radius: 8px; - border: 1px solid #ddd; - text-align: center; -} - -.stat-card h3 { - margin: 0 0 8px 0; - font-size: 32px; - font-weight: bold; -} - -.stat-card h3.connected { - color: #28a745; -} - -.stat-card h3.disconnected { - color: #dc3545; -} - -.stat-card p { - margin: 0; - color: #666; - font-size: 14px; -} -``` - -## 🧪 Testing & Manual Triggers - -### Manual Email Check - -```typescript -// API call to manually trigger email check -const triggerEmailCheck = async () => { - try { - const response = await fetch("/api/email/gateway/check-emails", { - method: "POST", - headers: { - Authorization: `Bearer ${userToken}`, - "Content-Type": "application/json", - }, - }); - - const result = await response.json(); - console.log("✅ Manual email check triggered:", result); - } catch (error) { - console.error("❌ Failed to trigger email check:", error); - } -}; -``` - -### Send Test Notification - -```typescript -const sendTestNotification = async () => { - try { - const response = await fetch("/api/email/gateway/test-notification", { - method: "POST", - headers: { - Authorization: `Bearer ${userToken}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - message: "Test notification from React app!", - }), - }); - - const result = await response.json(); - console.log("✅ Test notification sent:", result); - } catch (error) { - console.error("❌ Failed to send test notification:", error); - } -}; -``` - -## 🔧 Complete App Integration - -```typescript -// App.tsx - Complete example -import React from 'react'; -import { EmailWebSocketProvider } from './context/EmailWebSocketContext'; -import { EmailNotifications } from './components/EmailNotifications'; -import { EmailDashboard } from './components/EmailDashboard'; - -function App() { - const userToken = localStorage.getItem('jwt-token') || ''; - - const handleEmailClick = (email: any) => { - console.log('Email clicked:', email); - // Navigate to email detail page - // router.push(`/emails/${email.messageId}`); - }; - - if (!userToken) { - return
Please login to access email notifications
; - } - - return ( - -
-
-

📧 Email App

- -
- -
- -
-
-
- ); -} - -export default App; -``` - -## 🚨 Error Handling & Edge Cases - -### Connection Recovery - -```typescript -const useConnectionRecovery = (socket: Socket | null) => { - useEffect(() => { - if (!socket) return; - - const handleReconnect = () => { - console.log("🔄 WebSocket reconnecting..."); - // Optionally refresh data or show reconnecting state - }; - - const handleReconnectError = () => { - console.error("❌ Failed to reconnect"); - // Show user notification or retry logic - }; - - socket.on("reconnect", handleReconnect); - socket.on("reconnect_error", handleReconnectError); - - return () => { - socket.off("reconnect", handleReconnect); - socket.off("reconnect_error", handleReconnectError); - }; - }, [socket]); -}; -``` - -### Token Refresh - -```typescript -const useTokenRefresh = (socket: Socket | null) => { - useEffect(() => { - const refreshToken = () => { - const newToken = localStorage.getItem("jwt-token"); - if (socket && newToken) { - socket.auth = { token: newToken }; - socket.disconnect(); - socket.connect(); - } - }; - - // Listen for token refresh events - window.addEventListener("token-refreshed", refreshToken); - - return () => { - window.removeEventListener("token-refreshed", refreshToken); - }; - }, [socket]); -}; -``` - -## 📝 Environment Configuration - -```typescript -// config/websocket.ts -export const WEBSOCKET_CONFIG = { - development: { - serverUrl: "ws://localhost:3000/email", - reconnectionAttempts: 5, - timeout: 20000, - }, - production: { - serverUrl: "wss://your-domain.com/email", - reconnectionAttempts: 10, - timeout: 30000, - }, -}; - -export const getWebSocketConfig = () => { - const env = process.env.NODE_ENV || "development"; - return WEBSOCKET_CONFIG[env as keyof typeof WEBSOCKET_CONFIG]; -}; -``` - -## 🎯 Best Practices - -1. **🔐 Security**: Always use HTTPS/WSS in production -2. **🔄 Reconnection**: Implement proper reconnection logic -3. **🎯 Performance**: Limit notification history (max 10-50 items) -4. **📱 UX**: Request notification permissions gracefully -5. **🔊 Audio**: Make notification sounds optional -6. **💾 Persistence**: Consider storing notifications in localStorage -7. **🎨 Theming**: Support light/dark mode for notifications -8. **♿ Accessibility**: Add proper ARIA labels and keyboard navigation - -## 🐛 Troubleshooting - -**Connection Issues:** - -- Check JWT token validity -- Verify server URL and port -- Check browser console for errors -- Ensure CORS is properly configured - -**Missing Notifications:** - -- Verify WebSocket event names match backend -- Check browser notification permissions -- Confirm user is authenticated properly -- Test with manual triggers - -**Performance Issues:** - -- Limit notification history -- Debounce rapid events -- Use React.memo for components -- Implement virtual scrolling for long lists - ---- - -## 🚀 Ready to Go! - -Your React app is now ready to receive real-time email notifications! The WebSocket connection will automatically: - -- ✅ Authenticate users with JWT tokens -- ✅ Receive new email notifications instantly -- ✅ Update unread counts in real-time -- ✅ Handle connection recovery automatically -- ✅ Show browser notifications (with permission) -- ✅ Provide a beautiful UI for email notifications - -Happy coding! 🎉 diff --git a/src/common/enums/message.enum.ts b/src/common/enums/message.enum.ts index fb9d913..b3ec101 100755 --- a/src/common/enums/message.enum.ts +++ b/src/common/enums/message.enum.ts @@ -140,6 +140,7 @@ export const enum EmailMessage { PAYMENT_REMINDER = "یادآوری پرداخت", PAYMENT_CANCELLATION = "لغو پرداخت", EMAIL_SENDING_SUCCESS = "ارسال ایمیل با موفقیت انجام شد", + EMAIL_FORWARD_SUCCESS = "فوروارد ایمیل با موفقیت انجام شد", EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = "وضعیت ایمیل با موفقیت دریافت شد", MESSAGES_SEARCHED_SUCCESSFULLY = "جستجوی ایمیل با موفقیت انجام شد", MESSAGES_LISTED_SUCCESSFULLY = "لیست ایمیل ها با موفقیت دریافت شد", @@ -227,6 +228,7 @@ export const enum EmailMessage { MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = "پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد", MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد", ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد", + IS_FORWARD_MUST_BE_BOOLEAN = "IS_FORWARD_MUST_BE_BOOLEAN", } export const enum WebSocketMessage { diff --git a/src/modules/email/DTO/send-email.dto.ts b/src/modules/email/DTO/send-email.dto.ts index ec34722..375a2d2 100644 --- a/src/modules/email/DTO/send-email.dto.ts +++ b/src/modules/email/DTO/send-email.dto.ts @@ -176,7 +176,7 @@ export class SendEmailDto { @IsOptional() @ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string (optional)" }) - meta?: any; + meta?: Record; @IsOptional() @IsString({ message: EmailMessage.SESSION_ID_MUST_BE_STRING }) @@ -188,15 +188,20 @@ export class SendEmailDto { @ApiPropertyOptional({ description: "IP address for the logs (optional)" }) ip?: string; - @IsOptional() - @ApiPropertyOptional({ description: "Reference message data (optional)" }) - reference?: any; + // @IsOptional() + // @ApiPropertyOptional({ description: "Reference message data (optional)" }) + // reference?: any; @IsOptional() @IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN }) @ApiPropertyOptional({ description: "Is the message a draft or not (optional)", default: false }) isDraft?: boolean; + @IsOptional() + @IsBoolean({ message: EmailMessage.IS_FORWARD_MUST_BE_BOOLEAN }) + @ApiPropertyOptional({ description: "Is the message a forward or not (optional)", default: false }) + isForward?: boolean; + @IsOptional() @ValidateNested() @Type(() => DraftReferenceDto) diff --git a/src/modules/email/interfaces/email-map.interface.ts b/src/modules/email/interfaces/email-map.interface.ts new file mode 100644 index 0000000..155d444 --- /dev/null +++ b/src/modules/email/interfaces/email-map.interface.ts @@ -0,0 +1,6 @@ +export interface IEmailMap { + email: string; + name?: string; + count: number; + lastSeen: Date; +} diff --git a/src/modules/email/services/email.service.ts b/src/modules/email/services/email.service.ts index 112e6ce..e2814a8 100644 --- a/src/modules/email/services/email.service.ts +++ b/src/modules/email/services/email.service.ts @@ -7,14 +7,14 @@ import { EmailSpamService } from "./email-spam.service"; import { MailboxResolverService } from "./mailbox-resolver.service"; import { EmailMessage } from "../../../common/enums/message.enum"; import { MailServerService } from "../../mail-server/services/mail-server.service"; -import { MailboxEnum } from "../../mailbox/enums/mailbox.enum"; import { TemplateProcessorService } from "../../templates/services/template-processor.service"; import { User } from "../../users/entities/user.entity"; import { UserRepository } from "../../users/repositories/user.repository"; import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto"; import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.dto"; -import { EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; +import { EmailRecipientDto, EmailType, SendEmailDto, UpdateDraftDto } from "../DTO/send-email.dto"; import { Priority } from "../enums/email-header.enum"; +import { IEmailMap } from "../interfaces/email-map.interface"; @Injectable() export class EmailService { @@ -34,12 +34,19 @@ export class EmailService { async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) { this.logger.log(`Sending ${EmailType.GENERAL} email from user ${wildduckUserId} to ${sendEmailDto.to.map((t) => t.address).join(", ")}`); - try { - const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); - if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND); - this.logger.log(`Processing email through business template for business: ${user.business.id}`); + const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); + if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND); + this.logger.log(`Processing email through business template for business: ${user.business.id}`); - const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, userId, { + sendEmailDto.from = { + address: user.emailAddress, + name: user.displayName, + }; + + let processedTemplate = null; + + if (!sendEmailDto.replyTo && !sendEmailDto.isDraft && !sendEmailDto.isForward) { + processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, userId, { text: sendEmailDto.text, html: sendEmailDto.html, subject: sendEmailDto.subject, @@ -52,794 +59,519 @@ export class EmailService { sendEmailDto.text = processedTemplate.content; sendEmailDto.html = undefined; } - - if (processedTemplate.hasTemplate) { - this.logger.log(`Email content processed through template for business: ${user.business.id}`); - } - - sendEmailDto.from = { - address: user.emailAddress, - name: user.displayName, - }; - - const headers = this.generateHeadersForEmailType(user, { - html: sendEmailDto.html, - text: sendEmailDto.text, - }); - - sendEmailDto.headers = [...headers]; - - const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, sendEmailDto)); - - this.logger.log(`Email sent successfully: ${response.message.id} (Queue: ${response.message.queueId})`); - - const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); - - await this.emailNotificationService.notifyEmailSent({ - userId: wildduckUserId, - messageId: parseInt(response.message.id), - mailboxId: sentMailboxId, - recipients: sendEmailDto.to, - subject: sendEmailDto.subject, - from: sendEmailDto.from, - }); - - return { - message: EmailMessage.EMAIL_SENDING_SUCCESS, - messageId: response.message.id, - queueId: response.message.queueId, - }; - } catch (error) { - this.logger.error(`Failed to send email for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; } + + const headers = this.generateHeadersForEmailType(user, { + html: sendEmailDto.html, + text: sendEmailDto.text, + }); + + sendEmailDto.headers = [...headers]; + + const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, sendEmailDto)); + + const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); + + await this.emailNotificationService.notifyEmailSent({ + userId: wildduckUserId, + messageId: parseInt(response.message.id), + mailboxId: sentMailboxId, + recipients: sendEmailDto.to, + subject: sendEmailDto.subject, + from: sendEmailDto.from, + }); + + return { + message: EmailMessage.EMAIL_SENDING_SUCCESS, + messageId: response.message.id, + queueId: response.message.queueId, + }; } //######################################################## async getEmailStatus(queueId: string) { - this.logger.log(`Getting email status for queue: ${queueId}`); + const response = await firstValueFrom(this.mailServerService.submission.getQueueStatus(queueId)); - try { - const response = await firstValueFrom(this.mailServerService.submission.getQueueStatus(queueId)); + this.logger.log(`Retrieved status for queue ${queueId}: ${response.status}`); - this.logger.log(`Retrieved status for queue ${queueId}: ${response.status}`); - - return { - ...response, - }; - } catch (error) { - this.logger.error(`Failed to get email status for queue ${queueId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + ...response, + }; } //######################################################## async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) { - this.logger.log(`Searching messages for user ${wildduckUserId} with query: ${query.q}`); + // Transform query parameters for WildDuck API compatibility + const wildDuckQuery = { + query: query.q || "", + limit: query.limit || 20, + page: query.page || 1, + order: "desc" as const, + }; - try { - // Transform query parameters for WildDuck API compatibility - const wildDuckQuery = { - query: query.q || "", - limit: query.limit || 20, - page: query.page || 1, - order: "desc" as const, - }; + // Ensure limit is within bounds + if (wildDuckQuery.limit > 250) wildDuckQuery.limit = 250; + if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1; + if (wildDuckQuery.page < 1) wildDuckQuery.page = 1; - // Ensure limit is within bounds - if (wildDuckQuery.limit > 250) wildDuckQuery.limit = 250; - if (wildDuckQuery.limit < 1) wildDuckQuery.limit = 1; - if (wildDuckQuery.page < 1) wildDuckQuery.page = 1; + this.logger.debug("Search query for WildDuck:", wildDuckQuery); - this.logger.debug("Search query for WildDuck:", wildDuckQuery); + const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); - const response = await firstValueFrom(this.mailServerService.messages.searchMessages(wildduckUserId, wildDuckQuery)); + this.logger.log( + `Found ${response.results.length} messages (${response.total} total, page ${response.page}) matching search for user: ${wildduckUserId}`, + ); - this.logger.log( - `Found ${response.results.length} messages (${response.total} total, page ${response.page}) matching search for user: ${wildduckUserId}`, - ); - - return { - results: response.results, - total: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - query: response.query, - }; - } catch (error) { - this.logger.error(`Failed to search messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + results: response.results, + total: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + query: response.query, + }; } //######################################################## async downloadMessageAttachment(wildduckUserId: string, messageId: number, mailboxId: string, attachmentId: string) { - console.log("messageId", messageId); - console.log("mailboxId", mailboxId); - console.log("attachmentId", attachmentId); + const attachmentResponse = await firstValueFrom( + this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId), + ); - try { - const attachmentResponse = await firstValueFrom( - this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId), - ); - - return { - stream: attachmentResponse.success, - contentType: attachmentResponse.contentType || "application/octet-stream", - contentDisposition: attachmentResponse.contentDisposition, - contentLength: attachmentResponse.contentLength, - headers: attachmentResponse.headers, - }; - } catch (error) { - this.logger.error( - `Failed to download attachment ${attachmentId} for message ${messageId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + stream: attachmentResponse.success, + contentType: attachmentResponse.contentType || "application/octet-stream", + contentDisposition: attachmentResponse.contentDisposition, + contentLength: attachmentResponse.contentLength, + headers: attachmentResponse.headers, + }; } //######################################################## async listMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - this.logger.log(`Listing messages in mailbox ${inboxMailboxId} for user: ${wildduckUserId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); + const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, inboxMailboxId, wildDuckQuery)); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, inboxMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} total messages (page ${response.page}) in mailbox ${inboxMailboxId} for user: ${wildduckUserId}`); - - return { - results: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to list messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + results: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //######################################################## async getMessage(wildduckUserId: string, messageId: number, mailboxId: string) { - try { - this.logger.log(`Getting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`); + await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); + const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId)); - await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId); - const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId)); + const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId); - this.logger.log(`Retrieved message ${messageId} from ${mailboxId} for user: ${wildduckUserId}`); - const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId); - - return { - message: { - ...message, - mailboxName, - }, - }; - } catch (error) { - this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + message: { + ...message, + mailboxName, + }, + }; } //######################################################## async deleteMessage(wildduckUserId: string, messageId: number, mailboxId: string) { - try { - this.logger.log(`Deleting message ${messageId} from ${mailboxId} mailbox for user: ${wildduckUserId}`); + const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, mailboxId, messageId)); - const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, mailboxId, messageId)); - - this.logger.log(`Message ${messageId} deleted successfully from ${mailboxId}`); - - return { - message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to delete message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY, + result, + }; } //============================================== async deleteAllMessages(wildduckUserId: string, mailboxId: string) { - try { - const result = await firstValueFrom(this.mailServerService.messages.deleteAllFromMailbox(wildduckUserId, mailboxId)); + const result = await firstValueFrom(this.mailServerService.messages.deleteAllFromMailbox(wildduckUserId, mailboxId)); - this.logger.log(`All messages deleted successfully from ${mailboxId} for user: ${wildduckUserId}`); - - return { - message: EmailMessage.ALL_MESSAGES_DELETED_SUCCESSFULLY, - mailboxId, - result, - }; - } catch (error) { - this.logger.error(`Failed to delete all messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + message: EmailMessage.ALL_MESSAGES_DELETED_SUCCESSFULLY, + mailboxId, + result, + }; } //============================================== async markMessageAsSeen(wildduckUserId: string, messageId: number, mailboxId: string) { - try { - this.logger.log(`Marking message ${messageId} as seen in ${mailboxId} mailbox for user: ${wildduckUserId}`); + 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({ + userId: wildduckUserId, + messageId, + mailboxId, + }); - this.logger.log(`Message ${messageId} marked as seen successfully in ${mailboxId}`); - - await this.emailNotificationService.notifyEmailRead({ - userId: wildduckUserId, - messageId, - mailboxId, - }); - - return { - success: true, - message: EmailMessage.MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY, - updated, - }; - } catch (error) { - this.logger.error( - `Failed to mark message ${messageId} as seen for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY, + updated, + }; } //============================================== async markMessageAsUnseen(wildduckUserId: string, messageId: number, mailboxId: string) { - try { - this.logger.log(`Marking message ${messageId} as unseen in ${mailboxId} mailbox for user: ${wildduckUserId}`); + 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 })); - - this.logger.log(`Message ${messageId} marked as unseen successfully in ${mailboxId}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY, - updated, - }; - } catch (error) { - this.logger.error( - `Failed to mark message ${messageId} as unseen for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY, + updated, + }; } //============================================== async getSentMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, sentMailboxId, wildDuckQuery)); + const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, sentMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} sent messages (page ${response.page}) for user: ${wildduckUserId}`); + this.logger.log(`Found ${response.total} sent messages (page ${response.page}) for user: ${wildduckUserId}`); - return { - sentMails: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get sent messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + sentMails: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async getTrashMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, trashMailboxId, wildDuckQuery)); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, trashMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} trash messages (page ${response.page}) for user: ${wildduckUserId}`); - - return { - trashMails: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get trash messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + trashMails: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async getJunkMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, junkMailboxId, wildDuckQuery)); + const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, junkMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} junk messages (page ${response.page}) for user: ${wildduckUserId}`); - - return { - junkMails: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get junk messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + junkMails: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async getArchivedMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, archiveMailboxId, wildDuckQuery)); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, archiveMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} archived messages (page ${response.page}) for user: ${wildduckUserId}`); - - return { - archiveMails: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get archive messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + archiveMails: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async getFavoriteMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, favoriteMailboxId, wildDuckQuery)); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, favoriteMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} favorite messages (page ${response.page}) for user: ${wildduckUserId}`); - - return { - favoriteMails: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get favorite messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + favoriteMails: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async getDraftsMessages(wildduckUserId: string, query: MessageListQueryDto) { - try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); - const wildDuckQuery = this.transformPaginationQuery(query); - const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, draftsMailboxId, wildDuckQuery)); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const wildDuckQuery = this.transformPaginationQuery(query); + const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, draftsMailboxId, wildDuckQuery)); - this.logger.log(`Found ${response.total} draft messages (page ${response.page}) for user: ${wildduckUserId}`); - - return { - draftsMail: response.results, - count: response.total, - page: response.page, - previousCursor: response.previousCursor, - nextCursor: response.nextCursor, - paginate: true, - }; - } catch (error) { - this.logger.error(`Failed to get drafts messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + draftsMail: response.results, + count: response.total, + page: response.page, + previousCursor: response.previousCursor, + nextCursor: response.nextCursor, + paginate: true, + }; } //============================================== async updateDraft(wildduckUserId: string, messageId: number, updateDraftDto: UpdateDraftDto) { - this.logger.log(`Updating draft ${messageId} for user ${wildduckUserId}`); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId)); - try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); - const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId)); + const draftData: SendEmailDto = { + from: updateDraftDto.from || { address: existingDraft.from?.address || "" }, + to: updateDraftDto.to || [], + cc: updateDraftDto.cc, + bcc: updateDraftDto.bcc, + replyTo: updateDraftDto.replyTo, + headers: updateDraftDto.headers, + subject: updateDraftDto.subject, + text: updateDraftDto.text, + html: updateDraftDto.html, + attachments: updateDraftDto.attachments, + meta: updateDraftDto.meta, + sess: updateDraftDto.sess, + ip: updateDraftDto.ip, + isDraft: true, + uploadOnly: true, + draft: { + mailbox: draftsMailboxId, + id: messageId, + }, + }; - const draftData: SendEmailDto = { - // mailbox: updateDraftDto.mailbox, - from: updateDraftDto.from || { address: existingDraft.from?.address || "" }, - to: updateDraftDto.to || [], - cc: updateDraftDto.cc, - bcc: updateDraftDto.bcc, - replyTo: updateDraftDto.replyTo, - headers: updateDraftDto.headers, - subject: updateDraftDto.subject, + const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); + + if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) { + const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, user.id, { text: updateDraftDto.text, html: updateDraftDto.html, - attachments: updateDraftDto.attachments, - meta: updateDraftDto.meta, - sess: updateDraftDto.sess, - ip: updateDraftDto.ip, - reference: updateDraftDto.reference, - isDraft: true, - uploadOnly: true, - draft: { - mailbox: draftsMailboxId, - id: messageId, - }, - }; + subject: updateDraftDto.subject, + }); - // Process draft content through business template if available - const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] }); - - if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) { - const processedTemplate = await this.templateProcessorService.processEmailContent(user.business.id, user.id, { - text: updateDraftDto.text, - html: updateDraftDto.html, - subject: updateDraftDto.subject, - }); - - if (processedTemplate.isHtml) { - draftData.html = processedTemplate.content; - draftData.text = undefined; - } else { - draftData.text = processedTemplate.content; - draftData.html = undefined; - } - - if (processedTemplate.hasTemplate) { - this.logger.log(`Draft content processed through template for business: ${user.business.id}`); - } - - const headers = this.generateHeadersForEmailType(user, { - html: draftData.html, - text: draftData.text, - }); - draftData.headers = [...(draftData.headers || []), ...headers]; + if (processedTemplate.isHtml) { + draftData.html = processedTemplate.content; + draftData.text = undefined; + } else { + draftData.text = processedTemplate.content; + draftData.html = undefined; } - const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData)); - - this.logger.log(`Draft updated successfully: ${response.message.mailbox}`); - - return { - message: EmailMessage.DRAFT_UPDATED_SUCCESSFULLY, - messageId: response.message.id, - subject: "Draft", - isDraft: true, - }; - } catch (error) { - this.logger.error( - `Failed to update draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; + const headers = this.generateHeadersForEmailType(user, { + html: draftData.html, + text: draftData.text, + }); + draftData.headers = [...(draftData.headers || []), ...headers]; } + + const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData)); + + return { + message: EmailMessage.DRAFT_UPDATED_SUCCESSFULLY, + messageId: response.message.id, + subject: "Draft", + isDraft: true, + }; } //============================================== async sendDraft(wildduckUserId: string, messageId: number) { - this.logger.log(`Sending draft ${messageId} for user: ${wildduckUserId}`); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }), + ); - try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); - const result = await firstValueFrom( - this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }), - ); - - this.logger.log(`Draft ${messageId} sent successfully`); - - return { - message: EmailMessage.DRAFT_SENT_SUCCESSFULLY, - messageId: result.message.id, - queueId: result.queueId, - result, - }; - } catch (error) { - this.logger.error(`Failed to send draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`); - throw error; - } + return { + message: EmailMessage.DRAFT_SENT_SUCCESSFULLY, + messageId: result.message.id, + queueId: result.queueId, + result, + }; } //============================================== async deleteDraft(wildduckUserId: string, messageId: number) { - this.logger.log(`Deleting draft ${messageId} for user: ${wildduckUserId}`); + const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); + const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, draftsMailboxId, messageId)); - try { - const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId); - const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, draftsMailboxId, messageId)); - - this.logger.log(`Draft ${messageId} deleted successfully`); - - return { - message: EmailMessage.DRAFT_DELETED_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to delete draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + message: EmailMessage.DRAFT_DELETED_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToArchive(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`); + const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); - try { - const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: archiveMailboxId, + }), + ); - this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`); - - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: archiveMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved to archive successfully from ${mailboxId}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} to archive for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`); + const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); - try { - const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: favoriteMailboxId, + }), + ); - this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`); - - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: favoriteMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved to favorite successfully from ${mailboxId}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} to favorite for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`); + const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); - try { - const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: trashMailboxId, + }), + ); - this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`); - - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: trashMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved to trash successfully from ${mailboxId}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} to trash for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: inboxMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved from archive to inbox successfully`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} from archive to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: inboxMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved from favorite to inbox successfully`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} from favorite to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: inboxMailboxId, - }), - ); - - this.logger.log(`Message ${messageId} moved from trash to inbox successfully`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} from trash to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToJunk(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`); + const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); - try { - const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: junkMailboxId, + }), + ); - this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`); + const domainTag = await this.getUserBusinessId(wildduckUserId); - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: junkMailboxId, - }), - ); + await this.emailSpamService.markAsSpam({ + messageId, + wildduckUserId, + manageDomainList: true, + domainTag, + }); - this.logger.log(`Message ${messageId} moved to junk successfully from ${mailboxId}`); - - const domainTag = await this.getUserBusinessId(wildduckUserId); - - const spamResult = await this.emailSpamService.markAsSpam({ - messageId, - wildduckUserId, - manageDomainList: true, - domainTag, - }); - - this.logger.log(`Automatic domain management result for message ${messageId}: domainBlocked=${spamResult.domainBlocked}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} to junk for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY, + result, + }; } //============================================== async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number, mailboxId: string) { - this.logger.log(`Moving message ${messageId} from junk to inbox for user: ${wildduckUserId}`); + const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); - try { - const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId); + const result = await firstValueFrom( + this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { + moveTo: inboxMailboxId, + }), + ); - const result = await firstValueFrom( - this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, { - moveTo: inboxMailboxId, - }), - ); + const domainTag = await this.getUserBusinessId(wildduckUserId); - this.logger.log(`Message ${messageId} moved from junk to inbox successfully`); + await this.emailSpamService.markAsNotSpam({ + messageId, + wildduckUserId, + manageDomainList: true, + domainTag, + }); - const domainTag = await this.getUserBusinessId(wildduckUserId); - - const notSpamResult = await this.emailSpamService.markAsNotSpam({ - messageId, - wildduckUserId, - manageDomainList: true, - domainTag, - }); - - this.logger.log(`Automatic domain management result for message ${messageId}: domainAllowed=${notSpamResult.domainAllowed}`); - - return { - success: true, - message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY, - result, - }; - } catch (error) { - this.logger.error( - `Failed to move message ${messageId} from junk to inbox for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - throw error; - } + return { + success: true, + message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY, + result, + }; } //============================================== // BULK ACTIONS //============================================== async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) { - this.logger.log(`Performing bulk action ${bulkActionDto.action} on ${bulkActionDto.messageIds.length} messages for user: ${wildduckUserId}`); - const results = { successful: [] as number[], failed: [] as { messageId: number; error: string }[], @@ -1052,7 +784,7 @@ export class EmailService { } //============================================== - private addEmailToMap(emailMap: Map, recipient: any, timestamp: Date) { + private addEmailToMap(emailMap: Map, recipient: EmailRecipientDto, timestamp: Date) { if (!recipient || !recipient.address) return; const email = recipient.address.toLowerCase(); diff --git a/src/modules/mail-server/services/wildduck-messages.service.ts b/src/modules/mail-server/services/wildduck-messages.service.ts index 2a2bfa2..19158c0 100644 --- a/src/modules/mail-server/services/wildduck-messages.service.ts +++ b/src/modules/mail-server/services/wildduck-messages.service.ts @@ -146,9 +146,9 @@ export class WildDuckMessagesService extends WildDuckBaseService { mailboxId: string, messageId: number, data: { - target: string[]; - from?: string; - sessId?: string; + target: number; + addresses: string[]; + sess?: string; ip?: string; }, ): Observable {