512 lines
16 KiB
JavaScript
512 lines
16 KiB
JavaScript
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const simpleParser = require('mailparser').simpleParser;
|
|
const { models } = require(path.resolve(process.env.ROOT_PATH, './db/db.js'));
|
|
const BaseController = require(path.resolve(process.env.ROOT_PATH, './controllers/BaseController.js'));
|
|
|
|
class MessageController extends BaseController {
|
|
constructor() {
|
|
super();
|
|
|
|
// Protected endpoints
|
|
this.protected('getMessages', 'getMessage', 'deleteMessage');
|
|
}
|
|
|
|
/**
|
|
* Get all messages for a specific mailbox
|
|
*
|
|
* @swagger
|
|
* /mailboxes/{mailboxId}/messages:
|
|
* get:
|
|
* summary: Get all messages
|
|
* description: Retrieves all messages for a specific mailbox
|
|
* tags: [Messages]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: mailboxId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Mailbox ID
|
|
* responses:
|
|
* 200:
|
|
* description: List of messages
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: array
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* description: Message ID
|
|
* subject:
|
|
* type: string
|
|
* description: Email subject
|
|
* from:
|
|
* type: string
|
|
* description: Sender information
|
|
* date:
|
|
* type: string
|
|
* format: date-time
|
|
* description: Message date
|
|
* size:
|
|
* type: integer
|
|
* description: Message size in bytes
|
|
* hasAttachments:
|
|
* type: boolean
|
|
* description: Whether the message has attachments
|
|
* 404:
|
|
* description: Mailbox not found
|
|
* 500:
|
|
* description: Server error
|
|
*/
|
|
async getMessages(req, res) {
|
|
try {
|
|
const { mailboxId } = req.params;
|
|
|
|
// Verify mailbox exists
|
|
const mailbox = await models.Mailbox.query().findById(mailboxId);
|
|
if (!mailbox) {
|
|
return res.status(404).json({ error: 'Mailbox not found' });
|
|
}
|
|
|
|
// Get the physical path to the mailbox
|
|
const mailboxPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'new');
|
|
|
|
try {
|
|
// Check if directory exists
|
|
await fs.access(mailboxPath);
|
|
|
|
// Read all files in the directory
|
|
const files = await fs.readdir(mailboxPath);
|
|
|
|
// Process each file to extract basic email info
|
|
const messages = await Promise.all(
|
|
files.map(async (file) => {
|
|
const filePath = path.join(mailboxPath, file);
|
|
const stats = await fs.stat(filePath);
|
|
|
|
try {
|
|
// Read file content
|
|
const content = await fs.readFile(filePath, { encoding: 'utf-8' });
|
|
|
|
// Parse email content
|
|
const parsed = await simpleParser(content);
|
|
|
|
return {
|
|
id: file,
|
|
subject: parsed.subject || '(No Subject)',
|
|
from: parsed.from?.text || '(Unknown Sender)',
|
|
date: parsed.date || stats.mtime,
|
|
size: stats.size,
|
|
hasAttachments: parsed.attachments?.length > 0 || false
|
|
};
|
|
} catch (err) {
|
|
console.error(`Error parsing email ${file}:`, err);
|
|
return {
|
|
id: file,
|
|
subject: '(Error: Could not parse email)',
|
|
from: '(Unknown)',
|
|
date: stats.mtime,
|
|
size: stats.size,
|
|
hasAttachments: false,
|
|
error: true
|
|
};
|
|
}
|
|
})
|
|
);
|
|
|
|
// Sort messages by date (newest first)
|
|
messages.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
|
|
return res.json(messages);
|
|
} catch (err) {
|
|
// If directory doesn't exist or is empty
|
|
if (err.code === 'ENOENT') {
|
|
return res.json([]);
|
|
}
|
|
throw err;
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching messages:', error);
|
|
return res.status(500).json({ error: 'Failed to fetch messages' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a specific message from a mailbox
|
|
*
|
|
* @swagger
|
|
* /mailboxes/{mailboxId}/messages/{messageId}:
|
|
* get:
|
|
* summary: Get message details
|
|
* description: Retrieves a specific message from a mailbox
|
|
* tags: [Messages]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: mailboxId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Mailbox ID
|
|
* - in: path
|
|
* name: messageId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* description: Message ID
|
|
* responses:
|
|
* 200:
|
|
* description: Message details
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: string
|
|
* description: Message ID
|
|
* subject:
|
|
* type: string
|
|
* description: Email subject
|
|
* from:
|
|
* type: string
|
|
* description: Sender information
|
|
* to:
|
|
* type: string
|
|
* description: Recipient information
|
|
* cc:
|
|
* type: string
|
|
* description: CC recipients
|
|
* date:
|
|
* type: string
|
|
* format: date-time
|
|
* description: Message date
|
|
* textBody:
|
|
* type: string
|
|
* description: Plain text content
|
|
* htmlBody:
|
|
* type: string
|
|
* description: HTML content
|
|
* attachments:
|
|
* type: array
|
|
* description: List of attachments
|
|
* items:
|
|
* type: object
|
|
* properties:
|
|
* filename:
|
|
* type: string
|
|
* contentType:
|
|
* type: string
|
|
* size:
|
|
* type: integer
|
|
* contentId:
|
|
* type: string
|
|
* 404:
|
|
* description: Mailbox or message not found
|
|
* 500:
|
|
* description: Server error
|
|
*/
|
|
async getMessage(req, res) {
|
|
try {
|
|
const { mailboxId, messageId } = req.params;
|
|
|
|
// Verify mailbox exists
|
|
const mailbox = await models.Mailbox.query().findById(mailboxId);
|
|
if (!mailbox) {
|
|
return res.status(404).json({ error: 'Mailbox not found' });
|
|
}
|
|
|
|
// Check both new and cur directories for the message
|
|
const directories = ['new', 'cur'];
|
|
let messageContent = null;
|
|
let messageFound = false;
|
|
|
|
for (const dir of directories) {
|
|
const messagePath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir, messageId);
|
|
|
|
try {
|
|
// Check if file exists
|
|
await fs.access(messagePath);
|
|
|
|
// Read file content
|
|
const content = await fs.readFile(messagePath, { encoding: 'utf-8' });
|
|
|
|
// Parse email content
|
|
messageContent = await simpleParser(content);
|
|
messageFound = true;
|
|
|
|
// Move message from 'new' to 'cur' if it's in 'new'
|
|
if (dir === 'new') {
|
|
const curDir = path.join('/var/mail', mailbox.domain, mailbox.local_part, 'cur');
|
|
|
|
// Ensure cur directory exists
|
|
await fs.mkdir(curDir, { recursive: true });
|
|
|
|
// Move the file
|
|
const curPath = path.join(curDir, `${messageId}:2,S`); // Mark as seen
|
|
await fs.rename(messagePath, curPath);
|
|
}
|
|
|
|
break;
|
|
} catch (err) {
|
|
// Continue to next directory if file not found
|
|
if (err.code === 'ENOENT') {
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (!messageFound) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
|
|
// Format the response
|
|
const response = {
|
|
id: messageId,
|
|
subject: messageContent.subject || '(No Subject)',
|
|
from: messageContent.from?.text || '(Unknown Sender)',
|
|
to: messageContent.to?.text || '',
|
|
cc: messageContent.cc?.text || '',
|
|
date: messageContent.date,
|
|
textBody: messageContent.text,
|
|
htmlBody: messageContent.html,
|
|
attachments: messageContent.attachments?.map(attachment => ({
|
|
filename: attachment.filename,
|
|
contentType: attachment.contentType,
|
|
size: attachment.size,
|
|
contentId: attachment.contentId
|
|
})) || []
|
|
};
|
|
|
|
return res.json(response);
|
|
} catch (error) {
|
|
console.error('Error fetching message:', error);
|
|
return res.status(500).json({ error: 'Failed to fetch message' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a specific message from a mailbox
|
|
*
|
|
* @swagger
|
|
* /mailboxes/{mailboxId}/messages/{messageId}:
|
|
* delete:
|
|
* summary: Delete message
|
|
* description: Deletes a specific message from a mailbox
|
|
* tags: [Messages]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: mailboxId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Mailbox ID
|
|
* - in: path
|
|
* name: messageId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* description: Message ID
|
|
* responses:
|
|
* 200:
|
|
* description: Message deleted successfully
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* message:
|
|
* type: string
|
|
* example: Message deleted successfully
|
|
* 404:
|
|
* description: Mailbox or message not found
|
|
* 500:
|
|
* description: Server error
|
|
*/
|
|
async deleteMessage(req, res) {
|
|
try {
|
|
const { mailboxId, messageId } = req.params;
|
|
|
|
// Verify mailbox exists
|
|
const mailbox = await models.Mailbox.query().findById(mailboxId);
|
|
if (!mailbox) {
|
|
return res.status(404).json({ error: 'Mailbox not found' });
|
|
}
|
|
|
|
// Check both new and cur directories for the message
|
|
const directories = ['new', 'cur'];
|
|
let messageFound = false;
|
|
|
|
for (const dir of directories) {
|
|
// In 'cur' directory, the message might have flags appended
|
|
const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir);
|
|
|
|
try {
|
|
// Check if directory exists
|
|
await fs.access(dirPath);
|
|
|
|
// Read all files in the directory
|
|
const files = await fs.readdir(dirPath);
|
|
|
|
// Find files that start with the messageId
|
|
for (const file of files) {
|
|
if (file === messageId || file.startsWith(`${messageId}:`)) {
|
|
const filePath = path.join(dirPath, file);
|
|
await fs.unlink(filePath);
|
|
messageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (messageFound) break;
|
|
} catch (err) {
|
|
// Continue to next directory if directory not found
|
|
if (err.code === 'ENOENT') {
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (!messageFound) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
|
|
return res.json({ message: 'Message deleted successfully' });
|
|
} catch (error) {
|
|
console.error('Error deleting message:', error);
|
|
return res.status(500).json({ error: 'Failed to delete message' });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get attachment from a message
|
|
*
|
|
* @swagger
|
|
* /mailboxes/{mailboxId}/messages/{messageId}/attachments/{attachmentId}:
|
|
* get:
|
|
* summary: Get message attachment
|
|
* description: Retrieves a specific attachment from a message
|
|
* tags: [Messages]
|
|
* security:
|
|
* - bearerAuth: []
|
|
* parameters:
|
|
* - in: path
|
|
* name: mailboxId
|
|
* required: true
|
|
* schema:
|
|
* type: integer
|
|
* description: Mailbox ID
|
|
* - in: path
|
|
* name: messageId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* description: Message ID
|
|
* - in: path
|
|
* name: attachmentId
|
|
* required: true
|
|
* schema:
|
|
* type: string
|
|
* description: Attachment ID or filename
|
|
* responses:
|
|
* 200:
|
|
* description: Attachment file
|
|
* content:
|
|
* application/octet-stream:
|
|
* schema:
|
|
* type: string
|
|
* format: binary
|
|
* 404:
|
|
* description: Mailbox, message, or attachment not found
|
|
* 500:
|
|
* description: Server error
|
|
*/
|
|
async getAttachment(req, res) {
|
|
try {
|
|
const { mailboxId, messageId, attachmentId } = req.params;
|
|
|
|
// Verify mailbox exists
|
|
const mailbox = await models.Mailbox.query().findById(mailboxId);
|
|
if (!mailbox) {
|
|
return res.status(404).json({ error: 'Mailbox not found' });
|
|
}
|
|
|
|
// Check both new and cur directories for the message
|
|
const directories = ['new', 'cur'];
|
|
let messageContent = null;
|
|
let messageFound = false;
|
|
|
|
for (const dir of directories) {
|
|
// In 'cur' directory, the message might have flags appended
|
|
const dirPath = path.join('/var/mail', mailbox.domain, mailbox.local_part, dir);
|
|
|
|
try {
|
|
// Check if directory exists
|
|
await fs.access(dirPath);
|
|
|
|
// Read all files in the directory
|
|
const files = await fs.readdir(dirPath);
|
|
|
|
// Find files that start with the messageId
|
|
for (const file of files) {
|
|
if (file === messageId || file.startsWith(`${messageId}:`)) {
|
|
const filePath = path.join(dirPath, file);
|
|
|
|
// Read file content
|
|
const content = await fs.readFile(filePath, { encoding: 'utf-8' });
|
|
|
|
// Parse email content
|
|
messageContent = await simpleParser(content);
|
|
messageFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (messageFound) break;
|
|
} catch (err) {
|
|
// Continue to next directory if directory not found
|
|
if (err.code === 'ENOENT') {
|
|
continue;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
if (!messageFound) {
|
|
return res.status(404).json({ error: 'Message not found' });
|
|
}
|
|
|
|
// Find the attachment
|
|
const attachment = messageContent.attachments?.find(
|
|
att => att.contentId === attachmentId || att.filename === attachmentId
|
|
);
|
|
|
|
if (!attachment) {
|
|
return res.status(404).json({ error: 'Attachment not found' });
|
|
}
|
|
|
|
// Set appropriate headers
|
|
res.setHeader('Content-Type', attachment.contentType);
|
|
res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`);
|
|
|
|
// Send the attachment content
|
|
return res.send(attachment.content);
|
|
} catch (error) {
|
|
console.error('Error fetching attachment:', error);
|
|
return res.status(500).json({ error: 'Failed to fetch attachment' });
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new MessageController();
|