Skip to content

Commit 7470c1a

Browse files
ampazenicolas-grekas
authored andcommitted
[Mailer] Improve extensibility of EsmtpTransport
1 parent 22c271a commit 7470c1a

File tree

6 files changed

+177
-102
lines changed

6 files changed

+177
-102
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
---
66

77
* Make `start()` and `stop()` methods public on `SmtpTransport`
8+
* Improve extensibility of `EsmtpTransport`
89

910
6.0
1011
---
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Mailer\Tests\Transport\Smtp;
13+
14+
use Symfony\Component\Mailer\Exception\TransportException;
15+
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
16+
17+
class DummyStream extends AbstractStream
18+
{
19+
private string $nextResponse;
20+
private array $commands = [];
21+
private bool $closed = true;
22+
23+
public function initialize(): void
24+
{
25+
$this->closed = false;
26+
$this->nextResponse = '220 localhost ESMTP';
27+
}
28+
29+
public function disableTls(): static
30+
{
31+
return $this;
32+
}
33+
34+
public function isTLS(): bool
35+
{
36+
return false;
37+
}
38+
39+
public function setHost(string $host): static
40+
{
41+
return $this;
42+
}
43+
44+
public function setPort(int $port): static
45+
{
46+
return $this;
47+
}
48+
49+
public function write(string $bytes, $debug = true): void
50+
{
51+
if ($this->closed) {
52+
throw new TransportException('Unable to write bytes on the wire.');
53+
}
54+
55+
$this->commands[] = $bytes;
56+
57+
if (str_starts_with($bytes, 'EHLO')) {
58+
$this->nextResponse = '250 localhost';
59+
} elseif (str_starts_with($bytes, 'DATA')) {
60+
$this->nextResponse = '354 Enter message, ending with "." on a line by itself';
61+
} elseif (str_starts_with($bytes, 'QUIT')) {
62+
$this->nextResponse = '221 Goodbye';
63+
} else {
64+
$this->nextResponse = '250 OK';
65+
}
66+
}
67+
68+
public function readLine(): string
69+
{
70+
return $this->nextResponse."\r\n";
71+
}
72+
73+
public function flush(): void
74+
{
75+
}
76+
77+
/**
78+
* @return string[]
79+
*/
80+
public function getCommands(): array
81+
{
82+
return $this->commands;
83+
}
84+
85+
public function clearCommands(): void
86+
{
87+
$this->commands = [];
88+
}
89+
90+
protected function getReadConnectionDescription(): string
91+
{
92+
return 'null';
93+
}
94+
95+
public function close(): void
96+
{
97+
$this->closed = true;
98+
}
99+
100+
public function isClosed(): bool
101+
{
102+
return $this->closed;
103+
}
104+
105+
public function terminate(): void
106+
{
107+
parent::terminate();
108+
$this->closed = true;
109+
}
110+
}

Tests/Transport/Smtp/EsmtpTransportTest.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport;
16+
use Symfony\Component\Mime\Email;
1617

1718
class EsmtpTransportTest extends TestCase
1819
{
@@ -40,4 +41,40 @@ public function testToString()
4041
$t = new EsmtpTransport('example.com', 466, true);
4142
$this->assertEquals('smtps://example.com:466', (string) $t);
4243
}
44+
45+
public function testExtensibility()
46+
{
47+
$stream = new DummyStream();
48+
$transport = new CustomEsmtpTransport(stream: $stream);
49+
50+
$message = new Email();
51+
$message->from('[email protected]');
52+
$message->addTo('[email protected]');
53+
$message->text('.');
54+
55+
$transport->send($message);
56+
57+
$this->assertContains("MAIL FROM:<[email protected]> RET=HDRS\r\n", $stream->getCommands());
58+
$this->assertContains("RCPT TO:<[email protected]> NOTIFY=FAILURE\r\n", $stream->getCommands());
59+
}
60+
}
61+
62+
class CustomEsmtpTransport extends EsmtpTransport
63+
{
64+
public function executeCommand(string $command, array $codes): string
65+
{
66+
$command = match (true) {
67+
str_starts_with($command, 'MAIL FROM:') && isset($this->getCapabilities()['DSN']) => substr_replace($command, ' RET=HDRS', -2, 0),
68+
str_starts_with($command, 'RCPT TO:') && isset($this->getCapabilities()['DSN']) => substr_replace($command, ' NOTIFY=FAILURE', -2, 0),
69+
default => $command,
70+
};
71+
72+
$response = parent::executeCommand($command, $codes);
73+
74+
if (str_starts_with($command, 'EHLO ')) {
75+
$response .= "250 DSN\r\n";
76+
}
77+
78+
return $response;
79+
}
4380
}

