2weekmail/api/controllers/MessageController.js
2025-03-22 14:52:13 +00:00

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().where('id', mailboxId).first();
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().where('id', mailboxId).first();
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();