Initial commit: Mail service with Brevo SMTP
- Express server with REST API for sending emails - SQLite database for persistent email history - Web interface with form (recipient, CC, subject, text/HTML) - Email footer with embedded image (CID attachment) - Nodemailer configured for Brevo SMTP relay Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
66
src/database.js
Normal file
66
src/database.js
Normal file
@@ -0,0 +1,66 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
|
||||
const dbPath = path.join(__dirname, '..', 'data', 'emails.db');
|
||||
const db = new Database(dbPath);
|
||||
|
||||
// Initialize database schema
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS emails (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
to_email TEXT NOT NULL,
|
||||
cc_email TEXT,
|
||||
subject TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
is_html INTEGER DEFAULT 0,
|
||||
status TEXT NOT NULL,
|
||||
error_message TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now'))
|
||||
)
|
||||
`);
|
||||
|
||||
const insertEmail = db.prepare(`
|
||||
INSERT INTO emails (to_email, cc_email, subject, body, is_html, status, error_message)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const getHistory = db.prepare(`
|
||||
SELECT * FROM emails ORDER BY created_at DESC LIMIT 50
|
||||
`);
|
||||
|
||||
const deleteEmail = db.prepare(`
|
||||
DELETE FROM emails WHERE id = ?
|
||||
`);
|
||||
|
||||
const deleteAllEmails = db.prepare(`
|
||||
DELETE FROM emails
|
||||
`);
|
||||
|
||||
module.exports = {
|
||||
saveEmail(email, status, errorMessage = null) {
|
||||
const result = insertEmail.run(
|
||||
email.to,
|
||||
email.cc || null,
|
||||
email.subject,
|
||||
email.body,
|
||||
email.isHtml ? 1 : 0,
|
||||
status,
|
||||
errorMessage
|
||||
);
|
||||
return result.lastInsertRowid;
|
||||
},
|
||||
|
||||
getHistory() {
|
||||
return getHistory.all();
|
||||
},
|
||||
|
||||
deleteEmail(id) {
|
||||
const result = deleteEmail.run(id);
|
||||
return result.changes > 0;
|
||||
},
|
||||
|
||||
clearHistory() {
|
||||
const result = deleteAllEmails.run();
|
||||
return result.changes;
|
||||
}
|
||||
};
|
||||
84
src/mailer.js
Normal file
84
src/mailer.js
Normal file
@@ -0,0 +1,84 @@
|
||||
const nodemailer = require('nodemailer');
|
||||
const path = require('path');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: process.env.SMTP_HOST,
|
||||
port: parseInt(process.env.SMTP_PORT, 10),
|
||||
secure: process.env.SMTP_SECURE === 'true',
|
||||
auth: {
|
||||
user: process.env.SMTP_USER,
|
||||
pass: process.env.SMTP_PASSWORD
|
||||
},
|
||||
connectionTimeout: 30000,
|
||||
greetingTimeout: 30000,
|
||||
socketTimeout: 30000
|
||||
});
|
||||
|
||||
const FOOTER_NAME = process.env.MAIL_FOOTER_NAME || 'Joachim Hummel';
|
||||
const FOOTER_IMAGE = path.join(__dirname, '..', 'assets', 'homeicon.png');
|
||||
|
||||
function getHtmlFooter() {
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
function getTextFooter() {
|
||||
return `\n\n---\nAbsender - ${FOOTER_NAME}`;
|
||||
}
|
||||
|
||||
async function sendMail(email) {
|
||||
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'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (email.cc) {
|
||||
mailOptions.cc = email.cc;
|
||||
}
|
||||
|
||||
if (email.isHtml) {
|
||||
mailOptions.html = email.body + getHtmlFooter();
|
||||
} 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();
|
||||
}
|
||||
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
return info;
|
||||
}
|
||||
|
||||
async function verifyConnection() {
|
||||
try {
|
||||
await transporter.verify();
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMail,
|
||||
verifyConnection
|
||||
};
|
||||
126
src/server.js
Normal file
126
src/server.js
Normal file
@@ -0,0 +1,126 @@
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const path = require('path');
|
||||
const db = require('./database');
|
||||
const mailer = require('./mailer');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, '..', 'public')));
|
||||
|
||||
// Email validation helper
|
||||
function isValidEmail(email) {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(email);
|
||||
}
|
||||
|
||||
// API Routes
|
||||
|
||||
// Send email
|
||||
app.post('/api/send', async (req, res) => {
|
||||
const { to, cc, subject, body, isHtml } = req.body;
|
||||
|
||||
// Validation
|
||||
if (!to || !subject || !body) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Empfänger, Betreff und Nachricht sind Pflichtfelder'
|
||||
});
|
||||
}
|
||||
|
||||
if (!isValidEmail(to)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültige Empfänger-E-Mail-Adresse'
|
||||
});
|
||||
}
|
||||
|
||||
if (cc && !isValidEmail(cc)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Ungültige CC-E-Mail-Adresse'
|
||||
});
|
||||
}
|
||||
|
||||
const email = { to, cc, subject, body, isHtml: !!isHtml };
|
||||
|
||||
try {
|
||||
const info = await mailer.sendMail(email);
|
||||
const id = db.saveEmail(email, 'success');
|
||||
console.log(`Email sent successfully: ${info.messageId}`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'E-Mail erfolgreich gesendet',
|
||||
messageId: info.messageId,
|
||||
id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Email sending failed:', error.message);
|
||||
const id = db.saveEmail(email, 'failed', error.message);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: `Versand fehlgeschlagen: ${error.message}`,
|
||||
id
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get history
|
||||
app.get('/api/history', (req, res) => {
|
||||
try {
|
||||
const history = db.getHistory();
|
||||
res.json({ success: true, history });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Delete single entry
|
||||
app.delete('/api/history/:id', (req, res) => {
|
||||
const id = parseInt(req.params.id, 10);
|
||||
|
||||
if (isNaN(id)) {
|
||||
return res.status(400).json({ success: false, error: 'Ungültige ID' });
|
||||
}
|
||||
|
||||
try {
|
||||
const deleted = db.deleteEmail(id);
|
||||
if (deleted) {
|
||||
res.json({ success: true, message: 'Eintrag gelöscht' });
|
||||
} else {
|
||||
res.status(404).json({ success: false, error: 'Eintrag nicht gefunden' });
|
||||
}
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Clear all history
|
||||
app.delete('/api/history', (req, res) => {
|
||||
try {
|
||||
const count = db.clearHistory();
|
||||
res.json({ success: true, message: `${count} Einträge gelöscht` });
|
||||
} catch (error) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Mail-Service läuft auf http://localhost:${PORT}`);
|
||||
|
||||
// Verify SMTP connection on startup
|
||||
mailer.verifyConnection().then(result => {
|
||||
if (result.success) {
|
||||
console.log('SMTP-Verbindung erfolgreich verifiziert');
|
||||
} else {
|
||||
console.warn('SMTP-Verbindung konnte nicht verifiziert werden:', result.error);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user