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 { 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,
|
||||
});
|
||||
}
|
||||
const { email, expires, signature } = req.body;
|
||||
const result = validateToken(email, expires, signature);
|
||||
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}`));
|
||||
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