From 148cf428212db4ea1ce679f1f1aec30c80c6ceac Mon Sep 17 00:00:00 2001 From: Adam Wright Date: Tue, 20 Oct 2026 23:00:38 +0000 Subject: [PATCH] Contact form: a sendmail that exits before reading it all has not sent it sendmail resolved on the child's exit code alone. A sendmail that exits 0 before reading the whole message makes two events race: the EPIPE on its stdin (rejected) and the child's close (resolved). Whichever came first decided, so a message cut short could be reported sent, and the spec "fails, rather than taking the server down, when the MTA does not read it all" failed on CI twice on 20 Oct (passing locally). Now an exit of 0 is success only when stdin was also written in full; both orders reject. Co-Authored-By: Claude Opus 5.5 --- projects/website-angular/src/scripts/contact-route.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/projects/website-angular/src/scripts/contact-route.js b/projects/website-angular/src/scripts/contact-route.js index fd2612e5..4e33f49b 100644 --- a/projects/website-angular/src/scripts/contact-route.js +++ b/projects/website-angular/src/scripts/contact-route.js @@ -164,8 +164,13 @@ function sendmail(raw, from, sendmailPath = '/usr/sbin/sendmail', timeoutMs = 30 child.on('error', fail); child.on('close', (code) => { clearTimeout(timer); - if (code === 0) resolve(); - else reject(new Error(`sendmail exited ${code}: ${stderr.trim()}`)); + // Sent only when it was also all read: a sendmail that exits 0 before + // reading the whole message has not sent it, and whether its EPIPE or + // its exit is heard first is a race. + if (code !== 0) reject(new Error(`sendmail exited ${code}: ${stderr.trim()}`)); + else if (!child.stdin.writableFinished) + reject(new Error('sendmail exited before reading the whole message')); + else resolve(); }); // Unheard, a sendmail that exits before reading all of this (EPIPE) would // be an uncaught error, and take the whole server down with it.