diff --git a/send-test-invoice.js b/send-test-invoice.js new file mode 100644 index 0000000..65e5cd7 --- /dev/null +++ b/send-test-invoice.js @@ -0,0 +1,169 @@ +#!/usr/bin/env node +// send-test-invoice.js +// +// Eigenständiges CLI-Tool zum Testen von email-service.js OHNE die Invoicing-App. +// Nutzt denselben SMTP-Relay-Weg (Docker-Mailserver -> SES) wie der echte Versand, +// damit du die M365-Zustellbarkeit isoliert prüfen kannst. +// +// ----------------------------------------------------------------------------- +// SETUP +// 1) Lege email-service.js im selben Ordner ab (oder passe den require-Pfad an). +// 2) Setze die SMTP-Env-Variablen (siehe email-service.js), z.B. per .env: +// export SMTP_HOST=mail.bayarea-cc.com +// export SMTP_PORT=587 +// export SMTP_USER=accounting@bayarea-cc.com +// export SMTP_PASS='...' +// 3) npm i nodemailer mjml (falls noch nicht vorhanden) +// +// ----------------------------------------------------------------------------- +// VERWENDUNG +// +// # Nur die SMTP-Verbindung prüfen (Login + TLS), ohne zu senden: +// node send-test-invoice.js --verify +// +// # Test-Rechnung an eine Adresse senden: +// node send-test-invoice.js --to lcart@meyernow.com +// +// # Mit Stripe-Link und echtem PDF-Anhang: +// node send-test-invoice.js --to lcart@meyernow.com \ +// --stripe https://buy.stripe.com/test_xxx \ +// --pdf ./sample-invoice.pdf \ +// --number TEST-1001 +// +// # Mehrere Empfänger (kommagetrennt): +// node send-test-invoice.js --to a@x.com,b@y.com +// +// # Ohne Stripe-Button, eigener Text: +// node send-test-invoice.js --to lcart@meyernow.com \ +// --text "Hallo, anbei die Test-Rechnung. Viele Grüße" +// +// ----------------------------------------------------------------------------- + +const fs = require('fs'); +const path = require('path'); + +// Pfad ggf. anpassen, falls email-service.js woanders liegt +const { sendInvoiceEmail, verifyConnection } = require('./src/services/email-service'); + +// ------------------------------------------------------------ +// Minimaler Arg-Parser (--key value und --flag) +// ------------------------------------------------------------ +function parseArgs(argv) { + const args = {}; + for (let i = 0; i < argv.length; i++) { + const a = argv[i]; + if (a.startsWith('--')) { + const key = a.slice(2); + const next = argv[i + 1]; + if (next === undefined || next.startsWith('--')) { + args[key] = true; // boolean flag + } else { + args[key] = next; + i++; + } + } + } + return args; +} + +function printHelp() { + console.log(` +send-test-invoice.js — Test-Tool für email-service.js + +Optionen: + --to Empfänger (Pflicht, außer bei --verify) + --number Invoice-Nummer (default: TEST-1001) + --stripe Stripe-Zahlungslink (optional) + --text Eigener Nachrichtentext (optional) + --pdf PDF-Anhang von Datei (optional) + --verify Nur SMTP-Verbindung prüfen, nicht senden + --help Diese Hilfe + +Beispiel: + node send-test-invoice.js --to lcart@meyernow.com --number TEST-1001 \\ + --stripe https://buy.stripe.com/test_xxx --pdf ./sample.pdf +`); +} + +async function main() { + const args = parseArgs(process.argv.slice(2)); + + if (args.help) { + printHelp(); + return; + } + + // --- Nur Verbindung prüfen --- + if (args.verify) { + process.stdout.write(`Prüfe SMTP-Verbindung zu ${process.env.SMTP_HOST || 'mail.bayarea-cc.com'} ... `); + try { + await verifyConnection(); + console.log('OK — Login und TLS erfolgreich.'); + } catch (err) { + console.log('FEHLGESCHLAGEN.'); + console.error(err && err.message ? err.message : err); + process.exitCode = 1; + } + return; + } + + // --- Empfänger prüfen --- + if (!args.to || args.to === true) { + console.error('Fehler: --to ist erforderlich (oder --verify verwenden).'); + printHelp(); + process.exitCode = 1; + return; + } + + const recipients = String(args.to).split(',').map(s => s.trim()).filter(Boolean); + + // --- PDF-Anhang optional laden --- + let pdfBuffer = null; + if (args.pdf && args.pdf !== true) { + const pdfPath = path.resolve(String(args.pdf)); + if (!fs.existsSync(pdfPath)) { + console.error(`Fehler: PDF nicht gefunden: ${pdfPath}`); + process.exitCode = 1; + return; + } + pdfBuffer = fs.readFileSync(pdfPath); + console.log(`PDF-Anhang geladen: ${pdfPath} (${pdfBuffer.length} Bytes)`); + } + + // --- Test-Invoice-Objekt --- + const invoice = { + id: 'test', + invoice_number: args.number && args.number !== true ? String(args.number) : 'TEST-1001', + }; + + const customText = (args.text && args.text !== true) + ? String(args.text) + : `

