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.
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');
$recipient = new Mailbox('bob', 'example.com');
$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.
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('[email protected]', '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.
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.
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)
->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.
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('[email protected], [email protected], [email protected]')
->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: "[email protected]"
// $reply->code: 550
// $reply->message: "User not found"
IO\write_line($reply->message);
}
}
$transport->close();
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.
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 = ['[email protected]', '[email protected]', '[email protected]'];
// 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('[email protected]')
->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.
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/ for the full API.