261 lines
7.4 KiB
JavaScript
Executable File
261 lines
7.4 KiB
JavaScript
Executable File
#!/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();
|