config-email/backend/server.js

346 lines
9.4 KiB
JavaScript

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const crypto = require('crypto');
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
const { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } = require('@aws-sdk/lib-dynamodb');
const { body, param, validationResult, query } = require('express-validator');
const { spawn } = require('child_process');
const path = require('path');
const app = express();
const PORT = process.env.PORT || 3001;
const TOKEN_SECRET = process.env.TOKEN_SECRET_KEY;
// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
// AWS DynamoDB Configuration
const client = new DynamoDBClient({
region: process.env.AWS_REGION || 'us-east-2',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.DYNAMODB_TABLE || 'email-rules';
// Validation Middleware
const validateEmailRule = [
body('email_address').isEmail().withMessage('Valid email address is required'),
body('ooo_active').optional().isBoolean().withMessage('ooo_active must be a boolean'),
body('ooo_message').optional().isString().withMessage('ooo_message must be a string'),
body('ooo_content_type').optional().isIn(['text', 'html']).withMessage('ooo_content_type must be "text" or "html"'),
body('forwards').optional().isArray().withMessage('forwards must be an array'),
body('forwards.*').optional().isEmail().withMessage('All forward addresses must be valid emails'),
];
const validateEmail = [
param('email').isEmail().withMessage('Valid email address is required'),
];
// Error Handler Middleware
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array(),
});
}
next();
};
// Token Validation Function
const validateToken = (email, expires, signature) => {
// Check expiry
const now = Math.floor(Date.now() / 1000);
if (now > parseInt(expires)) {
return { valid: false, error: 'Token expired' };
}
// Verify signature
const data = `${email}|${expires}`;
const expected = crypto
.createHmac('sha256', TOKEN_SECRET)
.update(data)
.digest('hex');
if (signature !== expected) {
return { valid: false, error: 'Invalid signature' };
}
return { valid: true, email };
};
// Trigger Email Rules Synchronization
const triggerSync = () => {
const syncScriptPath = path.join(__dirname, '../sync/sync.js');
console.log('🔄 Triggering email rules synchronization...');
const syncProcess = spawn('sudo', ['node', syncScriptPath], {
detached: true,
stdio: 'ignore'
});
syncProcess.unref(); // Allow the parent to exit independently
console.log('✅ Sync triggered (running in background)');
};
// POST /api/auth/validate-token - Validate authentication token from Roundcube
app.post('/api/auth/validate-token', [
body('email').isEmail().withMessage('Valid email is required'),
body('expires').isNumeric().withMessage('Expires must be a number'),
body('signature').notEmpty().withMessage('Signature is required'),
], handleValidationErrors, (req, res) => {
try {
const { email, expires, signature } = req.body;
const result = validateToken(email, expires, signature);
if (!result.valid) {
return res.status(401).json({
error: 'Invalid token',
message: result.error,
});
}
res.json({
success: true,
email: result.email,
});
} catch (error) {
console.error('Token validation error:', error);
res.status(500).json({
error: 'Token validation failed',
message: error.message,
});
}
});
// Health Check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// GET /api/rules - Get all rules
app.get('/api/rules', async (req, res) => {
try {
const command = new ScanCommand({
TableName: TABLE_NAME,
});
const response = await docClient.send(command);
res.json({
rules: response.Items || [],
count: response.Count || 0,
});
} catch (error) {
console.error('Error fetching rules:', error);
res.status(500).json({
error: 'Failed to fetch rules',
message: error.message,
});
}
});
// GET /api/rules/:email - Get rule for specific email
app.get('/api/rules/:email', validateEmail, handleValidationErrors, async (req, res) => {
try {
const email = decodeURIComponent(req.params.email);
const command = new GetCommand({
TableName: TABLE_NAME,
Key: {
email_address: email,
},
});
const response = await docClient.send(command);
if (!response.Item) {
return res.status(404).json({
error: 'Rule not found',
message: `No rule exists for email: ${email}`,
});
}
res.json(response.Item);
} catch (error) {
console.error('Error fetching rule:', error);
res.status(500).json({
error: 'Failed to fetch rule',
message: error.message,
});
}
});
// POST /api/rules - Create or update rule
app.post('/api/rules', validateEmailRule, handleValidationErrors, async (req, res) => {
try {
const { email_address, ooo_active, ooo_message, ooo_content_type, forwards } = req.body;
const item = {
email_address,
ooo_active: ooo_active || false,
ooo_message: ooo_message || '',
ooo_content_type: ooo_content_type || 'text',
forwards: forwards || [],
last_updated: new Date().toISOString(),
};
const command = new PutCommand({
TableName: TABLE_NAME,
Item: item,
});
await docClient.send(command);
// Trigger immediate synchronization
triggerSync();
res.status(201).json({
message: 'Rule created/updated successfully',
rule: item,
});
} catch (error) {
console.error('Error creating/updating rule:', error);
res.status(500).json({
error: 'Failed to create/update rule',
message: error.message,
});
}
});
// PUT /api/rules/:email - Update existing rule
app.put('/api/rules/:email', validateEmail, validateEmailRule, handleValidationErrors, async (req, res) => {
try {
const email = decodeURIComponent(req.params.email);
const { ooo_active, ooo_message, ooo_content_type, forwards } = req.body;
// First check if rule exists
const getCommand = new GetCommand({
TableName: TABLE_NAME,
Key: { email_address: email },
});
const existingRule = await docClient.send(getCommand);
if (!existingRule.Item) {
return res.status(404).json({
error: 'Rule not found',
message: `No rule exists for email: ${email}`,
});
}
// Merge with existing data
const item = {
...existingRule.Item,
ooo_active: ooo_active !== undefined ? ooo_active : existingRule.Item.ooo_active,
ooo_message: ooo_message !== undefined ? ooo_message : existingRule.Item.ooo_message,
ooo_content_type: ooo_content_type !== undefined ? ooo_content_type : existingRule.Item.ooo_content_type,
forwards: forwards !== undefined ? forwards : existingRule.Item.forwards,
last_updated: new Date().toISOString(),
};
const putCommand = new PutCommand({
TableName: TABLE_NAME,
Item: item,
});
await docClient.send(putCommand);
// Trigger immediate synchronization
triggerSync();
res.json({
message: 'Rule updated successfully',
rule: item,
});
} catch (error) {
console.error('Error updating rule:', error);
res.status(500).json({
error: 'Failed to update rule',
message: error.message,
});
}
});
// DELETE /api/rules/:email - Delete rule
app.delete('/api/rules/:email', validateEmail, handleValidationErrors, async (req, res) => {
try {
const email = decodeURIComponent(req.params.email);
// First check if rule exists
const getCommand = new GetCommand({
TableName: TABLE_NAME,
Key: { email_address: email },
});
const existingRule = await docClient.send(getCommand);
if (!existingRule.Item) {
return res.status(404).json({
error: 'Rule not found',
message: `No rule exists for email: ${email}`,
});
}
const deleteCommand = new DeleteCommand({
TableName: TABLE_NAME,
Key: {
email_address: email,
},
});
await docClient.send(deleteCommand);
// Trigger immediate synchronization
triggerSync();
res.json({
message: 'Rule deleted successfully',
email_address: email,
});
} catch (error) {
console.error('Error deleting rule:', error);
res.status(500).json({
error: 'Failed to delete rule',
message: error.message,
});
}
});
// 404 Handler
app.use((req, res) => {
res.status(404).json({
error: 'Not Found',
message: `Cannot ${req.method} ${req.path}`,
});
});
// Global Error Handler
app.use((err, req, res, next) => {
console.error('Unhandled error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: err.message,
});
});
// Start Server
app.listen(PORT, () => {
console.log(`🚀 Email Config API running on port ${PORT}`);
console.log(`📊 DynamoDB Table: ${TABLE_NAME}`);
console.log(`🌍 Region: ${process.env.AWS_REGION || 'us-east-2'}`);
});