Hallo,

dies ist eine Test-Rechnung zur Prüfung der Zustellbarkeit über den Docker-Mailserver-Relay.

Wenn diese Mail ankommt, funktioniert der neue Sendeweg.

Viele Grüße
Bay Area Affiliates, Inc.

`; + + const stripeUrl = (args.stripe && args.stripe !== true) ? String(args.stripe) : null; + + console.log('---'); + console.log('An: ', recipients.join(', ')); + console.log('Invoice-Nummer:', invoice.invoice_number); + console.log('Stripe-Link: ', stripeUrl || '(keiner)'); + console.log('PDF-Anhang: ', pdfBuffer ? 'ja' : 'nein'); + console.log('---'); + + try { + const info = await sendInvoiceEmail(invoice, recipients, customText, stripeUrl, pdfBuffer); + console.log('Gesendet.'); + console.log(' messageId:', info.messageId); + if (info.accepted) console.log(' accepted: ', info.accepted.join(', ')); + if (info.rejected && info.rejected.length) console.log(' rejected: ', info.rejected.join(', ')); + if (info.response) console.log(' response: ', info.response); + console.log('\nHinweis: "accepted" bedeutet nur, dass der Docker-Mailserver die Mail'); + console.log('angenommen hat. Ob M365 sie zustellt oder quarantänisiert, zeigt erst'); + console.log('der Posteingang/Quarantäne des Empfängers bzw. das SES-Delivery-Event.'); + } catch (err) { + console.error('Senden fehlgeschlagen:'); + console.error(err && err.message ? err.message : err); + process.exitCode = 1; + } +} + +main(); \ No newline at end of file diff --git a/src/services/email-service.js b/src/services/email-service.js index c52292d..e36f1fc 100644 --- a/src/services/email-service.js +++ b/src/services/email-service.js @@ -1,21 +1,62 @@ // src/services/email-service.js -const { SESv2Client, SendEmailCommand } = require('@aws-sdk/client-sesv2'); +// +// Versendet Invoice-Mails über den Docker-Mailserver (SMTP-Relay) statt +// direkt über das SESv2-SDK. Dadurch nehmen Invoice-Mails EXAKT denselben +// Weg wie die funktionierenden Outlook-Mails: gleiche Sende-IP, gleiche +// Header-Profile, gleiche Message-ID-Erzeugung durch den MTA. +// +// Der Docker-Mailserver relayed anschließend selbst über Amazon SES. +// +// Benötigte Env-Variablen: +// SMTP_HOST z.B. mail.bayarea-cc.com (dein Docker-Mailserver) +// SMTP_PORT 587 (STARTTLS) oder 465 (implizit TLS) +// SMTP_USER accounting@bayarea-cc.com +// SMTP_PASS +// SMTP_SECURE "true" für Port 465, sonst "false" (STARTTLS auf 587) +// MAIL_FROM optional, default: accounting@bayarea-cc.com +// MAIL_FROM_NAME optional, default: Bay Area Affiliates Inc. Accounting +// MAIL_BCC optional; leer lassen, um kein BCC zu setzen + const nodemailer = require('nodemailer'); const mjml2html = require('mjml'); -const sesClient = new SESv2Client({ - region: process.env.AWS_REGION || 'us-east-2' +const SMTP_HOST = process.env.SMTP_HOST || 'smtp.bayarea-cc.com'; +const SMTP_PORT = parseInt(process.env.SMTP_PORT || '465', 10); +const SMTP_SECURE = String(process.env.SMTP_SECURE || 'true').toLowerCase() === 'true'; +const SMTP_USER = process.env.SMTP_USER || 'accounting@bayarea-cc.com'; +const SMTP_PASS = process.env.SMTP_PASS; + +const MAIL_FROM = process.env.MAIL_FROM || 'accounting@bayarea-cc.com'; +const MAIL_FROM_NAME = process.env.MAIL_FROM_NAME || 'Bay Area Affiliates Inc. Accounting'; +// BCC ist standardmäßig LEER — die auffällige Selbst-BCC war ein Filter-Trigger +// bei M365. Wer eine Kopie will, setzt MAIL_BCC explizit. +const MAIL_BCC = process.env.MAIL_BCC || ''; + +// ------------------------------------------------------------ +// SMTP-Transport über den Docker-Mailserver +// ------------------------------------------------------------ +const transporter = nodemailer.createTransport({ + host: SMTP_HOST, + port: SMTP_PORT, + secure: SMTP_SECURE, // true = 465, false = 587 (STARTTLS) + auth: SMTP_USER && SMTP_PASS ? { user: SMTP_USER, pass: SMTP_PASS } : undefined, + requireTLS: !SMTP_SECURE, // erzwingt STARTTLS auf 587 + tls: { + // Docker-Mailserver mit eigenem/Caddy-Wildcard-Cert: normal validieren. + // Nur auf true setzen, falls du mit self-signed testest. + rejectUnauthorized: String(process.env.SMTP_TLS_INSECURE || 'false').toLowerCase() !== 'true', + }, }); -const transporter = nodemailer.createTransport({ - SES: { - sesClient, - SendEmailCommand - } -}); +/** + * Prüft die SMTP-Verbindung (Login + TLS). Praktisch fürs CLI-Testen. + */ +async function verifyConnection() { + return transporter.verify(); +} function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) { - const formattedText = customText || ''; + const formattedText = customText || ''; // Stripe Pay Button — only if payment link exists let paymentButtonMjml = ''; @@ -23,13 +64,13 @@ function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) { paymentButtonMjml = ` - @@ -58,7 +99,7 @@ function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) { - + @@ -103,39 +144,45 @@ function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) { `; const result = mjml2html(template, { validationLevel: 'strict' }); - + if (result.errors && result.errors.length > 0) { console.error('MJML Parse Errors:', result.errors); } - + return result.html; } async function sendInvoiceEmail(invoice, recipients, customText, stripePaymentUrl, pdfBuffer) { const htmlContent = generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl); - // Akzeptiert String oder Array. nodemailer akzeptiert beides direkt im "to"-Feld, - // aber wir normalisieren für Konsistenz und einfacheres Logging. + // Akzeptiert String oder Array const toList = Array.isArray(recipients) ? recipients : [recipients].filter(Boolean); const mailOptions = { - from: '"Bay Area Affiliates Inc. Accounting" ', + from: `"${MAIL_FROM_NAME}" <${MAIL_FROM}>`, to: toList.join(', '), - bcc: 'accounting@bayarea-cc.com', subject: `Invoice #${invoice.invoice_number || invoice.id} from Bay Area Affiliates, Inc.`, html: htmlContent, - attachments: [ - { - filename: `Invoice_${invoice.invoice_number || invoice.id}_BayAreaAffiliates.pdf`, - content: pdfBuffer, - contentType: 'application/pdf' - } - ] + attachments: [], }; + // BCC nur setzen, wenn explizit gewünscht + if (MAIL_BCC) { + mailOptions.bcc = MAIL_BCC; + } + + // PDF-Anhang nur, wenn vorhanden (fürs CLI-Testen optional) + if (pdfBuffer) { + mailOptions.attachments.push({ + filename: `Invoice_${invoice.invoice_number || invoice.id}_BayAreaAffiliates.pdf`, + content: pdfBuffer, + contentType: 'application/pdf', + }); + } + return await transporter.sendMail(mailOptions); } -module.exports = { sendInvoiceEmail }; \ No newline at end of file +module.exports = { sendInvoiceEmail, generateInvoiceEmailHtml, verifyConnection, transporter }; \ No newline at end of file