initial
This commit is contained in:
260
sync/sync.js
Executable file
260
sync/sync.js
Executable 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();
|
||||
Reference in New Issue
Block a user