humanai-web/mailer/mailer.mjs
oskar 39d0662197 Add contact form mailer microservice and i18n support
- mailer/: standalone Express+nodemailer service on port 3000 with
  validation, honeypot (company field), per-IP rate limiting (5/min),
  Fastmail SMTP. Returns 400 on bad input, 429 on rate limit, 502 on
  SMTP failure, 200 {ok:true} on success or honeypot hit.
- Dockerfile: multi-stage nginx build for humanai-landing container.
- mailer/Dockerfile: node:22-alpine, no exposed host ports.
- deploy.sh: idempotent deploy of both containers to piha.local via
  Docker context 'piha', with smoke tests (400 on empty body, 200 on
  honeypot fill).
- src/i18n/: minimal pl/en translation files; useT() hook picks
  language from navigator.language.
- CTASection: replaced dead onSubmit with fetch POST /api/contact,
  added hidden honeypot field, success/error states using i18n strings.
- README: Mailer section with two manual steps (credentials + NPM proxy).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 15:12:05 +02:00

83 lines
2.2 KiB
JavaScript

import express from 'express';
import nodemailer from 'nodemailer';
const app = express();
app.use(express.json());
// Rate limiter: max 5 requests/min per IP
const rateLimiter = new Map();
function checkRateLimit(ip) {
const now = Date.now();
const window = 60_000;
const max = 5;
const entry = rateLimiter.get(ip) ?? { count: 0, start: now };
if (now - entry.start > window) {
entry.count = 0;
entry.start = now;
}
entry.count++;
rateLimiter.set(ip, entry);
return entry.count <= max;
}
// Prune rate limiter every 5 minutes
setInterval(() => {
const cutoff = Date.now() - 60_000;
for (const [ip, entry] of rateLimiter) {
if (entry.start < cutoff) rateLimiter.delete(ip);
}
}, 300_000);
const transporter = nodemailer.createTransport({
host: 'smtp.fastmail.com',
port: 465,
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
app.post('/api/contact', async (req, res) => {
const ip =
(req.headers['x-forwarded-for'] ?? '').split(',')[0].trim() ||
req.socket.remoteAddress;
if (!checkRateLimit(ip)) {
return res.status(429).json({ error: 'Too many requests' });
}
const { name, email, message, company } = req.body ?? {};
if (!name || typeof name !== 'string' || !name.trim()) {
return res.status(400).json({ error: 'name required' });
}
if (!email || typeof email !== 'string' || !email.includes('@')) {
return res.status(400).json({ error: 'valid email required' });
}
if (!message || typeof message !== 'string' || message.trim().length < 5) {
return res.status(400).json({ error: 'message too short' });
}
// Honeypot
if (company) {
return res.json({ ok: true });
}
try {
await transporter.sendMail({
from: 'oskar@gethumanai.com',
to: 'oskar@gethumanai.com',
replyTo: email,
subject: `Kontakt od ${name.trim()}`,
text: `Imię: ${name.trim()}\nEmail: ${email}\n\n${message.trim()}`,
});
res.json({ ok: true });
} catch (err) {
console.error('SMTP error:', err);
res.status(502).json({ error: 'mail delivery failed' });
}
});
app.listen(3000, () => console.log('mailer listening on :3000'));