update: for the reply message
This commit is contained in:
@@ -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<Socket | null>(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<string, number>;
|
|
||||||
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<EmailNotificationsProps> = ({
|
|
||||||
userToken,
|
|
||||||
onEmailClick
|
|
||||||
}) => {
|
|
||||||
const [notifications, setNotifications] = useState<EmailPayload[]>([]);
|
|
||||||
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 (
|
|
||||||
<div className="email-notifications">
|
|
||||||
{/* Connection Status */}
|
|
||||||
<div className={`connection-status ${isConnected ? 'connected' : 'disconnected'}`}>
|
|
||||||
<span className="status-dot"></span>
|
|
||||||
{isConnected ? 'Connected' : 'Disconnected'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notification Bell */}
|
|
||||||
<div
|
|
||||||
className="notification-bell"
|
|
||||||
onClick={() => setIsVisible(!isVisible)}
|
|
||||||
>
|
|
||||||
🔔
|
|
||||||
{unreadCount > 0 && (
|
|
||||||
<span className="badge">{unreadCount > 99 ? '99+' : unreadCount}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Notifications Dropdown */}
|
|
||||||
{isVisible && (
|
|
||||||
<div className="notifications-dropdown">
|
|
||||||
<div className="notifications-header">
|
|
||||||
<h3>📧 Email Notifications</h3>
|
|
||||||
<span>{unreadCount} unread</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="notifications-list">
|
|
||||||
{notifications.length === 0 ? (
|
|
||||||
<div className="no-notifications">No new emails</div>
|
|
||||||
) : (
|
|
||||||
notifications.map((email) => (
|
|
||||||
<div
|
|
||||||
key={email.messageId}
|
|
||||||
className="notification-item"
|
|
||||||
onClick={() => onEmailClick?.(email)}
|
|
||||||
>
|
|
||||||
<div className="sender">
|
|
||||||
<strong>{email.from.name || email.from.address}</strong>
|
|
||||||
{email.hasAttachments && <span className="attachment-icon">📎</span>}
|
|
||||||
</div>
|
|
||||||
<div className="subject">{email.subject}</div>
|
|
||||||
<div className="preview">{email.preview}</div>
|
|
||||||
<div className="timestamp">
|
|
||||||
{new Date(email.timestamp).toLocaleString()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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<EmailDashboardProps> = ({ 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 (
|
|
||||||
<div className="email-dashboard">
|
|
||||||
<h2>📧 Email Dashboard</h2>
|
|
||||||
|
|
||||||
<div className="stats-grid">
|
|
||||||
<div className="stat-card">
|
|
||||||
<h3>{emailStats.totalUnread}</h3>
|
|
||||||
<p>Unread Emails</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat-card">
|
|
||||||
<h3>{emailStats.newToday}</h3>
|
|
||||||
<p>Received Today</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat-card">
|
|
||||||
<h3>{emailStats.sentToday}</h3>
|
|
||||||
<p>Sent Today</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="stat-card">
|
|
||||||
<h3 className={isConnected ? 'connected' : 'disconnected'}>
|
|
||||||
{isConnected ? '🟢' : '🔴'}
|
|
||||||
</h3>
|
|
||||||
<p>{isConnected ? 'Connected' : 'Disconnected'}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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<EmailWebSocketContextValue | null>(null);
|
|
||||||
|
|
||||||
interface EmailWebSocketProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
token: string;
|
|
||||||
serverUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EmailWebSocketProvider: React.FC<EmailWebSocketProviderProps> = ({
|
|
||||||
children,
|
|
||||||
token,
|
|
||||||
serverUrl,
|
|
||||||
}) => {
|
|
||||||
const { socket, isConnected } = useEmailWebSocket({ token, serverUrl });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EmailWebSocketContext.Provider value={{ socket, isConnected }}>
|
|
||||||
{children}
|
|
||||||
</EmailWebSocketContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 <div>Please login to access email notifications</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EmailWebSocketProvider token={userToken}>
|
|
||||||
<div className="app">
|
|
||||||
<header>
|
|
||||||
<h1>📧 Email App</h1>
|
|
||||||
<EmailNotifications
|
|
||||||
userToken={userToken}
|
|
||||||
onEmailClick={handleEmailClick}
|
|
||||||
/>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<EmailDashboard userToken={userToken} />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</EmailWebSocketProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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! 🎉
|
|
||||||
@@ -140,6 +140,7 @@ export const enum EmailMessage {
|
|||||||
PAYMENT_REMINDER = "یادآوری پرداخت",
|
PAYMENT_REMINDER = "یادآوری پرداخت",
|
||||||
PAYMENT_CANCELLATION = "لغو پرداخت",
|
PAYMENT_CANCELLATION = "لغو پرداخت",
|
||||||
EMAIL_SENDING_SUCCESS = "ارسال ایمیل با موفقیت انجام شد",
|
EMAIL_SENDING_SUCCESS = "ارسال ایمیل با موفقیت انجام شد",
|
||||||
|
EMAIL_FORWARD_SUCCESS = "فوروارد ایمیل با موفقیت انجام شد",
|
||||||
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = "وضعیت ایمیل با موفقیت دریافت شد",
|
EMAIL_STATUS_RETRIEVED_SUCCESSFULLY = "وضعیت ایمیل با موفقیت دریافت شد",
|
||||||
MESSAGES_SEARCHED_SUCCESSFULLY = "جستجوی ایمیل با موفقیت انجام شد",
|
MESSAGES_SEARCHED_SUCCESSFULLY = "جستجوی ایمیل با موفقیت انجام شد",
|
||||||
MESSAGES_LISTED_SUCCESSFULLY = "لیست ایمیل ها با موفقیت دریافت شد",
|
MESSAGES_LISTED_SUCCESSFULLY = "لیست ایمیل ها با موفقیت دریافت شد",
|
||||||
@@ -227,6 +228,7 @@ export const enum EmailMessage {
|
|||||||
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = "پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد",
|
MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY = "پیام با موفقیت به عنوان خوانده نشده علامت گذاری شد",
|
||||||
MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد",
|
MESSAGE_MARK_AS_UNSEEN_FAILED = "علامت گذاری پیام به عنوان خوانده نشده با خطا مواجه شد",
|
||||||
ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد",
|
ATTACHMENT_ID_MUST_BE_STRING = "شناسه پیوست باید یک رشته باشد",
|
||||||
|
IS_FORWARD_MUST_BE_BOOLEAN = "IS_FORWARD_MUST_BE_BOOLEAN",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const enum WebSocketMessage {
|
export const enum WebSocketMessage {
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export class SendEmailDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string (optional)" })
|
@ApiPropertyOptional({ description: "Optional metadata, must be an object or JSON formatted string (optional)" })
|
||||||
meta?: any;
|
meta?: Record<string, unknown>;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString({ message: EmailMessage.SESSION_ID_MUST_BE_STRING })
|
@IsString({ message: EmailMessage.SESSION_ID_MUST_BE_STRING })
|
||||||
@@ -188,15 +188,20 @@ export class SendEmailDto {
|
|||||||
@ApiPropertyOptional({ description: "IP address for the logs (optional)" })
|
@ApiPropertyOptional({ description: "IP address for the logs (optional)" })
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
|
||||||
@IsOptional()
|
// @IsOptional()
|
||||||
@ApiPropertyOptional({ description: "Reference message data (optional)" })
|
// @ApiPropertyOptional({ description: "Reference message data (optional)" })
|
||||||
reference?: any;
|
// reference?: any;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN })
|
@IsBoolean({ message: EmailMessage.IS_DRAFT_MUST_BE_BOOLEAN })
|
||||||
@ApiPropertyOptional({ description: "Is the message a draft or not (optional)", default: false })
|
@ApiPropertyOptional({ description: "Is the message a draft or not (optional)", default: false })
|
||||||
isDraft?: boolean;
|
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()
|
@IsOptional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DraftReferenceDto)
|
@Type(() => DraftReferenceDto)
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface IEmailMap {
|
||||||
|
email: string;
|
||||||
|
name?: string;
|
||||||
|
count: number;
|
||||||
|
lastSeen: Date;
|
||||||
|
}
|
||||||
@@ -7,14 +7,14 @@ import { EmailSpamService } from "./email-spam.service";
|
|||||||
import { MailboxResolverService } from "./mailbox-resolver.service";
|
import { MailboxResolverService } from "./mailbox-resolver.service";
|
||||||
import { EmailMessage } from "../../../common/enums/message.enum";
|
import { EmailMessage } from "../../../common/enums/message.enum";
|
||||||
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
import { MailServerService } from "../../mail-server/services/mail-server.service";
|
||||||
import { MailboxEnum } from "../../mailbox/enums/mailbox.enum";
|
|
||||||
import { TemplateProcessorService } from "../../templates/services/template-processor.service";
|
import { TemplateProcessorService } from "../../templates/services/template-processor.service";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
import { UserRepository } from "../../users/repositories/user.repository";
|
import { UserRepository } from "../../users/repositories/user.repository";
|
||||||
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
import { BulkActionDto, BulkActionType } from "../DTO/bulk-actions.dto";
|
||||||
import { MessageListQueryDto, SearchMessagesQueryDto } from "../DTO/email-query.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 { Priority } from "../enums/email-header.enum";
|
||||||
|
import { IEmailMap } from "../interfaces/email-map.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
@@ -34,12 +34,19 @@ export class EmailService {
|
|||||||
async sendEmail(wildduckUserId: string, userId: string, sendEmailDto: SendEmailDto) {
|
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(", ")}`);
|
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"] });
|
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
||||||
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
if (!user) throw new BadRequestException(EmailMessage.USER_NOT_FOUND);
|
||||||
this.logger.log(`Processing email through business template for business: ${user.business.id}`);
|
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,
|
text: sendEmailDto.text,
|
||||||
html: sendEmailDto.html,
|
html: sendEmailDto.html,
|
||||||
subject: sendEmailDto.subject,
|
subject: sendEmailDto.subject,
|
||||||
@@ -52,16 +59,8 @@ export class EmailService {
|
|||||||
sendEmailDto.text = processedTemplate.content;
|
sendEmailDto.text = processedTemplate.content;
|
||||||
sendEmailDto.html = undefined;
|
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, {
|
const headers = this.generateHeadersForEmailType(user, {
|
||||||
html: sendEmailDto.html,
|
html: sendEmailDto.html,
|
||||||
text: sendEmailDto.text,
|
text: sendEmailDto.text,
|
||||||
@@ -71,8 +70,6 @@ export class EmailService {
|
|||||||
|
|
||||||
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, sendEmailDto));
|
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);
|
const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId);
|
||||||
|
|
||||||
await this.emailNotificationService.notifyEmailSent({
|
await this.emailNotificationService.notifyEmailSent({
|
||||||
@@ -89,17 +86,10 @@ export class EmailService {
|
|||||||
messageId: response.message.id,
|
messageId: response.message.id,
|
||||||
queueId: response.message.queueId,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async getEmailStatus(queueId: string) {
|
async getEmailStatus(queueId: string) {
|
||||||
this.logger.log(`Getting email status for queue: ${queueId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await firstValueFrom(this.mailServerService.submission.getQueueStatus(queueId));
|
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}`);
|
||||||
@@ -107,17 +97,10 @@ export class EmailService {
|
|||||||
return {
|
return {
|
||||||
...response,
|
...response,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get email status for queue ${queueId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
|
async searchMessages(wildduckUserId: string, query: SearchMessagesQueryDto) {
|
||||||
this.logger.log(`Searching messages for user ${wildduckUserId} with query: ${query.q}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Transform query parameters for WildDuck API compatibility
|
// Transform query parameters for WildDuck API compatibility
|
||||||
const wildDuckQuery = {
|
const wildDuckQuery = {
|
||||||
query: query.q || "",
|
query: query.q || "",
|
||||||
@@ -147,19 +130,10 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
query: response.query,
|
query: response.query,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to search messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async downloadMessageAttachment(wildduckUserId: string, messageId: number, mailboxId: string, attachmentId: string) {
|
async downloadMessageAttachment(wildduckUserId: string, messageId: number, mailboxId: string, attachmentId: string) {
|
||||||
console.log("messageId", messageId);
|
|
||||||
console.log("mailboxId", mailboxId);
|
|
||||||
console.log("attachmentId", attachmentId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const attachmentResponse = await firstValueFrom(
|
const attachmentResponse = await firstValueFrom(
|
||||||
this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId),
|
this.mailServerService.messages.getMessageAttachment(wildduckUserId, mailboxId, messageId, attachmentId),
|
||||||
);
|
);
|
||||||
@@ -171,26 +145,16 @@ export class EmailService {
|
|||||||
contentLength: attachmentResponse.contentLength,
|
contentLength: attachmentResponse.contentLength,
|
||||||
headers: attachmentResponse.headers,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async listMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async listMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
||||||
this.logger.log(`Listing messages in mailbox ${inboxMailboxId} for user: ${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 {
|
return {
|
||||||
results: response.results,
|
results: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -199,21 +163,13 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to list messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async getMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
|
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);
|
await this.markMessageAsSeen(wildduckUserId, messageId, mailboxId);
|
||||||
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
|
const message = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, mailboxId, messageId));
|
||||||
|
|
||||||
this.logger.log(`Retrieved message ${messageId} from ${mailboxId} for user: ${wildduckUserId}`);
|
|
||||||
const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId);
|
const mailboxName = await this.mailboxResolverService.getMailboxName(wildduckUserId, mailboxId);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -222,60 +178,33 @@ export class EmailService {
|
|||||||
mailboxName,
|
mailboxName,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//########################################################
|
//########################################################
|
||||||
async deleteMessage(wildduckUserId: string, messageId: number, mailboxId: string) {
|
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 {
|
return {
|
||||||
message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_DELETED_SUCCESSFULLY,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to delete message ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async deleteAllMessages(wildduckUserId: string, mailboxId: string) {
|
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 {
|
return {
|
||||||
message: EmailMessage.ALL_MESSAGES_DELETED_SUCCESSFULLY,
|
message: EmailMessage.ALL_MESSAGES_DELETED_SUCCESSFULLY,
|
||||||
mailboxId,
|
mailboxId,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to delete all messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async markMessageAsSeen(wildduckUserId: string, messageId: number, mailboxId: string) {
|
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 }));
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} marked as seen successfully in ${mailboxId}`);
|
|
||||||
|
|
||||||
await this.emailNotificationService.notifyEmailRead({
|
await this.emailNotificationService.notifyEmailRead({
|
||||||
userId: wildduckUserId,
|
userId: wildduckUserId,
|
||||||
messageId,
|
messageId,
|
||||||
@@ -287,38 +216,20 @@ export class EmailService {
|
|||||||
message: EmailMessage.MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MARKED_AS_SEEN_SUCCESSFULLY,
|
||||||
updated,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async markMessageAsUnseen(wildduckUserId: string, messageId: number, mailboxId: string) {
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MARKED_AS_UNSEEN_SUCCESSFULLY,
|
||||||
updated,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//==============================================
|
//==============================================
|
||||||
async getSentMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getSentMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId);
|
const sentMailboxId = await this.mailboxResolverService.getSentMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, sentMailboxId, wildDuckQuery));
|
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, sentMailboxId, wildDuckQuery));
|
||||||
@@ -333,21 +244,14 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get sent messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async getTrashMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getTrashMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
|
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, trashMailboxId, wildDuckQuery));
|
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 {
|
return {
|
||||||
trashMails: response.results,
|
trashMails: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -356,21 +260,14 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get trash messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|
||||||
async getJunkMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getJunkMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
|
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, junkMailboxId, wildDuckQuery));
|
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 {
|
return {
|
||||||
junkMails: response.results,
|
junkMails: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -379,21 +276,14 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get junk messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|
||||||
async getArchivedMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getArchivedMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
|
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, archiveMailboxId, wildDuckQuery));
|
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 {
|
return {
|
||||||
archiveMails: response.results,
|
archiveMails: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -402,21 +292,14 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get archive messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|
||||||
async getFavoriteMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getFavoriteMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
|
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, favoriteMailboxId, wildDuckQuery));
|
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 {
|
return {
|
||||||
favoriteMails: response.results,
|
favoriteMails: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -425,22 +308,15 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get favorite messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
|
|
||||||
async getDraftsMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
async getDraftsMessages(wildduckUserId: string, query: MessageListQueryDto) {
|
||||||
try {
|
|
||||||
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
||||||
const wildDuckQuery = this.transformPaginationQuery(query);
|
const wildDuckQuery = this.transformPaginationQuery(query);
|
||||||
const response = await firstValueFrom(this.mailServerService.messages.listMessages(wildduckUserId, draftsMailboxId, wildDuckQuery));
|
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 {
|
return {
|
||||||
draftsMail: response.results,
|
draftsMail: response.results,
|
||||||
count: response.total,
|
count: response.total,
|
||||||
@@ -449,22 +325,14 @@ export class EmailService {
|
|||||||
nextCursor: response.nextCursor,
|
nextCursor: response.nextCursor,
|
||||||
paginate: true,
|
paginate: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to get drafts messages for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async updateDraft(wildduckUserId: string, messageId: number, updateDraftDto: UpdateDraftDto) {
|
async updateDraft(wildduckUserId: string, messageId: number, updateDraftDto: UpdateDraftDto) {
|
||||||
this.logger.log(`Updating draft ${messageId} for user ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
||||||
const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId));
|
const existingDraft = await firstValueFrom(this.mailServerService.messages.getMessage(wildduckUserId, draftsMailboxId, messageId));
|
||||||
|
|
||||||
const draftData: SendEmailDto = {
|
const draftData: SendEmailDto = {
|
||||||
// mailbox: updateDraftDto.mailbox,
|
|
||||||
from: updateDraftDto.from || { address: existingDraft.from?.address || "" },
|
from: updateDraftDto.from || { address: existingDraft.from?.address || "" },
|
||||||
to: updateDraftDto.to || [],
|
to: updateDraftDto.to || [],
|
||||||
cc: updateDraftDto.cc,
|
cc: updateDraftDto.cc,
|
||||||
@@ -478,7 +346,6 @@ export class EmailService {
|
|||||||
meta: updateDraftDto.meta,
|
meta: updateDraftDto.meta,
|
||||||
sess: updateDraftDto.sess,
|
sess: updateDraftDto.sess,
|
||||||
ip: updateDraftDto.ip,
|
ip: updateDraftDto.ip,
|
||||||
reference: updateDraftDto.reference,
|
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
uploadOnly: true,
|
uploadOnly: true,
|
||||||
draft: {
|
draft: {
|
||||||
@@ -487,7 +354,6 @@ export class EmailService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process draft content through business template if available
|
|
||||||
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
const user = await this.userRepository.findOne({ wildduckUserId, deletedAt: null }, { populate: ["business"] });
|
||||||
|
|
||||||
if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) {
|
if (user && user.business && (updateDraftDto.text || updateDraftDto.html)) {
|
||||||
@@ -505,10 +371,6 @@ export class EmailService {
|
|||||||
draftData.html = undefined;
|
draftData.html = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (processedTemplate.hasTemplate) {
|
|
||||||
this.logger.log(`Draft content processed through template for business: ${user.business.id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const headers = this.generateHeadersForEmailType(user, {
|
const headers = this.generateHeadersForEmailType(user, {
|
||||||
html: draftData.html,
|
html: draftData.html,
|
||||||
text: draftData.text,
|
text: draftData.text,
|
||||||
@@ -518,163 +380,93 @@ export class EmailService {
|
|||||||
|
|
||||||
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData));
|
const response = await firstValueFrom(this.mailServerService.submission.submitMessage(wildduckUserId, draftData));
|
||||||
|
|
||||||
this.logger.log(`Draft updated successfully: ${response.message.mailbox}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: EmailMessage.DRAFT_UPDATED_SUCCESSFULLY,
|
message: EmailMessage.DRAFT_UPDATED_SUCCESSFULLY,
|
||||||
messageId: response.message.id,
|
messageId: response.message.id,
|
||||||
subject: "Draft",
|
subject: "Draft",
|
||||||
isDraft: true,
|
isDraft: true,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to update draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async sendDraft(wildduckUserId: string, messageId: number) {
|
async sendDraft(wildduckUserId: string, messageId: number) {
|
||||||
this.logger.log(`Sending draft ${messageId} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }),
|
this.mailServerService.messages.submitDraft(wildduckUserId, draftsMailboxId, messageId, { deleteFiles: false }),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Draft ${messageId} sent successfully`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: EmailMessage.DRAFT_SENT_SUCCESSFULLY,
|
message: EmailMessage.DRAFT_SENT_SUCCESSFULLY,
|
||||||
messageId: result.message.id,
|
messageId: result.message.id,
|
||||||
queueId: result.queueId,
|
queueId: result.queueId,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Failed to send draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async deleteDraft(wildduckUserId: string, messageId: number) {
|
async deleteDraft(wildduckUserId: string, messageId: number) {
|
||||||
this.logger.log(`Deleting draft ${messageId} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
const draftsMailboxId = await this.mailboxResolverService.getDraftsMailboxId(wildduckUserId);
|
||||||
const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, draftsMailboxId, messageId));
|
const result = await firstValueFrom(this.mailServerService.messages.deleteMessage(wildduckUserId, draftsMailboxId, messageId));
|
||||||
|
|
||||||
this.logger.log(`Draft ${messageId} deleted successfully`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: EmailMessage.DRAFT_DELETED_SUCCESSFULLY,
|
message: EmailMessage.DRAFT_DELETED_SUCCESSFULLY,
|
||||||
result,
|
result,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(
|
|
||||||
`Failed to delete draft ${messageId} for user ${wildduckUserId}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} to archive for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
|
const archiveMailboxId = await this.mailboxResolverService.getArchiveMailboxId(wildduckUserId);
|
||||||
|
|
||||||
this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.ARCHIVE} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
||||||
moveTo: archiveMailboxId,
|
moveTo: archiveMailboxId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved to archive successfully from ${mailboxId}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_ARCHIVE_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} to favorite for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
|
const favoriteMailboxId = await this.mailboxResolverService.getFavoriteMailboxId(wildduckUserId);
|
||||||
|
|
||||||
this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.FAVORITE} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
||||||
moveTo: favoriteMailboxId,
|
moveTo: favoriteMailboxId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved to favorite successfully from ${mailboxId}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_FAVORITE_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} to trash for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
|
const trashMailboxId = await this.mailboxResolverService.getTrashMailboxId(wildduckUserId);
|
||||||
|
|
||||||
this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.TRASH} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
||||||
moveTo: trashMailboxId,
|
moveTo: trashMailboxId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved to trash successfully from ${mailboxId}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_TRASH_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToInboxFromArchive(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} from archive to inbox for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
@@ -683,26 +475,15 @@ export class EmailService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved from archive to inbox successfully`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_ARCHIVE_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToInboxFromFavorite(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} from favorite to inbox for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
@@ -711,26 +492,15 @@ export class EmailService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved from favorite to inbox successfully`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_FAVORITE_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToInboxFromTrash(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} from trash to inbox for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
@@ -739,67 +509,41 @@ export class EmailService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved from trash to inbox successfully`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_TRASH_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} to junk for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
|
const junkMailboxId = await this.mailboxResolverService.getJunkMailboxId(wildduckUserId);
|
||||||
|
|
||||||
this.logger.log(`Moving message ${messageId} from ${mailboxId} to ${MailboxEnum.Junk} for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
this.mailServerService.messages.updateMessage(wildduckUserId, mailboxId, messageId, {
|
||||||
moveTo: junkMailboxId,
|
moveTo: junkMailboxId,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved to junk successfully from ${mailboxId}`);
|
|
||||||
|
|
||||||
const domainTag = await this.getUserBusinessId(wildduckUserId);
|
const domainTag = await this.getUserBusinessId(wildduckUserId);
|
||||||
|
|
||||||
const spamResult = await this.emailSpamService.markAsSpam({
|
await this.emailSpamService.markAsSpam({
|
||||||
messageId,
|
messageId,
|
||||||
wildduckUserId,
|
wildduckUserId,
|
||||||
manageDomainList: true,
|
manageDomainList: true,
|
||||||
domainTag,
|
domainTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Automatic domain management result for message ${messageId}: domainBlocked=${spamResult.domainBlocked}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_JUNK_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
|
async moveMessageToInboxFromJunk(wildduckUserId: string, messageId: number, mailboxId: string) {
|
||||||
this.logger.log(`Moving message ${messageId} from junk to inbox for user: ${wildduckUserId}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
const inboxMailboxId = await this.mailboxResolverService.getInboxMailboxId(wildduckUserId);
|
||||||
|
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
@@ -808,38 +552,26 @@ export class EmailService {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.log(`Message ${messageId} moved from junk to inbox successfully`);
|
|
||||||
|
|
||||||
const domainTag = await this.getUserBusinessId(wildduckUserId);
|
const domainTag = await this.getUserBusinessId(wildduckUserId);
|
||||||
|
|
||||||
const notSpamResult = await this.emailSpamService.markAsNotSpam({
|
await this.emailSpamService.markAsNotSpam({
|
||||||
messageId,
|
messageId,
|
||||||
wildduckUserId,
|
wildduckUserId,
|
||||||
manageDomainList: true,
|
manageDomainList: true,
|
||||||
domainTag,
|
domainTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.log(`Automatic domain management result for message ${messageId}: domainAllowed=${notSpamResult.domainAllowed}`);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY,
|
message: EmailMessage.MESSAGE_MOVED_TO_INBOX_FROM_JUNK_SUCCESSFULLY,
|
||||||
result,
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
// BULK ACTIONS
|
// BULK ACTIONS
|
||||||
//==============================================
|
//==============================================
|
||||||
async performBulkAction(wildduckUserId: string, bulkActionDto: BulkActionDto, mailboxId: string) {
|
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 = {
|
const results = {
|
||||||
successful: [] as number[],
|
successful: [] as number[],
|
||||||
failed: [] as { messageId: number; error: string }[],
|
failed: [] as { messageId: number; error: string }[],
|
||||||
@@ -1052,7 +784,7 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//==============================================
|
//==============================================
|
||||||
private addEmailToMap(emailMap: Map<string, { email: string; name?: string; count: number; lastSeen: Date }>, recipient: any, timestamp: Date) {
|
private addEmailToMap(emailMap: Map<string, IEmailMap>, recipient: EmailRecipientDto, timestamp: Date) {
|
||||||
if (!recipient || !recipient.address) return;
|
if (!recipient || !recipient.address) return;
|
||||||
|
|
||||||
const email = recipient.address.toLowerCase();
|
const email = recipient.address.toLowerCase();
|
||||||
|
|||||||
@@ -146,9 +146,9 @@ export class WildDuckMessagesService extends WildDuckBaseService {
|
|||||||
mailboxId: string,
|
mailboxId: string,
|
||||||
messageId: number,
|
messageId: number,
|
||||||
data: {
|
data: {
|
||||||
target: string[];
|
target: number;
|
||||||
from?: string;
|
addresses: string[];
|
||||||
sessId?: string;
|
sess?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
},
|
},
|
||||||
): Observable<MessageForwardResponse> {
|
): Observable<MessageForwardResponse> {
|
||||||
|
|||||||
Reference in New Issue
Block a user