CluPilotCloud/tests/Support/fake_smtp_server_process.php

100 lines
3.0 KiB
PHP

<?php
/**
* A one-shot ESMTP server, run as its own OS process (see FakeSmtpServer for
* why a subprocess and not an in-process fake is necessary here).
*
* Configuration arrives as one JSON object on STDIN: {"failAt": string|null,
* "failResponse": string}. failAt names the command after which the server
* answers with failResponse instead of success — 'mail', 'rcpt', or 'data'.
* null completes a normal, successful send. ('ehlo' is deliberately not a
* supported stage: EsmtpTransport retries a failed EHLO as plain HELO before
* giving up, so a clean single-rejection test needs a later stage.)
*
* Binds an ephemeral port on 127.0.0.1, prints it as the first line of
* STDOUT so the parent process can connect, accepts exactly one connection,
* and plays the minimal EHLO/MAIL FROM/RCPT TO/DATA dialogue. Neither
* STARTTLS nor AUTH is advertised in the EHLO response, so
* EsmtpTransport — auto_tls defaults true, but only upgrades when the
* server actually offers STARTTLS — never attempts either, and the whole
* exchange stays in plain text.
*/
$config = json_decode((string) stream_get_contents(STDIN), true) ?: [];
$failAt = $config['failAt'] ?? null;
$failResponse = $config['failResponse'] ?? '550 Rejected';
$server = stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr);
if ($server === false) {
fwrite(STDERR, "bind failed: {$errstr}\n");
exit(1);
}
$name = stream_socket_get_name($server, false);
$port = (int) substr($name, strrpos($name, ':') + 1);
echo $port, "\n";
flush();
$conn = @stream_socket_accept($server, 10);
if ($conn === false) {
fwrite(STDERR, "accept timed out\n");
exit(1);
}
// Not readLine()/respond(): PHP function names are case-insensitive, and
// readLine collides with the readline extension's built-in readline(),
// fataling with "Cannot redeclare readline()" the moment this file loads.
function smtpReadLine($conn): string
{
return (string) fgets($conn, 8192);
}
function smtpRespond($conn, string $line): void
{
fwrite($conn, $line."\r\n");
}
smtpRespond($conn, '220 fake.smtp ESMTP');
smtpReadLine($conn); // EHLO
smtpRespond($conn, '250 fake.smtp');
smtpReadLine($conn); // MAIL FROM
if ($failAt === 'mail') {
smtpRespond($conn, $failResponse);
exit(0);
}
smtpRespond($conn, '250 OK');
smtpReadLine($conn); // RCPT TO
if ($failAt === 'rcpt') {
smtpRespond($conn, $failResponse);
exit(0);
}
smtpRespond($conn, '250 OK');
smtpReadLine($conn); // DATA
if ($failAt === 'data') {
smtpRespond($conn, $failResponse);
exit(0);
}
smtpRespond($conn, '354 Go ahead');
// Message body: read until the bare "." terminator line.
while (($line = smtpReadLine($conn)) !== '' && rtrim($line, "\r\n") !== '.') {
// discard
}
smtpRespond($conn, '250 OK: queued as FAKE123');
// SmtpTransport::__destruct() calls stop(), which sends QUIT — but only
// after the send this helper exists for has already returned, so answering
// it is a courtesy, not a requirement.
stream_set_timeout($conn, 2);
smtpReadLine($conn);
smtpRespond($conn, '221 Bye');
fclose($conn);
fclose($server);