# SMTP

The `SMTP` component implements an RFC 5321 SMTP client with connection pooling, TLS, authentication, and support for modern SMTP extensions. It integrates with the `Message` and `MIME` components for complete email sending.

## Sending a Message

The `Transport` handles the full SMTP lifecycle: connection, EHLO, TLS, authentication, message transmission, and connection reuse. Pass an `Envelope` (derived from message headers) and a `MessageInterface` to send.

For envelope addresses, the transport lowercases and NFC-normalizes domain labels, then encodes each Unicode label as Punycode with the `xn--` prefix. Domain literals stay unchanged. It rejects encoded labels longer than 63 bytes with `TransmissionException`. This handles case and canonical Unicode forms, but does not apply the full UTS #46 mapping. Non-ASCII envelope local parts and transmitted message headers require SMTPUTF8; the transport throws `TransmissionException` before MAIL FROM when the server lacks it (RFC 6531 section 3.2). The check uses the serialized headers, including nested MIME part and embedded message headers, so removed Bcc fields and ASCII encoded-words do not require SMTPUTF8. Without server support, multipart and embedded message bodies spool to memory and then disk before MAIL FROM; these sends wait for EOF so all headers can be checked. Body text alone does not require SMTPUTF8.

```php
use Psl\IO;
use Psl\Message\Address\Mailbox;
use Psl\Message\Envelope;
use Psl\Message\Message;
use Psl\MIME\Part;
use Psl\SMTP\Client\Transport;
use Psl\SMTP\Client\TransportConfiguration;
use Psl\SMTP\Security;

$transport = new Transport(
    TransportConfiguration::default()->withHost('smtp.example.com')->withSecurity(Security::TLS),
);

$sender = new Mailbox('alice', 'example.com', 'Alice');
// Lowercasing and NFC normalization produce bob@xn--bcher-kva.example.
// Encoded domain labels must fit in 63 bytes.
// Non-ASCII local parts and transmitted headers require SMTPUTF8.
// Without it, multipart and embedded message bodies spool before MAIL FROM.
$recipient = new Mailbox('bob', 'BÜCHER.EXAMPLE');

$message = new Message()
    ->withFrom($sender)
    ->withTo($recipient)
    ->withSubject('Hello from PSL')
    ->withContent(new Part\Text(new IO\MemoryHandle('Hello, Bob!')));

$report = $transport->send(Envelope::fromMessage($message), $message);

$transport->close();
```

## Authentication

Five authentication mechanisms are supported. Pass any `AuthenticatorInterface` as the second argument to `Transport`.

```php
use Psl\SMTP\Client\Authentication;
use Psl\SMTP\Client\Transport;
use Psl\SMTP\Client\TransportConfiguration;
use Psl\SMTP\Security;

// PLAIN authentication
$plain = new Authentication\PlainAuthenticator('user', 'password');

// LOGIN authentication
$login = new Authentication\LoginAuthenticator('user', 'password');

// XOAUTH2 for Gmail and other OAuth2-enabled servers
$oauth = new Authentication\XOAuth2Authenticator('user@gmail.com', 'oauth2-access-token');

// CRAM-MD5 challenge-response (avoids sending password in plaintext)
$cramMd5 = new Authentication\CRAMMD5Authenticator('user', 'password');

// SCRAM-SHA-256 with mutual authentication (RFC 7677)
$scram = new Authentication\SCRAMSHA256Authenticator('user', 'password');

// Pass any authenticator as the second argument to Transport
$transport = new Transport(
    TransportConfiguration::default()->withHost('smtp.gmail.com')->withSecurity(Security::TLS),
    $oauth,
);
```

## Transport Configuration

`TransportConfiguration` controls connection-level settings: host, port, security mode, pipelining, chunking, and TCP/TLS options. All settings are immutable with fluent `with*()` builders.

