SendmailTransport.php

PHP

Path: src/Mail/SendmailTransport.php

<?php

namespace mini\Mail;

/**
 * Mail transport using the sendmail binary
 *
 * This transport pipes the complete RFC 5322 message directly to sendmail.
 * The Email's StreamInterface provides headers + body in wire format.
 *
 * Usage:
 *   $transport = new SendmailTransport(); // Uses /usr/sbin/sendmail
 *   $transport = new SendmailTransport('/usr/local/bin/sendmail');
 */
class SendmailTransport implements MailTransportInterface
{
    private string $command;

    /**
     * @param string $sendmailPath Path to sendmail binary
     */
    public function __construct(string $sendmailPath = '/usr/sbin/sendmail')
    {
        $this->command = $sendmailPath;
    }

    public function send(EmailInterface $email, string $sender, array $recipients): void
    {
        // Build sendmail command with envelope sender and recipients
        // -oi: don't treat a line with only . as end of input
        // -f: envelope sender
        // -t could be used to read recipients from headers, but we pass them explicitly
        $cmd = sprintf(
            '%s -oi -f %s -- %s',
            escapeshellcmd($this->command),
            escapeshellarg($sender),
            implode(' ', array_map('escapeshellarg', $recipients))
        );

        $descriptors = [
            0 => ['pipe', 'r'], // stdin
            1 => ['pipe', 'w'], // stdout
            2 => ['pipe', 'w'], // stderr
        ];

        $process = proc_open($cmd, $descriptors, $pipes);

        if (!is_resource($process)) {
            throw new MailTransportException("Failed to open sendmail process: $cmd");
        }

        try {
            // Stream the email to sendmail's stdin
            while (!$email->eof()) {
                $chunk = $email->read(8192);
                if ($chunk !== '') {
                    fwrite($pipes[0], $chunk);
                }
            }
            fclose($pipes[0]);

            // Read any output
            $stdout = stream_get_contents($pipes[1]);
            $stderr = stream_get_contents($pipes[2]);
            fclose($pipes[1]);
            fclose($pipes[2]);

            $exitCode = proc_close($process);

            if ($exitCode !== 0) {
                $error = trim($stderr ?: $stdout ?: "Exit code: $exitCode");
                throw new MailTransportException("sendmail failed: $error");
            }
        } catch (MailTransportException $e) {
            throw $e;
        } catch (\Throwable $e) {
            // Ensure process is closed on error
            if (is_resource($pipes[0])) fclose($pipes[0]);
            if (is_resource($pipes[1])) fclose($pipes[1]);
            if (is_resource($pipes[2])) fclose($pipes[2]);
            proc_close($process);
            throw new MailTransportException("sendmail failed: " . $e->getMessage(), 0, $e);
        }
    }
}