rm unnec. stuff
This commit is contained in:
parent
68caecf104
commit
6d1abf30ab
|
|
@ -6,9 +6,7 @@ const morgan = require('morgan');
|
||||||
const crypto = require('crypto');
|
const crypto = require('crypto');
|
||||||
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
|
const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
|
||||||
const { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } = require('@aws-sdk/lib-dynamodb');
|
const { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, ScanCommand } = require('@aws-sdk/lib-dynamodb');
|
||||||
const { body, param, validationResult, query } = require('express-validator');
|
const { body, param, validationResult } = require('express-validator');
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3001;
|
||||||
|
|
@ -16,22 +14,13 @@ const TOKEN_SECRET = process.env.TOKEN_SECRET_KEY;
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(helmet());
|
app.use(helmet());
|
||||||
//app.use(cors());
|
|
||||||
const corsOptions = {
|
const corsOptions = {
|
||||||
origin: [
|
origin: ['https://config.email-bayarea.com'],
|
||||||
'https://config.email-bayarea.com',
|
|
||||||
],
|
|
||||||
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
|
||||||
allowedHeaders: [
|
allowedHeaders: ['Content-Type', 'Accept', 'Authorization', 'x-hide-loading'],
|
||||||
'Content-Type',
|
credentials: false,
|
||||||
'Accept',
|
|
||||||
'Authorization',
|
|
||||||
'x-hide-loading',
|
|
||||||
],
|
|
||||||
credentials: false, // true nur wenn Cookies / Auth-Headers mit credentials genutzt werden
|
|
||||||
};
|
};
|
||||||
app.use(cors(corsOptions));
|
app.use(cors(corsOptions));
|
||||||
app.options('*', cors(corsOptions));
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(morgan('dev'));
|
app.use(morgan('dev'));
|
||||||
|
|
||||||
|
|
@ -43,318 +32,73 @@ const client = new DynamoDBClient({
|
||||||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const docClient = DynamoDBDocumentClient.from(client);
|
const docClient = DynamoDBDocumentClient.from(client);
|
||||||
const TABLE_NAME = process.env.DYNAMODB_TABLE || 'email-rules';
|
const TABLE_NAME = process.env.DYNAMODB_TABLE || 'email-rules';
|
||||||
|
|
||||||
// Validation Middleware
|
// Validation
|
||||||
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 handleValidationErrors = (req, res, next) => {
|
||||||
const errors = validationResult(req);
|
const errors = validationResult(req);
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) return res.status(400).json({ error: 'Validation failed', details: errors.array() });
|
||||||
return res.status(400).json({
|
|
||||||
error: 'Validation failed',
|
|
||||||
details: errors.array(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Token Validation Function
|
|
||||||
const validateToken = (email, expires, signature) => {
|
const validateToken = (email, expires, signature) => {
|
||||||
// Check expiry
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
if (now > parseInt(expires)) {
|
if (now > parseInt(expires)) return { valid: false, error: 'Token expired' };
|
||||||
return { valid: false, error: 'Token expired' };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify signature
|
|
||||||
const data = `${email}|${expires}`;
|
const data = `${email}|${expires}`;
|
||||||
const expected = crypto
|
const expected = crypto.createHmac('sha256', TOKEN_SECRET).update(data).digest('hex');
|
||||||
.createHmac('sha256', TOKEN_SECRET)
|
return signature === expected ? { valid: true, email } : { valid: false, error: 'Invalid signature' };
|
||||||
.update(data)
|
|
||||||
.digest('hex');
|
|
||||||
|
|
||||||
if (signature !== expected) {
|
|
||||||
return { valid: false, error: 'Invalid signature' };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { valid: true, email };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Trigger Email Rules Synchronization
|
// --- API Routes ---
|
||||||
const triggerSync = () => {
|
|
||||||
const syncScriptPath = path.join(__dirname, '../sync/sync.js');
|
|
||||||
|
|
||||||
console.log('🔄 Triggering email rules synchronization...');
|
// 1. Auth check
|
||||||
|
|
||||||
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', [
|
app.post('/api/auth/validate-token', [
|
||||||
body('email').isEmail().withMessage('Valid email is required'),
|
body('email').isEmail(),
|
||||||
body('expires').isNumeric().withMessage('Expires must be a number'),
|
body('expires').isNumeric(),
|
||||||
body('signature').notEmpty().withMessage('Signature is required'),
|
body('signature').notEmpty(),
|
||||||
], handleValidationErrors, (req, res) => {
|
], handleValidationErrors, (req, res) => {
|
||||||
try {
|
|
||||||
const { email, expires, signature } = req.body;
|
const { email, expires, signature } = req.body;
|
||||||
const result = validateToken(email, expires, signature);
|
const result = validateToken(email, expires, signature);
|
||||||
|
if (!result.valid) return res.status(401).json({ error: result.error });
|
||||||
if (!result.valid) {
|
res.json({ success: true, email: result.email });
|
||||||
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
|
// 2. Health check
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => res.json({ status: 'OK' }));
|
||||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
|
||||||
});
|
|
||||||
|
|
||||||
// GET /api/rules - Get all rules
|
// 3. Rules: Get One
|
||||||
app.get('/api/rules', async (req, res) => {
|
app.get('/api/rules/:email', [
|
||||||
|
param('email').isEmail()
|
||||||
|
], handleValidationErrors, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const command = new ScanCommand({
|
const response = await docClient.send(new GetCommand({
|
||||||
TableName: TABLE_NAME,
|
TableName: TABLE_NAME,
|
||||||
});
|
Key: { email_address: decodeURIComponent(req.params.email) }
|
||||||
|
}));
|
||||||
const response = await docClient.send(command);
|
if (!response.Item) return res.status(404).json({ error: 'No rule found' });
|
||||||
|
|
||||||
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);
|
res.json(response.Item);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching rule:', error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({
|
|
||||||
error: 'Failed to fetch rule',
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/rules - Create or update rule
|
// 4. Rules: Save/Update (OOO & Forwards)
|
||||||
app.post('/api/rules', validateEmailRule, handleValidationErrors, async (req, res) => {
|
app.post('/api/rules', [
|
||||||
|
body('email_address').isEmail(),
|
||||||
|
body('ooo_active').isBoolean(),
|
||||||
|
body('forwards').isArray()
|
||||||
|
], handleValidationErrors, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { email_address, ooo_active, ooo_message, ooo_content_type, forwards } = req.body;
|
|
||||||
|
|
||||||
const item = {
|
const item = {
|
||||||
email_address,
|
...req.body,
|
||||||
ooo_active: ooo_active || false,
|
|
||||||
ooo_message: ooo_message || '',
|
|
||||||
ooo_content_type: ooo_content_type || 'text',
|
|
||||||
forwards: forwards || [],
|
|
||||||
last_updated: new Date().toISOString(),
|
last_updated: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
await docClient.send(new PutCommand({ TableName: TABLE_NAME, Item: item }));
|
||||||
const command = new PutCommand({
|
res.json({ success: true, rule: item });
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error('Error creating/updating rule:', error);
|
res.status(500).json({ error: error.message });
|
||||||
res.status(500).json({
|
|
||||||
error: 'Failed to create/update rule',
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /api/rules/:email - Update existing rule
|
app.listen(PORT, () => console.log(`🚀 API active on port ${PORT}`));
|
||||||
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'}`);
|
|
||||||
});
|
|
||||||
13
sync/.env
13
sync/.env
|
|
@ -1,13 +0,0 @@
|
||||||
AWS_ACCESS_KEY_ID=AKIAU6GDWVAQXE5HQCWG
|
|
||||||
AWS_SECRET_ACCESS_KEY=S7cCm+zIPVXeOdKJmuvTKVh/Ul40jXKhHAv7OfIX
|
|
||||||
AWS_REGION=us-east-2
|
|
||||||
DYNAMODB_TABLE=email-rules
|
|
||||||
|
|
||||||
# Mail server paths (adjust based on your setup)
|
|
||||||
MAIL_DATA_PATH=/home/timo/docker-mailserver/docker-data/dms/mail-data
|
|
||||||
MAIL_STATE_PATH=/home/timo/docker-mailserver/docker-data/dms/mail-state
|
|
||||||
VIRTUAL_ALIASES_PATH=/home/timo/docker-mailserver/docker-data/dms/config/postfix-virtual.cf
|
|
||||||
SIEVE_BASE_PATH=/home/timo/docker-mailserver/docker-data/dms/mail-data
|
|
||||||
|
|
||||||
# Docker container name
|
|
||||||
MAILSERVER_CONTAINER=mailserver-new
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
# Quick Start Guide - Email Rules Sync
|
|
||||||
|
|
||||||
## What This Does
|
|
||||||
|
|
||||||
✅ **Reads email rules from AWS DynamoDB**
|
|
||||||
✅ **Generates Sieve scripts for Out-of-Office auto-replies**
|
|
||||||
✅ **Generates Postfix virtual aliases for email forwarding**
|
|
||||||
✅ **Syncs every 5 minutes automatically**
|
|
||||||
|
|
||||||
## Setup (5 Minutes)
|
|
||||||
|
|
||||||
### Step 1: Setup Sudo Permissions
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/timo/config-email/sync
|
|
||||||
./setup-sudo.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This allows the sync script to change file ownership without password prompts.
|
|
||||||
|
|
||||||
### Step 2: Test Manual Sync
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo node sync.js
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see:
|
|
||||||
```
|
|
||||||
✅ Found X email rules
|
|
||||||
✅ Created Sieve script for user@domain.com
|
|
||||||
✅ Updated virtual aliases
|
|
||||||
✅ Postfix reloaded
|
|
||||||
✅ Dovecot reloaded
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Install Cron Job
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./install-cron.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This sets up automatic syncing every 5 minutes.
|
|
||||||
|
|
||||||
## Verify It's Working
|
|
||||||
|
|
||||||
### Check Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f /tmp/email-rules-sync.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Sieve Scripts (Auto-Reply)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For support@qrmaster.net
|
|
||||||
cat /home/timo/docker-mailserver/docker-data/dms/mail-data/qrmaster.net/support/home/.dovecot.sieve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Virtual Aliases (Forwarding)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat /home/timo/docker-mailserver/docker-data/dms/config/postfix-virtual.cf
|
|
||||||
```
|
|
||||||
|
|
||||||
## Test Auto-Reply
|
|
||||||
|
|
||||||
1. Go to Roundcube → Settings → Email Configuration
|
|
||||||
2. Click "Open Email Configuration"
|
|
||||||
3. Enable "Out of Office"
|
|
||||||
4. Set message: "I'm out until Monday"
|
|
||||||
5. Click "Update Rule"
|
|
||||||
6. Wait 5 minutes (or run `sudo node sync.js` manually)
|
|
||||||
7. Send an email to that address
|
|
||||||
8. You should receive an auto-reply! 🎉
|
|
||||||
|
|
||||||
## Test Forwarding
|
|
||||||
|
|
||||||
1. Go to Roundcube → Settings → Email Configuration
|
|
||||||
2. Click "Open Email Configuration"
|
|
||||||
3. Go to "Email Forwarding" tab
|
|
||||||
4. Add forward address: `your@email.com`
|
|
||||||
5. Click "Update Rule"
|
|
||||||
6. Wait 5 minutes (or run `sudo node sync.js` manually)
|
|
||||||
7. Send an email to that address
|
|
||||||
8. You should receive it at your forward address! 🎉
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌──────────────┐
|
|
||||||
│ React UI │ ← User configures rules
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ Express API │ ← Saves to DynamoDB
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────┐
|
|
||||||
│ DynamoDB │ ← Rules stored here
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓ (Every 5 min)
|
|
||||||
┌──────────────┐
|
|
||||||
│ Sync Script │ ← YOU ARE HERE
|
|
||||||
└──────┬───────┘
|
|
||||||
│
|
|
||||||
↓
|
|
||||||
┌──────────────────────────────┐
|
|
||||||
│ Mail Server │
|
|
||||||
│ ┌────────┐ ┌────────────┐ │
|
|
||||||
│ │ Sieve │ │ Postfix │ │
|
|
||||||
│ │ OOO │ │ Forwarding │ │
|
|
||||||
│ └────────┘ └────────────┘ │
|
|
||||||
└──────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Sync fails with permission error
|
|
||||||
|
|
||||||
Run: `./setup-sudo.sh`
|
|
||||||
|
|
||||||
### Auto-reply not working
|
|
||||||
|
|
||||||
1. Check Sieve script was created:
|
|
||||||
```bash
|
|
||||||
sudo node sync.js
|
|
||||||
# Look for "✅ Created Sieve script for..."
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check Dovecot logs:
|
|
||||||
```bash
|
|
||||||
docker logs mailserver-new 2>&1 | grep -i sieve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Forwarding not working
|
|
||||||
|
|
||||||
1. Check virtual aliases:
|
|
||||||
```bash
|
|
||||||
cat /home/timo/docker-mailserver/docker-data/dms/config/postfix-virtual.cf
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check Postfix logs:
|
|
||||||
```bash
|
|
||||||
docker logs mailserver-new 2>&1 | grep -i virtual
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Manually reload:
|
|
||||||
```bash
|
|
||||||
docker exec mailserver-new postfix reload
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
1. ✅ Setup sudo permissions
|
|
||||||
2. ✅ Test manual sync
|
|
||||||
3. ✅ Install cron job
|
|
||||||
4. ✅ Test auto-reply
|
|
||||||
5. ✅ Test forwarding
|
|
||||||
6. 🎉 Enjoy automated email rules!
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `sync.js` - Main sync script
|
|
||||||
- `setup-sudo.sh` - Setup sudo permissions
|
|
||||||
- `install-cron.sh` - Install cron job
|
|
||||||
- `.env` - Configuration (AWS credentials)
|
|
||||||
- `QUICKSTART.md` - This file
|
|
||||||
- `README.md` - Detailed documentation
|
|
||||||
231
sync/README.md
231
sync/README.md
|
|
@ -1,231 +0,0 @@
|
||||||
# Email Rules Sync System
|
|
||||||
|
|
||||||
This script synchronizes email rules from AWS DynamoDB to the mail server, enabling:
|
|
||||||
- **Out-of-Office Auto-Replies** (Sieve scripts)
|
|
||||||
- **Email Forwarding** (Postfix virtual aliases)
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────┐
|
|
||||||
│ DynamoDB │ ← Rules stored here (via Web UI)
|
|
||||||
│ email-rules │
|
|
||||||
└──────┬──────┘
|
|
||||||
│
|
|
||||||
│ Sync Script (every 5 minutes)
|
|
||||||
↓
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ Mail Server (Docker) │
|
|
||||||
│ ┌────────────┐ ┌───────────────┐ │
|
|
||||||
│ │ Sieve │ │ Postfix │ │
|
|
||||||
│ │ Scripts │ │Virtual Aliases│ │
|
|
||||||
│ │ (OOO) │ │ (Forwarding) │ │
|
|
||||||
│ └────────────┘ └───────────────┘ │
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setup
|
|
||||||
|
|
||||||
### 1. Install Dependencies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/timo/config-email/sync
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Configure Environment
|
|
||||||
|
|
||||||
The `.env` file is already configured with:
|
|
||||||
- AWS credentials
|
|
||||||
- DynamoDB table name
|
|
||||||
- Mail server paths
|
|
||||||
|
|
||||||
### 3. Test Manual Sync
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
You should see output like:
|
|
||||||
```
|
|
||||||
🚀 Starting email rules sync...
|
|
||||||
📊 DynamoDB Table: email-rules
|
|
||||||
🌍 Region: us-east-2
|
|
||||||
|
|
||||||
📥 Fetching rules from DynamoDB...
|
|
||||||
✅ Found 2 email rules
|
|
||||||
|
|
||||||
📝 Processing Sieve scripts (Out-of-Office)...
|
|
||||||
✅ Created Sieve script for support@qrmaster.net
|
|
||||||
✅ Processed 1 Sieve scripts
|
|
||||||
|
|
||||||
📮 Updating virtual aliases (Forwarding)...
|
|
||||||
✅ Found 0 forwarding rules
|
|
||||||
✅ Updated virtual aliases
|
|
||||||
|
|
||||||
🔄 Applying changes to mail server...
|
|
||||||
✅ Postfix reloaded
|
|
||||||
✅ Dovecot reloaded
|
|
||||||
|
|
||||||
✨ Sync completed successfully!
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
Total Rules: 2
|
|
||||||
OOO Active: 1
|
|
||||||
Forwarding Active: 0
|
|
||||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Install as Cron Job (Automatic Sync)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./install-cron.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
This will:
|
|
||||||
- Run sync every 5 minutes
|
|
||||||
- Log to `/tmp/email-rules-sync.log`
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### View Sync Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
tail -f /tmp/email-rules-sync.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Sync
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /home/timo/config-email/sync
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Current Cron Jobs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
crontab -l
|
|
||||||
```
|
|
||||||
|
|
||||||
### Remove Cron Job
|
|
||||||
|
|
||||||
```bash
|
|
||||||
crontab -l | grep -v "email-rules-sync" | crontab -
|
|
||||||
```
|
|
||||||
|
|
||||||
## What Gets Generated
|
|
||||||
|
|
||||||
### 1. Sieve Scripts (Out-of-Office)
|
|
||||||
|
|
||||||
Location: `/home/timo/docker-mailserver/docker-data/dms/mail-data/{domain}/{user}/home/.dovecot.sieve`
|
|
||||||
|
|
||||||
Example for `support@qrmaster.net`:
|
|
||||||
```sieve
|
|
||||||
require ["vacation", "variables"];
|
|
||||||
|
|
||||||
# Auto-Reply / Out-of-Office
|
|
||||||
# Generated by Email Rules Sync System
|
|
||||||
# Last updated: 2025-12-27T12:00:00.000Z
|
|
||||||
|
|
||||||
if true {
|
|
||||||
vacation
|
|
||||||
:days 1
|
|
||||||
:subject "Out of Office"
|
|
||||||
"I am currently out of office and will respond when I return.";
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Virtual Aliases (Forwarding)
|
|
||||||
|
|
||||||
Location: `/home/timo/docker-mailserver/docker-data/dms/config/postfix-virtual.cf`
|
|
||||||
|
|
||||||
Example:
|
|
||||||
```
|
|
||||||
# Virtual Aliases - Email Forwarding
|
|
||||||
# Generated by Email Rules Sync System
|
|
||||||
# Last updated: 2025-12-27T12:00:00.000Z
|
|
||||||
|
|
||||||
# Forwarding for support@qrmaster.net
|
|
||||||
support@qrmaster.net forward1@example.com,forward2@example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## DynamoDB Schema
|
|
||||||
|
|
||||||
The sync script expects this schema:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
email_address: "support@qrmaster.net", // Primary Key
|
|
||||||
ooo_active: true, // Enable/disable auto-reply
|
|
||||||
ooo_message: "I am out of office...", // Auto-reply message
|
|
||||||
ooo_content_type: "text", // "text" or "html"
|
|
||||||
forwards: ["user@example.com"], // Array of forward addresses
|
|
||||||
last_updated: "2025-12-27T12:00:00.000Z" // Timestamp
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Script fails to connect to DynamoDB
|
|
||||||
|
|
||||||
Check AWS credentials in `.env`:
|
|
||||||
```bash
|
|
||||||
cat .env | grep AWS
|
|
||||||
```
|
|
||||||
|
|
||||||
### Sieve scripts not working
|
|
||||||
|
|
||||||
1. Check script was created:
|
|
||||||
```bash
|
|
||||||
ls -la /home/timo/docker-mailserver/docker-data/dms/mail-data/qrmaster.net/support/home/
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check Dovecot logs:
|
|
||||||
```bash
|
|
||||||
docker logs mailserver-new 2>&1 | grep -i sieve
|
|
||||||
```
|
|
||||||
|
|
||||||
### Forwarding not working
|
|
||||||
|
|
||||||
1. Check virtual aliases file:
|
|
||||||
```bash
|
|
||||||
cat /home/timo/docker-mailserver/docker-data/dms/config/postfix-virtual.cf
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check Postfix logs:
|
|
||||||
```bash
|
|
||||||
docker logs mailserver-new 2>&1 | grep -i virtual
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Reload Postfix manually:
|
|
||||||
```bash
|
|
||||||
docker exec mailserver-new postfix reload
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
Web UI (React)
|
|
||||||
↓
|
|
||||||
Backend API (Express)
|
|
||||||
↓
|
|
||||||
DynamoDB (email-rules)
|
|
||||||
↓
|
|
||||||
Sync Script (Node.js) ← You are here
|
|
||||||
↓
|
|
||||||
Mail Server (Dovecot + Postfix)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Files
|
|
||||||
|
|
||||||
- `sync.js` - Main sync script
|
|
||||||
- `package.json` - Dependencies
|
|
||||||
- `.env` - Configuration
|
|
||||||
- `install-cron.sh` - Cron job installer
|
|
||||||
- `README.md` - This file
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues, check:
|
|
||||||
1. Sync logs: `/tmp/email-rules-sync.log`
|
|
||||||
2. Mail server logs: `docker logs mailserver-new`
|
|
||||||
3. DynamoDB table: AWS Console → DynamoDB → email-rules
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Install Email Rules Sync as a cron job
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
SYNC_SCRIPT="$SCRIPT_DIR/sync.js"
|
|
||||||
|
|
||||||
echo "📦 Installing Email Rules Sync Cron Job..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Make sync script executable
|
|
||||||
chmod +x "$SYNC_SCRIPT"
|
|
||||||
|
|
||||||
# Create cron job to run every 30 minutes (as fallback - main sync is event-driven)
|
|
||||||
CRON_JOB="*/30 * * * * cd $SCRIPT_DIR && sudo /usr/bin/node sync.js >> /tmp/email-rules-sync.log 2>&1"
|
|
||||||
|
|
||||||
# Check if cron job already exists
|
|
||||||
if crontab -l 2>/dev/null | grep -q "email-rules-sync"; then
|
|
||||||
echo "⚠️ Cron job already exists. Updating..."
|
|
||||||
(crontab -l 2>/dev/null | grep -v "email-rules-sync"; echo "$CRON_JOB") | crontab -
|
|
||||||
else
|
|
||||||
echo "➕ Adding new cron job..."
|
|
||||||
(crontab -l 2>/dev/null; echo "$CRON_JOB") | crontab -
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Cron job installed successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "The sync script will run every 30 minutes as a fallback."
|
|
||||||
echo "Main synchronization is event-driven (triggered by API changes)."
|
|
||||||
echo "Logs are written to: /tmp/email-rules-sync.log"
|
|
||||||
echo ""
|
|
||||||
echo "To view current cron jobs:"
|
|
||||||
echo " crontab -l"
|
|
||||||
echo ""
|
|
||||||
echo "To view logs:"
|
|
||||||
echo " tail -f /tmp/email-rules-sync.log"
|
|
||||||
echo ""
|
|
||||||
echo "To run sync manually:"
|
|
||||||
echo " cd $SCRIPT_DIR && npm start"
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,16 +0,0 @@
|
||||||
{
|
|
||||||
"name": "email-rules-sync",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "Sync email rules from DynamoDB to mail server",
|
|
||||||
"main": "sync.js",
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node sync.js",
|
|
||||||
"sync": "node sync.js"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-dynamodb": "^3.679.0",
|
|
||||||
"@aws-sdk/lib-dynamodb": "^3.679.0",
|
|
||||||
"dotenv": "^16.4.5"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Setup sudo permissions for email sync script
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
SYNC_SCRIPT="$SCRIPT_DIR/sync.js"
|
|
||||||
USERNAME=$(whoami)
|
|
||||||
|
|
||||||
echo "🔐 Setting up sudo permissions for email rules sync..."
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Create sudoers file
|
|
||||||
SUDOERS_FILE="/etc/sudoers.d/email-rules-sync"
|
|
||||||
|
|
||||||
# Check if already configured
|
|
||||||
if [ -f "$SUDOERS_FILE" ]; then
|
|
||||||
echo "⚠️ Sudoers file already exists at $SUDOERS_FILE"
|
|
||||||
echo "Remove it first if you want to recreate it"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create temp file
|
|
||||||
TEMP_SUDOERS=$(mktemp)
|
|
||||||
|
|
||||||
cat > "$TEMP_SUDOERS" << EOF
|
|
||||||
# Allow $USERNAME to run email-rules-sync without password
|
|
||||||
# This is needed to change file ownership to mail server user (UID 5000)
|
|
||||||
$USERNAME ALL=(ALL) NOPASSWD: /usr/bin/node $SYNC_SCRIPT
|
|
||||||
EOF
|
|
||||||
|
|
||||||
# Validate sudoers syntax
|
|
||||||
if visudo -c -f "$TEMP_SUDOERS" 2>/dev/null; then
|
|
||||||
echo "✅ Sudoers file syntax is valid"
|
|
||||||
echo "Moving to $SUDOERS_FILE..."
|
|
||||||
sudo mv "$TEMP_SUDOERS" "$SUDOERS_FILE"
|
|
||||||
sudo chmod 0440 "$SUDOERS_FILE"
|
|
||||||
echo "✅ Sudo permissions configured successfully!"
|
|
||||||
echo ""
|
|
||||||
echo "You can now run:"
|
|
||||||
echo " sudo node $SYNC_SCRIPT"
|
|
||||||
echo ""
|
|
||||||
echo "Without entering a password."
|
|
||||||
else
|
|
||||||
echo "❌ Sudoers file syntax error!"
|
|
||||||
rm -f "$TEMP_SUDOERS"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Wrapper script to run sync with proper permissions
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
CONTAINER="mailserver-new"
|
|
||||||
|
|
||||||
# Run sync script and capture output
|
|
||||||
echo "🚀 Running email rules sync..."
|
|
||||||
|
|
||||||
# Copy sync script to container
|
|
||||||
docker cp "$SCRIPT_DIR/sync.js" $CONTAINER:/tmp/sync.js
|
|
||||||
docker cp "$SCRIPT_DIR/.env" $CONTAINER:/tmp/.env
|
|
||||||
docker cp "$SCRIPT_DIR/package.json" $CONTAINER:/tmp/package.json
|
|
||||||
|
|
||||||
# Install dependencies in container if needed
|
|
||||||
docker exec $CONTAINER bash -c "cd /tmp && npm install --quiet 2>/dev/null || true"
|
|
||||||
|
|
||||||
# Run sync inside container with proper environment
|
|
||||||
docker exec -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_REGION \
|
|
||||||
$CONTAINER bash -c "cd /tmp && node sync.js"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ Sync completed"
|
|
||||||
260
sync/sync.js
260
sync/sync.js
|
|
@ -1,260 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import 'dotenv/config';
|
|
||||||
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
||||||
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
||||||
import fs from 'fs/promises';
|
|
||||||
import path from 'path';
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
|
|
||||||
// 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';
|
|
||||||
|
|
||||||
// Paths
|
|
||||||
const VIRTUAL_ALIASES_PATH = process.env.VIRTUAL_ALIASES_PATH;
|
|
||||||
const SIEVE_BASE_PATH = process.env.SIEVE_BASE_PATH;
|
|
||||||
const MAILSERVER_CONTAINER = process.env.MAILSERVER_CONTAINER || 'mailserver-new';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Sieve script for Out-of-Office auto-reply
|
|
||||||
*/
|
|
||||||
function generateSieveScript(rule) {
|
|
||||||
const { ooo_message, ooo_content_type } = rule;
|
|
||||||
|
|
||||||
// Escape special characters in the message
|
|
||||||
const escapedMessage = ooo_message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
||||||
|
|
||||||
const script = `require ["vacation", "variables"];
|
|
||||||
|
|
||||||
# Auto-Reply / Out-of-Office
|
|
||||||
# Generated by Email Rules Sync System
|
|
||||||
# Last updated: ${new Date().toISOString()}
|
|
||||||
|
|
||||||
if true {
|
|
||||||
vacation
|
|
||||||
:days 1
|
|
||||||
:subject "Out of Office"
|
|
||||||
${ooo_content_type === 'html' ? ':mime' : ''}
|
|
||||||
"${escapedMessage}";
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return script;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Sieve script path for an email address
|
|
||||||
*/
|
|
||||||
function getSievePath(email) {
|
|
||||||
const [user, domain] = email.split('@');
|
|
||||||
return path.join(SIEVE_BASE_PATH, domain, user, 'home', '.dovecot.sieve');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write or remove Sieve script based on OOO status
|
|
||||||
*/
|
|
||||||
async function manageSieveScript(rule) {
|
|
||||||
const { email_address, ooo_active } = rule;
|
|
||||||
const [user, domain] = email_address.split('@');
|
|
||||||
|
|
||||||
// Check if mailbox exists first
|
|
||||||
const mailboxPath = path.join(SIEVE_BASE_PATH, domain, user);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.access(mailboxPath);
|
|
||||||
} catch (error) {
|
|
||||||
// Mailbox doesn't exist - skip silently
|
|
||||||
if (ooo_active) {
|
|
||||||
console.log(`⚠️ Skipping ${email_address} - mailbox not found (user might not exist yet)`);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sievePath = getSievePath(email_address);
|
|
||||||
const sieveDir = path.dirname(sievePath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (ooo_active) {
|
|
||||||
// Create Sieve script
|
|
||||||
const script = generateSieveScript(rule);
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
await fs.mkdir(sieveDir, { recursive: true });
|
|
||||||
|
|
||||||
// Write Sieve script
|
|
||||||
await fs.writeFile(sievePath, script, 'utf8');
|
|
||||||
console.log(`✅ Created Sieve script for ${email_address}`);
|
|
||||||
|
|
||||||
// Set proper permissions and ownership (important for mail server)
|
|
||||||
await fs.chmod(sievePath, 0o644);
|
|
||||||
|
|
||||||
// Change ownership to mail server user (UID 5000)
|
|
||||||
try {
|
|
||||||
await fs.chown(sievePath, 5000, 5000);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`⚠️ Could not change ownership for ${sievePath} - run with sudo`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// Remove Sieve script if it exists
|
|
||||||
try {
|
|
||||||
await fs.unlink(sievePath);
|
|
||||||
console.log(`🗑️ Removed Sieve script for ${email_address}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code !== 'ENOENT') {
|
|
||||||
console.error(`❌ Error removing Sieve for ${email_address}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error managing Sieve for ${email_address}:`, error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate Postfix virtual aliases content
|
|
||||||
*/
|
|
||||||
function generateVirtualAliases(rules) {
|
|
||||||
const lines = [
|
|
||||||
'# Virtual Aliases - Email Forwarding',
|
|
||||||
'# Generated by Email Rules Sync System',
|
|
||||||
`# Last updated: ${new Date().toISOString()}`,
|
|
||||||
'',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const rule of rules) {
|
|
||||||
const { email_address, forwards } = rule;
|
|
||||||
|
|
||||||
if (forwards && forwards.length > 0) {
|
|
||||||
// Add comment
|
|
||||||
lines.push(`# Forwarding for ${email_address}`);
|
|
||||||
|
|
||||||
// Add forwarding rule
|
|
||||||
// Format: source_email destination1,destination2,destination3
|
|
||||||
const destinations = forwards.join(',');
|
|
||||||
lines.push(`${email_address} ${destinations}`);
|
|
||||||
lines.push('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write virtual aliases file
|
|
||||||
*/
|
|
||||||
async function updateVirtualAliases(rules) {
|
|
||||||
try {
|
|
||||||
const content = generateVirtualAliases(rules);
|
|
||||||
await fs.writeFile(VIRTUAL_ALIASES_PATH, content, 'utf8');
|
|
||||||
console.log(`✅ Updated virtual aliases at ${VIRTUAL_ALIASES_PATH}`);
|
|
||||||
|
|
||||||
// Set proper permissions
|
|
||||||
await fs.chmod(VIRTUAL_ALIASES_PATH, 0o644);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`❌ Error updating virtual aliases:`, error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reload mail server services
|
|
||||||
*/
|
|
||||||
function reloadMailServer() {
|
|
||||||
try {
|
|
||||||
console.log('🔄 Reloading mail server services...');
|
|
||||||
|
|
||||||
// Reload Postfix
|
|
||||||
execSync(`docker exec ${MAILSERVER_CONTAINER} postfix reload`, { stdio: 'inherit' });
|
|
||||||
console.log('✅ Postfix reloaded');
|
|
||||||
|
|
||||||
// Reload Dovecot (for Sieve changes)
|
|
||||||
execSync(`docker exec ${MAILSERVER_CONTAINER} doveadm reload`, { stdio: 'inherit' });
|
|
||||||
console.log('✅ Dovecot reloaded');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error reloading mail server:', error.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main sync function
|
|
||||||
*/
|
|
||||||
async function syncEmailRules() {
|
|
||||||
console.log('🚀 Starting email rules sync...');
|
|
||||||
console.log(`📊 DynamoDB Table: ${TABLE_NAME}`);
|
|
||||||
console.log(`🌍 Region: ${process.env.AWS_REGION}`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. Fetch all rules from DynamoDB
|
|
||||||
console.log('📥 Fetching rules from DynamoDB...');
|
|
||||||
const command = new ScanCommand({
|
|
||||||
TableName: TABLE_NAME,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await docClient.send(command);
|
|
||||||
const rules = response.Items || [];
|
|
||||||
|
|
||||||
console.log(`✅ Found ${rules.length} email rules`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
if (rules.length === 0) {
|
|
||||||
console.log('ℹ️ No rules to sync. Exiting.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Process Sieve scripts (Out-of-Office)
|
|
||||||
console.log('📝 Processing Sieve scripts (Out-of-Office)...');
|
|
||||||
let sieveCount = 0;
|
|
||||||
for (const rule of rules) {
|
|
||||||
const success = await manageSieveScript(rule);
|
|
||||||
if (success) sieveCount++;
|
|
||||||
}
|
|
||||||
console.log(`✅ Processed ${sieveCount} Sieve scripts`);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// 3. Update virtual aliases (Forwarding)
|
|
||||||
console.log('📮 Updating virtual aliases (Forwarding)...');
|
|
||||||
const forwardingRules = rules.filter(r => r.forwards && r.forwards.length > 0);
|
|
||||||
console.log(`✅ Found ${forwardingRules.length} forwarding rules`);
|
|
||||||
await updateVirtualAliases(rules);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// 4. Reload mail server
|
|
||||||
console.log('🔄 Applying changes to mail server...');
|
|
||||||
reloadMailServer();
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
// 5. Summary
|
|
||||||
console.log('✨ Sync completed successfully!');
|
|
||||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
console.log(`Total Rules: ${rules.length}`);
|
|
||||||
console.log(`OOO Active: ${sieveCount}`);
|
|
||||||
console.log(`Forwarding Active: ${forwardingRules.length}`);
|
|
||||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Sync failed:', error.message);
|
|
||||||
console.error(error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run sync
|
|
||||||
syncEmailRules();
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
import 'dotenv/config';
|
|
||||||
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
||||||
import { DynamoDBDocumentClient, ScanCommand } from '@aws-sdk/lib-dynamodb';
|
|
||||||
|
|
||||||
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';
|
|
||||||
|
|
||||||
async function viewDatabase() {
|
|
||||||
console.log('📊 DynamoDB Table:', TABLE_NAME);
|
|
||||||
console.log('🌍 Region:', process.env.AWS_REGION);
|
|
||||||
console.log('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const command = new ScanCommand({
|
|
||||||
TableName: TABLE_NAME,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await docClient.send(command);
|
|
||||||
const rules = response.Items || [];
|
|
||||||
|
|
||||||
console.log(`✅ Found ${rules.length} email rules:\n`);
|
|
||||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
||||||
|
|
||||||
for (const rule of rules) {
|
|
||||||
console.log(`\n📧 Email: ${rule.email_address}`);
|
|
||||||
console.log(` OOO Active: ${rule.ooo_active ? '✅ YES' : '❌ NO'}`);
|
|
||||||
if (rule.ooo_active) {
|
|
||||||
console.log(` OOO Message: "${rule.ooo_message.substring(0, 50)}${rule.ooo_message.length > 50 ? '...' : ''}"`);
|
|
||||||
console.log(` Content Type: ${rule.ooo_content_type}`);
|
|
||||||
}
|
|
||||||
console.log(` Forwards: ${rule.forwards && rule.forwards.length > 0 ? rule.forwards.join(', ') : 'None'}`);
|
|
||||||
console.log(` Last Updated: ${rule.last_updated || 'N/A'}`);
|
|
||||||
console.log(' ─────────────────────────────────────────────────────────────────');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Error fetching data:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewDatabase();
|
|
||||||
Loading…
Reference in New Issue