<?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, "advertiseAuth": bool}. 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. STARTTLS is
 * never advertised in the EHLO response, so EsmtpTransport — auto_tls
 * defaults true, but only upgrades when the server actually offers
 * STARTTLS — never attempts it, and the whole exchange stays in plain text.
 *
 * AUTH is advertised only when advertiseAuth is true (see
 * FakeSmtpServer::succeedingUnlessAuthAttempted()) — the base configuration
 * never offers it, so EsmtpTransport::handleAuth() is never even reachable
 * against it regardless of what credentials a client might have set.
 */
$config = json_decode((string) stream_get_contents(STDIN), true) ?: [];
$failAt = $config['failAt'] ?? null;
$failResponse = $config['failResponse'] ?? '550 Rejected';
$advertiseAuth = $config['advertiseAuth'] ?? false;

$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
if ($advertiseAuth) {
    // Multi-line EHLO: smtpRespond() only ever sends the final line, so the
    // continuation is written directly.
    fwrite($conn, "250-fake.smtp\r\n");
    smtpRespond($conn, '250 AUTH LOGIN');
} else {
    smtpRespond($conn, '250 fake.smtp');
}

// AUTH, if the client attempts it, arrives here in place of MAIL FROM —
// immediately after EHLO and before the send dialogue proper, exactly where
// EsmtpTransport::handleAuth() sends it.
$line = smtpReadLine($conn);
if ($advertiseAuth && str_starts_with(strtoupper(ltrim($line)), 'AUTH')) {
    smtpRespond($conn, '535 5.7.8 Authentication attempted but this relay does not require it');
    exit(0);
}

// $line is MAIL FROM's own line when AUTH was not attempted.
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);
