100 lines
3.0 KiB
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);
|