This commit is contained in:
2025-10-17 09:44:58 -05:00
parent 45a61a032c
commit 655050bf46
6 changed files with 5182 additions and 714 deletions

View File

@@ -13,6 +13,28 @@ const CONCURRENT_S3_OPERATIONS = 10;
const BATCH_INSERT_SIZE = 100;
const CONCURRENT_EMAIL_PARSING = 5;
// Globale Helper function für sichere Date-Konvertierung
function parseDate(dateInput: string | Date | undefined | null): Date | null {
if (!dateInput) return null;
try {
const date = dateInput instanceof Date ? dateInput : new Date(dateInput);
// Prüfe ob das Datum gültig ist
if (isNaN(date.getTime())) {
return null;
}
// Prüfe ob das Datum in einem vernünftigen Bereich liegt (1970-2100)
const year = date.getFullYear();
if (year < 1970 || year > 2100) {
return null;
}
return date;
} catch (error) {
console.error('Error parsing date:', dateInput, error);
return null;
}
}
export async function syncAllDomains() {
console.log('Starting optimized syncAllDomains...');
const startTime = Date.now();
@@ -28,8 +50,17 @@ export async function syncAllDomains() {
domainBuckets.map(bucketObj =>
bucketLimit(async () => {
const bucket = bucketObj.Name!;
const domainName = bucket.replace('-emails', '').replace(/-/g, '.');
console.log(`Processing bucket: ${bucket}`);
// Korrekte Domain-Konvertierung: Nur den letzten '-' vor '-emails' zu '.' machen
// Beispiel: bayarea-cc-com-emails -> bayarea-cc-com -> bayarea-cc.com
let domainName = bucket.replace('-emails', ''); // Entferne '-emails'
const lastDashIndex = domainName.lastIndexOf('-'); // Finde letzten Bindestrich
if (lastDashIndex !== -1) {
// Ersetze nur den letzten Bindestrich durch einen Punkt
domainName = domainName.substring(0, lastDashIndex) + '.' + domainName.substring(lastDashIndex + 1);
}
console.log(`Processing bucket: ${bucket} -> Domain: ${domainName}`);
const [domain] = await db
.insert(domains)
@@ -114,7 +145,7 @@ async function syncEmailsForDomainOptimized(domainId: number, bucket: string, s3
const metadata = head.Metadata || {};
const processed = metadata[process.env.PROCESSED_META_KEY!] === process.env.PROCESSED_META_VALUE!;
const processedAt = metadata['processed_at'] ? new Date(metadata['processed_at']) : null;
const processedAt = parseDate(metadata['processed_at']);
const processedBy = metadata['processed_by'] || null;
const queuedTo = metadata['queued_to'] || null;
const status = metadata['status'] || null;
@@ -167,6 +198,25 @@ async function syncEmailsForDomainOptimized(domainId: number, bucket: string, s3
const cc = extractAddresses(parsed.cc);
const bcc = extractAddresses(parsed.bcc);
// Versuche verschiedene Datum-Quellen in Reihenfolge der Präferenz
let emailDate: Date | null = null;
// 1. Versuche parsed.date
if (parsed.date) {
emailDate = parseDate(parsed.date);
}
// 2. Falls parsed.date ungültig, versuche S3 LastModified
if (!emailDate && headResponse.LastModified) {
emailDate = parseDate(headResponse.LastModified);
}
// 3. Falls beides ungültig, verwende aktuelles Datum als Fallback
if (!emailDate) {
console.warn(`No valid date found for ${key}, using current date`);
emailDate = new Date();
}
return {
domainId,
s3Key: key,
@@ -178,9 +228,8 @@ async function syncEmailsForDomainOptimized(domainId: number, bucket: string, s3
html: parsed.html || parsed.textAsHtml,
raw: raw.toString('utf-8'),
processed: metadata[process.env.PROCESSED_META_KEY!] === process.env.PROCESSED_META_VALUE!,
date: parsed.date || headResponse.LastModified,
// Neue Metadaten
processedAt: metadata['processed_at'] ? new Date(metadata['processed_at']) : null,
date: emailDate,
processedAt: parseDate(metadata['processed_at']),
processedBy: metadata['processed_by'] || null,
queuedTo: metadata['queued_to'] || null,
status: metadata['status'] || null,

View File

@@ -17,7 +17,7 @@ export function getS3Client() {
socketTimeout: 60000,
httpsAgent: new https.Agent({
keepAlive: true,
maxSockets: 50 // Erhöhe parallele Verbindungen
maxSockets: 50
})
})
});
@@ -25,22 +25,66 @@ export function getS3Client() {
export function authenticate(req: NextRequest) {
const auth = req.headers.get('Authorization');
console.log('Received Auth Header:', auth); // Logge den Header
console.log('Expected Password:', process.env.APP_PASSWORD); // Logge das Env-Passwort (Achtung: Sensibel, nur für Debug!)
console.log('Received Auth Header:', auth);
console.log('Expected Password:', process.env.APP_PASSWORD);
if (!auth || !auth.startsWith('Basic ')) return false;
const [user, pass] = Buffer.from(auth.slice(6), 'base64').toString().split(':');
return user === 'admin' && pass === process.env.APP_PASSWORD;
}
export async function getBody(stream: Readable): Promise<Buffer> {
export async function getBody(stream: Readable, timeoutMs = 30000): Promise<Buffer> {
console.log('Getting body from stream...');
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', chunk => chunks.push(chunk));
stream.on('error', reject);
let totalSize = 0;
let timeoutHandle: NodeJS.Timeout;
// Timeout-Handler
const cleanup = () => {
if (timeoutHandle) clearTimeout(timeoutHandle);
stream.removeAllListeners();
};
timeoutHandle = setTimeout(() => {
cleanup();
reject(new Error(`Stream timeout after ${timeoutMs}ms`));
}, timeoutMs);
stream.on('data', (chunk: Buffer) => {
chunks.push(chunk);
totalSize += chunk.length;
// Optional: Limit für maximale Größe (z.B. 50MB)
if (totalSize > 50 * 1024 * 1024) {
cleanup();
reject(new Error('Stream size exceeded maximum limit of 50MB'));
}
});
stream.on('error', (err) => {
cleanup();
console.error('Stream error:', err);
reject(err);
});
stream.on('end', () => {
console.log('Body fetched, size:', Buffer.concat(chunks).length);
resolve(Buffer.concat(chunks));
cleanup();
const buffer = Buffer.concat(chunks);
console.log('Body fetched, size:', buffer.length);
resolve(buffer);
});
// Handle stream destroy/close
stream.on('close', () => {
cleanup();
if (chunks.length > 0) {
const buffer = Buffer.concat(chunks);
console.log('Stream closed, partial data fetched, size:', buffer.length);
resolve(buffer);
} else {
reject(new Error('Stream closed without data'));
}
});
});
}