346 lines
9.4 KiB
JavaScript
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'}`);
|
|
});
|