#!/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();