our mail system
This commit is contained in:
169
send-test-invoice.js
Normal file
169
send-test-invoice.js
Normal file
@@ -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 <addr[,addr]> Empfänger (Pflicht, außer bei --verify)
|
||||||
|
--number <str> Invoice-Nummer (default: TEST-1001)
|
||||||
|
--stripe <url> Stripe-Zahlungslink (optional)
|
||||||
|
--text <str> Eigener Nachrichtentext (optional)
|
||||||
|
--pdf <pfad> 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 <adresse> 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)
|
||||||
|
: `<p>Hallo,</p><p>dies ist eine <strong>Test-Rechnung</strong> zur Prüfung der Zustellbarkeit über den Docker-Mailserver-Relay.</p><p>Wenn diese Mail ankommt, funktioniert der neue Sendeweg.</p><p>Viele Grüße<br/>Bay Area Affiliates, Inc.</p>`;
|
||||||
|
|
||||||
|
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();
|
||||||
@@ -1,18 +1,59 @@
|
|||||||
// src/services/email-service.js
|
// 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 <mailbox-passwort>
|
||||||
|
// 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 nodemailer = require('nodemailer');
|
||||||
const mjml2html = require('mjml');
|
const mjml2html = require('mjml');
|
||||||
|
|
||||||
const sesClient = new SESv2Client({
|
const SMTP_HOST = process.env.SMTP_HOST || 'smtp.bayarea-cc.com';
|
||||||
region: process.env.AWS_REGION || 'us-east-2'
|
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: {
|
* Prüft die SMTP-Verbindung (Login + TLS). Praktisch fürs CLI-Testen.
|
||||||
sesClient,
|
*/
|
||||||
SendEmailCommand
|
async function verifyConnection() {
|
||||||
}
|
return transporter.verify();
|
||||||
});
|
}
|
||||||
|
|
||||||
function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) {
|
function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) {
|
||||||
const formattedText = customText || '';
|
const formattedText = customText || '';
|
||||||
@@ -114,28 +155,34 @@ function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) {
|
|||||||
async function sendInvoiceEmail(invoice, recipients, customText, stripePaymentUrl, pdfBuffer) {
|
async function sendInvoiceEmail(invoice, recipients, customText, stripePaymentUrl, pdfBuffer) {
|
||||||
const htmlContent = generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl);
|
const htmlContent = generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl);
|
||||||
|
|
||||||
// Akzeptiert String oder Array. nodemailer akzeptiert beides direkt im "to"-Feld,
|
// Akzeptiert String oder Array
|
||||||
// aber wir normalisieren für Konsistenz und einfacheres Logging.
|
|
||||||
const toList = Array.isArray(recipients)
|
const toList = Array.isArray(recipients)
|
||||||
? recipients
|
? recipients
|
||||||
: [recipients].filter(Boolean);
|
: [recipients].filter(Boolean);
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: '"Bay Area Affiliates Inc. Accounting" <accounting@bayarea-cc.com>',
|
from: `"${MAIL_FROM_NAME}" <${MAIL_FROM}>`,
|
||||||
to: toList.join(', '),
|
to: toList.join(', '),
|
||||||
bcc: 'accounting@bayarea-cc.com',
|
|
||||||
subject: `Invoice #${invoice.invoice_number || invoice.id} from Bay Area Affiliates, Inc.`,
|
subject: `Invoice #${invoice.invoice_number || invoice.id} from Bay Area Affiliates, Inc.`,
|
||||||
html: htmlContent,
|
html: htmlContent,
|
||||||
attachments: [
|
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`,
|
filename: `Invoice_${invoice.invoice_number || invoice.id}_BayAreaAffiliates.pdf`,
|
||||||
content: pdfBuffer,
|
content: pdfBuffer,
|
||||||
contentType: 'application/pdf'
|
contentType: 'application/pdf',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
return await transporter.sendMail(mailOptions);
|
return await transporter.sendMail(mailOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { sendInvoiceEmail };
|
module.exports = { sendInvoiceEmail, generateInvoiceEmailHtml, verifyConnection, transporter };
|
||||||
Reference in New Issue
Block a user