our mail system

This commit is contained in:
2026-07-07 13:43:00 -05:00
parent dca60248f4
commit 8d9387f0a0
2 changed files with 248 additions and 32 deletions

View File

@@ -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 <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 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 = `
<mj-section background-color="#ffffff" padding="0 30px">
<mj-column>
<mj-button
background-color="#635bff"
color="white"
border-radius="6px"
href="${stripePaymentUrl}"
font-weight="600"
font-size="16px"
<mj-button
background-color="#635bff"
color="white"
border-radius="6px"
href="${stripePaymentUrl}"
font-weight="600"
font-size="16px"
padding="25px 0 10px 0"
inner-padding="14px 30px"
width="100%">
@@ -58,7 +99,7 @@ function generateInvoiceEmailHtml(invoice, customText, stripePaymentUrl) {
</mj-style>
</mj-head>
<mj-body background-color="#f4f4f5">
<mj-section padding="0">
<mj-column>
<mj-spacer height="20px" />
@@ -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" <accounting@bayarea-cc.com>',
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 };
module.exports = { sendInvoiceEmail, generateInvoiceEmailHtml, verifyConnection, transporter };