```php
use Psl\SMTP\Client\TransportConfiguration;
use Psl\SMTP\Security;
use Psl\TCP;
use Psl\TLS;

// All transport settings are immutable with fluent with*() builders
$config = TransportConfiguration::default()
    ->withHost('smtp.example.com')
    ->withPort(465)
    ->withSecurity(Security::TLS)
    ->withLocalHostname('client.example.com')
    ->withPipelining(true)
    ->withChunking(true)
    ->withChunkSize(32_768)
    ->withAllowPartialSuccess(false);

// TCP and TLS settings are configurable independently
$config = $config
    ->withConnectConfiguration(new TCP\ConnectConfiguration(noDelay: true))
    ->withTlsClientConfiguration(new TLS\ClientConfiguration(peerVerification: true, allowSelfSigned: false));
```

## Send Configuration

`SendConfiguration` controls per-send MAIL FROM extension parameters: DSN notifications, REQUIRETLS, message priority, delivery deadlines, and deferred delivery. When `requireTls` is true, the transport throws `TransmissionException` before sending MAIL FROM if the server does not advertise REQUIRETLS or the connection uses `Security::None` (RFC 8689 section 4.2.1). The transport keeps EHLO capabilities when it reuses a pooled connection.

```php
use Psl\DateTime\DateTime;
use Psl\DateTime\Duration;
use Psl\SMTP\Client\SendConfiguration;
use Psl\SMTP\DeliverBy;
use Psl\SMTP\DeliverByMode;
use Psl\SMTP\Priority;

// Per-send parameters control MAIL FROM extensions
$config = new SendConfiguration()
    // DSN: request delivery status notifications (RFC 3461)
    ->withDsnReturn('FULL')
    ->withDsnEnvelopeId('msg-001')
    ->withDsnNotify('SUCCESS,FAILURE')
    // Require TLS for the entire delivery chain (RFC 8689).
    // Sending requires TLS and throws if the server lacks REQUIRETLS.
    ->withRequireTls(true)
    // Message priority (RFC 6710 / STANAG 4406)
    ->withPriority(Priority::Urgent)
    // Delivery deadline: return if not delivered within 2 hours (RFC 2852)
    ->withDeliverBy(new DeliverBy(Duration::hours(2), DeliverByMode::Return))
    // Deferred delivery: hold for 30 minutes (RFC 4865)
    ->withFutureRelease(Duration::minutes(30));

// Or hold until a specific time
$config = $config->withFutureRelease(DateTime::now()->plusHours(6));
```

## Partial Recipient Success

By default, the transport throws on the first rejected recipient. Enable `allowPartialSuccess` to deliver to accepted recipients and report failures via `DeliveryReport`.

```php
use Psl\IO;
use Psl\Message\Address\Mailbox;
use Psl\Message\Envelope;
use Psl\Message\Message;
use Psl\MIME\Part;
use Psl\SMTP\Client\Transport;
use Psl\SMTP\Client\TransportConfiguration;
use Psl\SMTP\Security;

// Enable partial success to deliver to accepted recipients
// even when some are rejected
$transport = new Transport(
    TransportConfiguration::default()
        ->withHost('smtp.example.com')
        ->withSecurity(Security::TLS)
        ->withAllowPartialSuccess(true),
);

$sender = new Mailbox('noreply', 'example.com');
$message = new Message()
    ->withFrom($sender)
    ->withTo('alice@example.com, bob@invalid.test, carol@example.com')
    ->withSubject('Team Update')
    ->withContent(new Part\Text(new IO\MemoryHandle('Hello team!')));

$report = $transport->send(Envelope::fromMessage($message), $message);

// Check for rejected recipients
if ($report->hasRejections()) {
    foreach ($report->rejectedRecipients as [$mailbox, $reply]) {
        // $mailbox->address: "bob@invalid.test"
        // $reply->code: 550
        // $reply->message: "User not found"
        IO\write_line($reply->message);
    }
}

$transport->close();
```

## Errors

Every failure of the exchange is an `SMTP\Exception\RuntimeException`. A connection that fails during the exchange, for example because the server resets it, throws `ConnectionException`, with the IO or Network exception as its previous exception. A failure to read the message content is not a failure of the connection: the IO exception passes through unchanged. After a failure or a cancellation, the transport closes the pooled connection, and the next send opens a new one.

## Async and Cancellation

The transport is fully async-capable. Use `Async\concurrently` to send multiple messages in parallel with automatic connection pooling. Use cancellation tokens for timeouts. Once the connection is open, a cancelled send throws `Async\Exception\CancelledException` unchanged.

