Skip to content

Commit 1b2ead3

Browse files
bobvandevijverfabpot
authored andcommitted
[Mailer] Fix sendmail transport failure handling and interactive mode
1 parent 184597d commit 1b2ead3

File tree

4 files changed

+106
-21
lines changed

4 files changed

+106
-21
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
#!/usr/bin/env php
22
<?php
3+
$argsPath = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'sendmail_args';
4+
5+
file_put_contents($argsPath, implode(' ', $argv));
6+
37
print "Sending failed";
48
exit(42);

src/Symfony/Component/Mailer/Tests/Transport/SendmailTransportTest.php

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,21 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Mailer\DelayedEnvelope;
16+
use Symfony\Component\Mailer\Envelope;
1617
use Symfony\Component\Mailer\Exception\TransportException;
18+
use Symfony\Component\Mailer\SentMessage;
1719
use Symfony\Component\Mailer\Transport\SendmailTransport;
20+
use Symfony\Component\Mailer\Transport\Smtp\Stream\ProcessStream;
21+
use Symfony\Component\Mailer\Transport\TransportInterface;
1822
use Symfony\Component\Mime\Address;
1923
use Symfony\Component\Mime\Email;
24+
use Symfony\Component\Mime\RawMessage;
2025

2126
class SendmailTransportTest extends TestCase
2227
{
2328
private const FAKE_SENDMAIL = __DIR__.'/Fixtures/fake-sendmail.php -t';
2429
private const FAKE_FAILING_SENDMAIL = __DIR__.'/Fixtures/fake-failing-sendmail.php -t';
30+
private const FAKE_INTERACTIVE_SENDMAIL = __DIR__.'/Fixtures/fake-failing-sendmail.php -bs';
2531

2632
/**
2733
* @var string
@@ -49,9 +55,7 @@ public function testToString()
4955

5056
public function testToIsUsedWhenRecipientsAreNotSet()
5157
{
52-
if ('\\' === \DIRECTORY_SEPARATOR) {
53-
$this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams');
54-
}
58+
$this->skipOnWindows();
5559

5660
$mail = new Email();
5761
$mail
@@ -71,20 +75,9 @@ public function testToIsUsedWhenRecipientsAreNotSet()
7175

7276
public function testRecipientsAreUsedWhenSet()
7377
{
74-
if ('\\' === \DIRECTORY_SEPARATOR) {
75-
$this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams');
76-
}
78+
$this->skipOnWindows();
7779

78-
$mail = new Email();
79-
$mail
80-
->from('from@mail.com')
81-
->to('to@mail.com')
82-
->subject('Subject')
83-
->text('Some text')
84-
;
85-
86-
$envelope = new DelayedEnvelope($mail);
87-
$envelope->setRecipients([new Address('recipient@mail.com')]);
80+
[$mail, $envelope] = $this->defaultMailAndEnvelope();
8881

8982
$sendmailTransport = new SendmailTransport(self::FAKE_SENDMAIL);
9083
$sendmailTransport->send($mail, $envelope);
@@ -93,11 +86,90 @@ public function testRecipientsAreUsedWhenSet()
9386
}
9487

9588
public function testThrowsTransportExceptionOnFailure()
89+
{
90+
$this->skipOnWindows();
91+
92+
[$mail, $envelope] = $this->defaultMailAndEnvelope();
93+
94+
$sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL);
95+
$this->expectException(TransportException::class);
96+
$this->expectExceptionMessage('Process failed with exit code 42: Sending failed');
97+
$sendmailTransport->send($mail, $envelope);
98+
99+
$streamProperty = new \ReflectionProperty(SendmailTransport::class, 'stream');
100+
$streamProperty->setAccessible(true);
101+
$stream = $streamProperty->getValue($sendmailTransport);
102+
$this->assertNull($stream->stream);
103+
}
104+
105+
public function testStreamIsClearedOnFailure()
106+
{
107+
$this->skipOnWindows();
108+
109+
[$mail, $envelope] = $this->defaultMailAndEnvelope();
110+
111+
$sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL);
112+
try {
113+
$sendmailTransport->send($mail, $envelope);
114+
} catch (TransportException $e) {
115+
}
116+
117+
$streamProperty = new \ReflectionProperty(SendmailTransport::class, 'stream');
118+
$streamProperty->setAccessible(true);
119+
$stream = $streamProperty->getValue($sendmailTransport);
120+
$innerStreamProperty = new \ReflectionProperty(ProcessStream::class, 'stream');
121+
$innerStreamProperty->setAccessible(true);
122+
$this->assertNull($innerStreamProperty->getValue($stream));
123+
}
124+
125+
public function testDoesNotThrowWhenInteractive()
126+
{
127+
$this->skipOnWindows();
128+
129+
[$mail, $envelope] = $this->defaultMailAndEnvelope();
130+
131+
$sendmailTransport = new SendmailTransport(self::FAKE_INTERACTIVE_SENDMAIL);
132+
$transportProperty = new \ReflectionProperty(SendmailTransport::class, 'transport');
133+
$transportProperty->setAccessible(true);
134+
135+
// Replace the transport with an anonymous consumer that trigger the stream methods
136+
$transportProperty->setValue($sendmailTransport, new class($transportProperty->getValue($sendmailTransport)->getStream()) implements TransportInterface {
137+
private $stream;
138+
139+
public function __construct(ProcessStream $stream)
140+
{
141+
$this->stream = $stream;
142+
}
143+
144+
public function send(RawMessage $message, ?Envelope $envelope = null): ?SentMessage
145+
{
146+
$this->stream->initialize();
147+
$this->stream->write('SMTP');
148+
$this->stream->terminate();
149+
150+
return new SentMessage($message, $envelope);
151+
}
152+
153+
public function __toString(): string
154+
{
155+
return 'Interactive mode test';
156+
}
157+
});
158+
159+
$sendmailTransport->send($mail, $envelope);
160+
161+
$this->assertStringEqualsFile($this->argsPath, __DIR__.'/Fixtures/fake-failing-sendmail.php -bs');
162+
}
163+
164+
private function skipOnWindows()
96165
{
97166
if ('\\' === \DIRECTORY_SEPARATOR) {
98167
$this->markTestSkipped('Windows does not support shebangs nor non-blocking standard streams');
99168
}
169+
}
100170

171+
private function defaultMailAndEnvelope(): array
172+
{
101173
$mail = new Email();
102174
$mail
103175
->from('from@mail.com')
@@ -109,9 +181,6 @@ public function testThrowsTransportExceptionOnFailure()
109181
$envelope = new DelayedEnvelope($mail);
110182
$envelope->setRecipients([new Address('recipient@mail.com')]);
111183

112-
$sendmailTransport = new SendmailTransport(self::FAKE_FAILING_SENDMAIL);
113-
$this->expectException(TransportException::class);
114-
$this->expectExceptionMessage('Process failed with exit code 42: Sending failed');
115-
$sendmailTransport->send($mail, $envelope);
184+
return [$mail, $envelope];
116185
}
117186
}

src/Symfony/Component/Mailer/Transport/SendmailTransport.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ public function __construct(?string $command = null, ?EventDispatcherInterface $
6464
$this->stream = new ProcessStream();
6565
if (str_contains($this->command, ' -bs')) {
6666
$this->stream->setCommand($this->command);
67+
$this->stream->setInteractive(true);
6768
$this->transport = new SmtpTransport($this->stream, $dispatcher, $logger);
6869
}
6970
}

src/Symfony/Component/Mailer/Transport/Smtp/Stream/ProcessStream.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,18 @@ final class ProcessStream extends AbstractStream
2525
{
2626
private $command;
2727

28+
private $interactive = false;
29+
2830
public function setCommand(string $command)
2931
{
3032
$this->command = $command;
3133
}
3234

35+
public function setInteractive(bool $interactive)
36+
{
37+
$this->interactive = $interactive;
38+
}
39+
3340
public function initialize(): void
3441
{
3542
$descriptorSpec = [
@@ -57,11 +64,15 @@ public function terminate(): void
5764
$err = stream_get_contents($this->err);
5865
fclose($this->err);
5966
if (0 !== $exitCode = proc_close($this->stream)) {
60-
throw new TransportException('Process failed with exit code '.$exitCode.': '.$out.$err);
67+
$errorMessage = 'Process failed with exit code '.$exitCode.': '.$out.$err;
6168
}
6269
}
6370

6471
parent::terminate();
72+
73+
if (!$this->interactive && isset($errorMessage)) {
74+
throw new TransportException($errorMessage);
75+
}
6576
}
6677

6778
protected function getReadConnectionDescription(): string

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy