BugFixes
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
/**
|
||||
* Email rules processing (Auto-Reply / OOO and Forwarding)
|
||||
*
|
||||
* Removed: Legacy SMTP forward (forward_smtp_override)
|
||||
* Remaining paths:
|
||||
* - OOO → internal (SMTP port 25) or external (SES)
|
||||
* - Forward → internal (SMTP port 25) or external (SES)
|
||||
* * CLEANED UP & FIXED:
|
||||
* - Uses MailComposer for ALL message generation (safer MIME handling)
|
||||
* - Fixes broken attachment forwarding
|
||||
* - Removed legacy SMTP forwarding
|
||||
* - Removed manual string concatenation for MIME boundaries
|
||||
*/
|
||||
|
||||
import { createTransport, type Transporter } from 'nodemailer';
|
||||
import { createTransport } from 'nodemailer';
|
||||
import type { ParsedMail } from 'mailparser';
|
||||
import type { DynamoDBHandler, EmailRule } from '../aws/dynamodb.js';
|
||||
import type { SESHandler } from '../aws/ses.js';
|
||||
import { extractBodyParts } from './parser.js';
|
||||
import { config, isInternalAddress } from '../config.js';
|
||||
import { log } from '../logger.js';
|
||||
// Wir nutzen MailComposer direkt für das Erstellen der Raw Bytes
|
||||
import MailComposer from 'nodemailer/lib/mail-composer/index.js';
|
||||
|
||||
export type MetricsCallback = (action: 'autoreply' | 'forward', domain: string) => void;
|
||||
|
||||
@@ -25,7 +27,6 @@ export class RulesProcessor {
|
||||
|
||||
/**
|
||||
* Process OOO and Forward rules for a single recipient.
|
||||
* Returns false always (no skip_local_delivery since legacy SMTP removed).
|
||||
*/
|
||||
async processRulesForRecipient(
|
||||
recipient: string,
|
||||
@@ -103,7 +104,9 @@ export class RulesProcessor {
|
||||
try {
|
||||
const oooMsg = (rule.ooo_message as string) ?? 'I am out of office.';
|
||||
const contentType = (rule.ooo_content_type as string) ?? 'text';
|
||||
const oooBuffer = buildOooReply(parsed, recipient, oooMsg, contentType);
|
||||
|
||||
// FIX: Use MailComposer via await
|
||||
const oooBuffer = await buildOooReply(parsed, recipient, oooMsg, contentType);
|
||||
|
||||
if (isInternalAddress(senderAddr)) {
|
||||
const ok = await sendInternalEmail(recipient, senderAddr, oooBuffer, workerName);
|
||||
@@ -134,7 +137,8 @@ export class RulesProcessor {
|
||||
): Promise<void> {
|
||||
for (const forwardTo of forwards) {
|
||||
try {
|
||||
const fwdBuffer = buildForwardMessage(parsed, recipient, forwardTo, originalFrom);
|
||||
// FIX: Correctly await the composer result
|
||||
const fwdBuffer = await buildForwardMessage(parsed, recipient, forwardTo, originalFrom);
|
||||
|
||||
if (isInternalAddress(forwardTo)) {
|
||||
const ok = await sendInternalEmail(recipient, forwardTo, fwdBuffer, workerName);
|
||||
@@ -154,15 +158,15 @@ export class RulesProcessor {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Message building
|
||||
// Message building (Using Nodemailer MailComposer for Safety)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildOooReply(
|
||||
async function buildOooReply(
|
||||
original: ParsedMail,
|
||||
recipient: string,
|
||||
oooMsg: string,
|
||||
contentType: string,
|
||||
): Buffer {
|
||||
): Promise<Buffer> {
|
||||
const { text: textBody, html: htmlBody } = extractBodyParts(original);
|
||||
const originalSubject = original.subject ?? '(no subject)';
|
||||
const originalFrom = original.from?.text ?? 'unknown';
|
||||
@@ -184,33 +188,34 @@ function buildOooReply(
|
||||
|
||||
const includeHtml = contentType === 'html' || !!htmlBody;
|
||||
|
||||
return buildMimeMessage({
|
||||
const composer = new MailComposer({
|
||||
from: recipient,
|
||||
to: originalFrom,
|
||||
subject: `Out of Office: ${originalSubject}`,
|
||||
inReplyTo: originalMsgId,
|
||||
references: originalMsgId,
|
||||
domain: recipientDomain,
|
||||
textContent,
|
||||
htmlContent: includeHtml ? htmlContent : undefined,
|
||||
extraHeaders: {
|
||||
references: [originalMsgId], // Nodemailer wants array
|
||||
text: textContent,
|
||||
html: includeHtml ? htmlContent : undefined,
|
||||
headers: {
|
||||
'Auto-Submitted': 'auto-replied',
|
||||
'X-SES-Worker-Processed': 'ooo-reply',
|
||||
},
|
||||
messageId: `<${Date.now()}.${Math.random().toString(36).slice(2)}@${recipientDomain}>`
|
||||
});
|
||||
|
||||
return composer.compile().build();
|
||||
}
|
||||
|
||||
function buildForwardMessage(
|
||||
async function buildForwardMessage(
|
||||
original: ParsedMail,
|
||||
recipient: string,
|
||||
forwardTo: string,
|
||||
originalFrom: string,
|
||||
): Buffer {
|
||||
): Promise<Buffer> {
|
||||
const { text: textBody, html: htmlBody } = extractBodyParts(original);
|
||||
const originalSubject = original.subject ?? '(no subject)';
|
||||
const originalDate = original.date?.toUTCString() ?? 'unknown';
|
||||
const recipientDomain = recipient.split('@')[1];
|
||||
|
||||
|
||||
// Text version
|
||||
let fwdText = '---------- Forwarded message ---------\n';
|
||||
fwdText += `From: ${originalFrom}\n`;
|
||||
@@ -232,144 +237,32 @@ function buildForwardMessage(
|
||||
fwdHtml += '</div>';
|
||||
}
|
||||
|
||||
// Build base message
|
||||
const baseBuffer = buildMimeMessage({
|
||||
// Config object for MailComposer
|
||||
const mailOptions: any = {
|
||||
from: recipient,
|
||||
to: forwardTo,
|
||||
subject: `FWD: ${originalSubject}`,
|
||||
replyTo: originalFrom,
|
||||
domain: recipientDomain,
|
||||
textContent: fwdText,
|
||||
htmlContent: fwdHtml,
|
||||
extraHeaders: {
|
||||
'X-SES-Worker-Processed': 'forwarded',
|
||||
},
|
||||
});
|
||||
|
||||
// For attachments, we re-build using nodemailer which handles them properly
|
||||
if (original.attachments && original.attachments.length > 0) {
|
||||
return buildForwardWithAttachments(
|
||||
recipient, forwardTo, originalFrom, originalSubject,
|
||||
fwdText, fwdHtml, original.attachments, recipientDomain,
|
||||
);
|
||||
}
|
||||
|
||||
return baseBuffer;
|
||||
}
|
||||
|
||||
function buildForwardWithAttachments(
|
||||
from: string,
|
||||
to: string,
|
||||
replyTo: string,
|
||||
subject: string,
|
||||
textContent: string,
|
||||
htmlContent: string | undefined,
|
||||
attachments: ParsedMail['attachments'],
|
||||
domain: string,
|
||||
): Buffer {
|
||||
// Use nodemailer's mail composer to build the MIME message
|
||||
const MailComposer = require('nodemailer/lib/mail-composer');
|
||||
|
||||
const mailOptions: any = {
|
||||
from,
|
||||
to,
|
||||
subject: `FWD: ${subject}`,
|
||||
replyTo,
|
||||
text: textContent,
|
||||
text: fwdText,
|
||||
html: fwdHtml,
|
||||
headers: {
|
||||
'X-SES-Worker-Processed': 'forwarded',
|
||||
},
|
||||
attachments: attachments.map((att) => ({
|
||||
};
|
||||
|
||||
// Attachments
|
||||
if (original.attachments && original.attachments.length > 0) {
|
||||
mailOptions.attachments = original.attachments.map((att) => ({
|
||||
filename: att.filename ?? 'attachment',
|
||||
content: att.content,
|
||||
contentType: att.contentType,
|
||||
cid: att.cid ?? undefined,
|
||||
})),
|
||||
};
|
||||
|
||||
if (htmlContent) {
|
||||
mailOptions.html = htmlContent;
|
||||
contentDisposition: att.contentDisposition || 'attachment'
|
||||
}));
|
||||
}
|
||||
|
||||
const composer = new MailComposer(mailOptions);
|
||||
// build() returns a stream, but we can use buildAsync pattern
|
||||
// For synchronous buffer we use the compile + createReadStream approach
|
||||
const mail = composer.compile();
|
||||
mail.keepBcc = true;
|
||||
const chunks: Buffer[] = [];
|
||||
const stream = mail.createReadStream();
|
||||
|
||||
// Since we need sync-ish behavior, we collect chunks
|
||||
// Actually, let's build it properly as a Buffer
|
||||
return buildMimeMessage({
|
||||
from,
|
||||
to,
|
||||
subject: `FWD: ${subject}`,
|
||||
replyTo,
|
||||
domain,
|
||||
textContent,
|
||||
htmlContent,
|
||||
extraHeaders: { 'X-SES-Worker-Processed': 'forwarded' },
|
||||
});
|
||||
// Note: For full attachment support, the caller should use nodemailer transport
|
||||
// which handles attachments natively. This is a simplified version.
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Low-level MIME builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface MimeOptions {
|
||||
from: string;
|
||||
to: string;
|
||||
subject: string;
|
||||
domain: string;
|
||||
textContent: string;
|
||||
htmlContent?: string;
|
||||
inReplyTo?: string;
|
||||
references?: string;
|
||||
replyTo?: string;
|
||||
extraHeaders?: Record<string, string>;
|
||||
}
|
||||
|
||||
function buildMimeMessage(opts: MimeOptions): Buffer {
|
||||
const boundary = `----=_Part_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
||||
const msgId = `<${Date.now()}.${Math.random().toString(36).slice(2)}@${opts.domain}>`;
|
||||
|
||||
let headers = '';
|
||||
headers += `From: ${opts.from}\r\n`;
|
||||
headers += `To: ${opts.to}\r\n`;
|
||||
headers += `Subject: ${opts.subject}\r\n`;
|
||||
headers += `Date: ${new Date().toUTCString()}\r\n`;
|
||||
headers += `Message-ID: ${msgId}\r\n`;
|
||||
headers += `MIME-Version: 1.0\r\n`;
|
||||
|
||||
if (opts.inReplyTo) headers += `In-Reply-To: ${opts.inReplyTo}\r\n`;
|
||||
if (opts.references) headers += `References: ${opts.references}\r\n`;
|
||||
if (opts.replyTo) headers += `Reply-To: ${opts.replyTo}\r\n`;
|
||||
|
||||
if (opts.extraHeaders) {
|
||||
for (const [k, v] of Object.entries(opts.extraHeaders)) {
|
||||
headers += `${k}: ${v}\r\n`;
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.htmlContent) {
|
||||
// multipart/alternative
|
||||
headers += `Content-Type: multipart/alternative; boundary="${boundary}"\r\n`;
|
||||
let body = `\r\n--${boundary}\r\n`;
|
||||
body += `Content-Type: text/plain; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n`;
|
||||
body += opts.textContent;
|
||||
body += `\r\n--${boundary}\r\n`;
|
||||
body += `Content-Type: text/html; charset=utf-8\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n`;
|
||||
body += opts.htmlContent;
|
||||
body += `\r\n--${boundary}--\r\n`;
|
||||
return Buffer.from(headers + body, 'utf-8');
|
||||
} else {
|
||||
headers += `Content-Type: text/plain; charset=utf-8\r\n`;
|
||||
headers += `Content-Transfer-Encoding: quoted-printable\r\n`;
|
||||
return Buffer.from(headers + '\r\n' + opts.textContent, 'utf-8');
|
||||
}
|
||||
return composer.compile().build();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -410,4 +303,4 @@ async function sendInternalEmail(
|
||||
function extractSenderAddress(fromHeader: string): string {
|
||||
const match = fromHeader.match(/<([^>]+)>/);
|
||||
return match ? match[1] : fromHeader;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user