Tests/Transport/Smtp/SmtpTransportTest.php

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use Symfony\Component\Mailer\Envelope;
1616
use Symfony\Component\Mailer\Exception\TransportException;
1717
use Symfony\Component\Mailer\Transport\Smtp\SmtpTransport;
18-
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
1918
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
2019
use Symfony\Component\Mime\Address;
2120
use Symfony\Component\Mime\Email;
@@ -147,87 +146,3 @@ public function testStop()
147146
$this->assertTrue($stream->isClosed());
148147
}
149148
}
150-
151-
class DummyStream extends AbstractStream
152-
{
153-
/**
154-
* @var string
155-
*/
156-
private $nextResponse;
157-
158-
/**
159-
* @var string[]
160-
*/
161-
private $commands;
162-
163-
/**
164-
* @var bool
165-
*/
166-
private $closed = true;
167-
168-
public function initialize(): void
169-
{
170-
$this->closed = false;
171-
$this->nextResponse = '220 localhost';
172-
}
173-
174-
public function write(string $bytes, $debug = true): void
175-
{
176-
if ($this->closed) {
177-
throw new TransportException('Unable to write bytes on the wire.');
178-
}
179-
180-
$this->commands[] = $bytes;
181-
182-
if (str_starts_with($bytes, 'DATA')) {
183-
$this->nextResponse = '354 Enter message, ending with "." on a line by itself';
184-
} elseif (str_starts_with($bytes, 'QUIT')) {
185-
$this->nextResponse = '221 Goodbye';
186-
} else {
187-
$this->nextResponse = '250 OK';
188-
}
189-
}
190-
191-
public function readLine(): string
192-
{
193-
return $this->nextResponse;
194-
}
195-
196-
public function flush(): void
197-
{
198-
}
199-
200-
/**
201-
* @return string[]
202-
*/
203-
public function getCommands(): array
204-
{
205-
return $this->commands;
206-
}
207-
208-
public function clearCommands(): void
209-
{
210-
$this->commands = [];
211-
}
212-
213-
protected function getReadConnectionDescription(): string
214-
{
215-
return 'null';
216-
}
217-
218-
public function close(): void
219-
{
220-
$this->closed = true;
221-
}
222-
223-
public function isClosed(): bool
224-
{
225-
return $this->closed;
226-
}
227-
228-
public function terminate(): void
229-
{
230-
parent::terminate();
231-
$this->closed = true;
232-
}
233-
}

Transport/Smtp/EsmtpTransport.php

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Symfony\Component\Mailer\Exception\TransportException;
1717
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
1818
use Symfony\Component\Mailer\Transport\Smtp\Auth\AuthenticatorInterface;
19+
use Symfony\Component\Mailer\Transport\Smtp\Stream\AbstractStream;
1920
use Symfony\Component\Mailer\Transport\Smtp\Stream\SocketStream;
2021

