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

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