This commit is contained in:
2025-12-29 10:34:28 +01:00
commit 0347ee1342
35 changed files with 9593 additions and 0 deletions

13
sync/.env Normal file
View File

@@ -0,0 +1,13 @@
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

171
sync/QUICKSTART.md Normal file
View File

@@ -0,0 +1,171 @@
# 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 Normal file
View File

@@ -0,0 +1,231 @@
# 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

39
sync/install-cron.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/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 Normal file

File diff suppressed because it is too large Load Diff

16
sync/package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"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"
}
}

46
sync/setup-sudo.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/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

23
sync/sync-wrapper.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/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 Executable file
View File

@@ -0,0 +1,260 @@
#!/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();

53
sync/view-db.js Executable file
View File

@@ -0,0 +1,53 @@
#!/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();