2122
/**
@@ -29,10 +30,11 @@ class EsmtpTransport extends SmtpTransport
2930
private array $authenticators = [];
3031
private string $username = '';
3132
private string $password = '';
33+
private array $capabilities;
3234

33-
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null)
35+
public function __construct(string $host = 'localhost', int $port = 0, bool $tls = null, EventDispatcherInterface $dispatcher = null, LoggerInterface $logger = null, AbstractStream $stream = null)
3436
{
35-
parent::__construct(null, $dispatcher, $logger);
37+
parent::__construct($stream, $dispatcher, $logger);
3638

3739
// order is important here (roughly most secure and popular first)
3840
$this->authenticators = [
@@ -98,24 +100,32 @@ public function addAuthenticator(AuthenticatorInterface $authenticator): void
98100
$this->authenticators[] = $authenticator;
99101
}
100102

101-
protected function doHeloCommand(): void
103+
public function executeCommand(string $command, array $codes): string
104+
{
105+
return [250] === $codes && str_starts_with($command, 'HELO ') ? $this->doEhloCommand() : parent::executeCommand($command, $codes);
106+
}
107+
108+
final protected function getCapabilities(): array
109+
{
110+
return $this->capabilities;
111+
}
112+
113+
private function doEhloCommand(): string
102114
{
103115
try {
104116
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
105117
} catch (TransportExceptionInterface $e) {
106-
parent::doHeloCommand();
107-
108-
return;
118+
return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
109119
}
110120

111-
$capabilities = $this->getCapabilities($response);
121+
$this->capabilities = $this->parseCapabilities($response);
112122

113123
/** @var SocketStream $stream */
114124
$stream = $this->getStream();
115125
// WARNING: !$stream->isTLS() is right, 100% sure :)
116126
// if you think that the ! should be removed, read the code again
117127
// if doing so "fixes" your issue then it probably means your SMTP server behaves incorrectly or is wrongly configured
118-
if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $capabilities)) {
128+
if (!$stream->isTLS() && \defined('OPENSSL_VERSION_NUMBER') && \array_key_exists('STARTTLS', $this->capabilities)) {
119129
$this->executeCommand("STARTTLS\r\n", [220]);
120130

121131
if (!$stream->startTLS()) {
@@ -124,20 +134,20 @@ protected function doHeloCommand(): void
124134

125135
try {
126136
$response = $this->executeCommand(sprintf("EHLO %s\r\n", $this->getLocalDomain()), [250]);
127-
$capabilities = $this->getCapabilities($response);
137+
$this->capabilities = $this->parseCapabilities($response);
128138
} catch (TransportExceptionInterface $e) {
129-
parent::doHeloCommand();
130-
131-
return;
139+
return parent::executeCommand(sprintf("HELO %s\r\n", $this->getLocalDomain()), [250]);
132140
}
133141
}
134142

135-
if (\array_key_exists('AUTH', $capabilities)) {
136-
$this->handleAuth($capabilities['AUTH']);
143+
if (\array_key_exists('AUTH', $this->capabilities)) {
144+
$this->handleAuth($this->capabilities['AUTH']);
137145
}
146+
147+
return $response;
138148
}
139149

140-
private function getCapabilities(string $ehloResponse): array
150+
private function parseCapabilities(string $ehloResponse): array
141151
{
142152
$capabilities = [];
143153
$lines = explode("\r\n", trim($ehloResponse));

Transport/Smtp/SmtpTransport.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,6 @@ public function __toString(): string
172172
* @param int[] $codes
173173
*
174174
* @throws TransportException when an invalid response if received
175-
*
176-
* @internal
177175
*/
178176
public function executeCommand(string $command, array $codes): string
179177
{
@@ -225,6 +223,10 @@ protected function doSend(SentMessage $message): void
225223
}
226224
}
227225

226+
/**
227+
* @internal since version 6.1, to be made private in 7.0
228+
* @final since version 6.1, to be made private in 7.0
229+
*/
228230
protected function doHeloCommand(): void
229231
{
230232
$this->executeCommand(sprintf("HELO %s\r\n", $this->domain), [250]);

0 commit comments

Comments
 (0)