refactor: contact form delegates to mail-api

Replace nodemailer SMTP with HTTP fetch to mail-api
container (http://mail-api:3200/api/mail/send) which
already holds the Brevo SMTP creds. Single source of
truth for mail credentials, no duplication across
electron-rare-site and mail-api.

Drop nodemailer + @types/nodemailer from deps. Restore
npm ci in Dockerfile since the lock no longer drifts.

Override defaults via env: MAIL_API_URL, MAIL_FROM,
MAIL_TO. mail-api intra-Docker network reachable
because both services are on the traefik network.
This commit is contained in:
L'électron rare
2026-04-28 10:02:07 +02:00
parent 225bd1d457
commit 7787bed2e9
3 changed files with 31 additions and 40 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
COPY apps/lab-interactif/package.json apps/lab-interactif/package-lock.json ./apps/lab-interactif/
RUN npm install --no-audit --no-fund && npm install --no-audit --no-fund --prefix apps/lab-interactif
RUN npm ci && npm ci --prefix apps/lab-interactif
FROM base AS build
ARG PUBLIC_SITE_URL=https://www.lelectronrare.fr
-2
View File
@@ -33,7 +33,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"motion": "^12.23.24",
"nodemailer": "^6.9.16",
"postprocessing": "^6.39.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
@@ -42,7 +41,6 @@
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"@types/nodemailer": "^6.4.16",
"@storybook/addon-a11y": "^9.1.5",
"@storybook/addon-docs": "^9.1.5",
"@storybook/addon-onboarding": "^9.1.5",
+30 -37
View File
@@ -1,5 +1,4 @@
import type { APIRoute } from 'astro';
import nodemailer from 'nodemailer';
interface ContactSubmission {
name: string;
@@ -68,21 +67,10 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
});
}
const SMTP_HOST = process.env.SMTP_HOST;
const SMTP_PORT = Number(process.env.SMTP_PORT || '587');
const SMTP_USER = process.env.SMTP_USER;
const SMTP_PASS = process.env.SMTP_PASS;
const MAIL_FROM = process.env.MAIL_FROM || SMTP_USER || '';
const MAIL_API_URL = process.env.MAIL_API_URL || 'http://mail-api:3200/api/mail/send';
const MAIL_FROM = process.env.MAIL_FROM || 'contact@lelectronrare.fr';
const MAIL_TO = process.env.MAIL_TO || 'contact@lelectronrare.fr';
if (!SMTP_HOST || !SMTP_USER || !SMTP_PASS) {
console.error('[submit-lead] Missing SMTP env vars (SMTP_HOST/SMTP_USER/SMTP_PASS)');
return new Response(JSON.stringify({ error: 'server misconfigured' }), {
status: 500,
headers: corsHeaders(origin),
});
}
let body: ContactSubmission;
try {
body = await request.json();
@@ -124,13 +112,13 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
const text = `Nouveau message depuis lelectronrare.fr
Nom : ${name}
Email : ${email}
Téléphone : ${phone || '-'}
Nom : ${name}
Email : ${email}
Téléphone : ${phone || '-'}
Organisation: ${organization || '-'}
Type : ${needType || '-'}
Page : ${pagePath || '-'}
IP : ${ip}
Type : ${needType || '-'}
Page : ${pagePath || '-'}
IP : ${ip}
Message :
${message || '(vide)'}
@@ -148,34 +136,39 @@ ${message || '(vide)'}
</table>
<h3 style="margin-top:20px;font-size:14px;color:#666;">Message</h3>
<pre style="background:#f5f5f5;padding:12px;border-radius:6px;white-space:pre-wrap;font-family:inherit;font-size:13px;line-height:1.5;">${escapeHtml(message || '(vide)')}</pre>
<p style="margin-top:20px;font-size:11px;color:#aaa;">Pour répondre, utilise simplement Reply — l'adresse du visiteur est en Reply-To.</p>
<p style="margin-top:20px;font-size:11px;color:#aaa;">Reply-To pointe vers l'email du visiteur — répondre directement.</p>
</body></html>`;
try {
const transporter = nodemailer.createTransport({
host: SMTP_HOST,
port: SMTP_PORT,
secure: SMTP_PORT === 465,
auth: { user: SMTP_USER, pass: SMTP_PASS },
const resp = await fetch(MAIL_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
from: MAIL_FROM,
to: MAIL_TO,
subject,
text,
html,
replyTo: `${name.replace(/[<>"]/g, '')} <${email}>`,
}),
});
await transporter.sendMail({
from: MAIL_FROM,
to: MAIL_TO,
replyTo: `${name.replace(/[<>"]/g, '')} <${email}>`,
subject,
text,
html,
});
if (!resp.ok) {
console.error('[submit-lead] mail-api returned', resp.status);
return new Response(JSON.stringify({ error: 'send failed' }), {
status: 502,
headers: corsHeaders(origin),
});
}
console.info('[submit-lead] Mail sent', { needType, hasOrg: !!organization });
console.info('[submit-lead] Mail relayed via mail-api', { needType, hasOrg: !!organization });
return new Response(JSON.stringify({ ok: true }), {
status: 201,
headers: corsHeaders(origin),
});
} catch (err) {
console.error('[submit-lead] SMTP error', err instanceof Error ? err.message : 'unknown');
return new Response(JSON.stringify({ error: 'send failed' }), {
console.error('[submit-lead] mail-api unreachable', err instanceof Error ? err.message : 'unknown');
return new Response(JSON.stringify({ error: 'mail service unreachable' }), {
status: 502,
headers: corsHeaders(origin),
});