config-email/sync/sync.js

261 lines
7.4 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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