Add footer asset selector for email customization
- New /api/assets endpoint to list available icons from assets folder - Dynamic footer generation with multiple selectable icons - Thumbnail grid UI for asset selection in the mail form - Selected icons are embedded as CID attachments in emails Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,23 +15,25 @@ const transporter = nodemailer.createTransport({
|
||||
});
|
||||
|
||||
const FOOTER_NAME = process.env.MAIL_FOOTER_NAME || 'Joachim Hummel';
|
||||
const FOOTER_IMAGE = path.join(__dirname, '..', 'assets', 'homeicon.png');
|
||||
|
||||
function getHtmlFooter() {
|
||||
function getHtmlFooter(assets) {
|
||||
if (!assets || assets.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const icons = assets.map(filename => {
|
||||
const cid = filename.replace(/\.[^.]+$/, '');
|
||||
return `<img src="cid:${cid}" alt="${cid}" width="40" height="40" style="display:inline-block; margin-right:8px;"/>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<br/><br/>
|
||||
<hr style="border: none; border-top: 1px solid #ddd; margin: 20px 0;"/>
|
||||
<table cellpadding="0" cellspacing="0" style="font-family: Arial, sans-serif; font-size: 14px; color: #666;">
|
||||
<tr>
|
||||
<td style="vertical-align: middle; padding-right: 10px;">
|
||||
<img src="cid:homeicon" alt="Home" width="40" height="40" style="display: block;"/>
|
||||
</td>
|
||||
<td style="vertical-align: middle;">
|
||||
<strong>Absender</strong><br/>
|
||||
${FOOTER_NAME}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="font-family: Arial, sans-serif; font-size: 14px; color: #666;">
|
||||
<div style="margin-bottom: 8px;">${icons}</div>
|
||||
<strong>Absender</strong><br/>
|
||||
${FOOTER_NAME}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -40,17 +42,19 @@ function getTextFooter() {
|
||||
}
|
||||
|
||||
async function sendMail(email) {
|
||||
const footerAssets = email.footerAssets || [];
|
||||
|
||||
const attachments = footerAssets.map(filename => ({
|
||||
filename,
|
||||
path: path.join(__dirname, '..', 'assets', filename),
|
||||
cid: filename.replace(/\.[^.]+$/, '')
|
||||
}));
|
||||
|
||||
const mailOptions = {
|
||||
from: `"${process.env.MAIL_FROM_NAME}" <${process.env.MAIL_FROM_EMAIL}>`,
|
||||
to: email.to,
|
||||
subject: email.subject,
|
||||
attachments: [
|
||||
{
|
||||
filename: 'homeicon.png',
|
||||
path: FOOTER_IMAGE,
|
||||
cid: 'homeicon'
|
||||
}
|
||||
]
|
||||
attachments
|
||||
};
|
||||
|
||||
if (email.cc) {
|
||||
@@ -58,11 +62,10 @@ async function sendMail(email) {
|
||||
}
|
||||
|
||||
if (email.isHtml) {
|
||||
mailOptions.html = email.body + getHtmlFooter();
|
||||
mailOptions.html = email.body + getHtmlFooter(footerAssets);
|
||||
} else {
|
||||
mailOptions.text = email.body + getTextFooter();
|
||||
// Also send HTML version with footer for better display
|
||||
mailOptions.html = `<pre style="font-family: inherit; white-space: pre-wrap;">${email.body}</pre>` + getHtmlFooter();
|
||||
mailOptions.html = `<pre style="font-family: inherit; white-space: pre-wrap;">${email.body}</pre>` + getHtmlFooter(footerAssets);
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const db = require('./database');
|
||||
const mailer = require('./mailer');
|
||||
@@ -11,6 +12,7 @@ const PORT = process.env.PORT || 3000;
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
app.use('/assets', express.static(path.join(__dirname, '..', 'assets')));
|
||||
|
||||
// Email validation helper
|
||||
function isValidEmail(email) {
|
||||
@@ -20,9 +22,29 @@ function isValidEmail(email) {
|
||||
|
||||
// API Routes
|
||||
|
||||
// Get available assets
|
||||
app.get('/api/assets', (req, res) => {
|
||||
const assetsDir = path.join(__dirname, '..', 'assets');
|
||||
const allowedExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.svg'];
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(assetsDir);
|
||||
const assets = files
|
||||
.filter(file => {
|
||||
const ext = path.extname(file).toLowerCase();
|
||||
return allowedExtensions.includes(ext);
|
||||
})
|
||||
.sort();
|
||||
|
||||
res.json({ success: true, assets });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Send email
|
||||
app.post('/api/send', async (req, res) => {
|
||||
const { to, cc, subject, body, isHtml } = req.body;
|
||||
const { to, cc, subject, body, isHtml, footerAssets } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!to || !subject || !body) {
|
||||
@@ -46,7 +68,7 @@ app.post('/api/send', async (req, res) => {
|
||||
});
|
||||
}
|
||||
|
||||
const email = { to, cc, subject, body, isHtml: !!isHtml };
|
||||
const email = { to, cc, subject, body, isHtml: !!isHtml, footerAssets: footerAssets || [] };
|
||||
|
||||
try {
|
||||
const info = await mailer.sendMail(email);
|
||||
|
||||
Reference in New Issue
Block a user