188 lines
7.1 KiB
JavaScript
188 lines
7.1 KiB
JavaScript
// src/services/email-service.js
|
|
//
|
|
// 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 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',
|
|
},
|
|
});
|
|
|
|
/**
|
|
* 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 || '';
|
|
|
|
// Stripe Pay Button — only if payment link exists
|
|
let paymentButtonMjml = '';
|
|
if (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"
|
|
padding="25px 0 10px 0"
|
|
inner-padding="14px 30px"
|
|
width="100%">
|
|
Pay Online — Credit Card or ACH
|
|
</mj-button>
|
|
<mj-text font-size="12px" color="#94a3b8" align="center" padding="0 0 20px 0">
|
|
ACH payments incur lower processing fees. Secure payment powered by Stripe.
|
|
</mj-text>
|
|
</mj-column>
|
|
</mj-section>`;
|
|
}
|
|
|
|
const template = `
|
|
<mjml>
|
|
<mj-head>
|
|
<mj-attributes>
|
|
<mj-all font-family="ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif" />
|
|
</mj-attributes>
|
|
<mj-style inline="inline">
|
|
.email-body p {
|
|
margin: 0 0 14px 0 !important;
|
|
}
|
|
.email-body p:last-child {
|
|
margin-bottom: 0 !important;
|
|
}
|
|
</mj-style>
|
|
</mj-head>
|
|
<mj-body background-color="#f4f4f5">
|
|
|
|
<mj-section padding="0">
|
|
<mj-column>
|
|
<mj-spacer height="20px" />
|
|
</mj-column>
|
|
</mj-section>
|
|
|
|
<mj-section background-color="#ffffff" padding="30px" border-radius="8px 8px 0 0">
|
|
<mj-column>
|
|
<mj-text font-size="22px" font-weight="700" color="#1e3a8a" padding="0">
|
|
Bay Area Affiliates, Inc.
|
|
</mj-text>
|
|
<mj-text font-size="15px" color="#64748b" padding="5px 0 0 0">
|
|
Invoice #${invoice.invoice_number || invoice.id}
|
|
</mj-text>
|
|
</mj-column>
|
|
</mj-section>
|
|
|
|
<mj-section background-color="#ffffff" padding="0 30px 30px 30px">
|
|
<mj-column>
|
|
<mj-text css-class="email-body" font-size="15px" color="#334155" line-height="1.5" padding="0">
|
|
${formattedText}
|
|
</mj-text>
|
|
</mj-column>
|
|
</mj-section>
|
|
|
|
${paymentButtonMjml}
|
|
|
|
<mj-section background-color="#ffffff" padding="0 30px 30px 30px" border-radius="0 0 8px 8px">
|
|
<mj-column>
|
|
<mj-divider border-color="#e2e8f0" border-width="1px" padding-top="10px" padding-bottom="20px" />
|
|
|
|
<mj-text font-size="14px" color="#64748b" line-height="1.5" padding="0">
|
|
<strong>Prefer to pay by check?</strong><br/>
|
|
Please make checks payable to Bay Area Affiliates, Inc. and mail to:<br/>
|
|
1001 Blucher Street<br/>
|
|
Corpus Christi, Texas 78401
|
|
</mj-text>
|
|
</mj-column>
|
|
</mj-section>
|
|
</mj-body>
|
|
</mjml>
|
|
`;
|
|
|
|
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
|
|
const toList = Array.isArray(recipients)
|
|
? recipients
|
|
: [recipients].filter(Boolean);
|
|
|
|
const mailOptions = {
|
|
from: `"${MAIL_FROM_NAME}" <${MAIL_FROM}>`,
|
|
to: toList.join(', '),
|
|
subject: `Invoice #${invoice.invoice_number || invoice.id} from Bay Area Affiliates, Inc.`,
|
|
html: htmlContent,
|
|
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, generateInvoiceEmailHtml, verifyConnection, transporter }; |