rm unnec. stuff

This commit is contained in:
Andreas Knuth 2026-01-01 08:37:57 -06:00
parent 68caecf104
commit 6d1abf30ab
11 changed files with 40 additions and 2568 deletions

View File

@ -6,9 +6,7 @@ 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 { body, param, validationResult } = require('express-validator');
const app = express();
const PORT = process.env.PORT || 3001;
@ -16,22 +14,13 @@ const TOKEN_SECRET = process.env.TOKEN_SECRET_KEY;
// Middleware
app.use(helmet());
//app.use(cors());
const corsOptions = {
origin: [
'https://config.email-bayarea.com',
],
origin: ['https://config.email-bayarea.com'],
methods: ['GET', 'HEAD', 'PUT', 'PATCH', 'POST', 'DELETE'],
allowedHeaders: [
'Content-Type',
'Accept',
'Authorization',
'x-hide-loading',
],
credentials: false, // true nur wenn Cookies / Auth-Headers mit credentials genutzt werden
allowedHeaders: ['Content-Type', 'Accept', 'Authorization', 'x-hide-loading'],
credentials: false,
};
app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
app.use(express.json());
app.use(morgan('dev'));
@ -43,318 +32,73 @@ const client = new DynamoDBClient({
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
// Validation
const handleValidationErrors = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array(),
});
}
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
if (now > parseInt(expires)) return { valid: false, error: 'Token expired' };
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 };
const expected = crypto.createHmac('sha256', TOKEN_SECRET).update(data).digest('hex');
return signature === expected ? { valid: true, email } : { valid: false, error: 'Invalid signature' };
};
// Trigger Email Rules Synchronization
const triggerSync = () => {
const syncScriptPath = path.join(__dirname, '../sync/sync.js');
// --- API Routes ---
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
// 1. Auth check
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'),
body('email').isEmail(),
body('expires').isNumeric(),
body('signature').notEmpty(),
], 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,
});
}
if (!result.valid) return res.status(401).json({ error: result.error });
res.json({ success: true, email: result.email });
});
// Health Check
app.get('/health', (req, res) => {
res.json({ status: 'OK', timestamp: new Date().toISOString() });
});
// 2. Health check
app.get('/health', (req, res) => res.json({ status: 'OK' }));
// GET /api/rules - Get all rules
app.get('/api/rules', async (req, res) => {
// 3. Rules: Get One
app.get('/api/rules/:email', [
param('email').isEmail()
], handleValidationErrors, async (req, res) => {
try {
const command = new ScanCommand({
const response = await docClient.send(new GetCommand({
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}`,
});
}
Key: { email_address: decodeURIComponent(req.params.email) }
}));
if (!response.Item) return res.status(404).json({ error: 'No rule found' });
res.json(response.Item);
} catch (error) {
console.error('Error fetching rule:', error);
res.status(500).json({
error: 'Failed to fetch rule',
message: error.message,
});
res.status(500).json({ error: error.message });
}
});
// POST /api/rules - Create or update rule
app.post('/api/rules', validateEmailRule, handleValidationErrors, async (req, res) => {
// 4. Rules: Save/Update (OOO & Forwards)
app.post('/api/rules', [
body('email_address').isEmail(),
body('ooo_active').isBoolean(),
body('forwards').isArray()
], 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 || [],
...req.body,
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,
});
await docClient.send(new PutCommand({ TableName: TABLE_NAME, Item: item }));
res.json({ success: true, rule: item });
} catch (error) {
console.error('Error creating/updating rule:', error);
res.status(500).json({
error: 'Failed to create/update rule',
message: error.message,
});
res.status(500).json({ error: 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'}`);
});
app.listen(PORT, () => console.log(`🚀 API active on port ${PORT}`));

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"

1420
sync/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View File

@ -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

View File

@ -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"

View File

@ -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();

View File

@ -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();