```php
use Psl\Async;
use Psl\IO;
use Psl\Message\Address\Mailbox;
use Psl\Message\Envelope;
use Psl\Message\Message;
use Psl\MIME\Part;
use Psl\SMTP\Client\Transport;
use Psl\SMTP\Client\TransportConfiguration;
use Psl\SMTP\Security;
use Psl\Vec;

$transport = new Transport(
    TransportConfiguration::default()->withHost('smtp.example.com')->withSecurity(Security::TLS),
);

$sender = new Mailbox('noreply', 'example.com');
$recipients = ['alice@example.com', 'bob@example.com', 'carol@example.com'];

// Send to all recipients concurrently, reusing pooled connections
Async\concurrently(Vec\map($recipients, static fn(string $address): Closure => static function () use (
    $transport,
    $sender,
    $address,
): void {
    $recipient = Mailbox::parse($address);
    $message = new Message()
        ->withFrom($sender)
        ->withTo($recipient)
        ->withSubject('Hello!')
        ->withContent(new Part\Text(new IO\MemoryHandle('Hi there!')));

    $transport->send(Envelope::fromMessage($message), $message);
}));

// Send with a timeout using a cancellation token
$message = new Message()
    ->withFrom($sender)
    ->withTo('dave@example.com')
    ->withSubject('Urgent')
    ->withContent(new Part\Text(new IO\MemoryHandle('Time-sensitive!')));

$transport->send(
    Envelope::fromMessage($message),
    $message,
    cancellation: new Async\TimeoutCancellationToken(Psl\DateTime\Duration::seconds(10)),
);

$transport->close();
```

## Low-Level Connection

`Connection` implements `Network\StreamInterface` and speaks raw SMTP commands. Use it for direct protocol interaction or to build custom transports.

```php
use Psl\SMTP\Client\Connection;
use Psl\SMTP\Command;
use Psl\TCP;

// Low-level connection for direct SMTP protocol interaction
$stream = TCP\connect('smtp.example.com', 25);
$connection = new Connection($stream);

// Read the server greeting
$greeting = $connection->readGreeting();
// $greeting->code: 220
// $greeting->message: "smtp.example.com ESMTP"

// EHLO to discover capabilities
$ehlo = $connection->ehlo('client.example.com');

// Check capabilities
$connection->supportsCapability('PIPELINING'); // bool
$connection->supportsCapability('8BITMIME'); // bool
$connection->getCapabilityValue('AUTH'); // "PLAIN LOGIN" or null
$connection->maxSize; // int or null

// Send arbitrary commands
$reply = $connection->sendCommand(new Command('NOOP'));
// $reply->code: 250
// $reply->isPositiveCompletion(): true

// Or send without reading reply (for pipelining)
$connection->writeCommand(new Command('NOOP'));
$reply = $connection->readReply();

$connection->close();
```

## SMTP Extensions

The transport automatically negotiates these extensions when the server advertises them:

| Extension | RFC | Description |
|-----------|-----|-------------|
| PIPELINING | RFC 2920 | Send multiple commands before reading replies |
| CHUNKING | RFC 3030 | BDAT transfer without dot-stuffing |
| BINARYMIME | RFC 3030 | Binary content without transfer encoding |
| 8BITMIME | RFC 6152 | 8-bit MIME transport |
| SMTPUTF8 | RFC 6531 | Internationalized email addresses |
| STARTTLS | RFC 3207 | TLS upgrade on plaintext connections |
| DSN | RFC 3461 | Delivery status notifications |
| REQUIRETLS | RFC 8689 | End-to-end TLS enforcement |
| MT-PRIORITY | RFC 6710 | Message priority levels |
| DELIVERBY | RFC 2852 | Delivery deadline specification |
| FUTURERELEASE | RFC 4865 | Deferred delivery |
| SIZE | RFC 1870 | Server maximum message size |

## Security

The transport validates all SMTP commands for CRLF and null byte injection attacks. Malicious input in addresses, hostnames, or DSN parameters throws `PossibleAttackException` before reaching the wire.

See [src/Psl/SMTP/](https://github.com/php-standard-library/php-standard-library/tree/6.4.1/packages/smtp/src/Psl/SMTP/) for the full API.
